diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 8f235c87f..7352a0e7d 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -82,6 +82,7 @@ class AddressList(MyTreeView): ROLE_SORT_ORDER = Qt.UserRole + 1000 ROLE_ADDRESS_STR = Qt.UserRole + 1001 + key_role = ROLE_ADDRESS_STR def __init__(self, parent): super().__init__(parent, self.create_menu, @@ -150,7 +151,7 @@ class AddressList(MyTreeView): def update(self): if self.maybe_defer_update(): return - current_address = self.get_role_data_for_current_item(col=self.Columns.LABEL, role=self.ROLE_ADDRESS_STR) + current_address = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR) if self.show_change == AddressTypeFilter.RECEIVING: addr_list = self.wallet.get_receiving_addresses() elif self.show_change == AddressTypeFilter.CHANGE: @@ -164,8 +165,6 @@ class AddressList(MyTreeView): set_address = None addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit() for address in addr_list: - num = self.wallet.get_address_history_len(address) - label = self.wallet.get_label(address) c, u, x = self.wallet.get_addr_balance(address) balance = c + u + x is_used_and_empty = self.wallet.is_used(address) and balance == 0 @@ -177,14 +176,7 @@ class AddressList(MyTreeView): continue if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty: continue - balance_text = self.parent.format_amount(balance, whitespaces=True) - # create item - if fx and fx.get_fiat_address_config(): - rate = fx.exchange_rate() - fiat_balance = fx.value_str(balance, rate) - else: - fiat_balance = '' - labels = ['', address, label, balance_text, fiat_balance, "%d"%num] + labels = ['', address, '', '', '', ''] address_item = [QStandardItem(e) for e in labels] # align text and set fonts for i, item in enumerate(address_item): @@ -200,21 +192,18 @@ class AddressList(MyTreeView): else: address_item[self.Columns.TYPE].setText(_('receiving')) address_item[self.Columns.TYPE].setBackground(ColorScheme.GREEN.as_color(True)) - address_item[self.Columns.LABEL].setData(address, self.ROLE_ADDRESS_STR) + address_item[0].setData(address, self.ROLE_ADDRESS_STR) address_path = self.wallet.get_address_index(address) address_item[self.Columns.TYPE].setData(address_path, self.ROLE_SORT_ORDER) address_path_str = self.wallet.get_address_path_str(address) if address_path_str is not None: address_item[self.Columns.TYPE].setToolTip(address_path_str) - address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER) - # setup column 1 - if self.wallet.is_frozen_address(address): - address_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True)) if address in addresses_beyond_gap_limit: address_item[self.Columns.ADDRESS].setBackground(ColorScheme.RED.as_color(True)) # add item count = self.std_model.rowCount() self.std_model.insertRow(count, address_item) + self.refresh_row(address, count) address_idx = self.std_model.index(count, self.Columns.LABEL) if address == current_address: set_address = QPersistentModelIndex(address_idx) @@ -227,6 +216,29 @@ class AddressList(MyTreeView): self.filter() self.proxy.setDynamicSortFilter(True) + def refresh_row(self, key, row): + address = key + label = self.wallet.get_label(address) + num = self.wallet.get_address_history_len(address) + c, u, x = self.wallet.get_addr_balance(address) + balance = c + u + x + balance_text = self.parent.format_amount(balance, whitespaces=True) + # create item + fx = self.parent.fx + if fx and fx.get_fiat_address_config(): + rate = fx.exchange_rate() + fiat_balance_str = fx.value_str(balance, rate) + else: + fiat_balance_str = '' + address_item = [self.std_model.item(row, col) for col in self.Columns] + address_item[self.Columns.LABEL].setText(label) + address_item[self.Columns.COIN_BALANCE].setText(balance_text) + address_item[self.Columns.COIN_BALANCE].setData(balance, self.ROLE_SORT_ORDER) + address_item[self.Columns.FIAT_BALANCE].setText(fiat_balance_str) + address_item[self.Columns.NUM_TXS].setText("%d"%num) + c = ColorScheme.BLUE if self.wallet.is_frozen_address(address) else ColorScheme.DEFAULT + address_item[self.Columns.ADDRESS].setBackground(c.as_color(True)) + def create_menu(self, position): from electrum.wallet import Multisig_Wallet is_multisig = isinstance(self.wallet, Multisig_Wallet) @@ -268,6 +280,11 @@ class AddressList(MyTreeView): else: menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) + else: + # multiple items selected + menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses(addrs, True)) + menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False)) + coins = self.wallet.get_spendable_coins(addrs) if coins: menu.addAction(_("Spend from"), lambda: self.parent.utxo_list.set_spend_list(coins)) @@ -287,7 +304,7 @@ class AddressList(MyTreeView): def get_edit_key_from_coordinate(self, row, col): if col != self.Columns.LABEL: return None - return self.get_role_data_from_coordinate(row, col, role=self.ROLE_ADDRESS_STR) + return self.get_role_data_from_coordinate(row, 0, role=self.ROLE_ADDRESS_STR) def on_edited(self, idx, edit_key, *, text): self.parent.wallet.set_label(edit_key, text) diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 35fe48575..a4c7b76d6 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -50,6 +50,7 @@ class ContactList(MyTreeView): filter_columns = [Columns.NAME, Columns.ADDRESS] ROLE_CONTACT_KEY = Qt.UserRole + 1000 + key_role = ROLE_CONTACT_KEY def __init__(self, parent): super().__init__(parent, self.create_menu, @@ -58,6 +59,7 @@ class ContactList(MyTreeView): self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) + self.std_model = self.model() self.update() def on_edited(self, idx, edit_key, *, text): @@ -121,6 +123,10 @@ class ContactList(MyTreeView): self.filter() run_hook('update_contacts_tab', self) + def refresh_row(self, key): + # nothing to update here + pass + def get_edit_key_from_coordinate(self, row, col): if col != self.Columns.NAME: return None diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index be3507dcf..5c21f5db1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -331,7 +331,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def on_fx_history(self): self.history_model.refresh('fx_history') - self.address_list.update() + self.address_list.refresh_all() def on_fx_quotes(self): self.update_status() @@ -343,7 +343,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): # History tab needs updating if it used spot if self.fx.history_used_spot: self.history_model.refresh('fx_quotes') - self.address_list.update() + self.address_list.refresh_all() def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) @@ -431,7 +431,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.network_signal.emit('status', None) elif event == 'blockchain_updated': # to update number of confirmations in history - self.need_update.set() + self.refresh_tabs() elif event == 'new_transaction': wallet, tx = args if wallet == self.wallet: @@ -456,6 +456,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): elif event == 'invoice_status': self.on_invoice_status(*args) elif event == 'payment_succeeded': + # sent by lnworker, redundant with invoice_status wallet = args[0] if wallet == self.wallet: self.on_payment_succeeded(*args) @@ -875,6 +876,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000) def timer_actions(self): + # refresh invoices and requests because they show ETA self.request_list.refresh_all() self.invoice_list.refresh_all() # Note this runs in the GUI thread @@ -1029,14 +1031,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if wallet != self.wallet: return self.history_model.refresh('update_tabs') - self.request_list.refresh_all() - self.invoice_list.refresh_all() + self.request_list.update() + self.invoice_list.update() self.address_list.update() self.utxo_list.update() self.contact_list.update() self.channels_list.update_rows.emit(wallet) self.update_completions() + def refresh_tabs(self, wallet=None): + self.history_model.refresh('refresh_tabs') + self.request_list.refresh_all() + self.invoice_list.refresh_all() + self.address_list.refresh_all() + self.utxo_list.refresh_all() + self.contact_list.refresh_all() + self.channels_list.update_rows.emit(self.wallet) + def create_channels_tab(self): self.channels_list = ChannelsList(self) t = self.channels_list.get_toolbar() @@ -1256,7 +1267,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): key = self.create_bitcoin_request(amount, message, expiry) if not key: return - self.address_list.update() + self.address_list.refresh_all() except InvoiceError as e: self.show_error(_('Error creating payment request') + ':\n' + str(e)) return @@ -2064,13 +2075,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def set_frozen_state_of_addresses(self, addrs, freeze: bool): self.wallet.set_frozen_state_of_addresses(addrs, freeze) - self.address_list.update() - self.utxo_list.update() + self.address_list.refresh_all() + self.utxo_list.refresh_all() + self.address_list.selectionModel().clearSelection() def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool): utxos_str = {utxo.prevout.to_str() for utxo in utxos} self.wallet.set_frozen_state_of_coins(utxos_str, freeze) - self.utxo_list.update() + self.utxo_list.refresh_all() + self.utxo_list.selectionModel().clearSelection() def create_list_tab(self, l, toolbar=None): w = QWidget() @@ -3197,7 +3210,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.fiat_receive_e.setVisible(b) self.history_list.update() self.address_list.refresh_headers() - self.address_list.update() + self.address_list.refresh_all() self.update_status() def settings_dialog(self): diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index e451fe6d1..91c8141bb 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -98,7 +98,7 @@ class SettingsDialog(WindowModalDialog): if self.config.num_zeros != value: self.config.num_zeros = value self.config.set_key('num_zeros', value, True) - self.window.need_update.set() + self.window.refresh_tabs() nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) @@ -197,7 +197,7 @@ class SettingsDialog(WindowModalDialog): if self.config.amt_precision_post_satoshi != prec: self.config.amt_precision_post_satoshi = prec self.config.set_key('amt_precision_post_satoshi', prec) - self.window.need_update.set() + self.window.refresh_tabs() msat_cb.stateChanged.connect(on_msat_checked) lightning_widgets.append((msat_cb, None)) @@ -232,7 +232,7 @@ class SettingsDialog(WindowModalDialog): if self.config.amt_add_thousands_sep != checked: self.config.amt_add_thousands_sep = checked self.config.set_key('amt_add_thousands_sep', checked) - self.window.need_update.set() + self.window.refresh_tabs() thousandsep_cb.stateChanged.connect(on_set_thousandsep) gui_widgets.append((thousandsep_cb, None)) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 8de591556..c33c40a53 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -59,6 +59,7 @@ class UTXOList(MyTreeView): stretch_column = Columns.LABEL ROLE_PREVOUT_STR = Qt.UserRole + 1000 + key_role = ROLE_PREVOUT_STR def __init__(self, parent): super().__init__(parent, self.create_menu, @@ -67,7 +68,8 @@ class UTXOList(MyTreeView): self._utxo_dict = {} self.wallet = self.parent.wallet - self.setModel(QStandardItemModel(self)) + self.std_model = QStandardItemModel(self) + self.setModel(self.std_model) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) self.update() @@ -80,37 +82,44 @@ class UTXOList(MyTreeView): self.model().clear() self.update_headers(self.__class__.headers) for idx, utxo in enumerate(utxos): - self.insert_utxo(idx, utxo) + name = utxo.prevout.to_str() + self._utxo_dict[name] = utxo + address = utxo.address + height = utxo.block_height + name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx + amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) + labels = [name_short, address, '', amount, '%d'%height] + utxo_item = [QStandardItem(x) for x in labels] + self.set_editability(utxo_item) + utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) + utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) + utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT)) + utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) + utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT)) + self.model().insertRow(idx, utxo_item) + self.refresh_row(name, idx) self.filter() + self.update_coincontrol_bar() + + def update_coincontrol_bar(self): # update coincontrol status bar if self._spend_set is not None: coins = [self._utxo_dict[x] for x in self._spend_set] coins = self._filter_frozen_coins(coins) amount = sum(x.value_sats() for x in coins) amount_str = self.parent.format_amount_and_units(amount) - num_outputs_str = _("{} outputs available ({} total)").format(len(coins), len(utxos)) + num_outputs_str = _("{} outputs available ({} total)").format(len(coins), len(self._utxo_dict)) self.parent.set_coincontrol_msg(_("Coin control active") + f': {num_outputs_str}, {amount_str}') else: self.parent.set_coincontrol_msg(None) - def insert_utxo(self, idx, utxo: PartialTxInput): + def refresh_row(self, key, row): + utxo = self._utxo_dict[key] + utxo_item = [self.std_model.item(row, col) for col in self.Columns] address = utxo.address - height = utxo.block_height - name = utxo.prevout.to_str() - name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx - self._utxo_dict[name] = utxo label = self.wallet.get_label_for_txid(utxo.prevout.txid.hex()) or self.wallet.get_label(address) - amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) - labels = [name_short, address, label, amount, '%d'%height] - utxo_item = [QStandardItem(x) for x in labels] - self.set_editability(utxo_item) - utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) - utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) - utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT)) - utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) - utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT)) SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent') - if name in (self._spend_set or set()): + if key in (self._spend_set or set()): for col in utxo_item: col.setBackground(ColorScheme.GREEN.as_color(True)) if col != self.Columns.OUTPOINT: @@ -120,11 +129,11 @@ class UTXOList(MyTreeView): utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen')) if self.wallet.is_frozen_coin(utxo): utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True)) - utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}") + utxo_item[self.Columns.OUTPOINT].setToolTip(f"{key}\n{_('Coin is frozen')}") else: - tooltip = ("\n" + SELECTED_TO_SPEND_TOOLTIP) if name in (self._spend_set or set()) else "" - utxo_item[self.Columns.OUTPOINT].setToolTip(name + tooltip) - self.model().insertRow(idx, utxo_item) + tooltip = ("\n" + SELECTED_TO_SPEND_TOOLTIP) if key in (self._spend_set or set()) else "" + utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.DEFAULT.as_color(True)) + utxo_item[self.Columns.OUTPOINT].setToolTip(key + tooltip) def get_selected_outpoints(self) -> Optional[List[str]]: if not self.model(): @@ -144,7 +153,9 @@ class UTXOList(MyTreeView): self._spend_set = {utxo.prevout.to_str() for utxo in coins} else: self._spend_set = None - self.update() + self.refresh_all() + self.update_coincontrol_bar() + self.selectionModel().clearSelection() def get_spend_list(self) -> Optional[Sequence[PartialTxInput]]: if self._spend_set is None: