1
0

Merge branch 202505_choiceitem': refactor qt.util.ChoiceWidget

This introduces util.ChoiceItem, which makes query_choice()
and ChoiceWidget better type-able and hence statically analysable.
This commit is contained in:
SomberNight
2025-05-06 18:13:15 +00:00
18 changed files with 163 additions and 126 deletions

View File

@@ -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,16 +1326,16 @@ 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.")
])
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

View File

@@ -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,8 +195,9 @@ 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())
v = self.window.query_choice(msg, choices, title=_('Expiry'), default_choice=expiry)
choices = [ChoiceItem(key=exptime, label=label)
for (exptime, label) in pr_expiration_values().items()]
v = self.window.query_choice(msg, choices, title=_('Expiry'), default_key=expiry)
if v is None:
return
self.config.WALLET_PAYREQ_EXPIRY_SECONDS = v

View File

@@ -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'
@@ -206,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)

View File

@@ -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())

View File

@@ -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,17 +315,23 @@ 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_key: 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)
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)
@@ -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.
Callers can pre-select an item by key, through the 'selected' parameter.
"""Renders a list of ChoiceItems as a radiobuttons group.
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 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,
default_key: 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 default_key is None) or c.key == default_key:
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()

View File

@@ -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))

View File

@@ -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,14 +399,14 @@ 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.choice_w = ChoiceWidget(message=message, choices=choices, default_key='standard')
self.layout().addWidget(self.choice_w)
self.layout().addStretch(1)
self._valid = True
@@ -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,8 +728,8 @@ 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.choice_w = ChoiceWidget(message=message1, choices=choices, selected=default_choice)
self.derivation_path_edit.setText(self.choice_w.selected_item.extra_data)
self.choice_w = ChoiceWidget(message=message1, choices=choices, default_key=default_choice)
self.choice_w.itemSelected.connect(on_choice_click)
if not hide_choices:
@@ -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:

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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