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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user