Browse Source

kivy updates

patch-4
ThomasV 3 years ago
parent
commit
cbb51b31d8
  1. 6
      electrum/gui/kivy/main_window.py
  2. 2
      electrum/gui/kivy/uix/dialogs/invoice_dialog.py
  3. 5
      electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py
  4. 88
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  5. 73
      electrum/gui/kivy/uix/screens.py
  6. 40
      electrum/gui/kivy/uix/ui_screens/receive.kv

6
electrum/gui/kivy/main_window.py

@ -505,17 +505,17 @@ class ElectrumWindow(App, Logger):
tab = self.tabs.ids[name + '_tab'] tab = self.tabs.ids[name + '_tab']
panel.switch_to(tab) panel.switch_to(tab)
def show_request(self, is_lightning, key): def show_request(self, key):
from .uix.dialogs.request_dialog import RequestDialog from .uix.dialogs.request_dialog import RequestDialog
self.request_popup = RequestDialog('Request', key) self.request_popup = RequestDialog('Request', key)
self.request_popup.open() self.request_popup.open()
def show_invoice(self, is_lightning, key): def show_invoice(self, key):
from .uix.dialogs.invoice_dialog import InvoiceDialog from .uix.dialogs.invoice_dialog import InvoiceDialog
invoice = self.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
if not invoice: if not invoice:
return return
data = invoice.invoice if is_lightning else key data = invoice.lightning_invoice if invoice.is_lightning() else key
self.invoice_popup = InvoiceDialog('Invoice', data, key) self.invoice_popup = InvoiceDialog('Invoice', data, key)
self.invoice_popup.open() self.invoice_popup.open()

2
electrum/gui/kivy/uix/dialogs/invoice_dialog.py

@ -8,7 +8,7 @@ from kivy.clock import Clock
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.invoices import pr_tooltips, pr_color from electrum.invoices import pr_tooltips, pr_color
from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.gui.kivy.main_window import ElectrumWindow from electrum.gui.kivy.main_window import ElectrumWindow

5
electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py

@ -13,7 +13,7 @@ from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button from kivy.uix.button import Button
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.invoices import LNInvoice from electrum.invoices import Invoice
if TYPE_CHECKING: if TYPE_CHECKING:
@ -121,7 +121,6 @@ class LightningTxDialog(Factory.Popup):
invoice = (self.app.wallet.get_invoice(self.payment_hash) invoice = (self.app.wallet.get_invoice(self.payment_hash)
or self.app.wallet.get_request(self.payment_hash)) or self.app.wallet.get_request(self.payment_hash))
if invoice: if invoice:
assert isinstance(invoice, LNInvoice), f"{self.invoice!r}" self.invoice = invoice.lightning_invoice or ''
self.invoice = invoice.invoice
else: else:
self.invoice = '' self.invoice = ''

88
electrum/gui/kivy/uix/dialogs/request_dialog.py

@ -5,15 +5,22 @@ from kivy.lang import Builder
from kivy.core.clipboard import Clipboard from kivy.core.clipboard import Clipboard
from kivy.app import App from kivy.app import App
from kivy.clock import Clock from kivy.clock import Clock
from kivy.properties import NumericProperty, StringProperty
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.invoices import pr_tooltips, pr_color from electrum.invoices import pr_tooltips, pr_color
from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED
if TYPE_CHECKING: if TYPE_CHECKING:
from ...main_window import ElectrumWindow from ...main_window import ElectrumWindow
MODE_ADDRESS = 0
MODE_URI = 1
MODE_LIGHTNING = 2
Builder.load_string(''' Builder.load_string('''
#:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH
@ -22,8 +29,9 @@ Builder.load_string('''
amount_str: '' amount_str: ''
title: '' title: ''
description:'' description:''
is_lightning: False mode:0
key:'' key:''
data:''
warning: '' warning: ''
status_str: '' status_str: ''
status_color: 1,1,1,1 status_color: 1,1,1,1
@ -43,15 +51,37 @@ Builder.load_string('''
on_touch_down: on_touch_down:
touch = args[1] touch = args[1]
if self.collide_point(*touch.pos): self.shaded = not self.shaded if self.collide_point(*touch.pos): self.shaded = not self.shaded
TopLabel:
text: root.data[0:70] + ('...' if len(root.data)>70 else '')
BoxLayout:
size_hint: 1, None
height: '48dp'
ToggleButton:
id: b0
group:'g'
size_hint: 1, None
height: '48dp'
text: _('Address')
on_release: root.mode = 0
ToggleButton:
id: b1
group:'g'
size_hint: 1, None
height: '48dp'
text: _('URI')
on_release: root.mode = 1
state: 'down'
ToggleButton:
id: b2
group:'g'
size_hint: 1, None
height: '48dp'
text: _('Lightning')
on_release: root.mode = 2
TopLabel: TopLabel:
text: _('Description') + ': ' + root.description or _('None') text: _('Description') + ': ' + root.description or _('None')
TopLabel: TopLabel:
text: _('Amount') + ': ' + root.amount_str text: _('Amount') + ': ' + root.amount_str
TopLabel:
text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': '
RefLabel:
data: root.key
name: (_('Address') if not root.is_lightning else _('Payment hash'))
TopLabel: TopLabel:
text: _('Status') + ': ' + root.status_str text: _('Status') + ': ' + root.status_str
color: root.status_color color: root.status_color
@ -87,6 +117,9 @@ Builder.load_string('''
class RequestDialog(Factory.Popup): class RequestDialog(Factory.Popup):
mode = NumericProperty(0)
data = StringProperty('')
def __init__(self, title, key): def __init__(self, title, key):
self.status = PR_UNKNOWN self.status = PR_UNKNOWN
Factory.Popup.__init__(self) Factory.Popup.__init__(self)
@ -94,33 +127,50 @@ class RequestDialog(Factory.Popup):
self.title = title self.title = title
self.key = key self.key = key
r = self.app.wallet.get_request(key) r = self.app.wallet.get_request(key)
self.is_lightning = r.is_lightning()
self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
self.amount_sat = r.get_amount_sat() self.amount_sat = r.get_amount_sat()
self.amount_str = self.app.format_amount_and_units(self.amount_sat) self.amount_str = self.app.format_amount_and_units(self.amount_sat)
self.description = r.message self.description = r.message
self.mode = 1
self.on_mode(0, 0)
self.ids.b0.pressed = True
self.update_status() self.update_status()
def on_open(self): def on_mode(self, instance, x):
data = self.data r = self.app.wallet.get_request(self.key)
if self.is_lightning: if self.mode == MODE_ADDRESS:
self.data = r.get_address() or ''
elif self.mode == MODE_URI:
self.data = self.app.wallet.get_request_URI(r) or ''
else:
self.data = r.lightning_invoice or ''
qr_data = self.data
if self.mode == MODE_LIGHTNING:
# encode lightning invoices as uppercase so QR encoding can use # encode lightning invoices as uppercase so QR encoding can use
# alphanumeric mode; resulting in smaller QR codes # alphanumeric mode; resulting in smaller QR codes
data = data.upper() qr_data = qr_data.upper()
self.ids.qr.set_data(data) if qr_data:
self.ids.qr.set_data(qr_data)
self.ids.qr.opacity = 1
else:
self.ids.qr.opacity = 0
self.update_status()
def update_status(self): def update_status(self):
req = self.app.wallet.get_request(self.key) req = self.app.wallet.get_request(self.key)
self.status = self.app.wallet.get_request_status(self.key) self.status = self.app.wallet.get_request_status(self.key)
self.status_str = req.get_status_str(self.status) self.status_str = req.get_status_str(self.status)
self.status_color = pr_color[self.status] self.status_color = pr_color[self.status]
if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker: warning = ''
if self.status == PR_UNPAID and self.mode == MODE_LIGHTNING and self.app.wallet.lnworker:
if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive(): if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive():
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels')
if self.status == PR_UNPAID and not self.is_lightning: if not self.mode == MODE_LIGHTNING:
address = req.get_address() address = req.get_address()
if self.app.wallet.is_used(address): if not address:
self.warning = _('Warning') + ': ' + _('This address is being reused') warning = _('Warning') + ': ' + _('This request cannot be paid on-chain')
elif self.app.wallet.is_used(address):
warning = _('Warning') + ': ' + _('This address is being reused')
self.warning = warning
def on_dismiss(self): def on_dismiss(self):
self.app.request_popup = None self.app.request_popup = None

73
electrum/gui/kivy/uix/screens.py

@ -9,10 +9,11 @@ from kivy.properties import ObjectProperty
from kivy.lang import Builder from kivy.lang import Builder
from kivy.factory import Factory from kivy.factory import Factory
from kivy.uix.recycleview import RecycleView from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty
from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, from electrum.invoices import (PR_DEFAULT_EXPIRATION_WHEN_CREATING,
PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
LNInvoice, pr_expiration_values, Invoice, OnchainInvoice) pr_expiration_values, Invoice)
from electrum import bitcoin, constants from electrum import bitcoin, constants
from electrum.transaction import tx_from_any, PartialTxOutput from electrum.transaction import tx_from_any, PartialTxOutput
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice, from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice,
@ -224,15 +225,14 @@ class SendScreen(CScreen, Logger):
payments_container.refresh_from_data() payments_container.refresh_from_data()
def show_item(self, obj): def show_item(self, obj):
self.app.show_invoice(obj.is_lightning, obj.key) self.app.show_invoice(obj.key)
def get_card(self, item: Invoice) -> Dict[str, Any]: def get_card(self, item: Invoice) -> Dict[str, Any]:
status = self.app.wallet.get_invoice_status(item) status = self.app.wallet.get_invoice_status(item)
status_str = item.get_status_str(status) status_str = item.get_status_str(status)
is_lightning = item.type == PR_TYPE_LN is_lightning = item.is_lightning()
key = self.app.wallet.get_key_for_outgoing_invoice(item) key = self.app.wallet.get_key_for_outgoing_invoice(item)
if is_lightning: if is_lightning:
assert isinstance(item, LNInvoice)
address = item.rhash address = item.rhash
if self.app.wallet.lnworker: if self.app.wallet.lnworker:
log = self.app.wallet.lnworker.logs.get(key) log = self.app.wallet.lnworker.logs.get(key)
@ -240,7 +240,6 @@ class SendScreen(CScreen, Logger):
status_str += '... (%d)'%len(log) status_str += '... (%d)'%len(log)
is_bip70 = False is_bip70 = False
else: else:
assert isinstance(item, OnchainInvoice)
address = item.get_address() address = item.get_address()
is_bip70 = bool(item.bip70) is_bip70 = bool(item.bip70)
return { return {
@ -313,7 +312,7 @@ class SendScreen(CScreen, Logger):
message = self.message message = self.message
try: try:
if self.is_lightning: if self.is_lightning:
return LNInvoice.from_bech32(address) return Invoice.from_bech32(address)
else: # on-chain else: # on-chain
if self.payment_request: if self.payment_request:
outputs = self.payment_request.get_outputs() outputs = self.payment_request.get_outputs()
@ -358,10 +357,11 @@ class SendScreen(CScreen, Logger):
else: else:
self._do_pay_onchain(invoice) self._do_pay_onchain(invoice)
def _do_pay_lightning(self, invoice: LNInvoice, pw) -> None: def _do_pay_lightning(self, invoice: Invoice, pw) -> None:
amount_msat = invoice.get_amount_msat()
def pay_thread(): def pay_thread():
try: try:
coro = self.app.wallet.lnworker.pay_invoice(invoice.invoice, attempts=10) coro = self.app.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat, attempts=10)
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop) fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
fut.result() fut.result()
except Exception as e: except Exception as e:
@ -369,7 +369,7 @@ class SendScreen(CScreen, Logger):
self.save_invoice(invoice) self.save_invoice(invoice)
threading.Thread(target=pay_thread).start() threading.Thread(target=pay_thread).start()
def _do_pay_onchain(self, invoice: OnchainInvoice) -> None: def _do_pay_onchain(self, invoice: Invoice) -> None:
outputs = invoice.outputs outputs = invoice.outputs
amount = sum(map(lambda x: x.value, outputs)) if not any(parse_max_spend(x.value) for x in outputs) else '!' amount = sum(map(lambda x: x.value, outputs)) if not any(parse_max_spend(x.value) for x in outputs) else '!'
coins = self.app.wallet.get_spendable_coins(None) coins = self.app.wallet.get_spendable_coins(None)
@ -399,11 +399,17 @@ class SendScreen(CScreen, Logger):
class ReceiveScreen(CScreen): class ReceiveScreen(CScreen):
kvname = 'receive' kvname = 'receive'
expiration_text = StringProperty('')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(ReceiveScreen, self).__init__(**kwargs) super(ReceiveScreen, self).__init__(**kwargs)
Clock.schedule_interval(lambda dt: self.update(), 5) Clock.schedule_interval(lambda dt: self.update(), 5)
self.is_max = False # not used for receiving (see app.amount_dialog) self.is_max = False # not used for receiving (see app.amount_dialog)
self.expiration_text = pr_expiration_values[self.expiry()]
def on_open(self):
c = self.expiry()
self.expiration_text = pr_expiration_values[c]
def expiry(self): def expiry(self):
return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
@ -437,45 +443,39 @@ class ReceiveScreen(CScreen):
self.app._clipboard.copy(uri) self.app._clipboard.copy(uri)
self.app.show_info(_('Request copied to clipboard')) self.app.show_info(_('Request copied to clipboard'))
def new_request(self, lightning): def new_request(self):
amount = self.amount amount_str = self.amount
amount = self.app.get_amount(amount) if amount else 0 amount_sat = self.app.get_amount(amount_str) if amount_str else 0
message = self.message message = self.message
lnworker = self.app.wallet.lnworker expiry = self.expiry()
try: if amount_sat and amount_sat < self.wallet.dust_threshold():
if lightning: self.address = ''
if lnworker: if not self.app.wallet.has_lightning():
key = lnworker.add_request(amount, message, self.expiry()) return
else:
addr = self.address or self.app.wallet.get_unused_address()
if not addr:
if not self.app.wallet.is_deterministic():
addr = self.app.wallet.get_receiving_address()
else: else:
self.app.show_error(_("Lightning payments are not available for this wallet")) self.app.show_info(_('No address available. Please remove some of your pending requests.'))
return return
else: self.address = addr
addr = self.address or self.app.wallet.get_unused_address() try:
if not addr: key = self.app.wallet.create_request(amount_sat, message, expiry, self.address, lightning=self.app.wallet.has_lightning())
if not self.app.wallet.is_deterministic():
addr = self.app.wallet.get_receiving_address()
else:
self.app.show_info(_('No address available. Please remove some of your pending requests.'))
return
self.address = addr
req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry())
self.app.wallet.add_payment_request(req)
key = addr
except InvoiceError as e: except InvoiceError as e:
self.app.show_error(_('Error creating payment request') + ':\n' + str(e)) self.app.show_error(_('Error creating payment request') + ':\n' + str(e))
return return
self.clear() self.clear()
self.update() self.update()
self.app.show_request(lightning, key) self.app.show_request(key)
def get_card(self, req: Invoice) -> Dict[str, Any]: def get_card(self, req: Invoice) -> Dict[str, Any]:
is_lightning = req.is_lightning() is_lightning = req.is_lightning()
if not is_lightning: if not is_lightning:
assert isinstance(req, OnchainInvoice)
address = req.get_address() address = req.get_address()
else: else:
assert isinstance(req, LNInvoice) address = req.lightning_invoice
address = req.invoice
key = self.app.wallet.get_key_for_receive_request(req) key = self.app.wallet.get_key_for_receive_request(req)
amount = req.get_amount_sat() amount = req.get_amount_sat()
description = req.message description = req.message
@ -513,12 +513,13 @@ class ReceiveScreen(CScreen):
payments_container.refresh_from_data() payments_container.refresh_from_data()
def show_item(self, obj): def show_item(self, obj):
self.app.show_request(obj.is_lightning, obj.key) self.app.show_request(obj.key)
def expiration_dialog(self, obj): def expiration_dialog(self, obj):
from .dialogs.choice_dialog import ChoiceDialog from .dialogs.choice_dialog import ChoiceDialog
def callback(c): def callback(c):
self.app.electrum_config.set_key('request_expiry', c) self.app.electrum_config.set_key('request_expiry', c)
self.expiration_text = pr_expiration_values[c]
d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback) d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
d.open() d.open()

40
electrum/gui/kivy/uix/ui_screens/receive.kv

@ -67,7 +67,7 @@
amount: '' amount: ''
message: '' message: ''
status: '' status: ''
is_lightning: False expiration_text: _('Expiry')
BoxLayout BoxLayout
padding: '12dp', '12dp', '12dp', '12dp' padding: '12dp', '12dp', '12dp', '12dp'
@ -83,33 +83,34 @@
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
Image: Image:
source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/lightning' if root.is_lightning else f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/globe' source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/calculator'
opacity: 0.7
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: address_label id: amount_label
text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address')) default_text: _('Amount')
shorten: True text: s.amount if s.amount else _('Amount')
on_release: root.is_lightning = not root.is_lightning if app.wallet.has_lightning() else False on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, False))
CardSeparator: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
id: message_selection
opacity: 1
size_hint: 1, None size_hint: 1, None
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
Image: Image:
source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/calculator' source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/pen'
opacity: 0.7
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: amount_label id: description
default_text: _('Amount') text: s.message if s.message else _('Description')
text: s.amount if s.amount else _('Amount') on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, False))
CardSeparator: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
@ -120,32 +121,27 @@
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
Image: Image:
source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/pen' source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/clock1'
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: description id: description
text: s.message if s.message else _('Description') text: s.expiration_text
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) on_release: Clock.schedule_once(lambda dt: s.expiration_dialog(s))
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
IconButton:
icon: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/clock1'
size_hint: 0.5, None
height: '48dp'
on_release: Clock.schedule_once(lambda dt: s.expiration_dialog(s))
Button: Button:
text: _('Clear') text: _('Clear')
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
on_release: Clock.schedule_once(lambda dt: s.clear()) on_release: Clock.schedule_once(lambda dt: s.clear())
Button: Button:
text: _('Request') text: _('New Request')
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
on_release: Clock.schedule_once(lambda dt: s.new_request(root.is_lightning)) on_release: Clock.schedule_once(lambda dt: s.new_request())
Widget: Widget:
size_hint: 1, 0.1 size_hint: 1, 0.1
RequestRecycleView: RequestRecycleView:

Loading…
Cancel
Save