1
0

refactor qt.util.ChoiceWidget: introduce ChoiceItem

This commit is contained in:
SomberNight
2025-05-06 17:47:11 +00:00
parent ef49bb2109
commit ba3783f998
18 changed files with 153 additions and 116 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,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.")

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

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'

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

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