tKivy GUI improvements: - create unique instances of channels_dialog and addresses_dialog - display and refresh balances in channels_dialog - improve formatting of tx history - repurpose left button in receive_tab - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit 587f8aa48702496407f5044fa3a3d9f9aae36f34 (DIR) parent 8010123c0858bf9735cdfc6637899799ea6891f2 (HTM) Author: ThomasV <thomasv@electrum.org> Date: Thu, 22 Aug 2019 18:48:52 +0200 Kivy GUI improvements: - create unique instances of channels_dialog and addresses_dialog - display and refresh balances in channels_dialog - improve formatting of tx history - repurpose left button in receive_tab Diffstat: M electrum/gui/kivy/main_window.py | 23 +++++++++-------------- M electrum/gui/kivy/uix/dialogs/ligh… | 90 +++++++++++++++++++++++++------- M electrum/gui/kivy/uix/screens.py | 50 +++++++++++++++++++------------- M electrum/gui/kivy/uix/ui_screens/h… | 39 ++++++++++++++++++++++--------- M electrum/gui/kivy/uix/ui_screens/r… | 2 +- M electrum/wallet.py | 7 +++++++ 6 files changed, 147 insertions(+), 64 deletions(-) --- (DIR) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py t@@ -333,6 +333,8 @@ class ElectrumWindow(App): # cached dialogs self._settings_dialog = None self._password_dialog = None + self._channels_dialog = None + self._addresses_dialog = None self.fee_status = self.electrum_config.get_fee_status() self.request_popup = None t@@ -666,8 +668,9 @@ class ElectrumWindow(App): d.open() def lightning_channels_dialog(self): - d = LightningChannelsDialog(self) - d.open() + if self._channels_dialog is None: + self._channels_dialog = LightningChannelsDialog(self) + self._channels_dialog.open() def popup_dialog(self, name): if name == 'settings': t@@ -1054,20 +1057,12 @@ class ElectrumWindow(App): popup.update() popup.open() - def requests_dialog(self, screen): - from .uix.dialogs.requests import RequestsDialog - if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0: - self.show_info(_('No saved requests.')) - return - popup = RequestsDialog(self, screen, None) - popup.update() - popup.open() - def addresses_dialog(self): from .uix.dialogs.addresses import AddressesDialog - popup = AddressesDialog(self) - popup.update() - popup.open() + if self._addresses_dialog is None: + self._addresses_dialog = AddressesDialog(self) + self._addresses_dialog.update() + self._addresses_dialog.open() def fee_dialog(self, label, dt): from .uix.dialogs.fee_dialog import FeeDialog (DIR) diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py t@@ -6,26 +6,52 @@ from kivy.uix.popup import Popup from kivy.clock import Clock from electrum.gui.kivy.uix.context_menu import ContextMenu from electrum.util import bh2u -from electrum.lnutil import LOCAL, REMOTE +from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id from electrum.gui.kivy.i18n import _ Builder.load_string(r''' <LightningChannelItem@CardItem> details: {} active: False - channelId: '<channelId not set>' - id: card + short_channel_id: '<channelId not set>' + status: '' + local_balance: '' + remote_balance: '' _chan: None - Label: - color: (.5,.5,.5,1) if not card.active else (1,1,1,1) - text: root.channelId - Label: - text: (card._chan.get_state() if card._chan else 'n/a') - + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + CardLabel: + color: (.5,.5,.5,1) if not root.active else (1,1,1,1) + text: root.short_channel_id + font_size: '15sp' + Widget + CardLabel: + font_size: '13sp' + shorten: True + text: root.status + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + CardLabel: + text: root.local_balance + font_size: '13sp' + halign: 'right' + Widget + CardLabel: + text: root.remote_balance + font_size: '13sp' + halign: 'right' + Widget <LightningChannelsDialog@Popup>: name: 'lightning_channels' - title: _('Lightning channels. Tap for options.') + title: _('Lightning channels.') id: popup BoxLayout: id: box t@@ -103,12 +129,13 @@ class LightningChannelsDialog(Factory.Popup): self.clocks = [] self.app = app self.context_menu = None - self.app.wallet.network.register_callback(self.channels_update, ['channels']) - self.channels_update('bogus evt') + self.app.wallet.network.register_callback(self.on_channels, ['channels']) + self.app.wallet.network.register_callback(self.on_channel, ['channel']) + self.update() def show_channel_details(self, obj): p = Factory.ChannelDetailsPopup() - p.title = _('Details for channel ') + self.presentable_chan_id(obj._chan) + p.title = _('Details for channel ') + format_short_channel_id(obj.chan.short_channel_id) p.data = [{'keyName': key, 'value': str(obj.details[key])} for key in obj.details.keys()] p.open() t@@ -146,10 +173,37 @@ class LightningChannelsDialog(Factory.Popup): self.ids.box.remove_widget(self.context_menu) self.context_menu = None - def presentable_chan_id(self, i): - return bh2u(i.short_channel_id) if i.short_channel_id else bh2u(i.channel_id)[:16] - - def channels_update(self, evt): + def format_fields(self, chan): + labels = {} + for subject in (REMOTE, LOCAL): + bal_minus_htlcs = chan.balance_minus_outgoing_htlcs(subject)//1000 + label = self.app.format_amount(bal_minus_htlcs) + other = subject.inverted() + bal_other = chan.balance(other)//1000 + bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(other)//1000 + if bal_other != bal_minus_htlcs_other: + label += ' (+' + self.app.format_amount(bal_other - bal_minus_htlcs_other) + ')' + labels[subject] = label + return [ + labels[LOCAL], + labels[REMOTE], + ] + + def on_channel(self, evt, chan): + Clock.schedule_once(lambda dt: self.update()) + + def on_channels(self, evt): + Clock.schedule_once(lambda dt: self.update()) + + def update_item(self, item): + chan = item._chan + item.status = chan.get_state() + item.short_channel_id = format_short_channel_id(chan.short_channel_id) + l, r = self.format_fields(chan) + item.local_balance = _('Local') + ':' + l + item.remote_balance = _('Remote') + ': ' + r + + def update(self): channel_cards = self.ids.lightning_channels_container channel_cards.clear_widgets() if not self.app.wallet: t@@ -158,10 +212,10 @@ class LightningChannelsDialog(Factory.Popup): for i in lnworker.channels.values(): item = Factory.LightningChannelItem() item.screen = self - item.channelId = self.presentable_chan_id(i) item.active = i.node_id in lnworker.peers item.details = self.channel_details(i) item._chan = i + self.update_item(item) channel_cards.add_widget(item) def channel_details(self, chan): (DIR) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py t@@ -34,6 +34,7 @@ from electrum.lnaddr import lndecode from electrum.lnutil import RECEIVED, SENT, PaymentFailure from .context_menu import ContextMenu +from .dialogs.question import Question from .dialogs.lightning_open_channel import LightningOpenChannelDialog from electrum.gui.kivy.i18n import _ t@@ -132,15 +133,15 @@ class HistoryScreen(CScreen): self.menu_actions = [ ('Label', self.label_dialog), ('Details', self.show_tx)] def show_tx(self, obj): - tx_hash = obj.tx_hash - tx = self.app.wallet.db.get_transaction(tx_hash) + key = obj.key + tx = self.app.wallet.db.get_transaction(key) if not tx: return self.app.tx_dialog(tx) def label_dialog(self, obj): from .dialogs.label_dialog import LabelDialog - key = obj.tx_hash + key = obj.key text = self.app.wallet.get_label(key) def callback(text): self.app.wallet.set_label(key, text) t@@ -151,14 +152,13 @@ class HistoryScreen(CScreen): def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance): is_lightning = tx_item.get('lightning', False) timestamp = tx_item['timestamp'] + key = tx_item.get('txid') or tx_item['payment_hash'] if is_lightning: status = 0 txpos = tx_item['txpos'] - if timestamp is None: - status_str = 'unconfirmed' - else: - status_str = format_time(int(timestamp)) + status_str = 'unconfirmed' if timestamp is None else format_time(int(timestamp)) icon = "atlas://electrum/gui/kivy/theming/light/lightning" + message = tx_item['label'] else: tx_hash = tx_item['txid'] conf = tx_item['confirmations'] t@@ -169,18 +169,19 @@ class HistoryScreen(CScreen): timestamp=tx_item['timestamp']) status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_info) icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status] + message = tx_item['label'] or tx_hash ri = {} ri['screen'] = self + ri['key'] = key ri['icon'] = icon ri['date'] = status_str - ri['message'] = tx_item['label'] + ri['message'] = message value = tx_item['value'].value if value is not None: ri['is_mine'] = value < 0 - if value < 0: value = - value - ri['amount'] = self.app.format_amount_and_units(value) + ri['amount'] = self.app.format_amount(value, is_diff = True) if 'fiat_value' in tx_item: - ri['quote_text'] = tx_item['fiat_value'].to_ui_string() + ri['quote_text'] = str(tx_item['fiat_value']) return ri def update(self, see_all=False): t@@ -344,7 +345,6 @@ class SendScreen(CScreen): message = self.screen.message amount = sum(map(lambda x:x[2], outputs)) if self.app.electrum_config.get('use_rbf'): - from .dialogs.question import Question d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b)) d.open() else: t@@ -406,7 +406,7 @@ class ReceiveScreen(CScreen): def __init__(self, **kwargs): super(ReceiveScreen, self).__init__(**kwargs) - self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)] + self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.delete_request_dialog)] Clock.schedule_interval(lambda dt: self.update(), 5) def expiry(self): t@@ -509,17 +509,27 @@ class ReceiveScreen(CScreen): d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback) d.open() - def do_delete(self, req): - from .dialogs.question import Question + def clear_requests_dialog(self): + expired = [req for req in self.app.wallet.get_sorted_requests(self.app.electrum_config) if req['status'] == PR_EXPIRED] + if len(expired) == 0: + return + def callback(c): + if c: + for req in expired: + is_lightning = req.get('lightning', False) + key = req['rhash'] if is_lightning else req['address'] + self.app.wallet.delete_request(key) + self.update() + d = Question(_('Delete expired requests?'), callback) + d.open() + + def delete_request_dialog(self, req): def cb(result): if result: - if req.is_lightning: - self.app.wallet.lnworker.delete_invoice(req.key) - else: - self.app.wallet.remove_payment_request(req.key, self.app.electrum_config) + self.app.wallet.delete_request(req.key) self.hide_menu() self.update() - d = Question(_('Delete request'), cb) + d = Question(_('Delete request?'), cb) d.open() def show_menu(self, obj): (DIR) diff --git a/electrum/gui/kivy/uix/ui_screens/history.kv b/electrum/gui/kivy/uix/ui_screens/history.kv t@@ -7,11 +7,9 @@ <CardLabel@Label> - color: 0.95, 0.95, 0.95, 1 - size_hint: 1, None - text: '' + color: .7, .7, .7, 1 text_size: self.width, None - height: self.texture_size[1] + #height: self.texture_size[1] halign: 'left' valign: 'top' t@@ -21,11 +19,12 @@ message: '' is_mine: True amount: '--' - action: _('Sent') if self.is_mine else _('Received') amount_color: '#FF6657' if self.is_mine else '#2EA442' confirmations: 0 date: '' quote_text: '' + amount_str: self.quote_text if app.is_fiat else self.amount + unit_str: app.fx.ccy if app.is_fiat else app.base_unit Image: id: icon source: root.icon t@@ -34,18 +33,36 @@ width: self.height*1.5 mipmap: True BoxLayout: + spacing: '8dp' + height: '32dp' orientation: 'vertical' Widget CardLabel: - text: - u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\ - + ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount) + color: 0.95, 0.95, 0.95, 1 + text: root.message + shorten: True + shorten_from: 'right' font_size: '15sp' + Widget CardLabel: - color: .699, .699, .699, 1 - font_size: '14sp' + font_size: '12sp' shorten: True - text: root.date + ' ' + root.message + text: root.date + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + CardLabel: + text: u'[color={color}]{s}[/color]'.format(s=root.amount_str, color=root.amount_color) + ' ' + '[size=12sp]' + root.unit_str + '[/size]' + halign: 'right' + font_size: '15sp' + Widget + CardLabel: + text: '' + halign: 'right' + font_size: '12sp' Widget <HistoryRecycleView>: (DIR) diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv t@@ -135,7 +135,7 @@ ReceiveScreen: icon: 'atlas://electrum/gui/kivy/theming/light/list' size_hint: 0.5, None height: '48dp' - on_release: Clock.schedule_once(lambda dt: app.addresses_dialog()) + on_release: Clock.schedule_once(lambda dt: s.clear_requests_dialog()) IconButton: icon: 'atlas://electrum/gui/kivy/theming/light/clock1' size_hint: 0.5, None (DIR) diff --git a/electrum/wallet.py b/electrum/wallet.py t@@ -1356,6 +1356,13 @@ class Abstract_Wallet(AddressSynchronizer): f.write(json.dumps(req)) return req + def delete_request(self, key): + """ lightning or on-chain """ + if key in self.receive_requests: + self.remove_payment_request(key, {}) + elif self.lnworker: + self.lnworker.delete_invoice(key) + def remove_payment_request(self, addr, config): if addr not in self.receive_requests: return False