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