From b6010aad0fe3559fd9f4831bff4efb7be189949f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Jul 2023 13:59:57 +0200 Subject: [PATCH] paytoedit: promote to QWidget and encapsulate QLineEdit vs QTextEdit juggling --- electrum/gui/qt/paytoedit.py | 181 ++++++++++++++++++++++++----------- electrum/gui/qt/send_tab.py | 4 +- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 418cc9cd6..320e75999 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -24,19 +24,16 @@ # SOFTWARE. from functools import partial -from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import Qt, QTimer, QSize from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QFontMetrics, QFont -from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QTextEdit, QWidget, QLineEdit, QStackedLayout, QSizePolicy -from electrum.i18n import _ from electrum.payment_identifier import PaymentIdentifier from electrum.logging import Logger -from .qrtextedit import ScanQRTextEdit -from .completion_text_edit import CompletionTextEdit from . import util from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent, ColorScheme @@ -54,10 +51,15 @@ class InvalidPaymentIdentifier(Exception): class ResizingTextEdit(QTextEdit): + textReallyChanged = pyqtSignal() + resized = pyqtSignal() + def __init__(self): QTextEdit.__init__(self) + self._text = '' + self.setAcceptRichText(False) + self.textChanged.connect(self.on_text_changed) document = self.document() - document.contentsChanged.connect(self.update_size) fontMetrics = QFontMetrics(document.defaultFont()) self.fontSpacing = fontMetrics.lineSpacing() margins = self.contentsMargins() @@ -67,49 +69,81 @@ class ResizingTextEdit(QTextEdit): self.verticalMargins += documentMargin * 2 self.heightMin = self.fontSpacing + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins - self.single_line = True self.update_size() + def on_text_changed(self): + # QTextEdit emits spurious textChanged events + if self.toPlainText() != self._text: + self._text = self.toPlainText() + self.textReallyChanged.emit() + self.update_size() + def update_size(self): docLineCount = self.document().lineCount() - docHeight = max(1 if self.single_line else 3, docLineCount) * self.fontSpacing + docHeight = max(3, docLineCount) * self.fontSpacing h = docHeight + self.verticalMargins h = min(max(h, self.heightMin), self.heightMax) self.setMinimumHeight(int(h)) self.setMaximumHeight(int(h)) - if self.single_line: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) - else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) - self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) + self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) + self.resized.emit() + + def sizeHint(self) -> QSize: + return QSize(0, self.minimumHeight()) -class PayToEdit(QObject, Logger, GenericInputHandler): +class PayToEdit(QWidget, Logger, GenericInputHandler): paymentIdentifierChanged = pyqtSignal() + textChanged = pyqtSignal() def __init__(self, send_tab: 'SendTab'): - QObject.__init__(self, parent=send_tab) + QWidget.__init__(self, parent=send_tab) Logger.__init__(self) GenericInputHandler.__init__(self) + self._text = '' + self._layout = QStackedLayout() + self.setLayout(self._layout) + + def text_edit_changed(): + text = self.text_edit.toPlainText() + if self._text != text: + # sync and emit + self._text = text + self.line_edit.setText(text) + self.textChanged.emit() + + def text_edit_resized(): + self.update_height() + + def line_edit_changed(): + text = self.line_edit.text() + if self._text != text: + # sync and emit + self._text = text + self.text_edit.setPlainText(text) + self.textChanged.emit() + + self.line_edit = QLineEdit() + self.line_edit.textChanged.connect(line_edit_changed) self.text_edit = ResizingTextEdit() - self.text_edit.textChanged.connect(self._handle_text_change) + self.text_edit.textReallyChanged.connect(text_edit_changed) + self.text_edit.resized.connect(text_edit_resized) + + self.textChanged.connect(self._handle_text_change) + + self._layout.addWidget(self.line_edit) + self._layout.addWidget(self.text_edit) + + self.multiline = False + self._is_paytomany = False self.text_edit.setFont(QFont(MONOSPACE_FONT)) self.send_tab = send_tab self.config = send_tab.config - self.app = QApplication.instance() - - self.is_multiline = False - self.payto_scriptpubkey = None # type: Optional[bytes] - self.previous_payto = '' - # editor methods - self.setStyleSheet = self.text_edit.setStyleSheet - self.setText = self.text_edit.setText - self.setFocus = self.text_edit.setFocus - self.setToolTip = self.text_edit.setToolTip + # button handlers self.on_qr_from_camera_input_btn = partial( self.input_qr_from_camera, @@ -141,24 +175,46 @@ class PayToEdit(QObject, Logger, GenericInputHandler): self.payment_identifier = None - def set_text(self, text: str): - self.text_edit.setText(text) + @property + def multiline(self): + return self._multiline + + @multiline.setter + def multiline(self, b: bool) -> None: + if b is None: + return + self._multiline = b + self._layout.setCurrentWidget(self.text_edit if b else self.line_edit) + self.update_height() + + def update_height(self) -> None: + h = self._layout.currentWidget().sizeHint().height() + self.setMaximumHeight(h) + + def setText(self, text: str) -> None: + if self._text != text: + self.line_edit.setText(text) + self.text_edit.setText(text) + + def setFocus(self, reason=None) -> None: + if self.multiline: + self.text_edit.setFocus(reason) + else: + self.line_edit.setFocus(reason) - def update_editor(self): - if self.text_edit.toPlainText() != self.payment_identifier.text: - self.text_edit.setText(self.payment_identifier.text) - self.text_edit.single_line = not self.payment_identifier.is_multiline() - self.text_edit.update_size() + def setToolTip(self, tt: str) -> None: + self.line_edit.setToolTip(tt) + self.text_edit.setToolTip(tt) '''set payment identifier only if valid, else exception''' - def try_payment_identifier(self, text): + def try_payment_identifier(self, text) -> None: text = text.strip() pi = PaymentIdentifier(self.send_tab.wallet, text) if not pi.is_valid(): raise InvalidPaymentIdentifier('Invalid payment identifier') self.set_payment_identifier(text) - def set_payment_identifier(self, text): + def set_payment_identifier(self, text) -> None: text = text.strip() if self.payment_identifier and self.payment_identifier.text == text: # no change. @@ -167,60 +223,69 @@ class PayToEdit(QObject, Logger, GenericInputHandler): self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text) # toggle to multiline if payment identifier is a multiline - self.is_multiline = self.payment_identifier.is_multiline() - if self.is_multiline and not self._is_paytomany: + if self.payment_identifier.is_multiline() and not self._is_paytomany: self.set_paytomany(True) - # if payment identifier gets set externally, we want to update the text_edit + # if payment identifier gets set externally, we want to update the edit control # Note: this triggers the change handler, but we shortcut if it's the same payment identifier - self.update_editor() + self.setText(text) self.paymentIdentifierChanged.emit() def set_paytomany(self, b): self._is_paytomany = b - self.text_edit.single_line = not self._is_paytomany - self.text_edit.update_size() + self.multiline = b self.send_tab.paytomany_menu.setChecked(b) - def toggle_paytomany(self): + def toggle_paytomany(self) -> None: self.set_paytomany(not self._is_paytomany) def is_paytomany(self): return self._is_paytomany - def setFrozen(self, b): + def setReadOnly(self, b: bool) -> None: + self.line_edit.setReadOnly(b) self.text_edit.setReadOnly(b) - self.text_edit.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') + + def isReadOnly(self): + return self.line_edit.isReadOnly() + + def setStyleSheet(self, stylesheet: str) -> None: + self.line_edit.setStyleSheet(stylesheet) + self.text_edit.setStyleSheet(stylesheet) + + def setFrozen(self, b) -> None: + self.setReadOnly(b) + self.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') def isFrozen(self): - return self.text_edit.isReadOnly() + return self.isReadOnly() - def do_clear(self): - self.is_multiline = False + def do_clear(self) -> None: self.set_paytomany(False) - self.text_edit.setText('') + self.setText('') + self.setToolTip('') self.payment_identifier = None - def setGreen(self): + def setGreen(self) -> None: self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) - def setExpired(self): + def setExpired(self) -> None: self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def _handle_text_change(self): + def _handle_text_change(self) -> None: if self.isFrozen(): # if editor is frozen, we ignore text changes as they might not be a payment identifier # but a user friendly representation. return # pushback timer if timer active or PI needs resolving - pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText()) + pi = PaymentIdentifier(self.send_tab.wallet, self._text) if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive(): self.edit_timer.start() else: - self.set_payment_identifier(self.text_edit.toPlainText()) + self.set_payment_identifier(self._text) - def _on_edit_timer(self): + def _on_edit_timer(self) -> None: if not self.isFrozen(): - self.set_payment_identifier(self.text_edit.toPlainText()) + self.set_payment_identifier(self._text) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 670111281..94838c248 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -73,7 +73,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 0, 0) - grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) + grid.addWidget(self.payto_e, 0, 1, 1, 4) #completer = QCompleter() #completer.setCaseSensitivity(False) @@ -339,6 +339,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger): for w in [self.save_button, self.send_button]: w.setEnabled(False) self.window.update_status() + self.paytomany_menu.setChecked(self.payto_e.multiline) + run_hook('do_clear', self) def prepare_for_send_tab_network_lookup(self):