From 5b29e6d4f56abde76d38c398d35dd6ce2d67d0e5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 1 Jul 2022 16:03:28 +0200 Subject: [PATCH] qt: (refactor) split "receive tab" out from main_window.py --- electrum/gui/qt/invoice_list.py | 5 +- electrum/gui/qt/main_window.py | 461 ++-------------------------- electrum/gui/qt/rebalance_dialog.py | 11 +- electrum/gui/qt/receive_tab.py | 422 +++++++++++++++++++++++++ electrum/gui/qt/request_list.py | 29 +- electrum/gui/qt/send_tab.py | 2 + electrum/plugins/hw_wallet/qt.py | 2 +- 7 files changed, 485 insertions(+), 447 deletions(-) create mode 100644 electrum/gui/qt/receive_tab.py diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 096346118..3ea804398 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -71,6 +71,8 @@ class InvoiceList(MyTreeView): window = send_tab.window super().__init__(window, self.create_menu, stretch_column=self.Columns.DESCRIPTION) + self.wallet = window.wallet + self.send_tab = send_tab self.std_model = QStandardItemModel(self) self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) self.proxy.setSourceModel(self.std_model) @@ -78,9 +80,6 @@ class InvoiceList(MyTreeView): self.setSortingEnabled(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.send_tab = send_tab - self.wallet = window.wallet - def refresh_row(self, key, row): assert row is not None invoice = self.wallet.invoices.get(key) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 6d47d64d7..62d4a4c19 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -28,7 +28,6 @@ import threading import os import traceback import json -import shutil import weakref import csv from decimal import Decimal @@ -38,17 +37,15 @@ import queue import asyncio from typing import Optional, TYPE_CHECKING, Sequence, List, Union, Dict, Set import concurrent.futures -from urllib.parse import urlparse from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont -from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal, QPoint -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget, +from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal +from PyQt5.QtWidgets import (QMessageBox, QSystemTrayIcon, QTabWidget, QMenuBar, QFileDialog, QCheckBox, QLabel, QVBoxLayout, QGridLayout, QLineEdit, QHBoxLayout, QPushButton, QScrollArea, QTextEdit, - QShortcut, QMainWindow, QCompleter, QInputDialog, - QWidget, QSizePolicy, QStatusBar, QToolTip, QDialog, + QShortcut, QMainWindow, QInputDialog, + QWidget, QSizePolicy, QStatusBar, QToolTip, QMenu, QAction, QStackedWidget, QToolButton) import electrum @@ -63,30 +60,24 @@ from electrum.util import (format_time, get_asyncio_loop, bh2u, bfh, InvalidPassword, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, - InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds, - NoDynamicFeeEstimates, - AddTransactionException, BITCOIN_BIP21_URI_SCHEME, - InvoiceError, parse_max_spend) -from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice -from electrum.invoices import PR_PAID, PR_UNPAID, PR_FAILED, PR_EXPIRED, pr_expiration_values, Invoice + AddTransactionException, BITCOIN_BIP21_URI_SCHEME) +from electrum.invoices import PR_PAID, Invoice from electrum.transaction import (Transaction, PartialTxInput, PartialTransaction, PartialTxOutput) -from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, +from electrum.wallet import (Multisig_Wallet, Abstract_Wallet, sweep_preparations, InternalAddressCorruption, - CannotDoubleSpendTx, CannotCPFP) + CannotCPFP) from electrum.version import ELECTRUM_VERSION -from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed, - UntrustedServerReturnedError, NetworkException) +from electrum.network import Network, UntrustedServerReturnedError, NetworkException from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig from electrum.logging import Logger from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError -from electrum.lnaddr import lndecode, LnInvoiceException -from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data +from electrum.lnaddr import lndecode from .exception_window import Exception_Hook -from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit -from .qrcodewidget import QRCodeWidget, QRDialog +from .amountedit import BTCAmountEdit +from .qrcodewidget import QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit, ScanShowQRTextEdit from .transaction_dialog import show_transaction from .fee_slider import FeeSlider, FeeComboBox @@ -98,14 +89,13 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo filename_field, address_field, char_width_in_lineedit, webopen, TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, getOpenFileName, getSaveFileName, BlockingWaitingDialog) -from .util import ButtonsTextEdit, ButtonsLineEdit +from .util import ButtonsLineEdit from .util import QtEventListener, qt_event_listener, event_listener from .installwizard import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog -from .transaction_dialog import PreviewTxDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog from .qrreader import scan_qrcode from .swap_dialog import SwapDialog @@ -141,36 +131,6 @@ class StatusBarButton(QToolButton): if e.key() in [Qt.Key_Return, Qt.Key_Enter]: self.func() -class ReceiveTabWidget(QWidget): - min_size = QSize(200, 200) - def __init__(self, window, textedit, qr, help_widget): - self.textedit = textedit - self.qr = qr - self.help_widget = help_widget - QWidget.__init__(self) - for w in [textedit, qr, help_widget]: - w.setMinimumSize(self.min_size) - for w in [textedit, qr]: - w.mousePressEvent = window.toggle_receive_qr - tooltip = _('Click to switch between text and QR code view') - w.setToolTip(tooltip) - textedit.setFocusPolicy(Qt.NoFocus) - hbox = QHBoxLayout() - hbox.setContentsMargins(0, 0, 0, 0) - hbox.addWidget(textedit) - hbox.addWidget(help_widget) - hbox.addWidget(qr) - self.setLayout(hbox) - - def update_visibility(self, is_qr): - if str(self.textedit.text()): - self.help_widget.setVisible(False) - self.textedit.setVisible(not is_qr) - self.qr.setVisible(is_qr) - else: - self.help_widget.setVisible(True) - self.textedit.setVisible(False) - self.qr.setVisible(False) def protected(func): '''Password request wrapper. The password is passed to the function @@ -365,7 +325,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): # Refresh edits with the new rate edit = self.send_tab.fiat_send_e if self.send_tab.fiat_send_e.is_last_edited else self.send_tab.amount_e edit.textEdited.emit(edit.text()) - edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e + edit = self.receive_tab.fiat_receive_e if self.receive_tab.fiat_receive_e.is_last_edited else self.receive_tab.receive_amount_e edit.textEdited.emit(edit.text()) # History tab needs updating if it used spot if self.fx.history_used_spot: @@ -528,8 +488,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.update_lock_icon() self.update_buttons_on_seed() self.update_console() - self.clear_receive_tab() - self.request_list.update() + self.receive_tab.clear_receive_tab() + self.receive_tab.request_list.update() self.channels_list.update() self.tabs.show() self.init_geometry() @@ -893,7 +853,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def timer_actions(self): # refresh invoices and requests because they show ETA - self.request_list.refresh_all() + self.receive_tab.request_list.refresh_all() self.send_tab.invoice_list.refresh_all() # Note this runs in the GUI thread if self.need_update.is_set(): @@ -938,7 +898,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def base_unit(self): return self.config.get_base_unit() - def connect_fields(self, window, btc_e, fiat_e, fee_e): + def connect_fields(self, btc_e, fiat_e): def edit_changed(edit): if edit.follows: @@ -950,8 +910,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if rate.is_nan() or amount is None: if edit is fiat_e: btc_e.setText("") - if fee_e: - fee_e.setText("") else: fiat_e.setText("") else: @@ -960,8 +918,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): btc_e.setAmount(int(amount / Decimal(rate) * COIN)) btc_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) btc_e.follows = False - if fee_e: - window.update_fee() else: fiat_e.follows = True fiat_e.setText(self.fx.ccy_amount_str( @@ -1064,8 +1020,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if wallet != self.wallet: return self.history_model.refresh('update_tabs') - self.request_list.update() - self.update_current_request() + self.receive_tab.request_list.update() + self.receive_tab.update_current_request() self.send_tab.invoice_list.update() self.address_list.update() self.utxo_list.update() @@ -1075,7 +1031,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def refresh_tabs(self, wallet=None): self.history_model.refresh('refresh_tabs') - self.request_list.refresh_all() + self.receive_tab.request_list.refresh_all() self.send_tab.invoice_list.refresh_all() self.address_list.refresh_all() self.utxo_list.refresh_all() @@ -1116,345 +1072,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): d = LightningTxDialog(self, tx_item) d.show() - def toggle_receive_qr(self, e): - b = not self.config.get('receive_qr_visible', False) - self.config.set_key('receive_qr_visible', b) - self.update_receive_widgets() - - def update_receive_widgets(self): - b = self.config.get('receive_qr_visible', False) - self.receive_URI_widget.update_visibility(b) - self.receive_address_widget.update_visibility(b) - self.receive_lightning_widget.update_visibility(b) - def create_receive_tab(self): - # A 4-column grid layout. All the stretch is in the last column. - # The exchange rate plugin adds a fiat widget in column 2 - self.receive_grid = grid = QGridLayout() - grid.setSpacing(8) - grid.setColumnStretch(3, 1) - - self.receive_message_e = SizedFreezableLineEdit(width=400) - grid.addWidget(QLabel(_('Description')), 0, 0) - grid.addWidget(self.receive_message_e, 0, 1, 1, 4) - - self.receive_amount_e = BTCAmountEdit(self.get_decimal_point) - grid.addWidget(QLabel(_('Requested amount')), 1, 0) - grid.addWidget(self.receive_amount_e, 1, 1) - - self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '') - if not self.fx or not self.fx.is_enabled(): - self.fiat_receive_e.setVisible(False) - grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft) - - self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None) - self.connect_fields(self, self.send_tab.amount_e, self.send_tab.fiat_send_e, None) - - self.expires_combo = QComboBox() - evl = sorted(pr_expiration_values.items()) - evl_keys = [i[0] for i in evl] - evl_values = [i[1] for i in evl] - default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - try: - i = evl_keys.index(default_expiry) - except ValueError: - i = 0 - self.expires_combo.addItems(evl_values) - self.expires_combo.setCurrentIndex(i) - def on_expiry(i): - self.config.set_key('request_expiry', evl_keys[i]) - self.expires_combo.currentIndexChanged.connect(on_expiry) - msg = ''.join([ - _('Expiration date of your request.'), ' ', - _('This information is seen by the recipient if you send them a signed payment request.'), - '\n\n', - _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', - _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ', - _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'), - '\n\n', - _('For Lightning requests, payments will not be accepted after the expiration.'), - ]) - grid.addWidget(HelpLabel(_('Expires after') + ' (?)', msg), 2, 0) - grid.addWidget(self.expires_combo, 2, 1) - self.expires_label = QLineEdit('') - self.expires_label.setReadOnly(1) - self.expires_label.setFocusPolicy(Qt.NoFocus) - self.expires_label.hide() - grid.addWidget(self.expires_label, 2, 1) - - self.clear_invoice_button = QPushButton(_('Clear')) - self.clear_invoice_button.clicked.connect(self.clear_receive_tab) - self.create_invoice_button = QPushButton(_('Create Request')) - self.create_invoice_button.clicked.connect(lambda: self.create_invoice()) - self.receive_buttons = buttons = QHBoxLayout() - buttons.addStretch(1) - buttons.addWidget(self.clear_invoice_button) - buttons.addWidget(self.create_invoice_button) - grid.addLayout(buttons, 4, 0, 1, -1) - - self.receive_address_e = ButtonsTextEdit() - self.receive_address_help_text = WWLabel('') - vbox = QVBoxLayout() - vbox.addWidget(self.receive_address_help_text) - self.receive_address_help = QWidget() - self.receive_address_help.setVisible(False) - self.receive_address_help.setLayout(vbox) - - self.receive_URI_e = ButtonsTextEdit() - self.receive_URI_help = WWLabel('') - self.receive_lightning_e = ButtonsTextEdit() - self.receive_lightning_help_text = WWLabel('') - self.receive_rebalance_button = QPushButton('Rebalance') - self.receive_rebalance_button.suggestion = None - def on_receive_rebalance(): - if self.receive_rebalance_button.suggestion: - chan1, chan2, delta = self.receive_rebalance_button.suggestion - self.rebalance_dialog(chan1, chan2, amount_sat=delta) - self.receive_rebalance_button.clicked.connect(on_receive_rebalance) - self.receive_swap_button = QPushButton('Swap') - self.receive_swap_button.suggestion = None - def on_receive_swap(): - if self.receive_swap_button.suggestion: - chan, swap_recv_amount_sat = self.receive_swap_button.suggestion - self.run_swap_dialog(is_reverse=True, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) - self.receive_swap_button.clicked.connect(on_receive_swap) - buttons = QHBoxLayout() - buttons.addWidget(self.receive_rebalance_button) - buttons.addWidget(self.receive_swap_button) - vbox = QVBoxLayout() - vbox.addWidget(self.receive_lightning_help_text) - vbox.addLayout(buttons) - self.receive_lightning_help = QWidget() - self.receive_lightning_help.setVisible(False) - self.receive_lightning_help.setLayout(vbox) - self.receive_address_qr = QRCodeWidget() - self.receive_URI_qr = QRCodeWidget() - self.receive_lightning_qr = QRCodeWidget() - - for e in [self.receive_address_e, self.receive_URI_e, self.receive_lightning_e]: - e.setFont(QFont(MONOSPACE_FONT)) - e.addCopyButton() - e.setReadOnly(True) - - self.receive_lightning_e.textChanged.connect(self.update_receive_widgets) - - self.receive_address_widget = ReceiveTabWidget(self, - self.receive_address_e, self.receive_address_qr, self.receive_address_help) - self.receive_URI_widget = ReceiveTabWidget(self, - self.receive_URI_e, self.receive_URI_qr, self.receive_URI_help) - self.receive_lightning_widget = ReceiveTabWidget(self, - self.receive_lightning_e, self.receive_lightning_qr, self.receive_lightning_help) - - from .util import VTabWidget - self.receive_tabs = VTabWidget() - self.receive_tabs.setMinimumHeight(ReceiveTabWidget.min_size.height() + 4) # for margins - self.receive_tabs.addTab(self.receive_URI_widget, read_QIcon("link.png"), _('URI')) - self.receive_tabs.addTab(self.receive_address_widget, read_QIcon("bitcoin.png"), _('Address')) - self.receive_tabs.addTab(self.receive_lightning_widget, read_QIcon("lightning.png"), _('Lightning')) - self.receive_tabs.currentChanged.connect(self.update_receive_qr_window) - self.receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) - self.receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) - receive_tabs_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) - receive_tabs_sp.setRetainSizeWhenHidden(True) - self.receive_tabs.setSizePolicy(receive_tabs_sp) - self.receive_tabs.setVisible(False) - - self.receive_requests_label = QLabel(_('Receive queue')) - from .request_list import RequestList - self.request_list = RequestList(self) - - # layout - vbox_g = QVBoxLayout() - vbox_g.addLayout(grid) - vbox_g.addStretch() - hbox = QHBoxLayout() - hbox.addLayout(vbox_g) - hbox.addStretch() - hbox.addWidget(self.receive_tabs) - - w = QWidget() - w.searchable_list = self.request_list - vbox = QVBoxLayout(w) - vbox.addLayout(hbox) - vbox.addStretch() - vbox.addWidget(self.receive_requests_label) - vbox.addWidget(self.request_list) - vbox.setStretchFactor(hbox, 40) - vbox.setStretchFactor(self.request_list, 60) - self.request_list.update() # after parented and put into a layout, can update without flickering - - return w - - def update_current_request(self): - key = self.request_list.get_current_key() - req = self.wallet.get_request(key) if key else None - if req is None: - self.receive_URI_e.setText('') - self.receive_lightning_e.setText('') - self.receive_address_e.setText('') - return - addr = req.get_address() or '' - amount_sat = req.get_amount_sat() or 0 - address_help = '' if addr else _('Amount too small to be received onchain') - URI_help = '' - lnaddr = req.lightning_invoice - bip21_lightning = lnaddr if self.config.get('bip21_lightning', False) else None - URI = req.get_bip21_URI(lightning=bip21_lightning) - lightning_online = self.wallet.lnworker and self.wallet.lnworker.num_peers() > 0 - can_receive_lightning = self.wallet.lnworker and amount_sat <= self.wallet.lnworker.num_sats_can_receive() - has_expired = self.wallet.get_request_status(key) == PR_EXPIRED - if has_expired: - URI_help = ln_help = address_help = _('This request has expired') - URI = lnaddr = address = '' - can_rebalance = False - can_swap = False - elif lnaddr is None: - ln_help = _('This request does not have a Lightning invoice.') - lnaddr = '' - can_rebalance = False - can_swap = False - elif not lightning_online: - ln_help = _('You must be online to receive Lightning payments.') - lnaddr = '' - can_rebalance = False - can_swap = False - elif not can_receive_lightning: - self.receive_rebalance_button.suggestion = self.wallet.lnworker.suggest_rebalance_to_receive(amount_sat) - self.receive_swap_button.suggestion = self.wallet.lnworker.suggest_swap_to_receive(amount_sat) - can_rebalance = bool(self.receive_rebalance_button.suggestion) - can_swap = bool(self.receive_swap_button.suggestion) - lnaddr = '' - ln_help = _('You do not have the capacity to receive that amount with Lightning.') - if can_rebalance: - ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.') - elif can_swap: - ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.') - else: - ln_help = '' - can_rebalance = False - can_swap = False - self.receive_rebalance_button.setVisible(can_rebalance) - self.receive_swap_button.setVisible(can_swap) - self.receive_rebalance_button.setEnabled(can_rebalance and self.num_tasks() == 0) - self.receive_swap_button.setEnabled(can_swap and self.num_tasks() == 0) - icon_name = "lightning.png" if lnaddr else "lightning_disconnected.png" - self.receive_tabs.setTabIcon(2, read_QIcon(icon_name)) - # encode lightning invoices as uppercase so QR encoding can use - # alphanumeric mode; resulting in smaller QR codes - lnaddr_qr = lnaddr.upper() - self.receive_address_e.setText(addr) - self.update_receive_address_styling() - self.receive_address_qr.setData(addr) - self.receive_address_help_text.setText(address_help) - self.receive_URI_e.setText(URI) - self.receive_URI_qr.setData(URI) - self.receive_URI_help.setText(URI_help) - self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? - self.receive_lightning_help_text.setText(ln_help) - self.receive_lightning_qr.setData(lnaddr_qr) - # macOS hack (similar to #4777) - self.receive_lightning_e.repaint() - self.receive_URI_e.repaint() - self.receive_address_e.repaint() - # always show - self.receive_tabs.setVisible(True) - self.update_receive_qr_window() - - def update_receive_qr_window(self): - if self.qr_window and self.qr_window.isVisible(): - i = self.receive_tabs.currentIndex() - if i == 0: - data = self.receive_URI_qr.data - elif i == 1: - data = self.receive_address_qr.data - else: - data = self.receive_lightning_qr.data - self.qr_window.qrw.setData(data) - - def delete_requests(self, keys): - for key in keys: - self.wallet.delete_request(key) - self.request_list.delete_item(key) - self.clear_receive_tab() - - def sign_payment_request(self, addr): - alias = self.config.get('alias') - if alias and self.alias_info: - alias_addr, alias_name, validated = self.alias_info - if alias_addr: - if self.wallet.is_mine(alias_addr): - msg = _('This payment request will be signed.') + '\n' + _('Please enter your password') - password = None - if self.wallet.has_keystore_encryption(): - password = self.password_dialog(msg) - if not password: - return - try: - self.wallet.sign_payment_request(addr, alias, alias_addr, password) - except Exception as e: - self.show_error(repr(e)) - return - else: - return - - def create_invoice(self): - amount_sat = self.receive_amount_e.get_amount() - message = self.receive_message_e.text() - expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - - if amount_sat and amount_sat < self.wallet.dust_threshold(): - address = None - if not self.wallet.has_lightning(): - return - else: - address = self.get_bitcoin_address_for_request(amount_sat) - if not address: - return - self.address_list.update() - - # generate even if we cannot receive - lightning = self.wallet.has_lightning() - try: - key = self.wallet.create_request(amount_sat, message, expiry, address, lightning=lightning) - except InvoiceError as e: - self.show_error(_('Error creating payment request') + ':\n' + str(e)) - return - except Exception as e: - self.logger.exception('Error adding payment request') - self.show_error(_('Error adding payment request') + ':\n' + repr(e)) - return - self.sign_payment_request(address) - assert key is not None - self.address_list.refresh_all() - self.request_list.update() - self.request_list.set_current_key(key) - # clear request fields - self.receive_amount_e.setText('') - self.receive_message_e.setText('') - # copy to clipboard - r = self.wallet.get_request(key) - content = r.lightning_invoice if r.is_lightning() else r.get_address() - title = _('Invoice') if r.is_lightning() else _('Address') - self.do_copy(content, title=title) - - def get_bitcoin_address_for_request(self, amount) -> Optional[str]: - addr = self.wallet.get_unused_address() - if addr is None: - if not self.wallet.is_deterministic(): # imported wallet - msg = [ - _('No more addresses in your wallet.'), ' ', - _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', - _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', - _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), - ] - if not self.question(''.join(msg)): - return - addr = self.wallet.get_receiving_address() - else: # deterministic wallet - if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): - return - addr = self.wallet.create_new_address(False) - return addr + from .receive_tab import ReceiveTab + return ReceiveTab(self) def do_copy(self, content: str, *, title: str = None) -> None: self.app.clipboard().setText(content) @@ -1464,17 +1084,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): tooltip_text = _("{} copied to clipboard").format(title) QToolTip.showText(QCursor.pos(), tooltip_text, self) - def clear_receive_tab(self): - self.receive_address_e.setText('') - self.receive_URI_e.setText('') - self.receive_lightning_e.setText('') - self.receive_tabs.setVisible(False) - self.receive_message_e.setText('') - self.receive_amount_e.setAmount(None) - self.expires_label.hide() - self.expires_combo.show() - self.request_list.clearSelection() - def toggle_qr_window(self): from . import qrwindow if not self.qr_window: @@ -1495,16 +1104,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def show_receive_tab(self): self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab)) - def update_receive_address_styling(self): - addr = str(self.receive_address_e.text()) - if is_address(addr) and self.wallet.adb.is_used(addr): - self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) - self.receive_address_e.setToolTip(_("This address has already been used. " - "For better privacy, do not reuse it for new payments.")) - else: - self.receive_address_e.setStyleSheet("") - self.receive_address_e.setToolTip("") - def create_send_tab(self): from .send_tab import SendTab return SendTab(self) @@ -1546,11 +1145,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): msg += ' ' + self.format_amount_and_units(amount) msg += '\n' + req.get_message() self.notify(msg) - self.request_list.delete_item(key) - self.receive_tabs.setVisible(False) + self.receive_tab.request_list.delete_item(key) + self.receive_tab.receive_tabs.setVisible(False) self.need_update.set() else: - self.request_list.refresh_item(key) + self.receive_tab.request_list.refresh_item(key) @qt_event_listener def on_event_invoice_status(self, wallet, key): @@ -1770,7 +1369,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.show_error(str(e)) else: self.need_update.set() # history, addresses, coins - self.clear_receive_tab() + self.receive_tab.clear_receive_tab() def payto_contacts(self, labels): self.send_tab.payto_contacts(labels) @@ -2723,7 +2322,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): export_meta_gui(self, _('invoices'), self.wallet.export_invoices) def import_requests(self): - import_meta_gui(self, _('requests'), self.wallet.import_requests, self.request_list.update) + import_meta_gui(self, _('requests'), self.wallet.import_requests, self.receive_tab.request_list.update) def export_requests(self): export_meta_gui(self, _('requests'), self.wallet.export_requests) @@ -2850,7 +2449,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password)) def refresh_amount_edits(self): - edits = self.send_tab.amount_e, self.receive_amount_e + edits = self.send_tab.amount_e, self.receive_tab.receive_amount_e amounts = [edit.get_amount() for edit in edits] for edit, amount in zip(edits, amounts): edit.setAmount(amount) @@ -2858,7 +2457,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def update_fiat(self): b = self.fx and self.fx.is_enabled() self.send_tab.fiat_send_e.setVisible(b) - self.fiat_receive_e.setVisible(b) + self.receive_tab.fiat_receive_e.setVisible(b) self.history_model.refresh('update_fiat') self.history_list.update() self.address_list.refresh_headers() diff --git a/electrum/gui/qt/rebalance_dialog.py b/electrum/gui/qt/rebalance_dialog.py index 408dfcb0c..281b9491c 100644 --- a/electrum/gui/qt/rebalance_dialog.py +++ b/electrum/gui/qt/rebalance_dialog.py @@ -1,14 +1,21 @@ +from typing import TYPE_CHECKING + from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton from electrum.i18n import _ +from electrum.lnchannel import Channel + from .util import WindowModalDialog, Buttons, OkButton, CancelButton, WWLabel from .amountedit import BTCAmountEdit +if TYPE_CHECKING: + from .main_window import ElectrumWindow + class RebalanceDialog(WindowModalDialog): - def __init__(self, window, chan1, chan2, amount_sat): + def __init__(self, window: 'ElectrumWindow', chan1: Channel, chan2: Channel, amount_sat): WindowModalDialog.__init__(self, window, _("Rebalance channels")) self.window = window self.wallet = window.wallet @@ -66,4 +73,4 @@ class RebalanceDialog(WindowModalDialog): amount_msat = self.amount_e.get_amount() * 1000 coro = self.wallet.lnworker.rebalance_channels(self.chan1, self.chan2, amount_msat=amount_msat) self.window.run_coroutine_from_thread(coro, _('Rebalancing channels')) - self.window.update_current_request() # this will gray out the button + self.window.receive_tab.update_current_request() # this will gray out the button diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py new file mode 100644 index 000000000..a6e86cd57 --- /dev/null +++ b/electrum/gui/qt/receive_tab.py @@ -0,0 +1,422 @@ +# Copyright (C) 2022 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +from typing import Optional, TYPE_CHECKING + +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtWidgets import (QComboBox, QLabel, QVBoxLayout, QGridLayout, QLineEdit, + QHBoxLayout, QPushButton, QWidget, QSizePolicy) + +from electrum.bitcoin import is_address +from electrum.i18n import _ +from electrum.util import InvoiceError +from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING +from electrum.invoices import PR_EXPIRED, pr_expiration_values +from electrum.logging import Logger + +from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit +from .qrcodewidget import QRCodeWidget +from .util import read_QIcon, ColorScheme, HelpLabel, WWLabel, MessageBoxMixin, MONOSPACE_FONT +from .util import ButtonsTextEdit + +if TYPE_CHECKING: + from . import ElectrumGui + from .main_window import ElectrumWindow + + +class ReceiveTab(QWidget, MessageBoxMixin, Logger): + + def __init__(self, window: 'ElectrumWindow'): + QWidget.__init__(self, window) + Logger.__init__(self) + + self.window = window + self.wallet = window.wallet + self.fx = window.fx + self.config = window.config + + # A 4-column grid layout. All the stretch is in the last column. + # The exchange rate plugin adds a fiat widget in column 2 + self.receive_grid = grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnStretch(3, 1) + + self.receive_message_e = SizedFreezableLineEdit(width=400) + grid.addWidget(QLabel(_('Description')), 0, 0) + grid.addWidget(self.receive_message_e, 0, 1, 1, 4) + + self.receive_amount_e = BTCAmountEdit(self.window.get_decimal_point) + grid.addWidget(QLabel(_('Requested amount')), 1, 0) + grid.addWidget(self.receive_amount_e, 1, 1) + + self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '') + if not self.fx or not self.fx.is_enabled(): + self.fiat_receive_e.setVisible(False) + grid.addWidget(self.fiat_receive_e, 1, 2, Qt.AlignLeft) + + self.window.connect_fields(self.receive_amount_e, self.fiat_receive_e) + + self.expires_combo = QComboBox() + evl = sorted(pr_expiration_values.items()) + evl_keys = [i[0] for i in evl] + evl_values = [i[1] for i in evl] + default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + try: + i = evl_keys.index(default_expiry) + except ValueError: + i = 0 + self.expires_combo.addItems(evl_values) + self.expires_combo.setCurrentIndex(i) + def on_expiry(i): + self.config.set_key('request_expiry', evl_keys[i]) + self.expires_combo.currentIndexChanged.connect(on_expiry) + msg = ''.join([ + _('Expiration date of your request.'), ' ', + _('This information is seen by the recipient if you send them a signed payment request.'), + '\n\n', + _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', + _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ', + _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'), + '\n\n', + _('For Lightning requests, payments will not be accepted after the expiration.'), + ]) + grid.addWidget(HelpLabel(_('Expires after') + ' (?)', msg), 2, 0) + grid.addWidget(self.expires_combo, 2, 1) + self.expires_label = QLineEdit('') + self.expires_label.setReadOnly(1) + self.expires_label.setFocusPolicy(Qt.NoFocus) + self.expires_label.hide() + grid.addWidget(self.expires_label, 2, 1) + + self.clear_invoice_button = QPushButton(_('Clear')) + self.clear_invoice_button.clicked.connect(self.clear_receive_tab) + self.create_invoice_button = QPushButton(_('Create Request')) + self.create_invoice_button.clicked.connect(lambda: self.create_invoice()) + self.receive_buttons = buttons = QHBoxLayout() + buttons.addStretch(1) + buttons.addWidget(self.clear_invoice_button) + buttons.addWidget(self.create_invoice_button) + grid.addLayout(buttons, 4, 0, 1, -1) + + self.receive_address_e = ButtonsTextEdit() + self.receive_address_help_text = WWLabel('') + vbox = QVBoxLayout() + vbox.addWidget(self.receive_address_help_text) + self.receive_address_help = QWidget() + self.receive_address_help.setVisible(False) + self.receive_address_help.setLayout(vbox) + + self.receive_URI_e = ButtonsTextEdit() + self.receive_URI_help = WWLabel('') + self.receive_lightning_e = ButtonsTextEdit() + self.receive_lightning_help_text = WWLabel('') + self.receive_rebalance_button = QPushButton('Rebalance') + self.receive_rebalance_button.suggestion = None + def on_receive_rebalance(): + if self.receive_rebalance_button.suggestion: + chan1, chan2, delta = self.receive_rebalance_button.suggestion + self.window.rebalance_dialog(chan1, chan2, amount_sat=delta) + self.receive_rebalance_button.clicked.connect(on_receive_rebalance) + self.receive_swap_button = QPushButton('Swap') + self.receive_swap_button.suggestion = None + def on_receive_swap(): + if self.receive_swap_button.suggestion: + chan, swap_recv_amount_sat = self.receive_swap_button.suggestion + self.window.run_swap_dialog(is_reverse=True, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) + self.receive_swap_button.clicked.connect(on_receive_swap) + buttons = QHBoxLayout() + buttons.addWidget(self.receive_rebalance_button) + buttons.addWidget(self.receive_swap_button) + vbox = QVBoxLayout() + vbox.addWidget(self.receive_lightning_help_text) + vbox.addLayout(buttons) + self.receive_lightning_help = QWidget() + self.receive_lightning_help.setVisible(False) + self.receive_lightning_help.setLayout(vbox) + self.receive_address_qr = QRCodeWidget() + self.receive_URI_qr = QRCodeWidget() + self.receive_lightning_qr = QRCodeWidget() + + for e in [self.receive_address_e, self.receive_URI_e, self.receive_lightning_e]: + e.setFont(QFont(MONOSPACE_FONT)) + e.addCopyButton() + e.setReadOnly(True) + + self.receive_lightning_e.textChanged.connect(self.update_receive_widgets) + + self.receive_address_widget = ReceiveTabWidget(self, + self.receive_address_e, self.receive_address_qr, self.receive_address_help) + self.receive_URI_widget = ReceiveTabWidget(self, + self.receive_URI_e, self.receive_URI_qr, self.receive_URI_help) + self.receive_lightning_widget = ReceiveTabWidget(self, + self.receive_lightning_e, self.receive_lightning_qr, self.receive_lightning_help) + + from .util import VTabWidget + self.receive_tabs = VTabWidget() + self.receive_tabs.setMinimumHeight(ReceiveTabWidget.min_size.height() + 4) # for margins + self.receive_tabs.addTab(self.receive_URI_widget, read_QIcon("link.png"), _('URI')) + self.receive_tabs.addTab(self.receive_address_widget, read_QIcon("bitcoin.png"), _('Address')) + self.receive_tabs.addTab(self.receive_lightning_widget, read_QIcon("lightning.png"), _('Lightning')) + self.receive_tabs.currentChanged.connect(self.update_receive_qr_window) + self.receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) + self.receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) + receive_tabs_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + receive_tabs_sp.setRetainSizeWhenHidden(True) + self.receive_tabs.setSizePolicy(receive_tabs_sp) + self.receive_tabs.setVisible(False) + + self.receive_requests_label = QLabel(_('Receive queue')) + from .request_list import RequestList + self.request_list = RequestList(self) + + # layout + vbox_g = QVBoxLayout() + vbox_g.addLayout(grid) + vbox_g.addStretch() + hbox = QHBoxLayout() + hbox.addLayout(vbox_g) + hbox.addStretch() + hbox.addWidget(self.receive_tabs) + + self.searchable_list = self.request_list + vbox = QVBoxLayout(self) + vbox.addLayout(hbox) + vbox.addStretch() + vbox.addWidget(self.receive_requests_label) + vbox.addWidget(self.request_list) + vbox.setStretchFactor(hbox, 40) + vbox.setStretchFactor(self.request_list, 60) + self.request_list.update() # after parented and put into a layout, can update without flickering + + def toggle_receive_qr(self, e): + b = not self.config.get('receive_qr_visible', False) + self.config.set_key('receive_qr_visible', b) + self.update_receive_widgets() + + def update_receive_widgets(self): + b = self.config.get('receive_qr_visible', False) + self.receive_URI_widget.update_visibility(b) + self.receive_address_widget.update_visibility(b) + self.receive_lightning_widget.update_visibility(b) + + def update_current_request(self): + key = self.request_list.get_current_key() + req = self.wallet.get_request(key) if key else None + if req is None: + self.receive_URI_e.setText('') + self.receive_lightning_e.setText('') + self.receive_address_e.setText('') + return + addr = req.get_address() or '' + amount_sat = req.get_amount_sat() or 0 + address_help = '' if addr else _('Amount too small to be received onchain') + URI_help = '' + lnaddr = req.lightning_invoice + bip21_lightning = lnaddr if self.config.get('bip21_lightning', False) else None + URI = req.get_bip21_URI(lightning=bip21_lightning) + lightning_online = self.wallet.lnworker and self.wallet.lnworker.num_peers() > 0 + can_receive_lightning = self.wallet.lnworker and amount_sat <= self.wallet.lnworker.num_sats_can_receive() + has_expired = self.wallet.get_request_status(key) == PR_EXPIRED + if has_expired: + URI_help = ln_help = address_help = _('This request has expired') + URI = lnaddr = address = '' + can_rebalance = False + can_swap = False + elif lnaddr is None: + ln_help = _('This request does not have a Lightning invoice.') + lnaddr = '' + can_rebalance = False + can_swap = False + elif not lightning_online: + ln_help = _('You must be online to receive Lightning payments.') + lnaddr = '' + can_rebalance = False + can_swap = False + elif not can_receive_lightning: + self.receive_rebalance_button.suggestion = self.wallet.lnworker.suggest_rebalance_to_receive(amount_sat) + self.receive_swap_button.suggestion = self.wallet.lnworker.suggest_swap_to_receive(amount_sat) + can_rebalance = bool(self.receive_rebalance_button.suggestion) + can_swap = bool(self.receive_swap_button.suggestion) + lnaddr = '' + ln_help = _('You do not have the capacity to receive that amount with Lightning.') + if can_rebalance: + ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.') + elif can_swap: + ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.') + else: + ln_help = '' + can_rebalance = False + can_swap = False + self.receive_rebalance_button.setVisible(can_rebalance) + self.receive_swap_button.setVisible(can_swap) + self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0) + self.receive_swap_button.setEnabled(can_swap and self.window.num_tasks() == 0) + icon_name = "lightning.png" if lnaddr else "lightning_disconnected.png" + self.receive_tabs.setTabIcon(2, read_QIcon(icon_name)) + # encode lightning invoices as uppercase so QR encoding can use + # alphanumeric mode; resulting in smaller QR codes + lnaddr_qr = lnaddr.upper() + self.receive_address_e.setText(addr) + self.update_receive_address_styling() + self.receive_address_qr.setData(addr) + self.receive_address_help_text.setText(address_help) + self.receive_URI_e.setText(URI) + self.receive_URI_qr.setData(URI) + self.receive_URI_help.setText(URI_help) + self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? + self.receive_lightning_help_text.setText(ln_help) + self.receive_lightning_qr.setData(lnaddr_qr) + # macOS hack (similar to #4777) + self.receive_lightning_e.repaint() + self.receive_URI_e.repaint() + self.receive_address_e.repaint() + # always show + self.receive_tabs.setVisible(True) + self.update_receive_qr_window() + + def update_receive_qr_window(self): + if self.window.qr_window and self.window.qr_window.isVisible(): + i = self.receive_tabs.currentIndex() + if i == 0: + data = self.receive_URI_qr.data + elif i == 1: + data = self.receive_address_qr.data + else: + data = self.receive_lightning_qr.data + self.window.qr_window.qrw.setData(data) + + def sign_payment_request(self, addr): + alias = self.config.get('alias') + if alias and self.wallet.contacts.alias_info: + alias_addr, alias_name, validated = self.wallet.contacts.alias_info + if alias_addr: + if self.wallet.is_mine(alias_addr): + msg = _('This payment request will be signed.') + '\n' + _('Please enter your password') + password = None + if self.wallet.has_keystore_encryption(): + password = self.window.password_dialog(msg) + if not password: + return + try: + self.wallet.sign_payment_request(addr, alias, alias_addr, password) + except Exception as e: + self.show_error(repr(e)) + return + else: + return + + def create_invoice(self): + amount_sat = self.receive_amount_e.get_amount() + message = self.receive_message_e.text() + expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + + if amount_sat and amount_sat < self.wallet.dust_threshold(): + address = None + if not self.wallet.has_lightning(): + return + else: + address = self.get_bitcoin_address_for_request(amount_sat) + if not address: + return + self.window.address_list.update() + + # generate even if we cannot receive + lightning = self.wallet.has_lightning() + try: + key = self.wallet.create_request(amount_sat, message, expiry, address, lightning=lightning) + except InvoiceError as e: + self.show_error(_('Error creating payment request') + ':\n' + str(e)) + return + except Exception as e: + self.logger.exception('Error adding payment request') + self.show_error(_('Error adding payment request') + ':\n' + repr(e)) + return + self.sign_payment_request(address) + assert key is not None + self.window.address_list.refresh_all() + self.request_list.update() + self.request_list.set_current_key(key) + # clear request fields + self.receive_amount_e.setText('') + self.receive_message_e.setText('') + # copy to clipboard + r = self.wallet.get_request(key) + content = r.lightning_invoice if r.is_lightning() else r.get_address() + title = _('Invoice') if r.is_lightning() else _('Address') + self.window.do_copy(content, title=title) + + def get_bitcoin_address_for_request(self, amount) -> Optional[str]: + addr = self.wallet.get_unused_address() + if addr is None: + if not self.wallet.is_deterministic(): # imported wallet + msg = [ + _('No more addresses in your wallet.'), ' ', + _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', + _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', + _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), + ] + if not self.question(''.join(msg)): + return + addr = self.wallet.get_receiving_address() + else: # deterministic wallet + if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): + return + addr = self.wallet.create_new_address(False) + return addr + + def clear_receive_tab(self): + self.receive_address_e.setText('') + self.receive_URI_e.setText('') + self.receive_lightning_e.setText('') + self.receive_tabs.setVisible(False) + self.receive_message_e.setText('') + self.receive_amount_e.setAmount(None) + self.expires_label.hide() + self.expires_combo.show() + self.request_list.clearSelection() + + def update_receive_address_styling(self): + addr = str(self.receive_address_e.text()) + if is_address(addr) and self.wallet.adb.is_used(addr): + self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + self.receive_address_e.setToolTip(_("This address has already been used. " + "For better privacy, do not reuse it for new payments.")) + else: + self.receive_address_e.setStyleSheet("") + self.receive_address_e.setToolTip("") + + +class ReceiveTabWidget(QWidget): + min_size = QSize(200, 200) + + def __init__(self, receive_tab: 'ReceiveTab', textedit, qr, help_widget): + self.textedit = textedit + self.qr = qr + self.help_widget = help_widget + QWidget.__init__(self) + for w in [textedit, qr, help_widget]: + w.setMinimumSize(self.min_size) + for w in [textedit, qr]: + w.mousePressEvent = receive_tab.toggle_receive_qr + tooltip = _('Click to switch between text and QR code view') + w.setToolTip(tooltip) + textedit.setFocusPolicy(Qt.NoFocus) + hbox = QHBoxLayout() + hbox.setContentsMargins(0, 0, 0, 0) + hbox.addWidget(textedit) + hbox.addWidget(help_widget) + hbox.addWidget(qr) + self.setLayout(hbox) + + def update_visibility(self, is_qr): + if str(self.textedit.text()): + self.help_widget.setVisible(False) + self.textedit.setVisible(not is_qr) + self.qr.setVisible(is_qr) + else: + self.help_widget.setVisible(True) + self.textedit.setVisible(False) + self.qr.setVisible(False) + diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 808737e61..1fbd43d7e 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -39,6 +39,7 @@ from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel if TYPE_CHECKING: from .main_window import ElectrumWindow + from .receive_tab import ReceiveTab ROLE_REQUEST_TYPE = Qt.UserRole @@ -63,10 +64,12 @@ class RequestList(MyTreeView): } filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] - def __init__(self, parent: 'ElectrumWindow'): - super().__init__(parent, self.create_menu, + def __init__(self, receive_tab: 'ReceiveTab'): + window = receive_tab.window + super().__init__(window, self.create_menu, stretch_column=self.Columns.DESCRIPTION) - self.wallet = self.parent.wallet + self.wallet = window.wallet + self.receive_tab = receive_tab self.std_model = QStandardItemModel(self) self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) self.proxy.setSourceModel(self.std_model) @@ -88,7 +91,7 @@ class RequestList(MyTreeView): def item_changed(self, idx: Optional[QModelIndex]): if idx is None: - self.parent.update_current_request() + self.receive_tab.update_current_request() return if not idx.isValid(): return @@ -98,7 +101,7 @@ class RequestList(MyTreeView): req = self.wallet.get_request(key) if req is None: self.update() - self.parent.update_current_request() + self.receive_tab.update_current_request() def clearSelection(self): super().clearSelection() @@ -111,7 +114,7 @@ class RequestList(MyTreeView): if request is None: return status_item = model.item(row, self.Columns.STATUS) - status = self.parent.wallet.get_request_status(key) + status = self.wallet.get_request_status(key) status_str = request.get_status_str(status) status_item.setText(status_str) status_item.setIcon(read_QIcon(pr_icons.get(status))) @@ -124,7 +127,7 @@ class RequestList(MyTreeView): self.update_headers(self.__class__.headers) for req in self.wallet.get_unpaid_requests(): key = self.wallet.get_key_for_receive_request(req) - status = self.parent.wallet.get_request_status(key) + status = self.wallet.get_request_status(key) status_str = req.get_status_str(status) timestamp = req.get_time() amount = req.get_amount_sat() @@ -150,7 +153,7 @@ class RequestList(MyTreeView): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) - self.parent.receive_requests_label.setVisible(b) + self.receive_tab.receive_requests_label.setVisible(b) if not b: # list got hidden, so selected item should also be cleared: self.item_changed(None) @@ -160,7 +163,7 @@ class RequestList(MyTreeView): if len(items)>1: keys = [item.data(ROLE_KEY) for item in items] menu = QMenu(self) - menu.addAction(_("Delete requests"), lambda: self.parent.delete_requests(keys)) + menu.addAction(_("Delete requests"), lambda: self.delete_requests(keys)) menu.exec_(self.viewport().mapToGlobal(position)) return idx = self.indexAt(position) @@ -183,6 +186,12 @@ class RequestList(MyTreeView): self.add_copy_menu(menu, idx) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) - menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key])) + menu.addAction(_("Delete"), lambda: self.delete_requests([key])) run_hook('receive_list_menu', self.parent, menu, key) menu.exec_(self.viewport().mapToGlobal(position)) + + def delete_requests(self, keys): + for key in keys: + self.wallet.delete_request(key) + self.delete_item(key) + self.receive_tab.clear_receive_tab() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 763dc78be..8cb61f395 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -115,6 +115,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger): self.amount_e.frozen.connect( lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly())) + self.window.connect_fields(self.amount_e, self.fiat_send_e) + self.max_button = EnterButton(_("Max"), self.spend_max) self.max_button.setFixedWidth(100) self.max_button.setCheckable(True) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 88b51d670..cefc35616 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -280,7 +280,7 @@ class QtPluginBase(object): keystore: 'Hardware_KeyStore', main_window: ElectrumWindow): plugin = keystore.plugin - receive_address_e = main_window.receive_address_e + receive_address_e = main_window.receive_tab.receive_address_e def show_address(): addr = str(receive_address_e.text())