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.
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)

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."))
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):

Loading…
Cancel
Save