@ -25,14 +25,15 @@
import re
import decimal
from functools import partial
from decimal import Decimal
from typing import NamedTuple , Sequence , Optional , List , TYPE_CHECKING
from PyQt5 . QtGui import QFontMetrics , QFont
from PyQt5 . QtWidgets import QApplication
from PyQt5 . QtWidgets import QApplication , QWidget , QLineEdit , QTextEdit , QVBoxLayout
from electrum import bitcoin
from electrum . util import bfh , parse_max_spend , FailedToParsePaymentIdentifier
from electrum . util import parse_max_spend , FailedToParsePaymentIdentifier
from electrum . transaction import PartialTxOutput
from electrum . bitcoin import opcodes , construct_script
from electrum . logging import Logger
@ -41,7 +42,7 @@ from electrum.lnurl import LNURLError
from . qrtextedit import ScanQRTextEdit
from . completion_text_edit import CompletionTextEdit
from . import util
from . util import MONOSPACE_FONT
from . util import MONOSPACE_FONT , GenericInputHandler , editor_contextMenuEvent
if TYPE_CHECKING :
from . main_window import ElectrumWindow
@ -61,48 +62,112 @@ class PayToLineError(NamedTuple):
is_multiline : bool = False
class PayToEdit ( CompletionTextEdit , ScanQRTextEdit , Logger ) :
def __init__ ( self , send_tab : ' SendTab ' ) :
CompletionTextEdit . __init__ ( self )
ScanQRTextEdit . __init__ ( self , config = send_tab . config , setText = self . _on_input_btn , is_payto = True )
Logger . __init__ ( self )
self . send_tab = send_tab
self . win = send_tab . window
self . app = QApplication . instance ( )
self . amount_edit = self . send_tab . amount_e
self . setFont ( QFont ( MONOSPACE_FONT ) )
class ResizingTextEdit ( QTextEdit ) :
def __init__ ( self ) :
QTextEdit . __init__ ( self )
document = self . document ( )
document . contentsChanged . connect ( self . update_size )
fontMetrics = QFontMetrics ( document . defaultFont ( ) )
self . fontSpacing = fontMetrics . lineSpacing ( )
margins = self . contentsMargins ( )
documentMargin = document . documentMargin ( )
self . verticalMargins = margins . top ( ) + margins . bottom ( )
self . verticalMargins + = self . frameWidth ( ) * 2
self . verticalMargins + = documentMargin * 2
self . heightMin = self . fontSpacing + self . verticalMargins
self . heightMax = ( self . fontSpacing * 10 ) + self . verticalMargins
self . update_size ( )
self . c = None
self . addPasteButton ( setText = self . _on_input_btn )
self . textChanged . connect ( self . _on_text_changed )
def update_size ( self ) :
docLineCount = self . document ( ) . lineCount ( )
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 ) )
self . verticalScrollBar ( ) . setHidden ( docHeight + self . verticalMargins < self . heightMax )
class PayToEdit ( Logger , GenericInputHandler ) :
def __init__ ( self , send_tab : ' SendTab ' ) :
Logger . __init__ ( self )
GenericInputHandler . __init__ ( self )
self . line_edit = QLineEdit ( )
self . text_edit = ResizingTextEdit ( )
self . text_edit . hide ( )
self . _is_paytomany = False
for w in [ self . line_edit , self . text_edit ] :
w . setFont ( QFont ( MONOSPACE_FONT ) )
w . textChanged . connect ( self . _on_text_changed )
self . send_tab = send_tab
self . config = send_tab . config
self . win = send_tab . window
self . app = QApplication . instance ( )
self . amount_edit = self . send_tab . amount_e
self . is_multiline = False
self . outputs = [ ] # type: List[PartialTxOutput]
self . errors = [ ] # type: List[PayToLineError]
self . disable_checks = False
self . is_alias = False
self . update_size ( )
self . payto_scriptpubkey = None # type: Optional[bytes]
self . lightning_invoice = None
self . previous_payto = ' '
# editor methods
self . setStyleSheet = self . editor . setStyleSheet
self . setText = self . editor . setText
self . setEnabled = self . editor . setEnabled
self . setReadOnly = self . editor . setReadOnly
# button handlers
self . on_qr_from_camera_input_btn = partial (
self . input_qr_from_camera ,
config = self . config ,
allow_multi = False ,
show_error = self . win . show_error ,
setText = self . _on_input_btn ,
)
self . on_qr_from_screenshot_input_btn = partial (
self . input_qr_from_screenshot ,
allow_multi = False ,
show_error = self . win . show_error ,
setText = self . _on_input_btn ,
)
self . on_input_file = partial (
self . input_file ,
config = self . config ,
show_error = self . win . show_error ,
setText = self . _on_input_btn ,
)
#
self . line_edit . contextMenuEvent = partial ( editor_contextMenuEvent , self . line_edit , self )
self . text_edit . contextMenuEvent = partial ( editor_contextMenuEvent , self . text_edit , self )
@property
def editor ( self ) :
return self . text_edit if self . is_paytomany ( ) else self . line_edit
def set_paytomany ( self , b ) :
self . _is_paytomany = b
self . line_edit . setVisible ( not b )
self . text_edit . setVisible ( b )
self . send_tab . paytomany_menu . setChecked ( b )
def toggle_paytomany ( self ) :
self . set_paytomany ( not self . _is_paytomany )
def toPlainText ( self ) :
return self . text_edit . toPlainText ( ) if self . is_paytomany ( ) else self . line_edit . text ( )
def is_paytomany ( self ) :
return self . _is_paytomany
def setFrozen ( self , b ) :
self . setReadOnly ( b )
self . setStyleSheet ( frozen_style if b else normal_style )
self . overlay_widget . setHidden ( b )
if not b :
self . setStyleSheet ( normal_style )
def setTextNoCheck ( self , text : str ) :
""" Sets the text, while also ensuring the new value will not be resolved/checked. """
@ -110,9 +175,11 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self . setText ( text )
def do_clear ( self ) :
self . set_paytomany ( False )
self . disable_checks = False
self . is_alias = False
self . setText ( ' ' )
self . line_edit . setText ( ' ' )
self . text_edit . setText ( ' ' )
self . setFrozen ( False )
self . setEnabled ( True )
@ -134,12 +201,12 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def parse_output ( self , x ) - > bytes :
try :
address = self . parse_address ( x )
return bfh ( bitcoin . address_to_script ( address ) )
return bytes . from hex ( bitcoin . address_to_script ( address ) )
except Exception :
pass
try :
script = self . parse_script ( x )
return bfh ( script )
return bytes . from hex ( script )
except Exception :
pass
raise Exception ( " Invalid address or script. " )
@ -151,7 +218,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
opcode_int = opcodes [ word ]
script + = construct_script ( [ opcode_int ] )
else :
bfh ( word ) # to test it is hex data
bytes . from hex ( word ) # to test it is hex data
script + = construct_script ( [ word ] )
return script
@ -176,30 +243,37 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def _on_input_btn ( self , text : str ) :
self . setText ( text )
self . _check_text ( full_check = True )
def _on_text_changed ( self ) :
if self . app . clipboard ( ) . text ( ) == self . toPlainText ( ) :
# user likely pasted from clipboard
self . _check_text ( full_check = True )
else :
self . _check_text ( full_check = False )
text = self . toPlainText ( )
# False if user pasted from clipboard
full_check = self . app . clipboard ( ) . text ( ) != text
self . _check_text ( text , full_check = full_check )
if self . is_multiline and not self . _is_paytomany :
self . set_paytomany ( True )
self . text_edit . setText ( text )
def on_timer_check_text ( self ) :
if self . hasFocus ( ) :
if self . editor . hasFocus ( ) :
return
self . _check_text ( full_check = True )
def _check_text ( self , * , full_check : bool ) :
if self . previous_payto == str ( self . toPlainText ( ) ) . strip ( ) :
text = self . toPlainText ( )
self . _check_text ( text , full_check = True )
def _check_text ( self , text , * , full_check : bool ) :
"""
side effects : self . is_multiline , self . errors , self . outputs
"""
if self . previous_payto == str ( text ) . strip ( ) :
return
if full_check :
self . previous_payto = str ( self . toPlainText ( ) ) . strip ( )
self . previous_payto = str ( text ) . strip ( )
self . errors = [ ]
if self . disable_checks :
return
# filter out empty lines
lines = [ i for i in self . lines ( ) if i ]
lines = text . split ( ' \n ' )
lines = [ i for i in lines if i ]
self . is_multiline = len ( lines ) > 1
self . payto_scriptpubkey = None
self . lightning_invoice = None
@ -242,6 +316,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
# there are multiple lines
self . _parse_as_multiline ( lines , raise_errors = False )
def _parse_as_multiline ( self , lines , * , raise_errors : bool ) :
outputs = [ ] # type: List[PartialTxOutput]
total = 0
@ -292,33 +367,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
return self . outputs [ : ]
def lines ( self ) :
return self . toPlainText ( ) . split ( ' \n ' )
def is_multiline ( self ) :
return len ( self . lines ( ) ) > 1
def paytomany ( self ) :
self . setTextNoCheck ( " \n \n \n " )
self . update_size ( )
def update_size ( self ) :
docLineCount = self . document ( ) . lineCount ( )
if self . cursorRect ( ) . right ( ) + 1 > = self . overlay_widget . pos ( ) . x ( ) :
# Add a line if we are under the overlay widget
docLineCount + = 1
docHeight = docLineCount * self . fontSpacing
h = docHeight + self . verticalMargins
h = min ( max ( h , self . heightMin ) , self . heightMax )
self . setMinimumHeight ( int ( h ) )
self . setMaximumHeight ( int ( h ) )
self . verticalScrollBar ( ) . setHidden ( docHeight + self . verticalMargins < self . heightMax )
# The scrollbar visibility can have changed so we update the overlay position here
self . _updateOverlayPos ( )
def _resolve_openalias ( self , text : str ) - > Optional [ dict ] :
key = text
key = key . strip ( ) # strip whitespaces