URI: 
       tMerge pull request #4915 from spesmilo/qabstractitemmodel - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 53b64a6367617d431d77635097af6fd5d55bdd57
   DIR parent 0ddccd56c70a5629618065c30526dcc38b9cdc90
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Mon, 10 Dec 2018 18:10:00 +0100
       
       Merge pull request #4915 from spesmilo/qabstractitemmodel
       
       use QAbstractItemModel in History tab
       Diffstat:
         M RELEASE-NOTES                       |      11 +++++++++++
         M electrum/daemon.py                  |       2 +-
         M electrum/gui/qt/__init__.py         |       1 +
         M electrum/gui/qt/address_dialog.py   |      19 ++++++++++++-------
         M electrum/gui/qt/contact_list.py     |       8 ++------
         M electrum/gui/qt/history_list.py     |     543 +++++++++++++++++--------------
         M electrum/gui/qt/main_window.py      |      24 +++++++++++++-----------
         M electrum/gui/qt/util.py             |      93 ++++++++++++++-----------------
         M electrum/gui/qt/utxo_list.py        |       2 +-
         M electrum/util.py                    |      42 +++++++++++++++++++++++--------
         M electrum/wallet.py                  |      15 ++++++++-------
       
       11 files changed, 428 insertions(+), 332 deletions(-)
       ---
   DIR diff --git a/RELEASE-NOTES b/RELEASE-NOTES
       t@@ -23,6 +23,17 @@
           - Trezor: refactoring and compat with python-trezor 0.11
           - Digital BitBox: support firmware v5.0.0
         * fix bitcoin URI handling when app already running (#4796)
       + * Qt listing fixes:
       +   - Selection by arrow keys disabled while editing e.g. label
       +   - Enter key on unedited value does not pop up context menu
       +   - Contacts:
       +     - Prevent editing of OpenAlias names
       +   - Receive:
       +     - Icon for status of payment requests
       +     - Disable editing of 'Description' in list, interaction
       +       between labels and memo of invoice confusing
       +   - Addresses:
       +     - Fiat prices would show "No Data" incorrectly upon start
         * Several other minor bugfixes and usability improvements.
        
        
   DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
       t@@ -319,12 +319,12 @@ class Daemon(DaemonThread):
                DaemonThread.stop(self)
        
            def init_gui(self, config, plugins):
       +        threading.current_thread().setName('GUI')
                gui_name = config.get('gui', 'qt')
                if gui_name in ['lite', 'classic']:
                    gui_name = 'qt'
                gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])
                self.gui = gui.ElectrumGui(config, self, plugins)
       -        threading.current_thread().setName('GUI')
                try:
                    self.gui.main()
                except BaseException as e:
   DIR diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
       t@@ -97,6 +97,7 @@ class ElectrumGui(PrintError):
                    QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
                if hasattr(QGuiApplication, 'setDesktopFileName'):
                    QGuiApplication.setDesktopFileName('electrum.desktop')
       +        self.gui_thread = threading.current_thread()
                self.config = config
                self.daemon = daemon
                self.plugins = plugins
   DIR diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py
       t@@ -30,9 +30,16 @@ from PyQt5.QtGui import *
        from PyQt5.QtWidgets import *
        
        from .util import *
       -from .history_list import HistoryList
       +from .history_list import HistoryList, HistoryModel
        from .qrtextedit import ShowQRTextEdit
        
       +class AddressHistoryModel(HistoryModel):
       +    def __init__(self, parent, address):
       +        super().__init__(parent)
       +        self.address = address
       +
       +    def get_domain(self):
       +        return [self.address]
        
        class AddressDialog(WindowModalDialog):
        
       t@@ -80,16 +87,14 @@ class AddressDialog(WindowModalDialog):
                    vbox.addWidget(redeem_e)
        
                vbox.addWidget(QLabel(_("History")))
       -        self.hw = HistoryList(self.parent)
       -        self.hw.get_domain = self.get_domain
       +        addr_hist_model = AddressHistoryModel(self.parent, self.address)
       +        self.hw = HistoryList(self.parent, addr_hist_model)
       +        addr_hist_model.set_view(self.hw)
                vbox.addWidget(self.hw)
        
                vbox.addLayout(Buttons(CloseButton(self)))
                self.format_amount = self.parent.format_amount
       -        self.hw.update()
       -
       -    def get_domain(self):
       -        return [self.address]
       +        addr_hist_model.refresh('address dialog constructor')
        
            def show_qr(self):
                text = self.address
   DIR diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py
       t@@ -49,12 +49,8 @@ class ContactList(MyTreeView):
        
            def on_edited(self, idx, user_role, text):
                _type, prior_name = self.parent.contacts.pop(user_role)
       -
       -        # TODO when min Qt >= 5.11, use siblingAtColumn
       -        col_1_sibling = idx.sibling(idx.row(), 1)
       -        col_1_item = self.model().itemFromIndex(col_1_sibling)
       -
       -        self.parent.set_contact(text, col_1_item.text())
       +        self.parent.set_contact(text, user_role)
       +        self.update()
        
            def import_contacts(self):
                import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.update)
   DIR diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
       t@@ -26,12 +26,13 @@
        import webbrowser
        import datetime
        from datetime import date
       -from typing import TYPE_CHECKING
       -from collections import OrderedDict
       +from typing import TYPE_CHECKING, Tuple, Dict
       +import threading
        
        from electrum.address_synchronizer import TX_HEIGHT_LOCAL
        from electrum.i18n import _
       -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedInfo, OrderedDictWithIndex
       +from electrum.util import (block_explorer_URL, profiler, print_error, TxMinedInfo,
       +                           OrderedDictWithIndex, PrintError)
        
        from .util import *
        
       t@@ -60,43 +61,288 @@ TX_ICONS = [
        
        class HistorySortModel(QSortFilterProxyModel):
            def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
       -        item1 = self.sourceModel().itemFromIndex(source_left)
       -        item2 = self.sourceModel().itemFromIndex(source_right)
       -        data1 = item1.data(HistoryList.SORT_ROLE)
       -        data2 = item2.data(HistoryList.SORT_ROLE)
       -        if data1 is not None and data2 is not None:
       -            return data1 < data2
       -        return item1.text() < item2.text()
       +        item1 = self.sourceModel().data(source_left, Qt.UserRole)
       +        item2 = self.sourceModel().data(source_right, Qt.UserRole)
       +        if item1 is None or item2 is None:
       +            raise Exception(f'UserRole not set for column {source_left.column()}')
       +        if item1.value() is None or item2.value() is None:
       +            return False
       +        return item1.value() < item2.value()
       +
       +class HistoryModel(QAbstractItemModel, PrintError):
       +
       +    NUM_COLUMNS = 9
       +
       +    def __init__(self, parent):
       +        super().__init__(parent)
       +        self.parent = parent
       +        self.view = None  # type: HistoryList
       +        self.transactions = OrderedDictWithIndex()
       +        self.tx_status_cache = {}  # type: Dict[str, Tuple[int, str]]
       +
       +    def set_view(self, history_list: 'HistoryList'):
       +        # FIXME HistoryModel and HistoryList mutually depend on each other.
       +        # After constructing both, this method needs to be called.
       +        self.view = history_list  # type: HistoryList
       +        self.set_visibility_of_columns()
       +
       +    def columnCount(self, parent: QModelIndex):
       +        return self.NUM_COLUMNS
       +
       +    def rowCount(self, parent: QModelIndex):
       +        return len(self.transactions)
       +
       +    def index(self, row: int, column: int, parent: QModelIndex):
       +        return self.createIndex(row, column)
       +
       +    def data(self, index: QModelIndex, role: Qt.ItemDataRole):
       +        # note: this method is performance-critical.
       +        # it is called a lot, and so must run extremely fast.
       +        assert index.isValid()
       +        col = index.column()
       +        tx_item = self.transactions.value_from_pos(index.row())
       +        tx_hash = tx_item['txid']
       +        conf = tx_item['confirmations']
       +        txpos = tx_item['txpos_in_block'] or 0
       +        height = tx_item['height']
       +        try:
       +            status, status_str = self.tx_status_cache[tx_hash]
       +        except KeyError:
       +            tx_mined_info = self.tx_mined_info_from_tx_item(tx_item)
       +            status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info)
       +        if role == Qt.UserRole:
       +            # for sorting
       +            d = {
       +                # height breaks ties for unverified txns
       +                # txpos breaks ties for verified same block txns
       +                0: (status, conf, -height, -txpos),
       +                1: status_str,
       +                2: tx_item['label'],
       +                3: tx_item['value'].value,
       +                4: tx_item['balance'].value,
       +                5: tx_item['fiat_value'].value if 'fiat_value' in tx_item else None,
       +                6: tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None,
       +                7: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None,
       +                8: tx_hash,
       +            }
       +            return QVariant(d[col])
       +        if role not in (Qt.DisplayRole, Qt.EditRole):
       +            if col == 0 and role == Qt.DecorationRole:
       +                return QVariant(self.view.icon_cache.get(":icons/" +  TX_ICONS[status]))
       +            elif col == 0 and role == Qt.ToolTipRole:
       +                return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else "")))
       +            elif col > 2 and role == Qt.TextAlignmentRole:
       +                return QVariant(Qt.AlignRight | Qt.AlignVCenter)
       +            elif col != 1 and role == Qt.FontRole:
       +                monospace_font = QFont(MONOSPACE_FONT)
       +                return QVariant(monospace_font)
       +            elif col == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash):
       +                return QVariant(self.view.icon_cache.get(":icons/seal"))
       +            elif col in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0:
       +                red_brush = QBrush(QColor("#BC1E1E"))
       +                return QVariant(red_brush)
       +            elif col == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None:
       +                blue_brush = QBrush(QColor("#1E1EFF"))
       +                return QVariant(blue_brush)
       +            return None
       +        if col == 1:
       +            return QVariant(status_str)
       +        elif col == 2:
       +            return QVariant(tx_item['label'])
       +        elif col == 3:
       +            value = tx_item['value'].value
       +            v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True)
       +            return QVariant(v_str)
       +        elif col == 4:
       +            balance = tx_item['balance'].value
       +            balance_str = self.parent.format_amount(balance, whitespaces=True)
       +            return QVariant(balance_str)
       +        elif col == 5 and 'fiat_value' in tx_item:
       +            value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value)
       +            return QVariant(value_str)
       +        elif col == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item:
       +            # fixme: should use is_mine
       +            acq = tx_item['acquisition_price'].value
       +            return QVariant(self.parent.fx.format_fiat(acq))
       +        elif col == 7 and 'capital_gain' in tx_item:
       +            cg = tx_item['capital_gain'].value
       +            return QVariant(self.parent.fx.format_fiat(cg))
       +        elif col == 8:
       +            return QVariant(tx_hash)
       +        return None
       +
       +    def parent(self, index: QModelIndex):
       +        return QModelIndex()
       +
       +    def hasChildren(self, index: QModelIndex):
       +        return not index.isValid()
       +
       +    def update_label(self, row):
       +        tx_item = self.transactions.value_from_pos(row)
       +        tx_item['label'] = self.parent.wallet.get_label(tx_item['txid'])
       +        topLeft = bottomRight = self.createIndex(row, 2)
       +        self.dataChanged.emit(topLeft, bottomRight, [Qt.DisplayRole])
       +
       +    def get_domain(self):
       +        '''Overridden in address_dialog.py'''
       +        return self.parent.wallet.get_addresses()
       +
       +    @profiler
       +    def refresh(self, reason: str):
       +        self.print_error(f"refreshing... reason: {reason}")
       +        assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread'
       +        assert self.view, 'view not set'
       +        selected = self.view.selectionModel().currentIndex()
       +        selected_row = None
       +        if selected:
       +            selected_row = selected.row()
       +        fx = self.parent.fx
       +        if fx: fx.history_used_spot = False
       +        r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx)
       +        self.set_visibility_of_columns()
       +        if r['transactions'] == list(self.transactions.values()):
       +            return
       +        old_length = len(self.transactions)
       +        if old_length != 0:
       +            self.beginRemoveRows(QModelIndex(), 0, old_length)
       +            self.transactions.clear()
       +            self.endRemoveRows()
       +        self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1)
       +        for tx_item in r['transactions']:
       +            txid = tx_item['txid']
       +            self.transactions[txid] = tx_item
       +        self.endInsertRows()
       +        if selected_row:
       +            self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent)
       +        f = self.view.current_filter
       +        if f:
       +            self.view.filter(f)
       +        # update summary
       +        self.summary = r['summary']
       +        if not self.view.years and self.transactions:
       +            start_date = date.today()
       +            end_date = date.today()
       +            if len(self.transactions) > 0:
       +                start_date = self.transactions.value_from_pos(0).get('date') or start_date
       +                end_date = self.transactions.value_from_pos(len(self.transactions) - 1).get('date') or end_date
       +            self.view.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
       +            self.view.period_combo.insertItems(1, self.view.years)
       +        # update tx_status_cache
       +        self.tx_status_cache.clear()
       +        for txid, tx_item in self.transactions.items():
       +            tx_mined_info = self.tx_mined_info_from_tx_item(tx_item)
       +            self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info)
       +
       +    def set_visibility_of_columns(self):
       +        hide = self.view.hideColumn
       +        show = self.view.showColumn
       +        # txid
       +        hide(8)
       +        # fiat
       +        history = self.parent.fx.show_history()
       +        cap_gains = self.parent.fx.get_history_capital_gains_config()
       +        if history and cap_gains:
       +            show(5)
       +            show(6)
       +            show(7)
       +        elif history:
       +            show(5)
       +            hide(6)
       +            hide(7)
       +        else:
       +            hide(5)
       +            hide(6)
       +            hide(7)
       +
       +    def update_fiat(self, row, idx):
       +        tx_item = self.transactions.value_from_pos(row)
       +        key = tx_item['txid']
       +        fee = tx_item.get('fee')
       +        value = tx_item['value'].value
       +        fiat_fields = self.parent.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None)
       +        tx_item.update(fiat_fields)
       +        self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole])
       +
       +    def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
       +        self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info)
       +        try:
       +            row = self.transactions.pos_from_key(tx_hash)
       +        except KeyError:
       +            return
       +        topLeft = self.createIndex(row, 0)
       +        bottomRight = self.createIndex(row, self.NUM_COLUMNS-1)
       +        self.dataChanged.emit(topLeft, bottomRight)
       +
       +    def on_fee_histogram(self):
       +        for tx_hash, tx_item in self.transactions.items():
       +            tx_mined_info = self.tx_mined_info_from_tx_item(tx_item)
       +            if tx_mined_info.conf > 0:
       +                # note: we could actually break here if we wanted to rely on the order of txns in self.transactions
       +                continue
       +            self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info)
       +            self.update_tx_mined_status(tx_hash, tx_mined_info)
       +
       +    def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole):
       +        assert orientation == Qt.Horizontal
       +        if role != Qt.DisplayRole:
       +            return None
       +        fx = self.parent.fx
       +        fiat_title = 'n/a fiat value'
       +        fiat_acq_title = 'n/a fiat acquisition price'
       +        fiat_cg_title = 'n/a fiat capital gains'
       +        if fx and fx.show_history():
       +            fiat_title = '%s '%fx.ccy + _('Value')
       +            fiat_acq_title = '%s '%fx.ccy + _('Acquisition price')
       +            fiat_cg_title =  '%s '%fx.ccy + _('Capital Gains')
       +        return {
       +            0: '',
       +            1: _('Date'),
       +            2: _('Description'),
       +            3: _('Amount'),
       +            4: _('Balance'),
       +            5: fiat_title,
       +            6: fiat_acq_title,
       +            7: fiat_cg_title,
       +            8: 'TXID',
       +        }[section]
       +
       +    def flags(self, idx):
       +        extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag
       +        if idx.column() in self.view.editable_columns:
       +            extra_flags |= Qt.ItemIsEditable
       +        return super().flags(idx) | extra_flags
       +
       +    @staticmethod
       +    def tx_mined_info_from_tx_item(tx_item):
       +        tx_mined_info = TxMinedInfo(height=tx_item['height'],
       +                                    conf=tx_item['confirmations'],
       +                                    timestamp=tx_item['timestamp'])
       +        return tx_mined_info
        
        class HistoryList(MyTreeView, AcceptFileDragDrop):
       -    filter_columns = [1, 2, 3]  # Date, Description, Amount
       -    TX_HASH_ROLE = Qt.UserRole
       -    SORT_ROLE = Qt.UserRole + 1
       +    filter_columns = [1, 2, 3, 8]  # Date, Description, Amount, TXID
       +
       +    def tx_item_from_proxy_row(self, proxy_row):
       +        hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0))
       +        return self.hm.transactions.value_from_pos(hm_idx.row())
        
            def should_hide(self, proxy_row):
                if self.start_timestamp and self.end_timestamp:
       -            item = self.item_from_coordinate(proxy_row, 0)
       -            txid = item.data(self.TX_HASH_ROLE)
       -            date = self.transactions[txid]['date']
       +            tx_item = self.tx_item_from_proxy_row(proxy_row)
       +            date = tx_item['date']
                    if date:
                        in_interval = self.start_timestamp <= date <= self.end_timestamp
                        if not in_interval:
                            return True
                    return False
        
       -    def __init__(self, parent=None):
       +    def __init__(self, parent, model: HistoryModel):
                super().__init__(parent, self.create_menu, 2)
       -        self.std_model = QStandardItemModel(self)
       +        self.hm = model
                self.proxy = HistorySortModel(self)
       -        self.proxy.setSourceModel(self.std_model)
       +        self.proxy.setSourceModel(model)
                self.setModel(self.proxy)
        
       -        self.txid_to_items = {}
       -        self.transactions = OrderedDictWithIndex()
       -        self.summary = {}
       -        self.blue_brush = QBrush(QColor("#1E1EFF"))
       -        self.red_brush = QBrush(QColor("#BC1E1E"))
       -        self.monospace_font = QFont(MONOSPACE_FONT)
                self.config = parent.config
                AcceptFileDragDrop.__init__(self, ".txn")
                self.setSortingEnabled(True)
       t@@ -105,45 +351,17 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                self.years = []
                self.create_toolbar_buttons()
                self.wallet = self.parent.wallet  # type: Abstract_Wallet
       -        self.refresh_headers()
                self.sortByColumn(0, Qt.AscendingOrder)
       +        self.editable_columns |= {5}
       +
       +        self.header().setStretchLastSection(False)
       +        for col in range(HistoryModel.NUM_COLUMNS):
       +            sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
       +            self.header().setSectionResizeMode(col, sm)
        
            def format_date(self, d):
                return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
        
       -    def refresh_headers(self):
       -        headers = ['', _('Date'), _('Description'), _('Amount'), _('Balance')]
       -        fx = self.parent.fx
       -        if fx and fx.show_history():
       -            headers.extend(['%s '%fx.ccy + _('Value')])
       -            self.editable_columns |= {5}
       -            if fx.get_history_capital_gains_config():
       -                headers.extend(['%s '%fx.ccy + _('Acquisition price')])
       -                headers.extend(['%s '%fx.ccy + _('Capital Gains')])
       -        else:
       -            self.editable_columns -= {5}
       -        col_count = self.std_model.columnCount()
       -        diff = col_count-len(headers)
       -        if col_count > len(headers):
       -            if diff == 2:
       -                self.std_model.removeColumns(6, diff)
       -            else:
       -                assert diff in [1, 3]
       -                self.std_model.removeColumns(5, diff)
       -            for items in self.txid_to_items.values():
       -                while len(items) > col_count:
       -                    items.pop()
       -        elif col_count < len(headers):
       -            self.std_model.clear()
       -            self.txid_to_items.clear()
       -            self.transactions.clear()
       -            self.summary.clear()
       -        self.update_headers(headers, self.std_model)
       -
       -    def get_domain(self):
       -        '''Replaced in address_dialog.py'''
       -        return self.wallet.get_addresses()
       -
            def on_combo(self, x):
                s = self.period_combo.itemText(x)
                x = s == _('Custom')
       t@@ -216,7 +434,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                    return datetime.datetime(date.year, date.month, date.day)
        
            def show_summary(self):
       -        h = self.summary
       +        h = self.model().sourceModel().summary
                if not h:
                    self.parent.show_message(_("Nothing to summarize."))
                    return
       t@@ -261,171 +479,38 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                        _("Perhaps some dependencies are missing...") + " (matplotlib?)")
                    return
                try:
       -            plt = plot_history(list(self.transactions.values()))
       +            plt = plot_history(list(self.hm.transactions.values()))
                    plt.show()
                except NothingToPlotException as e:
                    self.parent.show_message(str(e))
        
       -    def insert_tx(self, tx_item):
       -        fx = self.parent.fx
       -        tx_hash = tx_item['txid']
       -        height = tx_item['height']
       -        conf = tx_item['confirmations']
       -        timestamp = tx_item['timestamp']
       -        value = tx_item['value'].value
       -        balance = tx_item['balance'].value
       -        label = tx_item['label']
       -        tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp)
       -        status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
       -        has_invoice = self.wallet.invoices.paid.get(tx_hash)
       -        v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True)
       -        balance_str = self.parent.format_amount(balance, whitespaces=True)
       -        entry = ['', status_str, label, v_str, balance_str]
       -        item = [QStandardItem(e) for e in entry]
       -        item[3].setData(value, self.SORT_ROLE)
       -        item[4].setData(balance, self.SORT_ROLE)
       -        if has_invoice:
       -            item[2].setIcon(self.icon_cache.get(":icons/seal"))
       -        for i in range(len(entry)):
       -            self.set_item_properties(item[i], i, tx_hash)
       -        if value and value < 0:
       -            item[2].setForeground(self.red_brush)
       -            item[3].setForeground(self.red_brush)
       -        self.txid_to_items[tx_hash] = item
       -        self.update_item(tx_hash, self.wallet.get_tx_height(tx_hash))
       -        source_row_idx = self.std_model.rowCount()
       -        self.std_model.insertRow(source_row_idx, item)
       -        new_idx = self.std_model.index(source_row_idx, 0)
       -        history = fx.show_history()
       -        if history:
       -            self.update_fiat(tx_hash, tx_item)
       -        self.hide_row(self.proxy.mapFromSource(new_idx).row())
       -
       -    def set_item_properties(self, item, i, tx_hash):
       -        if i>2:
       -            item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
       -        if i!=1:
       -            item.setFont(self.monospace_font)
       -        item.setEditable(i in self.editable_columns)
       -        item.setData(tx_hash, self.TX_HASH_ROLE)
       -
       -    def ensure_fields_available(self, items, idx, txid):
       -        while len(items) < idx + 1:
       -            row = self.transactions.get_pos_of_key(txid)
       -            qidx = self.std_model.index(row, len(items))
       -            assert qidx.isValid(), (self.std_model.columnCount(), idx)
       -            item = self.std_model.itemFromIndex(qidx)
       -            self.set_item_properties(item, len(items), txid)
       -            items.append(item)
       -
       -    @profiler
       -    def update(self):
       -        fx = self.parent.fx
       -        if fx: fx.history_used_spot = False
       -        r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx)
       -        seen = set()
       -        history = fx.show_history()
       -        tx_list = list(self.transactions.values())
       -        if r['transactions'] == tx_list:
       -            return
       -        if r['transactions'][:-1] == tx_list:
       -            print_error('history_list: one new transaction')
       -            row = r['transactions'][-1]
       -            txid = row['txid']
       -            if txid not in self.transactions:
       -                self.transactions[txid] = row
       -                self.insert_tx(row)
       -                return
       -            else:
       -                print_error('history_list: tx added but txid is already in list (weird), txid: ', txid)
       -        for idx, row in enumerate(r['transactions']):
       -            txid = row['txid']
       -            seen.add(txid)
       -            if txid not in self.transactions:
       -                self.transactions[txid] = row
       -                self.insert_tx(row)
       -                continue
       -            old = self.transactions[txid]
       -            if old == row:
       -                continue
       -            self.update_item(txid, self.wallet.get_tx_height(txid))
       -            if history:
       -                self.update_fiat(txid, row)
       -            balance_str = self.parent.format_amount(row['balance'].value, whitespaces=True)
       -            self.txid_to_items[txid][4].setText(balance_str)
       -            self.txid_to_items[txid][4].setData(row['balance'].value, self.SORT_ROLE)
       -            old.clear()
       -            old.update(**row)
       -        removed = 0
       -        l = list(enumerate(self.transactions.keys()))
       -        for idx, txid in l:
       -            if txid not in seen:
       -                del self.transactions[txid]
       -                del self.txid_to_items[txid]
       -                items = self.std_model.takeRow(idx - removed)
       -                removed_txid = items[0].data(self.TX_HASH_ROLE)
       -                assert removed_txid == txid, (idx, removed)
       -                removed += 1
       -        self.apply_filter()
       -        # update summary
       -        self.summary = r['summary']
       -        if not self.years and self.transactions:
       -            start_date = next(iter(self.transactions.values())).get('date') or date.today()
       -            end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today()
       -            self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
       -            self.period_combo.insertItems(1, self.years)
       -
       -    def update_fiat(self, txid, row):
       -        cap_gains = self.parent.fx.get_history_capital_gains_config()
       -        items = self.txid_to_items[txid]
       -        self.ensure_fields_available(items, 7 if cap_gains else 5, txid)
       -        if not row['fiat_default'] and row['fiat_value']:
       -            items[5].setForeground(self.blue_brush)
       -        value_str = self.parent.fx.format_fiat(row['fiat_value'].value)
       -        items[5].setText(value_str)
       -        items[5].setData(row['fiat_value'].value, self.SORT_ROLE)
       -        # fixme: should use is_mine
       -        if row['value'].value < 0 and cap_gains:
       -            acq = row['acquisition_price'].value
       -            items[6].setText(self.parent.fx.format_fiat(acq))
       -            items[6].setData(acq, self.SORT_ROLE)
       -            cg = row['capital_gain'].value
       -            items[7].setText(self.parent.fx.format_fiat(cg))
       -            items[7].setData(cg, self.SORT_ROLE)
       -
       -    def update_on_new_fee_histogram(self):
       -        pass
       -        # TODO update unconfirmed tx'es
       -
            def on_edited(self, index, user_role, text):
       +        index = self.model().mapToSource(index)
                row, column = index.row(), index.column()
       -        item = self.item_from_coordinate(row, column)
       -        key = item.data(self.TX_HASH_ROLE)
       +        tx_item = self.hm.transactions.value_from_pos(row)
       +        key = tx_item['txid']
                # fixme
                if column == 2:
       -            self.wallet.set_label(key, text)
       -            self.update_labels()
       -            self.parent.update_completions()
       +            if self.wallet.set_label(key, text): #changed
       +                self.hm.update_label(row)
       +                self.parent.update_completions()
                elif column == 5:
       -            tx_item = self.transactions[key]
                    self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value)
                    value = tx_item['value'].value
                    if value is not None:
       -                fee = tx_item['fee']
       -                fiat_fields = self.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None)
       -                tx_item.update(fiat_fields)
       -                self.update_fiat(key, tx_item)
       +                self.hm.update_fiat(row, index)
                else:
                    assert False
        
            def mouseDoubleClickEvent(self, event: QMouseEvent):
                idx = self.indexAt(event.pos())
       -        item = self.item_from_coordinate(idx.row(), idx.column())
       -        if not item or item.isEditable():
       +        if not idx.isValid():
       +            return
       +        tx_item = self.tx_item_from_proxy_row(idx.row())
       +        if self.hm.flags(self.model().mapToSource(idx)) & Qt.ItemIsEditable:
                    super().mouseDoubleClickEvent(event)
       -        elif item:
       -            tx_hash = item.data(self.TX_HASH_ROLE)
       -            self.show_transaction(tx_hash)
       +        else:
       +            self.show_transaction(tx_item['txid'])
        
            def show_transaction(self, tx_hash):
                tx = self.wallet.transactions.get(tx_hash)
       t@@ -434,45 +519,22 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                label = self.wallet.get_label(tx_hash) or None # prefer 'None' if not defined (force tx dialog to hide Description field if missing)
                self.parent.show_transaction(tx, label)
        
       -    def update_labels(self):
       -        root = self.std_model.invisibleRootItem()
       -        child_count = root.rowCount()
       -        for i in range(child_count):
       -            item = root.child(i, 2)
       -            txid = item.data(self.TX_HASH_ROLE)
       -            label = self.wallet.get_label(txid)
       -            item.setText(label)
       -
       -    def update_item(self, tx_hash, tx_mined_status):
       -        conf = tx_mined_status.conf
       -        status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
       -        icon = self.icon_cache.get(":icons/" +  TX_ICONS[status])
       -        if tx_hash not in self.txid_to_items:
       -            return
       -        items = self.txid_to_items[tx_hash]
       -        items[0].setIcon(icon)
       -        items[0].setToolTip(str(conf) + _(" confirmation" + ("s" if conf != 1 else "")))
       -        items[0].setData((status, conf), self.SORT_ROLE)
       -        items[1].setText(status_str)
       -
            def create_menu(self, position: QPoint):
                org_idx: QModelIndex = self.indexAt(position)
                idx = self.proxy.mapToSource(org_idx)
       -        item: QStandardItem = self.std_model.itemFromIndex(idx)
       -        if not item:
       +        if not idx.isValid():
                    # can happen e.g. before list is populated for the first time
                    return
       -        tx_hash = idx.data(self.TX_HASH_ROLE)
       +        tx_item = self.hm.transactions.value_from_pos(idx.row())
                column = idx.column()
       -        assert tx_hash, "create_menu: no tx hash"
       -        tx = self.wallet.transactions.get(tx_hash)
       -        assert tx, "create_menu: no tx"
                if column == 0:
                    column_title = _('Transaction ID')
       -            column_data = tx_hash
       +            column_data = tx_item['txid']
                else:
       -            column_title = self.std_model.horizontalHeaderItem(column).text()
       -            column_data = item.text()
       +            column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole)
       +            column_data = self.hm.data(idx, Qt.DisplayRole).value()
       +        tx_hash = tx_item['txid']
       +        tx = self.wallet.transactions[tx_hash]
                tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
                height = self.wallet.get_tx_height(tx_hash).height
                is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
       t@@ -483,7 +545,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                    menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
                menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
                for c in self.editable_columns:
       -            label = self.std_model.horizontalHeaderItem(c).text()
       +            label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole)
                    # TODO use siblingAtColumn when min Qt version is >=5.11
                    persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c))
                    menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p)))
       t@@ -589,3 +651,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
                    else:
                        from electrum.util import json_encode
                        f.write(json_encode(txns))
       +
       +    def text_txid_from_coordinate(self, row, col):
       +        idx = self.model().mapToSource(self.model().index(row, col))
       +        tx_item = self.hm.transactions.value_from_pos(idx.row())
       +        return self.hm.data(idx, Qt.DisplayRole).value(), tx_item['txid']
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -73,6 +73,7 @@ from .transaction_dialog import show_transaction
        from .fee_slider import FeeSlider
        from .util import *
        from .installwizard import WIF_HELP_TEXT
       +from .history_list import HistoryList, HistoryModel
        
        
        class StatusBarButton(QPushButton):
       t@@ -113,6 +114,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
        
                self.gui_object = gui_object
                self.config = config = gui_object.config  # type: SimpleConfig
       +        self.gui_thread = gui_object.gui_thread
        
                self.setup_exception_hook()
        
       t@@ -230,8 +232,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                Exception_Hook(self)
        
            def on_fx_history(self):
       -        self.history_list.refresh_headers()
       -        self.history_list.update()
       +        self.history_model.refresh('fx_history')
                self.address_list.update()
        
            def on_quotes(self, b):
       t@@ -246,7 +247,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                edit.textEdited.emit(edit.text())
                # History tab needs updating if it used spot
                if self.fx.history_used_spot:
       -            self.history_list.update()
       +            self.history_model.refresh('fx_quotes')
       +        self.address_list.update()
        
            def toggle_tab(self, tab):
                show = not self.config.get('show_{}_tab'.format(tab.tab_name), False)
       t@@ -345,7 +347,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                elif event == 'verified':
                    wallet, tx_hash, tx_mined_status = args
                    if wallet == self.wallet:
       -                self.history_list.update_item(tx_hash, tx_mined_status)
       +                self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
                elif event == 'fee':
                    if self.config.is_dynfee():
                        self.fee_slider.update()
       t@@ -354,7 +356,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    if self.config.is_dynfee():
                        self.fee_slider.update()
                        self.do_update_fee()
       -            self.history_list.update_on_new_fee_histogram()
       +            self.history_model.on_fee_histogram()
                else:
                    self.print_error("unexpected network_qt signal:", event, args)
        
       t@@ -799,7 +801,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    wallet = self.wallet
                if wallet != self.wallet:
                    return
       -        self.history_list.update()
       +        self.history_model.refresh('update_tabs')
                self.request_list.update()
                self.address_list.update()
                self.utxo_list.update()
       t@@ -808,8 +810,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                self.update_completions()
        
            def create_history_tab(self):
       -        from .history_list import HistoryList
       -        self.history_list = l = HistoryList(self)
       +        self.history_model = HistoryModel(self)
       +        self.history_list = l = HistoryList(self, self.history_model)
       +        self.history_model.set_view(self.history_list)
                l.searchable_list = l
                toolbar = l.create_toolbar(self.config)
                toolbar_shown = self.config.get('show_toolbar_history', False)
       t@@ -2664,7 +2667,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                b = self.fx and self.fx.is_enabled()
                self.fiat_send_e.setVisible(b)
                self.fiat_receive_e.setVisible(b)
       -        self.history_list.refresh_headers()
                self.history_list.update()
                self.address_list.refresh_headers()
                self.address_list.update()
       t@@ -3022,7 +3024,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    if not self.fx: return
                    self.fx.set_history_config(checked)
                    update_exchanges()
       -            self.history_list.refresh_headers()
       +            self.history_model.refresh('on_history')
                    if self.fx.is_enabled() and checked:
                        self.fx.trigger_update()
                    update_history_capgains_cb()
       t@@ -3030,7 +3032,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                def on_history_capgains(checked):
                    if not self.fx: return
                    self.fx.set_history_capital_gains_config(checked)
       -            self.history_list.refresh_headers()
       +            self.history_model.refresh('on_history_capgains')
        
                def on_fiat_address(checked):
                    if not self.fx: return
   DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
       t@@ -398,8 +398,26 @@ def filename_field(parent, config, defaultname, select_msg):
            return vbox, filename_e, b1
        
        class ElectrumItemDelegate(QStyledItemDelegate):
       -    def createEditor(self, parent, option, index):
       -        return self.parent().createEditor(parent, option, index)
       +    def __init__(self, tv):
       +        super().__init__(tv)
       +        self.tv = tv
       +        self.opened = None
       +        def on_closeEditor(editor: QLineEdit, hint):
       +            self.opened = None
       +        def on_commitData(editor: QLineEdit):
       +            new_text = editor.text()
       +            idx = QModelIndex(self.opened)
       +            row, col = idx.row(), idx.column()
       +            _prior_text, user_role = self.tv.text_txid_from_coordinate(row, col)
       +            # check that we didn't forget to set UserRole on an editable field
       +            assert user_role is not None, (row, col)
       +            self.tv.on_edited(idx, user_role, new_text)
       +        self.closeEditor.connect(on_closeEditor)
       +        self.commitData.connect(on_commitData)
       +
       +    def createEditor(self, parent, option, idx):
       +        self.opened = QPersistentModelIndex(idx)
       +        return super().createEditor(parent, option, idx)
        
        class MyTreeView(QTreeView):
        
       t@@ -415,8 +433,6 @@ class MyTreeView(QTreeView):
                self.icon_cache = IconCache()
        
                # Control which columns are editable
       -        self.editor = None
       -        self.pending_update = False
                if editable_columns is None:
                    editable_columns = {stretch_column}
                else:
       t@@ -428,6 +444,13 @@ class MyTreeView(QTreeView):
                self.setRootIsDecorated(False)  # remove left margin
                self.toolbar_shown = False
        
       +        # When figuring out the size of columns, Qt by default looks at
       +        # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents).
       +        # This would be REALLY SLOW, and it's not perfect anyway.
       +        # So to speed the UI up considerably, set it to
       +        # only look at as many rows as currently visible.
       +        self.header().setResizeContentsPrecision(0)
       +
            def set_editability(self, items):
                for idx, i in enumerate(items):
                    i.setEditable(idx in self.editable_columns)
       t@@ -449,9 +472,8 @@ class MyTreeView(QTreeView):
                    assert set_current.isValid()
                    self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent)
        
       -    def update_headers(self, headers, model=None):
       -        if model is None:
       -            model = self.model()
       +    def update_headers(self, headers):
       +        model = self.model()
                model.setHorizontalHeaderLabels(headers)
                self.header().setStretchLastSection(False)
                for col in range(len(headers)):
       t@@ -459,7 +481,9 @@ class MyTreeView(QTreeView):
                    self.header().setSectionResizeMode(col, sm)
        
            def keyPressEvent(self, event):
       -        if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
       +        if self.itemDelegate().opened:
       +            return
       +        if event.key() in [ Qt.Key_F2, Qt.Key_Return ]:
                    self.on_activated(self.selectionModel().currentIndex())
                    return
                super().keyPressEvent(event)
       t@@ -470,34 +494,6 @@ class MyTreeView(QTreeView):
                pt.setX(50)
                self.customContextMenuRequested.emit(pt)
        
       -    def createEditor(self, parent, option, idx):
       -        self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),
       -                                                       parent, option, idx)
       -        item = self.item_from_coordinate(idx.row(), idx.column())
       -        user_role = item.data(Qt.UserRole)
       -        assert user_role is not None
       -        prior_text = item.text()
       -        def editing_finished():
       -            # Long-time QT bug - pressing Enter to finish editing signals
       -            # editingFinished twice.  If the item changed the sequence is
       -            # Enter key:  editingFinished, on_change, editingFinished
       -            # Mouse: on_change, editingFinished
       -            # This mess is the cleanest way to ensure we make the
       -            # on_edited callback with the updated item
       -            if self.editor is None:
       -                return
       -            if self.editor.text() == prior_text:
       -                self.editor = None # Unchanged - ignore any 2nd call
       -                return
       -            if item.text() == prior_text:
       -                return # Buggy first call on Enter key, item not yet updated
       -            if not idx.isValid():
       -                return
       -            self.on_edited(idx, user_role, self.editor.text())
       -            self.editor = None
       -        self.editor.editingFinished.connect(editing_finished)
       -        return self.editor
       -
            def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None):
                """
                this is to prevent:
       t@@ -508,13 +504,9 @@ class MyTreeView(QTreeView):
        
            def on_edited(self, idx: QModelIndex, user_role, text):
                self.parent.wallet.set_label(user_role, text)
       -        self.parent.history_list.update_labels()
       +        self.parent.history_model.refresh('on_edited in MyTreeView')
                self.parent.update_completions()
        
       -    def apply_filter(self):
       -        if self.current_filter:
       -            self.filter(self.current_filter)
       -
            def should_hide(self, row):
                """
                row_num is for self.model(). So if there is a proxy, it is the row number
       t@@ -522,13 +514,12 @@ class MyTreeView(QTreeView):
                """
                return False
        
       -    def item_from_coordinate(self, row_num, column):
       -        if isinstance(self.model(), QSortFilterProxyModel):
       -            idx = self.model().mapToSource(self.model().index(row_num, column))
       -            return self.model().sourceModel().itemFromIndex(idx)
       -        else:
       -            idx = self.model().index(row_num, column)
       -            return self.model().itemFromIndex(idx)
       +    def text_txid_from_coordinate(self, row_num, column):
       +        assert not isinstance(self.model(), QSortFilterProxyModel)
       +        idx = self.model().index(row_num, column)
       +        item = self.model().itemFromIndex(idx)
       +        user_role = item.data(Qt.UserRole)
       +        return item.text(), user_role
        
            def hide_row(self, row_num):
                """
       t@@ -541,14 +532,14 @@ class MyTreeView(QTreeView):
                    self.setRowHidden(row_num, QModelIndex(), False)
                    return
                for column in self.filter_columns:
       -            item = self.item_from_coordinate(row_num, column)
       -            txt = item.text().lower()
       +            txt, _ = self.text_txid_from_coordinate(row_num, column)
       +            txt = txt.lower()
                    if self.current_filter in txt:
                        # the filter matched, but the date filter might apply
                        self.setRowHidden(row_num, QModelIndex(), bool(should_hide))
                        break
                else:
       -            # we did not find the filter in any columns, show the item
       +            # we did not find the filter in any columns, hide the item
                    self.setRowHidden(row_num, QModelIndex(), True)
        
            def filter(self, p):
   DIR diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
       t@@ -33,7 +33,7 @@ class UTXOList(MyTreeView):
            filter_columns = [0, 1]  # Address, Label
        
            def __init__(self, parent=None):
       -        super().__init__(parent, self.create_menu, 1)
       +        super().__init__(parent, self.create_menu, 1, editable_columns=[])
                self.setModel(QStandardItemModel(self))
                self.setSelectionMode(QAbstractItemView.ExtendedSelection)
                self.setSortingEnabled(True)
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -146,6 +146,12 @@ class Satoshis(object):
            def __str__(self):
                return format_satoshis(self.value) + " BTC"
        
       +    def __eq__(self, other):
       +        return self.value == other.value
       +
       +    def __ne__(self, other):
       +        return not (self == other)
       +
        
        # note: this is not a NamedTuple as then its json encoding cannot be customized
        class Fiat(object):
       t@@ -166,6 +172,12 @@ class Fiat(object):
                else:
                    return "{:.2f}".format(self.value) + ' ' + self.ccy
        
       +    def __eq__(self, other):
       +        return self.ccy == other.ccy and self.value == other.value
       +
       +    def __ne__(self, other):
       +        return not (self == other)
       +
        
        class MyEncoder(json.JSONEncoder):
            def default(self, obj):
       t@@ -995,47 +1007,57 @@ class OrderedDictWithIndex(OrderedDict):
            Note: very inefficient to modify contents, except to add new items.
            """
        
       -    _key_to_pos = {}
       +    def __init__(self):
       +        super().__init__()
       +        self._key_to_pos = {}
       +        self._pos_to_key = {}
        
       -    def _recalc_key_to_pos(self):
       +    def _recalc_index(self):
                self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())}
       +        self._pos_to_key = {pos: key for (pos, key) in enumerate(self.keys())}
        
       -    def get_pos_of_key(self, key):
       +    def pos_from_key(self, key):
                return self._key_to_pos[key]
        
       +    def value_from_pos(self, pos):
       +        key = self._pos_to_key[pos]
       +        return self[key]
       +
            def popitem(self, *args, **kwargs):
                ret = super().popitem(*args, **kwargs)
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def move_to_end(self, *args, **kwargs):
                ret = super().move_to_end(*args, **kwargs)
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def clear(self):
                ret = super().clear()
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def pop(self, *args, **kwargs):
                ret = super().pop(*args, **kwargs)
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def update(self, *args, **kwargs):
                ret = super().update(*args, **kwargs)
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def __delitem__(self, *args, **kwargs):
                ret = super().__delitem__(*args, **kwargs)
       -        self._recalc_key_to_pos()
       +        self._recalc_index()
                return ret
        
            def __setitem__(self, key, *args, **kwargs):
                is_new_key = key not in self
                ret = super().__setitem__(key, *args, **kwargs)
                if is_new_key:
       -            self._key_to_pos[key] = len(self) - 1
       +            pos = len(self) - 1
       +            self._key_to_pos[key] = pos
       +            self._pos_to_key[pos] = key
                return ret
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -45,7 +45,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
                           format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
                           WalletFileException, BitcoinException,
                           InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
       -                   Fiat, bfh, bh2u)
       +                   Fiat, bfh, bh2u, TxMinedInfo)
        from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
                              is_minikey, relayfee, dust_threshold)
        from .version import *
       t@@ -239,7 +239,7 @@ class Abstract_Wallet(AddressSynchronizer):
                        self.labels[name] = text
                        changed = True
                else:
       -            if old_text:
       +            if old_text is not None:
                        self.labels.pop(name)
                        changed = True
                if changed:
       t@@ -420,6 +420,7 @@ class Abstract_Wallet(AddressSynchronizer):
                        'balance': Satoshis(balance),
                        'date': timestamp_to_datetime(timestamp),
                        'label': self.get_label(tx_hash),
       +                'txpos_in_block': tx_mined_status.txpos,
                    }
                    tx_fee = None
                    if show_fees:
       t@@ -522,11 +523,11 @@ class Abstract_Wallet(AddressSynchronizer):
                    return ', '.join(labels)
                return ''
        
       -    def get_tx_status(self, tx_hash, tx_mined_status):
       +    def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo):
                extra = []
       -        height = tx_mined_status.height
       -        conf = tx_mined_status.conf
       -        timestamp = tx_mined_status.timestamp
       +        height = tx_mined_info.height
       +        conf = tx_mined_info.conf
       +        timestamp = tx_mined_info.timestamp
                if conf == 0:
                    tx = self.transactions.get(tx_hash)
                    if not tx:
       t@@ -553,7 +554,7 @@ class Abstract_Wallet(AddressSynchronizer):
                    elif height == TX_HEIGHT_UNCONFIRMED:
                        status = 0
                    else:
       -                status = 2
       +                status = 2  # not SPV verified
                else:
                    status = 3 + min(conf, 6)
                time_str = format_time(timestamp) if timestamp else _("unknown")