From ba3783f9989cdf9c96550c5fe4305f99be518b5c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 May 2025 17:47:11 +0000 Subject: [PATCH 1/2] refactor qt.util.ChoiceWidget: introduce ChoiceItem --- electrum/gui/qt/main_window.py | 4 +- electrum/gui/qt/receive_tab.py | 5 +- electrum/gui/qt/seed_dialog.py | 13 ++-- electrum/gui/qt/send_tab.py | 10 +-- electrum/gui/qt/util.py | 62 ++++++++++--------- electrum/gui/qt/wallet_info_dialog.py | 10 ++- electrum/gui/qt/wizard/wallet.py | 58 +++++++++-------- electrum/hw_wallet/plugin.py | 5 +- electrum/hw_wallet/qt.py | 10 +-- electrum/plugin.py | 22 ++++--- electrum/plugins/coldcard/qt.py | 8 ++- .../plugins/digitalbitbox/digitalbitbox.py | 21 ++++--- electrum/plugins/digitalbitbox/qt.py | 4 -- electrum/plugins/keepkey/qt.py | 9 +-- electrum/plugins/safe_t/qt.py | 9 +-- electrum/plugins/trezor/qt.py | 5 +- electrum/plugins/trustedcoin/qt.py | 6 +- electrum/util.py | 8 +++ 18 files changed, 153 insertions(+), 116 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 315181398..51e129901 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -55,7 +55,7 @@ from electrum.plugin import run_hook from electrum.i18n import _ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, - AddTransactionException, os_chmod, UI_UNIT_NAME_TXSIZE_VBYTES) + AddTransactionException, os_chmod, UI_UNIT_NAME_TXSIZE_VBYTES, ChoiceItem) from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME from electrum.payment_identifier import PaymentIdentifier from electrum.invoices import PR_PAID, Invoice @@ -1326,7 +1326,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return (f"pubkey={x['pubkey'][0:10]}, " f"fee={x['percentage_fee']}% + {x['mining_fee']} sats, " f"last_seen: {last_seen}") - server_keys = [(x['pubkey'], descr(x)) for x in recent_offers] + server_keys = [ChoiceItem(key=x['pubkey'], label=descr(x)) for x in recent_offers] msg = '\n'.join([ _("Please choose a server from this list."), _("Note that fees may be updated frequently.") diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index be2bf0a00..51fe35606 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -10,7 +10,7 @@ from PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QTextEdit, QHBoxLayout, QPushButton, QWidget, QSizePolicy, QFrame) from electrum.i18n import _ -from electrum.util import InvoiceError +from electrum.util import InvoiceError, ChoiceItem from electrum.invoices import pr_expiration_values from electrum.logging import Logger @@ -195,7 +195,8 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): _('For Lightning requests, payments will not be accepted after the expiration.'), ]) expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS - choices = list(pr_expiration_values().items()) + choices = [ChoiceItem(key=exptime, label=label) + for (exptime, label) in pr_expiration_values().items()] v = self.window.query_choice(msg, choices, title=_('Expiry'), default_choice=expiry) if v is None: return diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 9e40802ad..4d71d648c 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -35,9 +35,12 @@ from electrum.i18n import _ from electrum.mnemonic import Mnemonic, calc_seed_type, is_any_2fa_seed_type from electrum import old_mnemonic from electrum import slip39 +from electrum.util import ChoiceItem -from .util import (Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path, - EnterButton, CloseButton, WindowModalDialog, ColorScheme, font_height, ChoiceWidget) +from .util import ( + Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path, EnterButton, + CloseButton, WindowModalDialog, ColorScheme, font_height, ChoiceWidget, +) from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -98,15 +101,15 @@ class SeedWidget(QWidget): if options: self.seed_types = [ - (value, title) for value, title in ( + ChoiceItem(key=stype, label=label) for stype, label in ( ('electrum', _('Electrum')), ('bip39', _('BIP39 seed')), ('slip39', _('SLIP39 seed')), ) - if value in self.options + if stype in self.options ] assert len(self.seed_types) - self.seed_type = self.seed_types[0][0] + self.seed_type = self.seed_types[0].key else: self.seed_type = 'electrum' diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 81c9ca3fd..1f8a43136 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -13,7 +13,7 @@ from electrum.i18n import _ from electrum.logging import Logger from electrum.bitcoin import DummyAddress from electrum.plugin import run_hook -from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled, ChoiceItem from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed @@ -672,25 +672,25 @@ class SendTab(QWidget, MessageBoxMixin, Logger): can_pay_with_swap = lnworker.suggest_swap_to_send(amount_sat, coins=coins) rebalance_suggestion = lnworker.suggest_rebalance_to_send(amount_sat) can_rebalance = bool(rebalance_suggestion) and self.window.num_tasks() == 0 - choices = [] + choices = [] # type: List[ChoiceItem] if can_rebalance: msg = ''.join([ _('Rebalance existing channels'), '\n', _('Move funds between your channels in order to increase your sending capacity.') ]) - choices.append(('rebalance', msg)) + choices.append(ChoiceItem(key='rebalance', label=msg)) if can_pay_with_new_channel: msg = ''.join([ _('Open a new channel'), '\n', _('You will be able to pay once the channel is open.') ]) - choices.append(('new_channel', msg)) + choices.append(ChoiceItem(key='new_channel', label=msg)) if can_pay_with_swap: msg = ''.join([ _('Swap onchain funds for lightning funds'), '\n', _('You will be able to pay once the swap is confirmed.') ]) - choices.append(('swap', msg)) + choices.append(ChoiceItem(key='swap', label=msg)) msg = _('You cannot pay that invoice using Lightning.') if lnworker and lnworker.channels: num_sats_can_send = int(lnworker.num_sats_can_send()) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index d9d539549..7a30690d7 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -21,7 +21,7 @@ from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBo from electrum.i18n import _ from electrum.util import (FileImportFailed, FileExportFailed, resource_path, EventListener, event_listener, - get_logger, UserCancelled, UserFacingException) + get_logger, UserCancelled, UserFacingException, ChoiceItem) from electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST) from electrum.qrreader import MissingQrDetectionLib, QrCodeResult @@ -315,12 +315,18 @@ class MessageBoxMixin(object): rich_text=rich_text, checkbox=checkbox ) - def query_choice(self, - msg: Optional[str], - choices: Sequence[Tuple], - title: Optional[str] = None, - default_choice: Optional[Any] = None) -> Optional[Any]: - # Needed by QtHandler for hardware wallets + def query_choice( + self, + msg: Optional[str], + choices: Sequence['ChoiceItem'], + *, + title: Optional[str] = None, + default_choice: Optional[Any] = None, + ) -> Optional[Any]: + """Returns ChoiceItem.key (for selected item), or None if the user cancels the dialog. + + Needed by QtHandler for hardware wallets. + """ if title is None: title = _('Question') dialog = WindowModalDialog(self.top_level_window(), title=title) @@ -506,18 +512,21 @@ def text_dialog( class ChoiceWidget(QWidget): - """Renders a list of tuples as a radiobuttons group. - The first element of each tuple is used as a key. - The second element of each tuple is used as user facing string. - The remainder of the tuple can be any additional data. + """Renders a list of ChoiceItems as a radiobuttons group. Callers can pre-select an item by key, through the 'selected' parameter. The selected item is made available by index (selected_index), - by key (selected_key) and by whole tuple (selected_item). + by key (selected_key) and by Choice (selected_item). """ itemSelected = pyqtSignal([int], arguments=['index']) - def __init__(self, *, message: Optional[str] = None, choices: Sequence[Tuple] = None, selected: Optional[Any] = None): + def __init__( + self, + *, + message: Optional[str] = None, + choices: Sequence[ChoiceItem] = None, + selected: Optional[Any] = None, + ): QWidget.__init__(self) vbox = QVBoxLayout() self.setLayout(vbox) @@ -525,11 +534,10 @@ class ChoiceWidget(QWidget): if choices is None: choices = [] - self.selected_index = -1 # int - self.selected_item = None # Optional[Tuple] - self.selected_key = None # Optional[Any] - - self.choices = choices + self.selected_index = -1 # type: int + self.selected_item = None # type: Optional[ChoiceItem] + self.selected_key = None # type: Optional[Any] + self.choices = choices # type: Sequence[ChoiceItem] if message and len(message) > 50: vbox.addWidget(WWLabel(message)) @@ -540,31 +548,29 @@ class ChoiceWidget(QWidget): gb2.setLayout(vbox2) self.group = group = QButtonGroup() assert isinstance(choices, list) - iterator = enumerate(choices) - for i, c in iterator: - assert isinstance(c, tuple), f"{c=!r}" + for i, c in enumerate(choices): + assert isinstance(c, ChoiceItem), f"{c=!r}" button = QRadioButton(gb2) - button.setText(c[1]) + button.setText(c.label) vbox2.addWidget(button) group.addButton(button) group.setId(button, i) - if (i == 0 and selected is None) or c[0] == selected: + if (i == 0 and selected is None) or c.key == selected: self.selected_index = i self.selected_item = c - self.selected_key = c[0] + self.selected_key = c.key button.setChecked(True) group.buttonClicked.connect(self.on_selected) def on_selected(self, button): self.selected_index = self.group.id(button) self.selected_item = self.choices[self.selected_index] - self.selected_key = self.choices[self.selected_index][0] + self.selected_key = self.choices[self.selected_index].key self.itemSelected.emit(self.selected_index) def select(self, key): - iterator = enumerate(self.choices) - for i, c in iterator: - if key == c[0]: + for i, c in enumerate(self.choices): + if key == c.key: self.group.button(i).click() diff --git a/electrum/gui/qt/wallet_info_dialog.py b/electrum/gui/qt/wallet_info_dialog.py index 69e0f5b0b..97f063f12 100644 --- a/electrum/gui/qt/wallet_info_dialog.py +++ b/electrum/gui/qt/wallet_info_dialog.py @@ -12,10 +12,13 @@ from PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, from electrum.plugin import run_hook from electrum.i18n import _ from electrum.wallet import Multisig_Wallet +from electrum.util import ChoiceItem from .qrtextedit import ShowQRTextEdit -from .util import (read_QIcon, WindowModalDialog, Buttons, - WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit, ChoiceWidget) +from .util import ( + read_QIcon, WindowModalDialog, Buttons, + WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit, ChoiceWidget, +) if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -117,7 +120,8 @@ class WalletInfoDialog(WindowModalDialog): else: return _("keystore") + f' {idx+1}' - labels = [(idx, label(idx, ks)) for idx, ks in enumerate(wallet.get_keystores())] + labels = [ChoiceItem(key=idx, label=label(idx, ks)) + for idx, ks in enumerate(wallet.get_keystores())] keystore_choice = ChoiceWidget(message=_("Select keystore"), choices=labels) keystore_choice.itemSelected.connect(lambda x: select_ks(x)) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index d98ecc7ab..96eb6264e 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -3,7 +3,7 @@ import os import sys import threading -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, List, Tuple from PyQt6.QtCore import Qt, QTimer, QRect, pyqtSignal from PyQt6.QtGui import QPen, QPainter, QPalette, QPixmap @@ -17,7 +17,7 @@ from electrum.keystore import bip44_derivation, bip39_to_seed, purpose48_derivat from electrum.plugin import run_hook, HardwarePluginLibraryUnavailable from electrum.storage import StorageReadWriteError from electrum.util import WalletFileException, get_new_wallet_name, UserFacingException, InvalidPassword -from electrum.util import is_subpath +from electrum.util import is_subpath, ChoiceItem from electrum.wallet import wallet_types from .wizard import QEAbstractWizard, WizardComponent from electrum.logging import get_logger, Logger @@ -34,7 +34,7 @@ from electrum.gui.qt.plugins_dialog import PluginsDialog if TYPE_CHECKING: from electrum.simple_config import SimpleConfig - from electrum.plugin import Plugins + from electrum.plugin import Plugins, DeviceInfo from electrum.gui.qt import QElectrumApplication WIF_HELP_TEXT = (_('WIF keys are typed in Electrum, based on script type.') + '\n\n' + @@ -399,12 +399,12 @@ class WCWalletType(WalletWizardComponent): WalletWizardComponent.__init__(self, parent, wizard, title=_('Create new wallet')) message = _('What kind of wallet do you want to create?') wallet_kinds = [ - ('standard', _('Standard wallet')), - ('2fa', _('Wallet with two-factor authentication')), - ('multisig', _('Multi-signature wallet')), - ('imported', _('Import Bitcoin addresses or private keys')), + ChoiceItem(key='standard', label=_('Standard wallet')), + ChoiceItem(key='2fa', label=_('Wallet with two-factor authentication')), + ChoiceItem(key='multisig', label=_('Multi-signature wallet')), + ChoiceItem(key='imported', label=_('Import Bitcoin addresses or private keys')), ] - choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] + choices = [c for c in wallet_kinds if c.key in wallet_types] self.choice_w = ChoiceWidget(message=message, choices=choices, selected='standard') self.layout().addWidget(self.choice_w) @@ -420,10 +420,10 @@ class WCKeystoreType(WalletWizardComponent): WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore')) message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') choices = [ - ('createseed', _('Create a new seed')), - ('haveseed', _('I already have a seed')), - ('masterkey', _('Use a master key')), - ('hardware', _('Use a hardware device')) + ChoiceItem(key='createseed', label=_('Create a new seed')), + ChoiceItem(key='haveseed', label=_('I already have a seed')), + ChoiceItem(key='masterkey', label=_('Use a master key')), + ChoiceItem(key='hardware', label=_('Use a hardware device')), ] self.choice_w = ChoiceWidget(message=message, choices=choices) @@ -658,7 +658,7 @@ class WCScriptAndDerivation(WalletWizardComponent, Logger): WalletWizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path')) Logger.__init__(self) - self.choice_w = None + self.choice_w = None # type: ChoiceWidget self.derivation_path_edit = None self.warn_label = IconLabel(reverse=True, hide_if_empty=True) @@ -675,9 +675,12 @@ class WCScriptAndDerivation(WalletWizardComponent, Logger): if self.wizard_data['wallet_type'] == 'multisig': choices = [ # TODO: nicer to refactor 'standard' to 'p2sh', but backend wallet still uses 'standard' - ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), - ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), - ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), + ChoiceItem(key='standard', label='legacy multisig (p2sh)', + extra_data=normalize_bip32_derivation("m/45'/0")), + ChoiceItem(key='p2wsh-p2sh', label='p2sh-segwit multisig (p2wsh-p2sh)', + extra_data=purpose48_derivation(0, xtype='p2wsh-p2sh')), + ChoiceItem(key='p2wsh', label='native segwit multisig (p2wsh)', + extra_data=purpose48_derivation(0, xtype='p2wsh')), ] if 'multisig_current_cosigner' in self.wizard_data: # get script type of first cosigner @@ -690,9 +693,12 @@ class WCScriptAndDerivation(WalletWizardComponent, Logger): default_choice = 'p2wpkh' choices = [ # TODO: nicer to refactor 'standard' to 'p2pkh', but backend wallet still uses 'standard' - ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), - ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), - ('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), + ChoiceItem(key='standard', label='legacy (p2pkh)', + extra_data=bip44_derivation(0, bip43_purpose=44)), + ChoiceItem(key='p2wpkh-p2sh', label='p2sh-segwit (p2wpkh-p2sh)', + extra_data=bip44_derivation(0, bip43_purpose=49)), + ChoiceItem(key='p2wpkh', label='native segwit (p2wpkh)', + extra_data=bip44_derivation(0, bip43_purpose=84)), ] if self.wizard_data['wallet_type'] == 'standard' and not self.wizard_data['keystore_type'] == 'hardware': @@ -722,7 +728,7 @@ class WCScriptAndDerivation(WalletWizardComponent, Logger): self.layout().addWidget(QLabel(_("Or"))) def on_choice_click(index): - self.derivation_path_edit.setText(self.choice_w.selected_item[2]) + self.derivation_path_edit.setText(self.choice_w.selected_item.extra_data) self.choice_w = ChoiceWidget(message=message1, choices=choices, selected=default_choice) self.choice_w.itemSelected.connect(on_choice_click) @@ -768,9 +774,9 @@ class WCCosignerKeystore(WalletWizardComponent): message = _('Add a cosigner to your multi-sig wallet') choices = [ - ('masterkey', _('Enter cosigner key')), - ('haveseed', _('Enter cosigner seed')), - ('hardware', _('Cosign with hardware device')) + ChoiceItem(key='masterkey', label=_('Enter cosigner key')), + ChoiceItem(key='haveseed', label=_('Enter cosigner seed')), + ChoiceItem(key='hardware', label=_('Cosign with hardware device')), ] self.choice_w = ChoiceWidget(message=message, choices=choices) @@ -1090,7 +1096,7 @@ class WCChooseHWDevice(WalletWizardComponent, Logger): self.device_list = QWidget() self.device_list_layout = QVBoxLayout() self.device_list.setLayout(self.device_list_layout) - self.choice_w = None + self.choice_w = None # type: ChoiceWidget self.rescan_button = QPushButton(_('Rescan devices')) self.rescan_button.clicked.connect(self.on_rescan) @@ -1132,7 +1138,7 @@ class WCChooseHWDevice(WalletWizardComponent, Logger): self.error_l.setVisible(False) self.device_list.setVisible(True) - choices = [] + choices = [] # type: List[ChoiceItem] for name, info in self.devices: state = _("initialized") if info.initialized else _("wiped") label = info.label or _("An unnamed {}").format(name) @@ -1141,7 +1147,7 @@ class WCChooseHWDevice(WalletWizardComponent, Logger): except Exception: transport_str = 'unknown transport' descr = f"{label} [{info.model_name or name}, {state}, {transport_str}]" - choices.append(((name, info), descr)) + choices.append(ChoiceItem(key=(name, info), label=descr)) msg = _('Select a device') + ':' if self.choice_w: diff --git a/electrum/hw_wallet/plugin.py b/electrum/hw_wallet/plugin.py index 534877a99..8225ce194 100644 --- a/electrum/hw_wallet/plugin.py +++ b/electrum/hw_wallet/plugin.py @@ -30,7 +30,7 @@ from electrum.plugin import (BasePlugin, hook, Device, DeviceMgr, assert_runs_in_hwd_thread, runs_in_hwd_thread) from electrum.i18n import _ from electrum.bitcoin import is_address, opcodes -from electrum.util import versiontuple, UserFacingException +from electrum.util import versiontuple, UserFacingException, ChoiceItem from electrum.transaction import TxOutput, PartialTransaction from electrum.bip32 import BIP32Node from electrum.storage import get_derivation_used_for_hw_device_encryption @@ -316,7 +316,8 @@ class HardwareHandlerBase: def update_status(self, paired: bool) -> None: pass - def query_choice(self, msg: str, labels: Sequence[str]) -> Optional[int]: + def query_choice(self, msg: str, choices: Sequence[ChoiceItem]) -> Optional[Any]: + """Returns ChoiceItem.key (for selected item), or None if the user cancels the dialog.""" raise NotImplementedError() def yes_no_question(self, msg: str) -> bool: diff --git a/electrum/hw_wallet/qt.py b/electrum/hw_wallet/qt.py index 9398e6c89..a48311be6 100644 --- a/electrum/hw_wallet/qt.py +++ b/electrum/hw_wallet/qt.py @@ -41,7 +41,7 @@ from electrum.gui.qt.util import read_QIcon_from_bytes from electrum.i18n import _ from electrum.logging import Logger -from electrum.util import UserCancelled, UserFacingException +from electrum.util import UserCancelled, UserFacingException, ChoiceItem from electrum.plugin import hook, DeviceUnpairableError from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase @@ -98,9 +98,9 @@ class QtHandlerBase(HardwareHandlerBase, QObject, Logger): icon = read_QIcon_from_bytes(icon_bytes) button.setIcon(icon) - def query_choice(self, msg: str, labels: Sequence[Tuple]): + def query_choice(self, msg: str, choices: Sequence[ChoiceItem]): self.done.clear() - self.query_signal.emit(msg, labels) + self.query_signal.emit(msg, choices) self.done.wait() return self.choice @@ -197,9 +197,9 @@ class QtHandlerBase(HardwareHandlerBase, QObject, Logger): self.dialog.accept() self.dialog = None - def win_query_choice(self, msg: str, labels: Sequence[Tuple]): + def win_query_choice(self, msg: str, choices: Sequence[ChoiceItem]): try: - self.choice = self.win.query_choice(msg, labels) + self.choice = self.win.query_choice(msg, choices) except UserCancelled: self.choice = None self.done.set() diff --git a/electrum/plugin.py b/electrum/plugin.py index d9ccae939..f2a9c22b3 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -46,7 +46,7 @@ from electrum_ecc import ECPrivkey, ECPubkey from ._vendor.distutils.version import StrictVersion from .version import ELECTRUM_VERSION from .i18n import _ -from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) +from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException, ChoiceItem) from . import bip32 from . import plugins from .simple_config import SimpleConfig @@ -682,6 +682,17 @@ class DeviceInfo(NamedTuple): soft_device_id: Optional[str] = None # if available, used to distinguish same-type hw devices model_name: Optional[str] = None # e.g. "Ledger Nano S" + def label_for_device_select(self) -> str: + return ( + "{label} ({maybe_model}{init}, {transport})" + .format( + label=self.label or _("An unnamed {}").format(self.plugin_name), + init=(_("initialized") if self.initialized else _("wiped")), + transport=self.device.transport_ui_string, + maybe_model=f"{self.model_name}, " if self.model_name else "" + ) + ) + class HardwarePluginToScan(NamedTuple): name: str @@ -1068,15 +1079,10 @@ class DeviceMgr(ThreadJob): + f"bip32 root fingerprint: {keystore.get_root_fingerprint()!r})\n\n") msg += _("Please select which {} device to use:").format(plugin.device) msg += "\n(" + _("Or click cancel to skip this keystore instead.") + ")" - descriptions = ["{label} ({maybe_model}{init}, {transport})" - .format(label=info.label or _("An unnamed {}").format(info.plugin_name), - init=(_("initialized") if info.initialized else _("wiped")), - transport=info.device.transport_ui_string, - maybe_model=f"{info.model_name}, " if info.model_name else "") - for info in infos] + choices = [ChoiceItem(key=idx, label=info.label_for_device_select()) + for (idx, info) in enumerate(infos)] self.logger.debug(f"select_device. prompting user for manual selection of {plugin.device}. " f"num options: {len(infos)}. options: {infos}") - choices = list(enumerate(descriptions)) c = handler.query_choice(msg, choices) if c is None: raise UserCancelled() diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py index 78cd1335a..5956ae7f8 100644 --- a/electrum/plugins/coldcard/qt.py +++ b/electrum/plugins/coldcard/qt.py @@ -1,5 +1,5 @@ from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout @@ -7,6 +7,8 @@ from PyQt6.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayo from electrum.i18n import _ from electrum.plugin import hook from electrum.wallet import Multisig_Wallet +from electrum.keystore import Hardware_KeyStore +from electrum.util import ChoiceItem from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.hw_wallet.plugin import only_hook_if_libraries_available @@ -76,13 +78,13 @@ class Plugin(ColdcardPlugin, QtPluginBase): buttons.append(btn_import_usb) return buttons - def import_multisig_wallet_to_cc(self, main_window, coldcard_keystores): + def import_multisig_wallet_to_cc(self, main_window: 'ElectrumWindow', coldcard_keystores: Sequence[Hardware_KeyStore]): from io import StringIO from ckcc.protocol import CCProtocolPacker index = main_window.query_choice( _("Please select which {} device to use:").format(self.device), - [(i, ks.label) for i, ks in enumerate(coldcard_keystores)] + [ChoiceItem(key=i, label=ks.label) for i, ks in enumerate(coldcard_keystores)] ) if index is not None: selected_keystore = coldcard_keystores[index] diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index aafffb275..f3db13971 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -29,7 +29,7 @@ from electrum import constants from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore -from electrum.util import to_string, UserCancelled, UserFacingException, bfh +from electrum.util import to_string, UserCancelled, UserFacingException, bfh, ChoiceItem from electrum.network import Network from electrum.logging import get_logger from electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread @@ -239,13 +239,13 @@ class DigitalBitbox_Client(HardwareClientBase): def recover_or_erase_dialog(self): msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n" choices = [ - (_("Create a wallet using the current seed")), - (_("Erase the Digital Bitbox")) + ChoiceItem(key="create", label=_("Create a wallet using the current seed")), + ChoiceItem(key="erase", label=_("Erase the Digital Bitbox")), ] reply = self.handler.query_choice(msg, choices) if reply is None: raise UserCancelled() - if reply == 1: + if reply == "erase": self.dbb_erase() else: if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: @@ -256,13 +256,13 @@ class DigitalBitbox_Client(HardwareClientBase): def seed_device_dialog(self): msg = _("Choose how to initialize your Digital Bitbox:") + "\n" choices = [ - (_("Generate a new random wallet")), - (_("Load a wallet from the micro SD card")) + ChoiceItem(key="generate", label=_("Generate a new random wallet")), + ChoiceItem(key="load", label=_("Load a wallet from the micro SD card")), ] reply = self.handler.query_choice(msg, choices) if reply is None: raise UserCancelled() - if reply == 0: + if reply == "generate": self.dbb_generate_wallet() else: if not self.dbb_load_backup(show_msg=False): @@ -291,8 +291,8 @@ class DigitalBitbox_Client(HardwareClientBase): return choices = [ - _('Do not pair'), - _('Import pairing from the Digital Bitbox desktop app'), + ChoiceItem(key=0, label=_('Do not pair')), + ChoiceItem(key=1, label=_('Import pairing from the Digital Bitbox desktop app')), ] reply = self.handler.query_choice(_('Mobile pairing options'), choices) if reply is None: @@ -334,7 +334,8 @@ class DigitalBitbox_Client(HardwareClientBase): backups = self.hid_send_encrypt(b'{"backup":"list"}') if 'error' in backups: raise UserFacingException(backups['error']['message']) - f = self.handler.query_choice(_("Choose a backup file:"), backups['backup']) + backup_choices = [ChoiceItem(key=idx, label=v) for (idx, v) in enumerate(backups['backup'])] + f = self.handler.query_choice(_("Choose a backup file:"), backup_choices) if f is None: raise UserCancelled() key = self.backup_password_dialog() diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py index f7947a5bb..3fad9b6ec 100644 --- a/electrum/plugins/digitalbitbox/qt.py +++ b/electrum/plugins/digitalbitbox/qt.py @@ -69,10 +69,6 @@ class DigitalBitbox_Handler(QtHandlerBase): def __init__(self, win): super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox') - def query_choice(self, msg, labels): - choices = [(i, v) for i, v in enumerate(labels)] - return QtHandlerBase.query_choice(self, msg, choices) - class WCDigitalBitboxScriptAndDerivation(WCScriptAndDerivation): requestRecheck = pyqtSignal() diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index 432ba7674..401afba3a 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -14,6 +14,7 @@ from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelBut from electrum.i18n import _ from electrum.plugin import hook from electrum.logging import Logger +from electrum.util import ChoiceItem from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget @@ -619,10 +620,10 @@ class WCKeepkeyInitMethod(WalletWizardComponent): ).format(_info.model_name, _info.model_name) choices = [ # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) + ChoiceItem(key=TIM_NEW, label=_("Let the device generate a completely new seed randomly")), + ChoiceItem(key=TIM_RECOVER, label=_("Recover from a seed you have previously written down")), + ChoiceItem(key=TIM_MNEMONIC, label=_("Upload a BIP39 mnemonic to generate the seed")), + ChoiceItem(key=TIM_PRIVKEY, label=_("Upload a master private key")), ] self.choice_w = ChoiceWidget(message=msg, choices=choices) self.layout().addWidget(self.choice_w) diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 36e68dd49..58ff70019 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -14,6 +14,7 @@ from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelBut from electrum.i18n import _ from electrum.plugin import hook from electrum.logging import Logger +from electrum.util import ChoiceItem from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget @@ -551,10 +552,10 @@ class WCSafeTInitMethod(WalletWizardComponent): ).format(_info.model_name, _info.model_name) choices = [ # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) + ChoiceItem(key=TIM_NEW, label=_("Let the device generate a completely new seed randomly")), + ChoiceItem(key=TIM_RECOVER, label=_("Recover from a seed you have previously written down")), + ChoiceItem(key=TIM_MNEMONIC, label=_("Upload a BIP39 mnemonic to generate the seed")), + ChoiceItem(key=TIM_PRIVKEY, label=_("Upload a master private key")) ] self.choice_w = ChoiceWidget(message=msg, choices=choices) self.layout().addWidget(self.choice_w) diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 0ea4220b6..471aae59a 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -12,6 +12,7 @@ from electrum.i18n import _ from electrum.logging import Logger from electrum.plugin import hook from electrum.keystore import ScriptTypeNotSupported +from electrum.util import ChoiceItem from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.hw_wallet.trezor_qt_pinmatrix import PinMatrixWidget @@ -847,8 +848,8 @@ class WCTrezorInitMethod(WalletWizardComponent, Logger): message = _('Choose how you want to initialize your {}.').format(_info.model_name) choices = [ # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), + ChoiceItem(key=TIM_NEW, label=_("Let the device generate a completely new seed randomly")), + ChoiceItem(key=TIM_RECOVER, label=_("Recover from a seed you have previously written down")), ] self.choice_w = ChoiceWidget(message=message, choices=choices) self.layout().addWidget(self.choice_w) diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index db393fcd9..5ce9373b6 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -34,7 +34,7 @@ from PyQt6.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxL from electrum.i18n import _ from electrum.plugin import hook -from electrum.util import InvalidPassword +from electrum.util import InvalidPassword, ChoiceItem from electrum.logging import Logger, get_logger from electrum import keystore @@ -357,8 +357,8 @@ class WCChooseSeed(WalletWizardComponent): WalletWizardComponent.__init__(self, parent, wizard, title=_('Create or restore')) message = _('Do you want to create a new seed, or restore a wallet using an existing seed?') choices = [ - ('createseed', _('Create a new seed')), - ('haveseed', _('I already have a seed')), + ChoiceItem(key='createseed', label=_('Create a new seed')), + ChoiceItem(key='haveseed', label=_('I already have a seed')), ] self.choice_w = ChoiceWidget(message=message, choices=choices) diff --git a/electrum/util.py b/electrum/util.py index 0af4a70ae..be97c6949 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -22,6 +22,7 @@ # SOFTWARE. import binascii import concurrent.futures +from dataclasses import dataclass import logging import os, sys, re from collections import defaultdict, OrderedDict @@ -2311,3 +2312,10 @@ class LightningHistoryItem(NamedTuple): 'ln_value': Satoshis(Decimal(self.amount_msat) / 1000), 'direction': self.direction, } + + +@dataclass(kw_only=True, slots=True) +class ChoiceItem: + key: Any + label: str # user facing string + extra_data: Any = None From 42d810bc7d4f7cc18c5c9871475530b59b0995de Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 May 2025 17:56:52 +0000 Subject: [PATCH 2/2] refactor qt.util.ChoiceWidget: rename arg to default_key --- electrum/gui/qt/main_window.py | 8 ++++---- electrum/gui/qt/receive_tab.py | 2 +- electrum/gui/qt/seed_dialog.py | 2 +- electrum/gui/qt/util.py | 10 +++++----- electrum/gui/qt/wizard/wallet.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 51e129901..39ac578c6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1332,10 +1332,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): _("Note that fees may be updated frequently.") ]) choice = self.query_choice( - msg = msg, - choices = server_keys, - title = _("Choose Swap Server"), - default_choice = self.config.SWAPSERVER_NPUB + msg=msg, + choices=server_keys, + title=_("Choose Swap Server"), + default_key=self.config.SWAPSERVER_NPUB, ) if choice is None: return False diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 51fe35606..bd4f62f86 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -197,7 +197,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS choices = [ChoiceItem(key=exptime, label=label) for (exptime, label) in pr_expiration_values().items()] - v = self.window.query_choice(msg, choices, title=_('Expiry'), default_choice=expiry) + v = self.window.query_choice(msg, choices, title=_('Expiry'), default_key=expiry) if v is None: return self.config.WALLET_PAYREQ_EXPIRY_SECONDS = v diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 4d71d648c..3a93363e0 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -209,7 +209,7 @@ class SeedWidget(QWidget): self.initialize_completer() if len(self.seed_types) > 1: - seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=self.seed_types, selected=self.seed_type) + seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=self.seed_types, default_key=self.seed_type) seed_type_choice.itemSelected.connect(on_selected) vbox.addWidget(seed_type_choice) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 7a30690d7..ca333697f 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -321,7 +321,7 @@ class MessageBoxMixin(object): choices: Sequence['ChoiceItem'], *, title: Optional[str] = None, - default_choice: Optional[Any] = None, + default_key: Optional[Any] = None, ) -> Optional[Any]: """Returns ChoiceItem.key (for selected item), or None if the user cancels the dialog. @@ -331,7 +331,7 @@ class MessageBoxMixin(object): title = _('Question') dialog = WindowModalDialog(self.top_level_window(), title=title) dialog.setMinimumWidth(400) - choice_widget = ChoiceWidget(message=msg, choices=choices, selected=default_choice) + choice_widget = ChoiceWidget(message=msg, choices=choices, default_key=default_key) vbox = QVBoxLayout(dialog) vbox.addWidget(choice_widget) cancel_button = CancelButton(dialog) @@ -513,7 +513,7 @@ def text_dialog( class ChoiceWidget(QWidget): """Renders a list of ChoiceItems as a radiobuttons group. - Callers can pre-select an item by key, through the 'selected' parameter. + Callers can pre-select an item by key, through the 'default_key' parameter. The selected item is made available by index (selected_index), by key (selected_key) and by Choice (selected_item). """ @@ -525,7 +525,7 @@ class ChoiceWidget(QWidget): *, message: Optional[str] = None, choices: Sequence[ChoiceItem] = None, - selected: Optional[Any] = None, + default_key: Optional[Any] = None, ): QWidget.__init__(self) vbox = QVBoxLayout() @@ -555,7 +555,7 @@ class ChoiceWidget(QWidget): vbox2.addWidget(button) group.addButton(button) group.setId(button, i) - if (i == 0 and selected is None) or c.key == selected: + if (i == 0 and default_key is None) or c.key == default_key: self.selected_index = i self.selected_item = c self.selected_key = c.key diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 96eb6264e..dc6b3092e 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -406,7 +406,7 @@ class WCWalletType(WalletWizardComponent): ] choices = [c for c in wallet_kinds if c.key in wallet_types] - self.choice_w = ChoiceWidget(message=message, choices=choices, selected='standard') + self.choice_w = ChoiceWidget(message=message, choices=choices, default_key='standard') self.layout().addWidget(self.choice_w) self.layout().addStretch(1) self._valid = True @@ -729,7 +729,7 @@ class WCScriptAndDerivation(WalletWizardComponent, Logger): def on_choice_click(index): self.derivation_path_edit.setText(self.choice_w.selected_item.extra_data) - self.choice_w = ChoiceWidget(message=message1, choices=choices, selected=default_choice) + self.choice_w = ChoiceWidget(message=message1, choices=choices, default_key=default_choice) self.choice_w.itemSelected.connect(on_choice_click) if not hide_choices: