Browse Source

paytoedit: promote to QWidget and encapsulate QLineEdit vs QTextEdit juggling

master
Sander van Grieken 2 years ago
parent
commit
b6010aad0f
  1. 181
      electrum/gui/qt/paytoedit.py
  2. 4
      electrum/gui/qt/send_tab.py

181
electrum/gui/qt/paytoedit.py

@ -24,19 +24,16 @@
# SOFTWARE. # SOFTWARE.
from functools import partial 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.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QFontMetrics, QFont 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.payment_identifier import PaymentIdentifier
from electrum.logging import Logger from electrum.logging import Logger
from .qrtextedit import ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit
from . import util from . import util
from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent, ColorScheme from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent, ColorScheme
@ -54,10 +51,15 @@ class InvalidPaymentIdentifier(Exception):
class ResizingTextEdit(QTextEdit): class ResizingTextEdit(QTextEdit):
textReallyChanged = pyqtSignal()
resized = pyqtSignal()
def __init__(self): def __init__(self):
QTextEdit.__init__(self) QTextEdit.__init__(self)
self._text = ''
self.setAcceptRichText(False)
self.textChanged.connect(self.on_text_changed)
document = self.document() document = self.document()
document.contentsChanged.connect(self.update_size)
fontMetrics = QFontMetrics(document.defaultFont()) fontMetrics = QFontMetrics(document.defaultFont())
self.fontSpacing = fontMetrics.lineSpacing() self.fontSpacing = fontMetrics.lineSpacing()
margins = self.contentsMargins() margins = self.contentsMargins()
@ -67,49 +69,81 @@ class ResizingTextEdit(QTextEdit):
self.verticalMargins += documentMargin * 2 self.verticalMargins += documentMargin * 2
self.heightMin = self.fontSpacing + self.verticalMargins self.heightMin = self.fontSpacing + self.verticalMargins
self.heightMax = (self.fontSpacing * 10) + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins
self.single_line = True
self.update_size() 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): def update_size(self):
docLineCount = self.document().lineCount() 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 = docHeight + self.verticalMargins
h = min(max(h, self.heightMin), self.heightMax) h = min(max(h, self.heightMin), self.heightMax)
self.setMinimumHeight(int(h)) self.setMinimumHeight(int(h))
self.setMaximumHeight(int(h)) self.setMaximumHeight(int(h))
if self.single_line: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax)
self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
else: self.resized.emit()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) def sizeHint(self) -> QSize:
self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) return QSize(0, self.minimumHeight())
class PayToEdit(QObject, Logger, GenericInputHandler):
class PayToEdit(QWidget, Logger, GenericInputHandler):
paymentIdentifierChanged = pyqtSignal() paymentIdentifierChanged = pyqtSignal()
textChanged = pyqtSignal()
def __init__(self, send_tab: 'SendTab'): def __init__(self, send_tab: 'SendTab'):
QObject.__init__(self, parent=send_tab) QWidget.__init__(self, parent=send_tab)
Logger.__init__(self) Logger.__init__(self)
GenericInputHandler.__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 = 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._is_paytomany = False
self.text_edit.setFont(QFont(MONOSPACE_FONT)) self.text_edit.setFont(QFont(MONOSPACE_FONT))
self.send_tab = send_tab self.send_tab = send_tab
self.config = send_tab.config 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 # button handlers
self.on_qr_from_camera_input_btn = partial( self.on_qr_from_camera_input_btn = partial(
self.input_qr_from_camera, self.input_qr_from_camera,
@ -141,24 +175,46 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
self.payment_identifier = None self.payment_identifier = None
def set_text(self, text: str): @property
self.text_edit.setText(text) 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): def setToolTip(self, tt: str) -> None:
if self.text_edit.toPlainText() != self.payment_identifier.text: self.line_edit.setToolTip(tt)
self.text_edit.setText(self.payment_identifier.text) self.text_edit.setToolTip(tt)
self.text_edit.single_line = not self.payment_identifier.is_multiline()
self.text_edit.update_size()
'''set payment identifier only if valid, else exception''' '''set payment identifier only if valid, else exception'''
def try_payment_identifier(self, text): def try_payment_identifier(self, text) -> None:
text = text.strip() text = text.strip()
pi = PaymentIdentifier(self.send_tab.wallet, text) pi = PaymentIdentifier(self.send_tab.wallet, text)
if not pi.is_valid(): if not pi.is_valid():
raise InvalidPaymentIdentifier('Invalid payment identifier') raise InvalidPaymentIdentifier('Invalid payment identifier')
self.set_payment_identifier(text) self.set_payment_identifier(text)
def set_payment_identifier(self, text): def set_payment_identifier(self, text) -> None:
text = text.strip() text = text.strip()
if self.payment_identifier and self.payment_identifier.text == text: if self.payment_identifier and self.payment_identifier.text == text:
# no change. # no change.
@ -167,60 +223,69 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text) self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text)
# toggle to multiline if payment identifier is a multiline # toggle to multiline if payment identifier is a multiline
self.is_multiline = self.payment_identifier.is_multiline() if self.payment_identifier.is_multiline() and not self._is_paytomany:
if self.is_multiline and not self._is_paytomany:
self.set_paytomany(True) 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 # 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() self.paymentIdentifierChanged.emit()
def set_paytomany(self, b): def set_paytomany(self, b):
self._is_paytomany = b self._is_paytomany = b
self.text_edit.single_line = not self._is_paytomany self.multiline = b
self.text_edit.update_size()
self.send_tab.paytomany_menu.setChecked(b) self.send_tab.paytomany_menu.setChecked(b)
def toggle_paytomany(self): def toggle_paytomany(self) -> None:
self.set_paytomany(not self._is_paytomany) self.set_paytomany(not self._is_paytomany)
def is_paytomany(self): def is_paytomany(self):
return self._is_paytomany 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.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): def isFrozen(self):
return self.text_edit.isReadOnly() return self.isReadOnly()
def do_clear(self): def do_clear(self) -> None:
self.is_multiline = False
self.set_paytomany(False) self.set_paytomany(False)
self.text_edit.setText('') self.setText('')
self.setToolTip('')
self.payment_identifier = None self.payment_identifier = None
def setGreen(self): def setGreen(self) -> None:
self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))
def setExpired(self): def setExpired(self) -> None:
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
def _handle_text_change(self): def _handle_text_change(self) -> None:
if self.isFrozen(): if self.isFrozen():
# if editor is frozen, we ignore text changes as they might not be a payment identifier # if editor is frozen, we ignore text changes as they might not be a payment identifier
# but a user friendly representation. # but a user friendly representation.
return return
# pushback timer if timer active or PI needs resolving # 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(): if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive():
self.edit_timer.start() self.edit_timer.start()
else: 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(): if not self.isFrozen():
self.set_payment_identifier(self.text_edit.toPlainText()) self.set_payment_identifier(self._text)

4
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.")) "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60."))
payto_label = HelpLabel(_('Pay to'), msg) payto_label = HelpLabel(_('Pay to'), msg)
grid.addWidget(payto_label, 0, 0) 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 = QCompleter()
#completer.setCaseSensitivity(False) #completer.setCaseSensitivity(False)
@ -339,6 +339,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
for w in [self.save_button, self.send_button]: for w in [self.save_button, self.send_button]:
w.setEnabled(False) w.setEnabled(False)
self.window.update_status() self.window.update_status()
self.paytomany_menu.setChecked(self.payto_e.multiline)
run_hook('do_clear', self) run_hook('do_clear', self)
def prepare_for_send_tab_network_lookup(self): def prepare_for_send_tab_network_lookup(self):

Loading…
Cancel
Save