From 4eb4b341db58f127d4ba1eb1930b7d067c60fe85 Mon Sep 17 00:00:00 2001 From: Janus Date: Wed, 5 Dec 2018 20:57:21 +0100 Subject: [PATCH 01/20] QAbstractItemModel: initial version, filter not done --- electrum/gui/qt/address_dialog.py | 18 +- electrum/gui/qt/history_list.py | 471 +++++++++++++++--------------- electrum/gui/qt/main_window.py | 20 +- electrum/gui/qt/util.py | 39 ++- 4 files changed, 277 insertions(+), 271 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index 7b152b282..a54774a7a 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -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): @@ -80,16 +87,13 @@ 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) 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 diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b60f29d5f..1a2fb30af 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -27,11 +27,10 @@ import webbrowser import datetime from datetime import date from typing import TYPE_CHECKING -from collections import OrderedDict 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 from .util import * @@ -60,43 +59,227 @@ 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() + +# requires PyQt5 5.11 +indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) + +class HistoryModel(QAbstractItemModel): + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.transactions = [] + + def columnCount(self, parent: QModelIndex): + return 8 + + def rowCount(self, parent: QModelIndex): + l = len(self.transactions) + return l + + def index(self, row: int, column: int, parent : QModelIndex): + return self.createIndex(row,column) + + def data(self, index: QModelIndex, role: Qt.ItemDataRole): + assert self.checkIndex(index, indexIsValid) + assert index.isValid() + tx_item = self.transactions[index.row()] + tx_hash = tx_item['txid'] + height = tx_item['height'] + conf = tx_item['confirmations'] + timestamp = tx_item['timestamp'] + tx_mined_info = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + if role == Qt.UserRole: + # for sorting + d = { + 0: (status, conf), + 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, + } + return QVariant(d[index.column()]) + if role not in (Qt.DisplayRole, Qt.EditRole): + if index.column() == 0 and role == Qt.DecorationRole: + return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) + elif index.column() == 0 and role == Qt.ToolTipRole: + return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) + elif index.column() > 2 and role == Qt.TextAlignmentRole: + return QVariant(Qt.AlignRight | Qt.AlignVCenter) + elif index.column() != 1 and role == Qt.FontRole: + monospace_font = QFont(MONOSPACE_FONT) + return QVariant(monospace_font) + elif index.column() == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): + return QVariant(self.parent.history_list.icon_cache.get(":icons/seal")) + elif index.column() in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: + red_brush = QBrush(QColor("#BC1E1E")) + return QVariant(red_brush) + elif index.column() == 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 index.column() == 1: + return QVariant(status_str) + elif index.column() == 2: + return QVariant(tx_item['label']) + elif index.column() == 3: + value = tx_item['value'].value + v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) + return QVariant(v_str) + elif index.column() == 4: + balance = tx_item['balance'].value + balance_str = self.parent.format_amount(balance, whitespaces=True) + return QVariant(balance_str) + elif index.column() == 5 and 'fiat_value' in tx_item: + value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value) + return QVariant(value_str) + elif index.column() == 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 index.column() == 7 and 'capital_gain' in tx_item: + cg = tx_item['capital_gain'].value + return QVariant(self.parent.fx.format_fiat(cg)) + 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[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() + + def refresh(self, reason: str): + 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) + if r['transactions'] == self.transactions: + 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) + self.transactions = r['transactions'] + self.endInsertRows() + f = self.parent.history_list.current_filter + if f: + self.parent.history_list.filter(f) + # update summary + self.summary = r['summary'] + if not self.parent.history_list.years and self.transactions: + start_date = date.today() + end_date = date.today() + if len(self.transactions) > 0: + start_date = self.transactions[0].get('date') or start_date + end_date = self.transactions[-1].get('date') or end_date + self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + + history = self.parent.fx.show_history() + cap_gains = self.parent.fx.get_history_capital_gains_config() + hide = self.parent.history_list.hideColumn + show = self.parent.history_list.showColumn + 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[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_item(self, *args): + self.refresh('update_item') + + 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') + if fx.get_history_capital_gains_config(): + 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, + }[section] + + def flags(self, idx): + extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag + if idx.column() in self.parent.history_list.editable_columns: + extra_flags |= Qt.ItemIsEditable + return super().flags(idx) | extra_flags class HistoryList(MyTreeView, AcceptFileDragDrop): filter_columns = [1, 2, 3] # Date, Description, Amount - TX_HASH_ROLE = Qt.UserRole - SORT_ROLE = Qt.UserRole + 1 + + def tx_item_from_proxy_row(self, proxy_row): + hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) + return self.hm.transactions[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): 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) @@ -105,44 +288,19 @@ 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(8): + 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 update_headers(self, headers): + raise NotImplementedError def on_combo(self, x): s = self.period_combo.itemText(x) @@ -266,166 +424,33 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): 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): + print("on_edited") 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[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, self.model().mapToSource(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) @@ -434,45 +459,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[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 = str(self.hm.data(idx, Qt.DisplayRole)) + 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) @@ -483,7 +485,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))) @@ -589,3 +591,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[idx.row()] + return str(self.hm.data(idx, Qt.DisplayRole)), tx_item['txid'] diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d34f6f95a..2037ddc6b 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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): @@ -230,8 +231,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): @@ -246,7 +246,7 @@ 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') def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) @@ -345,7 +345,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_item(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): self.fee_slider.update() @@ -354,7 +354,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.refresh('fee_histogram') else: self.print_error("unexpected network_qt signal:", event, args) @@ -799,7 +799,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() @@ -808,8 +808,8 @@ 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) l.searchable_list = l toolbar = l.create_toolbar(self.config) toolbar_shown = self.config.get('show_toolbar_history', False) @@ -3022,7 +3022,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() @@ -3030,7 +3030,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 diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 3329168b8..7fba7ce67 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -449,9 +449,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)): @@ -473,11 +472,9 @@ class MyTreeView(QTreeView): 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() + prior_text, user_role = self.text_txid_from_coordinate(idx.row(), idx.column()) def editing_finished(): + print("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 @@ -487,12 +484,16 @@ class MyTreeView(QTreeView): if self.editor is None: return if self.editor.text() == prior_text: + print("unchanged ignore any 2nd call") 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(): + print("idx not valid") return + new_text, _ = self.text_txid_from_coordinate(idx.row(), idx.column()) + if new_text == prior_text: + print("buggy first call", new_text, prior_text) + return # Buggy first call on Enter key, item not yet updated self.on_edited(idx, user_role, self.editor.text()) self.editor = None self.editor.editingFinished.connect(editing_finished) @@ -511,10 +512,6 @@ class MyTreeView(QTreeView): self.parent.history_list.update_labels() 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 @@ -522,13 +519,11 @@ 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) + return item.text(), item.data(Qt.UserRole) def hide_row(self, row_num): """ @@ -541,8 +536,8 @@ 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)) From 3960070a509fad40ddf3f76b1995efe2a0d8eca0 Mon Sep 17 00:00:00 2001 From: Janus Date: Thu, 6 Dec 2018 20:22:38 +0100 Subject: [PATCH 02/20] QAbstractItemModel: fix sorting, QAbstractItemDelegate usage, QVariant usage --- electrum/gui/qt/contact_list.py | 8 ++--- electrum/gui/qt/history_list.py | 27 +++++++------- electrum/gui/qt/util.py | 62 +++++++++++++-------------------- electrum/gui/qt/utxo_list.py | 2 +- electrum/wallet.py | 2 +- 5 files changed, 44 insertions(+), 57 deletions(-) diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index e1915b1b6..5c167f3ec 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -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) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 1a2fb30af..b50aaa7d9 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -67,9 +67,6 @@ class HistorySortModel(QSortFilterProxyModel): return False return item1.value() < item2.value() -# requires PyQt5 5.11 -indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) - class HistoryModel(QAbstractItemModel): def __init__(self, parent): super().__init__(parent) @@ -80,14 +77,15 @@ class HistoryModel(QAbstractItemModel): return 8 def rowCount(self, parent: QModelIndex): - l = len(self.transactions) - return l + 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): - assert self.checkIndex(index, indexIsValid) + # requires PyQt5 5.11 + # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) + # assert self.checkIndex(index, indexIsValid) assert index.isValid() tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] @@ -169,6 +167,10 @@ class HistoryModel(QAbstractItemModel): return self.parent.wallet.get_addresses() def refresh(self, reason: str): + selected = self.parent.history_list.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) @@ -182,6 +184,8 @@ class HistoryModel(QAbstractItemModel): self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) self.transactions = r['transactions'] self.endInsertRows() + if selected_row: + self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) f = self.parent.history_list.current_filter if f: self.parent.history_list.filter(f) @@ -279,7 +283,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.proxy.setSourceModel(model) self.setModel(self.proxy) - self.summary = {} self.config = parent.config AcceptFileDragDrop.__init__(self, ".txn") self.setSortingEnabled(True) @@ -374,7 +377,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 @@ -425,7 +428,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.parent.show_message(str(e)) def on_edited(self, index, user_role, text): - print("on_edited") + index = self.model().mapToSource(index) row, column = index.row(), index.column() tx_item = self.hm.transactions[row] key = tx_item['txid'] @@ -438,7 +441,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): 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: - self.hm.update_fiat(row, self.model().mapToSource(index)) + self.hm.update_fiat(row, index) else: assert False @@ -472,7 +475,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): column_data = tx_item['txid'] else: column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) - column_data = str(self.hm.data(idx, 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) @@ -595,4 +598,4 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def text_txid_from_coordinate(self, row, col): idx = self.model().mapToSource(self.model().index(row, col)) tx_item = self.hm.transactions[idx.row()] - return str(self.hm.data(idx, Qt.DisplayRole)), tx_item['txid'] + return self.hm.data(idx, Qt.DisplayRole).value(), tx_item['txid'] diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 7fba7ce67..c88b9e333 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -398,8 +398,23 @@ 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) + _prior_text, user_role = self.tv.text_txid_from_coordinate(idx.row(), idx.column()) + 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): @@ -415,8 +430,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: @@ -458,7 +471,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) @@ -469,36 +484,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) - prior_text, user_role = self.text_txid_from_coordinate(idx.row(), idx.column()) - def editing_finished(): - print("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: - print("unchanged ignore any 2nd call") - self.editor = None # Unchanged - ignore any 2nd call - return - if not idx.isValid(): - print("idx not valid") - return - new_text, _ = self.text_txid_from_coordinate(idx.row(), idx.column()) - if new_text == prior_text: - print("buggy first call", new_text, prior_text) - return # Buggy first call on Enter key, item not yet updated - 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: @@ -509,7 +494,7 @@ 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 should_hide(self, row): @@ -523,7 +508,10 @@ class MyTreeView(QTreeView): assert not isinstance(self.model(), QSortFilterProxyModel) idx = self.model().index(row_num, column) item = self.model().itemFromIndex(idx) - return item.text(), item.data(Qt.UserRole) + user_role = item.data(Qt.UserRole) + # check that we didn't forget to set UserRole on an editable field + assert user_role is not None, (row_num, column) + return item.text(), user_role def hide_row(self, row_num): """ diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 0b9d85508..046f30fc4 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -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) diff --git a/electrum/wallet.py b/electrum/wallet.py index a32888cf2..746f25cac 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -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: From d2ddb255eff8590e3e1d504612666327bb2fa3f7 Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 7 Dec 2018 15:12:04 +0100 Subject: [PATCH 03/20] QAbstractItemModel: Release Notes and Address List fiat bug fix --- RELEASE-NOTES | 11 +++++++++++ electrum/gui/qt/main_window.py | 1 + 2 files changed, 12 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index bf831f913..932711f9b 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -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. diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2037ddc6b..afb6c67de 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -247,6 +247,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): # History tab needs updating if it used spot if self.fx.history_used_spot: 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) From 1c0c21159b33cf4ed3276e61a21e3e7ed3246cf5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 20:23:35 +0100 Subject: [PATCH 04/20] qt history list: performance optimisations --- electrum/gui/qt/history_list.py | 58 ++++++++++++++++++++------------- electrum/gui/qt/main_window.py | 2 +- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b50aaa7d9..6ea8054c3 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -26,7 +26,7 @@ import webbrowser import datetime from datetime import date -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple, Dict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ @@ -72,6 +72,7 @@ class HistoryModel(QAbstractItemModel): super().__init__(parent) self.parent = parent self.transactions = [] + self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] def columnCount(self, parent: QModelIndex): return 8 @@ -79,25 +80,27 @@ class HistoryModel(QAbstractItemModel): def rowCount(self, parent: QModelIndex): return len(self.transactions) - def index(self, row: int, column: int, parent : QModelIndex): - return self.createIndex(row,column) + def index(self, row: int, column: int, parent: QModelIndex): + return self.createIndex(row, column) def data(self, index: QModelIndex, role: Qt.ItemDataRole): # requires PyQt5 5.11 # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) # assert self.checkIndex(index, indexIsValid) assert index.isValid() + col = index.column() tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] - height = tx_item['height'] conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - tx_mined_info = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) - status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + try: + status, status_str = self.tx_status_cache[tx_hash] + except KeyError: + tx_mined_status = TxMinedInfo(height=tx_item['height'], conf=conf, timestamp=tx_item['timestamp']) + status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_status) if role == Qt.UserRole: # for sorting d = { - 0: (status, conf), + 0: (status, conf), # FIXME tx_pos needed as tie-breaker 1: status_str, 2: tx_item['label'], 3: tx_item['value'].value, @@ -106,46 +109,46 @@ class HistoryModel(QAbstractItemModel): 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, } - return QVariant(d[index.column()]) + return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): - if index.column() == 0 and role == Qt.DecorationRole: + if col == 0 and role == Qt.DecorationRole: return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) - elif index.column() == 0 and role == Qt.ToolTipRole: + elif col == 0 and role == Qt.ToolTipRole: return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) - elif index.column() > 2 and role == Qt.TextAlignmentRole: + elif col > 2 and role == Qt.TextAlignmentRole: return QVariant(Qt.AlignRight | Qt.AlignVCenter) - elif index.column() != 1 and role == Qt.FontRole: + elif col != 1 and role == Qt.FontRole: monospace_font = QFont(MONOSPACE_FONT) return QVariant(monospace_font) - elif index.column() == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): + elif col == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): return QVariant(self.parent.history_list.icon_cache.get(":icons/seal")) - elif index.column() in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: + 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 index.column() == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: + 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 index.column() == 1: + if col == 1: return QVariant(status_str) - elif index.column() == 2: + elif col == 2: return QVariant(tx_item['label']) - elif index.column() == 3: + 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 index.column() == 4: + elif col == 4: balance = tx_item['balance'].value balance_str = self.parent.format_amount(balance, whitespaces=True) return QVariant(balance_str) - elif index.column() == 5 and 'fiat_value' in tx_item: + 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 index.column() == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item: + 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 index.column() == 7 and 'capital_gain' in tx_item: + elif col == 7 and 'capital_gain' in tx_item: cg = tx_item['capital_gain'].value return QVariant(self.parent.fx.format_fiat(cg)) return None @@ -199,6 +202,15 @@ class HistoryModel(QAbstractItemModel): end_date = self.transactions[-1].get('date') or end_date self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + # update tx_status_cache + self.tx_status_cache.clear() + for tx_item in self.transactions: + txid = tx_item['txid'] + height = tx_item['height'] + conf = tx_item['confirmations'] + timestamp = tx_item['timestamp'] + tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index afb6c67de..087e14e8a 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -345,7 +345,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.console.showMessage(args[0]) elif event == 'verified': wallet, tx_hash, tx_mined_status = args - if wallet == self.wallet: + if wallet == self.wallet and wallet.up_to_date: self.history_model.update_item(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): From e023d8abddb36e843b240562bb1cdf8bfb43ef6c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 22:11:18 +0100 Subject: [PATCH 05/20] qt history list: sorting of first column now considers txpos same block txns were in unnatural order, maybe sort is not stable? --- electrum/gui/qt/history_list.py | 10 +++++----- electrum/wallet.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 6ea8054c3..f22b7f6ae 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -92,6 +92,7 @@ class HistoryModel(QAbstractItemModel): tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] conf = tx_item['confirmations'] + txpos = tx_item['txpos_in_block'] or 0 try: status, status_str = self.tx_status_cache[tx_hash] except KeyError: @@ -100,7 +101,7 @@ class HistoryModel(QAbstractItemModel): if role == Qt.UserRole: # for sorting d = { - 0: (status, conf), # FIXME tx_pos needed as tie-breaker + 0: (status, conf, -txpos), 1: status_str, 2: tx_item['label'], 3: tx_item['value'].value, @@ -206,10 +207,9 @@ class HistoryModel(QAbstractItemModel): self.tx_status_cache.clear() for tx_item in self.transactions: txid = tx_item['txid'] - height = tx_item['height'] - conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + tx_mined_status = TxMinedInfo(height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp']) self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) history = self.parent.fx.show_history() diff --git a/electrum/wallet.py b/electrum/wallet.py index 746f25cac..64cbb80b9 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -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: From 48e119b59e05be918b88267222b820214ddba846 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:07:46 +0100 Subject: [PATCH 06/20] qt history: minor clean-up and sanity checking --- electrum/daemon.py | 2 +- electrum/gui/qt/__init__.py | 1 + electrum/gui/qt/history_list.py | 11 ++++++++--- electrum/gui/qt/main_window.py | 1 + electrum/wallet.py | 12 ++++++------ 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index ad1c2a854..0db918c4b 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -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: diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 83f21cc25..885684ea1 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -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 diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index f22b7f6ae..aefd4e2cf 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -27,10 +27,12 @@ import webbrowser import datetime from datetime import date 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 +from electrum.util import (block_explorer_URL, profiler, print_error, TxMinedInfo, + PrintError) from .util import * @@ -67,7 +69,8 @@ class HistorySortModel(QSortFilterProxyModel): return False return item1.value() < item2.value() -class HistoryModel(QAbstractItemModel): +class HistoryModel(QAbstractItemModel, PrintError): + def __init__(self, parent): super().__init__(parent) self.parent = parent @@ -171,6 +174,8 @@ class HistoryModel(QAbstractItemModel): return self.parent.wallet.get_addresses() 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' selected = self.parent.history_list.selectionModel().currentIndex() selected_row = None if selected: @@ -288,7 +293,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): return True return False - def __init__(self, parent, model): + def __init__(self, parent, model: HistoryModel): super().__init__(parent, self.create_menu, 2) self.hm = model self.proxy = HistorySortModel(self) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 087e14e8a..f5a7b4207 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -114,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() diff --git a/electrum/wallet.py b/electrum/wallet.py index 64cbb80b9..0d58f44d3 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -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 * @@ -523,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: @@ -554,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") From 5e61ad09c195980212e01ead716a3e8609c3d938 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:09:38 +0100 Subject: [PATCH 07/20] qt addresses list: fix filtering --- electrum/gui/qt/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index c88b9e333..0b1043c92 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -407,7 +407,10 @@ class ElectrumItemDelegate(QStyledItemDelegate): def on_commitData(editor: QLineEdit): new_text = editor.text() idx = QModelIndex(self.opened) - _prior_text, user_role = self.tv.text_txid_from_coordinate(idx.row(), idx.column()) + 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) @@ -509,8 +512,6 @@ class MyTreeView(QTreeView): idx = self.model().index(row_num, column) item = self.model().itemFromIndex(idx) user_role = item.data(Qt.UserRole) - # check that we didn't forget to set UserRole on an editable field - assert user_role is not None, (row_num, column) return item.text(), user_role def hide_row(self, row_num): From 3c3fac7ca41396d25f6fe73e317f1af5158ea3d3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:12:07 +0100 Subject: [PATCH 08/20] qt history list: fix shortcut in refresh() --- electrum/util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index f770c67cc..fd2bb4721 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -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): @@ -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): From 8bb930dd041a18073f7274665adb9582e93399b9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:17:05 +0100 Subject: [PATCH 09/20] fix OrderedDictWithIndex setitem() would modify the dict of the class. oops. --- electrum/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index fd2bb4721..d2ca64a0f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1007,7 +1007,9 @@ 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 = {} def _recalc_key_to_pos(self): self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())} From 65e8eef87fdc0ae47fadca8b1def4607e73c0e73 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:22:53 +0100 Subject: [PATCH 10/20] qt history list: use OrderedDictWithIndex for txns --- electrum/gui/qt/history_list.py | 54 +++++++++++++++++++-------------- electrum/util.py | 26 ++++++++++------ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index aefd4e2cf..a05e9f31e 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -32,7 +32,7 @@ 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, - PrintError) + OrderedDictWithIndex, PrintError) from .util import * @@ -71,14 +71,16 @@ class HistorySortModel(QSortFilterProxyModel): class HistoryModel(QAbstractItemModel, PrintError): + NUM_COLUMNS = 8 + def __init__(self, parent): super().__init__(parent) self.parent = parent - self.transactions = [] + self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] def columnCount(self, parent: QModelIndex): - return 8 + return self.NUM_COLUMNS def rowCount(self, parent: QModelIndex): return len(self.transactions) @@ -92,15 +94,15 @@ class HistoryModel(QAbstractItemModel, PrintError): # assert self.checkIndex(index, indexIsValid) assert index.isValid() col = index.column() - tx_item = self.transactions[index.row()] + 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 try: status, status_str = self.tx_status_cache[tx_hash] except KeyError: - tx_mined_status = TxMinedInfo(height=tx_item['height'], conf=conf, timestamp=tx_item['timestamp']) - status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_status) + 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 = { @@ -164,7 +166,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return not index.isValid() def update_label(self, row): - tx_item = self.transactions[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]) @@ -183,7 +185,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 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) - if r['transactions'] == self.transactions: + if r['transactions'] == list(self.transactions.values()): return old_length = len(self.transactions) if old_length != 0: @@ -191,7 +193,9 @@ class HistoryModel(QAbstractItemModel, PrintError): self.transactions.clear() self.endRemoveRows() self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) - self.transactions = r['transactions'] + for tx_item in r['transactions']: + txid = tx_item['txid'] + self.transactions[txid] = tx_item self.endInsertRows() if selected_row: self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) @@ -204,18 +208,15 @@ class HistoryModel(QAbstractItemModel, PrintError): start_date = date.today() end_date = date.today() if len(self.transactions) > 0: - start_date = self.transactions[0].get('date') or start_date - end_date = self.transactions[-1].get('date') or end_date + 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.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) # update tx_status_cache self.tx_status_cache.clear() - for tx_item in self.transactions: - txid = tx_item['txid'] - tx_mined_status = TxMinedInfo(height=tx_item['height'], - conf=tx_item['confirmations'], - timestamp=tx_item['timestamp']) - self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) + 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) history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() @@ -235,7 +236,7 @@ class HistoryModel(QAbstractItemModel, PrintError): hide(7) def update_fiat(self, row, idx): - tx_item = self.transactions[row] + tx_item = self.transactions.value_from_pos(row) key = tx_item['txid'] fee = tx_item.get('fee') value = tx_item['value'].value @@ -276,12 +277,19 @@ class HistoryModel(QAbstractItemModel, PrintError): 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 def tx_item_from_proxy_row(self, proxy_row): hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) - return self.hm.transactions[hm_idx.row()] + return self.hm.transactions.value_from_pos(hm_idx.row()) def should_hide(self, proxy_row): if self.start_timestamp and self.end_timestamp: @@ -439,7 +447,7 @@ 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)) @@ -447,7 +455,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def on_edited(self, index, user_role, text): index = self.model().mapToSource(index) row, column = index.row(), index.column() - tx_item = self.hm.transactions[row] + tx_item = self.hm.transactions.value_from_pos(row) key = tx_item['txid'] # fixme if column == 2: @@ -485,7 +493,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): if not idx.isValid(): # can happen e.g. before list is populated for the first time return - tx_item = self.hm.transactions[idx.row()] + tx_item = self.hm.transactions.value_from_pos(idx.row()) column = idx.column() if column == 0: column_title = _('Transaction ID') @@ -614,5 +622,5 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def text_txid_from_coordinate(self, row, col): idx = self.model().mapToSource(self.model().index(row, col)) - tx_item = self.hm.transactions[idx.row()] + tx_item = self.hm.transactions.value_from_pos(idx.row()) return self.hm.data(idx, Qt.DisplayRole).value(), tx_item['txid'] diff --git a/electrum/util.py b/electrum/util.py index d2ca64a0f..f158d9d0f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1010,46 +1010,54 @@ class OrderedDictWithIndex(OrderedDict): 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 From b1e15751d6c0a70759c273ee1c28d2b41fa88bad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:24:49 +0100 Subject: [PATCH 11/20] qt history list: "status"-based sort should also tie-break on height --- electrum/gui/qt/history_list.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index a05e9f31e..4522bc092 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -98,6 +98,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 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: @@ -106,7 +107,9 @@ class HistoryModel(QAbstractItemModel, PrintError): if role == Qt.UserRole: # for sorting d = { - 0: (status, conf, -txpos), + # 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, From 696db310a573de9f1512ba0af44e2a5e571eee47 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:27:44 +0100 Subject: [PATCH 12/20] qt history list: optimise update_item (tx mined status) --- electrum/gui/qt/history_list.py | 11 +++++++++-- electrum/gui/qt/main_window.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 4522bc092..87a89e069 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -247,8 +247,15 @@ class HistoryModel(QAbstractItemModel, PrintError): tx_item.update(fiat_fields) self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole]) - def update_item(self, *args): - self.refresh('update_item') + 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 headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole): assert orientation == Qt.Horizontal diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f5a7b4207..71cde538e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -346,8 +346,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.console.showMessage(args[0]) elif event == 'verified': wallet, tx_hash, tx_mined_status = args - if wallet == self.wallet and wallet.up_to_date: - self.history_model.update_item(tx_hash, tx_mined_status) + if wallet == self.wallet: + self.history_model.update_tx_mined_status(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): self.fee_slider.update() From a99b92f613b40a1e65c6ea461039a8089911121f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:29:09 +0100 Subject: [PATCH 13/20] qt history list: optimise fee histogram induced refresh --- electrum/gui/qt/history_list.py | 9 +++++++++ electrum/gui/qt/main_window.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 87a89e069..6be2f8cc0 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -257,6 +257,15 @@ class HistoryModel(QAbstractItemModel, PrintError): 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: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 71cde538e..1a6d5be2a 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -356,7 +356,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.config.is_dynfee(): self.fee_slider.update() self.do_update_fee() - self.history_model.refresh('fee_histogram') + self.history_model.on_fee_histogram() else: self.print_error("unexpected network_qt signal:", event, args) From 0d755b86ab0141f753296294174a5c9cc794deab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:21:19 +0100 Subject: [PATCH 14/20] qt address dialog: HistoryModel needs reference to correct HistoryList refresh() was hiding/showing the headers of the main HistoryList --- electrum/gui/qt/address_dialog.py | 1 + electrum/gui/qt/history_list.py | 31 ++++++++++++++++--------------- electrum/gui/qt/main_window.py | 1 + 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index a54774a7a..d039ec42c 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -89,6 +89,7 @@ class AddressDialog(WindowModalDialog): vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.parent, self.address) self.hw = HistoryList(self.parent, addr_hist_model) + addr_hist_model.view = self.hw vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 6be2f8cc0..d303caa90 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -76,6 +76,7 @@ class HistoryModel(QAbstractItemModel, PrintError): def __init__(self, parent): super().__init__(parent) self.parent = parent + self.view = None # type: HistoryList # set by caller! FIXME... self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] @@ -121,7 +122,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): if col == 0 and role == Qt.DecorationRole: - return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) + 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: @@ -130,7 +131,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 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.parent.history_list.icon_cache.get(":icons/seal")) + 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) @@ -181,7 +182,8 @@ class HistoryModel(QAbstractItemModel, PrintError): 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' - selected = self.parent.history_list.selectionModel().currentIndex() + assert self.view, 'view not set' + selected = self.view.selectionModel().currentIndex() selected_row = None if selected: selected_row = selected.row() @@ -201,20 +203,20 @@ class HistoryModel(QAbstractItemModel, PrintError): self.transactions[txid] = tx_item self.endInsertRows() if selected_row: - self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) - f = self.parent.history_list.current_filter + self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) + f = self.view.current_filter if f: - self.parent.history_list.filter(f) + self.view.filter(f) # update summary self.summary = r['summary'] - if not self.parent.history_list.years and self.transactions: + 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.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] - self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + 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(): @@ -223,8 +225,8 @@ class HistoryModel(QAbstractItemModel, PrintError): history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() - hide = self.parent.history_list.hideColumn - show = self.parent.history_list.showColumn + hide = self.view.hideColumn + show = self.view.showColumn if history and cap_gains: show(5) show(6) @@ -276,9 +278,8 @@ class HistoryModel(QAbstractItemModel, PrintError): fiat_cg_title = 'n/a fiat capital gains' if fx and fx.show_history(): fiat_title = '%s '%fx.ccy + _('Value') - if fx.get_history_capital_gains_config(): - fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') - fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') + fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') + fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') return { 0: '', 1: _('Date'), @@ -292,7 +293,7 @@ class HistoryModel(QAbstractItemModel, PrintError): def flags(self, idx): extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag - if idx.column() in self.parent.history_list.editable_columns: + if idx.column() in self.view.editable_columns: extra_flags |= Qt.ItemIsEditable return super().flags(idx) | extra_flags diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 1a6d5be2a..c33429b61 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -812,6 +812,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def create_history_tab(self): self.history_model = HistoryModel(self) self.history_list = l = HistoryList(self, self.history_model) + self.history_model.view = self.history_list l.searchable_list = l toolbar = l.create_toolbar(self.config) toolbar_shown = self.config.get('show_toolbar_history', False) From 5be696646279d544ceceb2d773149f402331e90e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:50:19 +0100 Subject: [PATCH 15/20] qt history list: allow filtering by (partial) txid --- electrum/gui/qt/history_list.py | 19 ++++++++++++++----- electrum/gui/qt/util.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d303caa90..5b34ce442 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -71,7 +71,7 @@ class HistorySortModel(QSortFilterProxyModel): class HistoryModel(QAbstractItemModel, PrintError): - NUM_COLUMNS = 8 + NUM_COLUMNS = 9 def __init__(self, parent): super().__init__(parent) @@ -118,6 +118,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 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): @@ -161,6 +162,8 @@ class HistoryModel(QAbstractItemModel, PrintError): 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): @@ -222,11 +225,16 @@ class HistoryModel(QAbstractItemModel, PrintError): 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) + self.set_visibility_of_columns() - history = self.parent.fx.show_history() - cap_gains = self.parent.fx.get_history_capital_gains_config() + 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) @@ -289,6 +297,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 5: fiat_title, 6: fiat_acq_title, 7: fiat_cg_title, + 8: 'TXID', }[section] def flags(self, idx): @@ -305,7 +314,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return tx_mined_info class HistoryList(MyTreeView, AcceptFileDragDrop): - filter_columns = [1, 2, 3] # Date, Description, Amount + 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)) @@ -340,7 +349,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.editable_columns |= {5} self.header().setStretchLastSection(False) - for col in range(8): + for col in range(HistoryModel.NUM_COLUMNS): sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents self.header().setSectionResizeMode(col, sm) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 0b1043c92..f71243ed0 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -532,7 +532,7 @@ class MyTreeView(QTreeView): 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): From ca1043ffdab24a13dcec7521b09f0b73e597bb02 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:52:09 +0100 Subject: [PATCH 16/20] qt history list: hide columns sooner while wallet was starting up "hidden columns" were visible --- electrum/gui/qt/address_dialog.py | 2 +- electrum/gui/qt/history_list.py | 8 +++++++- electrum/gui/qt/main_window.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index d039ec42c..25d9dbf8c 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -89,7 +89,7 @@ class AddressDialog(WindowModalDialog): vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.parent, self.address) self.hw = HistoryList(self.parent, addr_hist_model) - addr_hist_model.view = self.hw + addr_hist_model.set_view(self.hw) vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5b34ce442..d11c4514c 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -76,10 +76,16 @@ class HistoryModel(QAbstractItemModel, PrintError): def __init__(self, parent): super().__init__(parent) self.parent = parent - self.view = None # type: HistoryList # set by caller! FIXME... + 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 diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c33429b61..037ff1bd1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -812,7 +812,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def create_history_tab(self): self.history_model = HistoryModel(self) self.history_list = l = HistoryList(self, self.history_model) - self.history_model.view = self.history_list + 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) From 059fb51893e68522fc6061f4bbda4b2c442dcb93 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 10 Dec 2018 10:18:24 +0100 Subject: [PATCH 17/20] reintroduce profiler --- electrum/gui/qt/history_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d11c4514c..8275d5512 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -188,6 +188,7 @@ class HistoryModel(QAbstractItemModel, PrintError): '''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' From e35ed172003ac9bcd586fbd3947b804bf2193bc8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 10 Dec 2018 13:07:03 +0100 Subject: [PATCH 18/20] remove call to undefined method refresh_headers --- electrum/gui/qt/history_list.py | 3 --- electrum/gui/qt/main_window.py | 1 - 2 files changed, 4 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 8275d5512..5c5b9223f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -363,9 +363,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') - def update_headers(self, headers): - raise NotImplementedError - def on_combo(self, x): s = self.period_combo.itemText(x) x = s == _('Custom') diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 037ff1bd1..eecf390a3 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2667,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() From b0631f90f807fd37d2d2e507157aedf6c826ba20 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 16:40:32 +0100 Subject: [PATCH 19/20] qt history: fix slowness arghhhhh finalllllllllllly figured it out... --- electrum/gui/qt/history_list.py | 5 ++--- electrum/gui/qt/util.py | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5c5b9223f..3b3a6fa2b 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -96,9 +96,8 @@ class HistoryModel(QAbstractItemModel, PrintError): return self.createIndex(row, column) def data(self, index: QModelIndex, role: Qt.ItemDataRole): - # requires PyQt5 5.11 - # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) - # assert self.checkIndex(index, indexIsValid) + # 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()) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index f71243ed0..409cbc8ab 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -444,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) From 4791c7f4240eb05812053a95b41d096bf960986a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 16:53:46 +0100 Subject: [PATCH 20/20] qt history: fix toggling fiat capital gains --- electrum/gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 3b3a6fa2b..d3e257b1e 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -199,6 +199,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 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) @@ -231,7 +232,6 @@ class HistoryModel(QAbstractItemModel, PrintError): 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) - self.set_visibility_of_columns() def set_visibility_of_columns(self): hide = self.view.hideColumn