diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 88a152175..9b8f7b4fb 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -34,7 +34,7 @@ import base64 from functools import partial import queue import asyncio -from typing import Optional, TYPE_CHECKING, Sequence, List, Union, Dict, Set, Mapping +from typing import Optional, TYPE_CHECKING, Sequence, Union, Dict, Mapping import concurrent.futures from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont, QFontMetrics @@ -45,7 +45,7 @@ from PyQt5.QtWidgets import (QMessageBox, QSystemTrayIcon, QTabWidget, QHBoxLayout, QPushButton, QScrollArea, QTextEdit, QShortcut, QMainWindow, QInputDialog, QWidget, QSizePolicy, QStatusBar, QToolTip, - QMenu, QAction, QStackedWidget, QToolButton) + QMenu, QAction, QToolButton) import electrum from electrum.gui import messages @@ -82,7 +82,7 @@ from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit, ScanShowQRTextEdit from .transaction_dialog import show_transaction from .fee_slider import FeeSlider, FeeComboBox from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, - WindowModalDialog, ChoicesLayout, HelpLabel, Buttons, + WindowModalDialog, HelpLabel, Buttons, OkButton, InfoButton, WWLabel, TaskThread, CancelButton, CloseButton, HelpButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui, @@ -1370,22 +1370,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): else: self.show_message(message) - def query_choice(self, msg, choices, title=None, default_choice=None): - # Needed by QtHandler for hardware wallets - if title is None: - title = _('Question') - dialog = WindowModalDialog(self.top_level_window(), title=title) - dialog.setMinimumWidth(400) - clayout = ChoicesLayout(msg, choices, checked_index=default_choice) - vbox = QVBoxLayout(dialog) - vbox.addLayout(clayout.layout()) - cancel_button = CancelButton(dialog) - vbox.addLayout(Buttons(cancel_button, OkButton(dialog))) - cancel_button.setFocus() - if not dialog.exec_(): - return None - return clayout.selected_index() - def handle_payment_identifier(self, text: str): pi = PaymentIdentifier(self.wallet, text) if pi.is_valid(): diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 79c71ae52..cda97d188 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -194,7 +194,8 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): _('For Lightning requests, payments will not be accepted after the expiration.'), ]) expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS - v = self.window.query_choice(msg, pr_expiration_values(), title=_('Expiry'), default_choice=expiry) + choices = list(pr_expiration_values().items()) + v = self.window.query_choice(msg, choices, title=_('Expiry'), default_choice=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 68f645a7e..e97881b1f 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -37,8 +37,7 @@ from electrum import old_mnemonic from electrum import slip39 from .util import (Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path, - EnterButton, CloseButton, WindowModalDialog, ColorScheme, - ChoicesLayout, font_height) + EnterButton, CloseButton, WindowModalDialog, ColorScheme, font_height, ChoiceWidget) from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -87,15 +86,15 @@ class SeedLayout(QVBoxLayout): ) if value in self.options or value == 'electrum' ] - seed_type_values = [t[0] for t in seed_types] if 'ext' in self.options: cb_ext = QCheckBox(_('Extend this seed with custom words')) cb_ext.setChecked(self.is_ext) vbox.addWidget(cb_ext) + if len(seed_types) >= 2: - def f(choices_layout): - self.seed_type = seed_type_values[choices_layout.selected_index()] + def on_selected(idx): + self.seed_type = seed_type_choice.selected_key self.is_seed = (lambda x: bool(x)) if self.seed_type != 'electrum' else self.saved_is_seed self.slip39_current_mnemonic_invalid = None self.seed_status.setText('') @@ -120,16 +119,15 @@ class SeedLayout(QVBoxLayout): self.initialize_completer() self.seed_warning.setText(msg) - checked_index = seed_type_values.index(self.seed_type) - titles = [t[1] for t in seed_types] - clayout = ChoicesLayout(_('Seed type'), titles, on_clicked=f, checked_index=checked_index) - vbox.addLayout(clayout.layout()) + seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=seed_types, selected=self.seed_type) + seed_type_choice.itemSelected.connect(on_selected) + vbox.addWidget(seed_type_choice) vbox.addLayout(Buttons(OkButton(dialog))) if not dialog.exec_(): return None self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False - self.seed_type = seed_type_values[clayout.selected_index()] if len(seed_types) >= 2 else 'electrum' + self.seed_type = seed_type_choice.selected_key if len(seed_types) >= 2 else 'electrum' self.updated.emit() def __init__( diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index e14ae70e2..8b59c3ed3 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -672,31 +672,31 @@ 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 = [] if can_rebalance: msg = ''.join([ _('Rebalance existing channels'), '\n', _('Move funds between your channels in order to increase your sending capacity.') ]) - choices[0] = msg + choices.append(('rebalance', 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[1] = msg + choices.append(('new_channel', 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[2] = msg + choices.append(('swap', msg)) if can_pay_onchain: msg = ''.join([ _('Pay onchain'), '\n', _('Funds will be sent to the invoice fallback address.') ]) - choices[3] = msg + choices.append(('onchain', msg)) msg = _('You cannot pay that invoice using Lightning.') if lnworker and lnworker.channels: num_sats_can_send = int(lnworker.num_sats_can_send()) @@ -709,16 +709,16 @@ class SendTab(QWidget, MessageBoxMixin, Logger): r = self.window.query_choice(msg, choices) if r is not None: self.save_pending_invoice() - if r == 0: + if r == 'rebalance': chan1, chan2, delta = rebalance_suggestion self.window.rebalance_dialog(chan1, chan2, amount_sat=delta) - elif r == 1: + elif r == 'new_channel': amount_sat, min_amount_sat = can_pay_with_new_channel self.window.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat) - elif r == 2: + elif r == 'swap': chan, swap_recv_amount_sat = can_pay_with_swap self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) - elif r == 3: + elif r == 'onchain': self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True) return diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 28392fa23..27c38bf11 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -283,6 +283,26 @@ 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 + 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) + vbox = QVBoxLayout(dialog) + vbox.addWidget(choice_widget) + cancel_button = CancelButton(dialog) + vbox.addLayout(Buttons(cancel_button, OkButton(dialog))) + cancel_button.setFocus() + if not dialog.exec_(): + return None + return choice_widget.selected_key + def custom_message_box(*, icon, parent, title, text, buttons=QMessageBox.Ok, defaultButton=QMessageBox.NoButton, rich_text=False, @@ -422,44 +442,19 @@ def text_dialog( return txt.toPlainText() -class ChoicesLayout(object): - def __init__(self, msg, choices, on_clicked=None, checked_index=0): - vbox = QVBoxLayout() - if len(msg) > 50: - vbox.addWidget(WWLabel(msg)) - msg = "" - gb2 = QGroupBox(msg) - vbox.addWidget(gb2) - vbox2 = QVBoxLayout() - gb2.setLayout(vbox2) - self.group = group = QButtonGroup(gb2) - if isinstance(choices, list): - iterator = enumerate(choices) - else: - iterator = choices.items() - for i, c in iterator: - button = QRadioButton(gb2) - button.setText(c) - vbox2.addWidget(button) - group.addButton(button) - group.setId(button, i) - if i == checked_index: - button.setChecked(True) - if on_clicked: - group.buttonClicked.connect(partial(on_clicked, self)) - self.vbox = vbox - - def layout(self): - return self.vbox - - def selected_index(self): - return self.group.checkedId() - - 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. + The selected item is made available by index (selected_index), + by key (selected_key) and by whole tuple (selected_item). + """ + itemSelected = pyqtSignal([int], arguments=['index']) - def __init__(self, *, message=None, choices=None, selected=None): + def __init__(self, *, message: Optional[str] = None, choices: Sequence[Tuple] = None, selected: Optional[Any] = None): QWidget.__init__(self) vbox = QVBoxLayout() self.setLayout(vbox) @@ -467,9 +462,9 @@ class ChoiceWidget(QWidget): if choices is None: choices = [] - self.selected_index = -1 - self.selected_item = None - self.selected_key = None + self.selected_index = -1 # int + self.selected_item = None # Optional[Tuple] + self.selected_key = None # Optional[Any] self.choices = choices diff --git a/electrum/gui/qt/wallet_info_dialog.py b/electrum/gui/qt/wallet_info_dialog.py index 545e44f05..2247cf374 100644 --- a/electrum/gui/qt/wallet_info_dialog.py +++ b/electrum/gui/qt/wallet_info_dialog.py @@ -9,14 +9,13 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, QPushButton, QWidget, QStackedWidget) -from electrum import keystore from electrum.plugin import run_hook from electrum.i18n import _ from electrum.wallet import Multisig_Wallet from .qrtextedit import ShowQRTextEdit -from .util import (read_QIcon, WindowModalDialog, ChoicesLayout, Buttons, - WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit) +from .util import (read_QIcon, WindowModalDialog, Buttons, + WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit, ChoiceWidget) if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -118,11 +117,11 @@ class WalletInfoDialog(WindowModalDialog): else: return _("keystore") + f' {idx+1}' - labels = [label(idx, ks) for idx, ks in enumerate(wallet.get_keystores())] + labels = [(idx, label(idx, ks)) for idx, ks in enumerate(wallet.get_keystores())] - on_click = lambda clayout: select_ks(clayout.selected_index()) - labels_clayout = ChoicesLayout(_("Select keystore"), labels, on_click) - vbox.addLayout(labels_clayout.layout()) + keystore_choice = ChoiceWidget(message=_("Select keystore"), choices=labels) + keystore_choice.itemSelected.connect(lambda x: select_ks(x)) + vbox.addWidget(keystore_choice) for ks in keystores: ks_w = QWidget() diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 749b49969..f39e96959 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -29,7 +29,7 @@ from electrum.gui.qt.bip39_recovery_dialog import Bip39RecoveryDialog from electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW from electrum.gui.qt.seed_dialog import SeedLayout, MSG_PASSPHRASE_WARN_ISSUE4566, KeysLayout from electrum.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height, - ChoiceWidget, MessageBoxMixin, WindowModalDialog, ChoicesLayout, CancelButton, + ChoiceWidget, MessageBoxMixin, WindowModalDialog, CancelButton, Buttons, OkButton, icon_path) if TYPE_CHECKING: @@ -237,22 +237,6 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard, MessageBoxMixin): if on_finished: on_finished() - def query_choice(self, msg, choices, title=None, default_choice=None): - # Needed by QtHandler for hardware wallets - if title is None: - title = _('Question') - dialog = WindowModalDialog(self.top_level_window(), title=title) - dialog.setMinimumWidth(400) - clayout = ChoicesLayout(msg, choices, checked_index=default_choice) - vbox = QVBoxLayout(dialog) - vbox.addLayout(clayout.layout()) - cancel_button = CancelButton(dialog) - vbox.addLayout(Buttons(cancel_button, OkButton(dialog))) - cancel_button.setFocus() - if not dialog.exec_(): - return None - return clayout.selected_index() - class WalletWizardComponent(WizardComponent, ABC): # ^ this class only exists to help with typing diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py index d72389b23..38826297c 100644 --- a/electrum/plugins/digitalbitbox/qt.py +++ b/electrum/plugins/digitalbitbox/qt.py @@ -69,6 +69,10 @@ 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/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index aedd16d61..9656019a0 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -26,7 +26,7 @@ import threading from functools import partial -from typing import TYPE_CHECKING, Union, Optional +from typing import TYPE_CHECKING, Union, Optional, Sequence, Tuple from PyQt5.QtCore import QObject, pyqtSignal, Qt from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel @@ -95,7 +95,7 @@ class QtHandlerBase(HardwareHandlerBase, QObject, Logger): icon_name = button.icon_paired if paired else button.icon_unpaired button.setIcon(read_QIcon(icon_name)) - def query_choice(self, msg, labels): + def query_choice(self, msg: str, labels: Sequence[Tuple]): self.done.clear() self.query_signal.emit(msg, labels) self.done.wait() @@ -194,7 +194,7 @@ class QtHandlerBase(HardwareHandlerBase, QObject, Logger): self.dialog.accept() self.dialog = None - def win_query_choice(self, msg, labels): + def win_query_choice(self, msg: str, labels: Sequence[Tuple]): try: self.choice = self.win.query_choice(msg, labels) except UserCancelled: