tMerge pull request #3206 from ariard/kivy-addr - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 786f9ce7ff8eab6355e2908a597828205ab5489e DIR parent 0ecb665b95917459d17ef3f9e47bde4864a2e318 HTML Author: ThomasV <thomasv@electrum.org> Date: Fri, 10 Nov 2017 10:56:32 +0100 Merge pull request #3206 from ariard/kivy-addr kivy: replace requests tab by address tab Diffstat: M gui/kivy/main.kv | 54 ++++++++++++++++++++++++++++--- M gui/kivy/main_window.py | 23 +++++++++++++++++++++++ M gui/kivy/uix/screens.py | 226 +++++++++++++++++++++++++------ A gui/kivy/uix/ui_screens/address.kv | 129 +++++++++++++++++++++++++++++++ M gui/kivy/uix/ui_screens/invoice.kv | 24 +++++++++++++++++++++--- 5 files changed, 407 insertions(+), 49 deletions(-) --- DIR diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv t@@ -191,6 +191,31 @@ pos: self.pos +<AddressFilter@GridLayout> + item_height: dp(42) + item_width: dp(60) + foreground_color: .843, .914, .972, 1 + cols: 1 + canvas.before: + Color: + rgba: 0.192, .498, 0.745, 1 + BorderImage: + source: 'atlas://gui/kivy/theming/light/card_bottom' + size: self.size + pos: self.pos + +<SearchBox@GridLayout> + item_height: dp(42) + foreground_color: .843, .914, .972, 1 + cols: 1 + padding: '12dp', 0 + canvas.before: + Color: + rgba: 0.192, .498, 0.745, 1 + BorderImage: + source: 'atlas://gui/kivy/theming/light/card_bottom' + size: self.size + pos: self.pos <CardSeparator@Widget> size_hint: 1, None t@@ -238,6 +263,25 @@ size: self.size pos: self.pos +<AddressButton@Button>: + background_color: 1, .585, .878, 0 + halign: 'center' + text_size: (self.width, None) + shorten: True + size_hint: 0.5, None + default_text: '' + text: self.default_text + padding: '5dp', '5dp' + height: '40dp' + text_color: self.foreground_color + disabled_color: 1, 1, 1, 1 + foreground_color: 1, 1, 1, 1 + canvas.before: + Color: + rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color + Rectangle: + size: self.size + pos: self.pos <KButton@Button>: size_hint: 1, None t@@ -340,9 +384,9 @@ ReceiveScreen: id: receive_screen tab: receive_tab - RequestsScreen: - id: requests_screen - tab: requests_tab + AddressScreen: + id: address_screen + tab: address_tab CleanHeader: id: invoices_tab text: _('Invoices') t@@ -360,8 +404,8 @@ text: _('Receive') slide: 3 CleanHeader: - id: requests_tab - text: _('Requests') + id: address_tab + text: _('Address') slide: 4 DIR diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py t@@ -347,6 +347,7 @@ class ElectrumWindow(App): exp = req.get('exp') memo = req.get('memo') amount = req.get('amount') + fund = req.get('fund') popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv') popup.is_invoice = is_invoice popup.amount = amount t@@ -355,9 +356,24 @@ class ElectrumWindow(App): popup.description = memo if memo else '' popup.signature = req.get('signature', '') popup.status = status + popup.fund = fund if fund else 0 txid = req.get('txid') popup.tx_hash = txid or '' popup.on_open = lambda: popup.ids.output_list.update(req.get('outputs', [])) + popup.export = self.export_private_keys + popup.open() + + def show_addr_details(self, req, status): + from electrum.util import format_time + fund = req.get('fund') + isaddr = 'y' + popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv') + popup.isaddr = isaddr + popup.is_invoice = False + popup.status = status + popup.requestor = req.get('address') + popup.fund = fund if fund else 0 + popup.export = self.export_private_keys popup.open() def qr_dialog(self, title, data, show_text=False): t@@ -587,6 +603,7 @@ class ElectrumWindow(App): self.invoices_screen = None self.receive_screen = None self.requests_screen = None + self.address_screen = None self.icon = "icons/electrum.png" self.tabs = self.root.ids['tabs'] t@@ -924,3 +941,9 @@ class ElectrumWindow(App): self._password_dialog = PasswordDialog() self._password_dialog.init(msg, callback) self._password_dialog.open() + + def export_private_keys(self, pk_label, addr): + def show_private_key(addr, pk_label, password): + key = str(self.wallet.export_private_key(addr, password)[0]) + pk_label.data = key + self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label)) DIR diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py t@@ -478,65 +478,122 @@ class InvoicesScreen(CScreen): d.open() -class RequestsScreen(CScreen): - kvname = 'requests' +address_icon = { + 'Pending' : 'atlas://gui/kivy/theming/light/important', + 'Paid' : 'atlas://gui/kivy/theming/light/confirmed' +} + +class AddressScreen(CScreen): + kvname = 'address' cards = {} - def get_card(self, req): - address = req['address'] - timestamp = req.get('time', 0) - amount = req.get('amount') - expiration = req.get('exp', None) - status = req.get('status') - signature = req.get('sig') - - ci = self.cards.get(address) + def get_card(self, addr, search): + ci = self.cards.get(addr) if ci is None: ci = Factory.RequestItem() ci.screen = self - ci.address = address - self.cards[address] = ci + ci.address = addr + ci.status = search + self.cards[addr] = ci + else: + ci.status = search - ci.memo = self.app.wallet.get_label(address) - if amount: - status = req.get('status') - ci.status = request_text[status] + ci.memo = self.app.wallet.get_label(addr) + if search == 'Pending' or search == 'Paid': + req = self.app.wallet.get_payment_request(addr, self.app.electrum_config) + timestamp = req.get('time', 0) + amount = req.get('amount') + ci.icon = address_icon[search] + ci.amount = self.app.format_amount_and_units(amount) if amount else _('No Amount') + ci.date = format_time(timestamp) else: - received = self.app.wallet.get_addr_received(address) - ci.status = self.app.format_amount_and_units(amount) - ci.icon = pr_icon[status] - ci.amount = self.app.format_amount_and_units(amount) if amount else _('No Amount') - ci.date = format_time(timestamp) + c, u, x = self.app.wallet.get_addr_balance(addr) + balance = c + u + x + ci.icon = '' + ci.amount = self.app.format_amount_and_units(balance) if balance else _('No Amount') + ci.date = '' return ci - def update(self): - self.menu_actions = [('Show', self.do_show), ('Details', self.do_view), ('Delete', self.do_delete)] - requests_list = self.screen.ids.requests_container - requests_list.clear_widgets() - _list = self.app.wallet.get_sorted_requests(self.app.electrum_config) if self.app.wallet else [] + def generic_search(self): + _list = self.ext_search(self.screen.message) + + search_list = self.screen.ids.search_container + search_list.clear_widgets() for req in _list: - ci = self.get_card(req) - requests_list.add_widget(ci) + status, conf = self.app.wallet.get_request_status(req) + if status == PR_PAID: + search = "Paid" + elif status == PR_UNPAID: + search = "Pending" + else: + search = "" + card = self.get_card(req, search) + search_list.add_widget(card) if not _list: - msg = _('This screen shows the list of payment requests you made.') - requests_list.add_widget(EmptyLabel(text=msg)) + msg = _('No address matching your search') + search_list.add_widget(EmptyLabel(text=msg)) + + def search(self, req): + + if req == 0: + self.screen.addr_type = 'Change' if self.screen.addr_type == 'Receiving' else 'Receiving' + + if req == 1: + status = { 'New' : 'Funded', 'Funded' : 'Unused', 'Unused' : 'New' } + for s in status: + if s == self.screen.addr_status: + self.screen.addr_status = status[s] + break + if req == 2: + pr_status = { 'Pending' : 'Paid', 'Paid' : 'Pending' } + for s in pr_status: + if s == self.screen.pr_status: + self.screen.pr_status = pr_status[s] + break + + search = self.screen.addr_type if req == 0 else (self.screen.addr_status if req == 1 else self.screen.pr_status) + _list = self.addr_search(search) + + search_list = self.screen.ids.search_container + search_list.clear_widgets() + if _list is None: + return + for addr in _list: + card = self.get_card(addr, search) + search_list.add_widget(card) + + def update(self): + self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] def do_show(self, obj): self.app.show_request(obj.address) def do_view(self, obj): req = self.app.wallet.get_payment_request(obj.address, self.app.electrum_config) - status = req.get('status') - amount = req.get('amount') - address = req['address'] - if amount: + if req: + c, u, x = self.app.wallet.get_addr_balance(obj.address) + balance = c + u + x + if balance > 0: + req['fund'] = balance status = req.get('status') - status = request_text[status] - else: - received_amount = self.app.wallet.get_addr_received(address) - status = self.app.format_amount_and_units(received_amount) + amount = req.get('amount') + address = req['address'] + if amount: + status = req.get('status') + status = request_text[status] + else: + received_amount = self.app.wallet.get_addr_received(address) + status = self.app.format_amount_and_units(received_amount) + self.app.show_pr_details(req, status, False) - self.app.show_pr_details(req, status, False) + else: + req = { 'address': obj.address, 'status' : obj.status } + status = obj.status + c, u, x = self.app.wallet.get_addr_balance(obj.address) + balance = c + u + x + if balance > 0: + req['fund'] = balance + self.app.show_addr_details(req, status) def do_delete(self, obj): from .dialogs.question import Question t@@ -547,7 +604,94 @@ class RequestsScreen(CScreen): d = Question(_('Delete request?'), cb) d.open() - + def addr_search(self, search): + + def get_addr_receiving(self): + return self.app.wallet.receiving_addresses + + def get_addr_change(self): + return self.app.wallet.change_addresses + + def get_addr_new(self): + _list = list() + for addr in self.app.wallet.receiving_addresses: + if not self.app.wallet.is_used(addr) and self.app.wallet.is_empty(addr) and addr not in self.app.wallet.receive_requests: + _list.append(addr) + for addr in self.app.wallet.change_addresses: + if not self.app.wallet.is_used(addr) and self.app.wallet.is_empty(addr): + _list.append(addr) + return _list + + def get_addr_unused(self): + _list = list() + for addr in self.app.wallet.receiving_addresses: + if self.app.wallet.is_used(addr): + _list.append(addr) + for addr in self.app.wallet.change_addresses: + if self.app.wallet.is_used(addr): + _list.append(addr) + return _list + + def get_addr_funded(self): + _list = list() + for addr in self.app.wallet.receiving_addresses: + c, u, x = self.app.wallet.get_addr_balance(addr) + balance = c + u + x + if balance > 0: + _list.append(addr) + for addr in self.app.wallet.change_addresses: + c, u, x = self.app.wallet.get_addr_balance(addr) + balance = c + u + x + if balance > 0: + _list.append(addr) + return _list + + def get_addr_pending(self): + _list = list() + for addr in self.app.wallet.receive_requests: + status, conf = self.app.wallet.get_request_status(addr) + if status == PR_UNPAID or status == PR_EXPIRED: + _list.append(addr) + return _list + + def get_addr_paid(self): + _list = list() + for addr in self.app.wallet.receive_requests: + status, conf = self.app.wallet.get_request_status(addr) + if status == PR_PAID: + _list.append(addr) + return _list + + addr_search = { 'Receiving' : get_addr_receiving, 'Change' : get_addr_change, + 'New' : get_addr_new, 'Unused' : get_addr_unused, 'Funded' : get_addr_funded, + 'Pending' : get_addr_pending, 'Paid' : get_addr_paid } + + for s in addr_search: + if search == s: + _list = addr_search[s](self) + return _list + + + def ext_search(self, search): + + def to_btc(amount): + return str(amount / 100000000) + + def to_mbtc(amount): + return str(amount / 100000) + + def to_time(time): + from time import gmtime, strftime + return strftime("%Y-%m-%d %M:%S", gmtime(time)) + + _list = [] + for addr in self.app.wallet.receive_requests: + r = self.app.wallet.receive_requests.get(addr) + if r['memo'].find(search) >= 0 or to_btc(r['amount']).find(search) >= 0 \ + or to_mbtc(r['amount']).find(search) >= 0 or to_time(r['time']).find(search) >= 0: + _list.append(addr) + + return _list class TabbedCarousel(Factory.TabbedPanel): DIR diff --git a/gui/kivy/uix/ui_screens/address.kv b/gui/kivy/uix/ui_screens/address.kv t@@ -0,0 +1,129 @@ +#:import _ electrum_gui.kivy.i18n._ +#:import Decimal decimal.Decimal +#:set btc_symbol chr(171) +#:set mbtc_symbol chr(187) +#:set font_light 'gui/kivy/data/fonts/Roboto-Condensed.ttf' + +<RequestLabel@Label> + text_size: self.width, None + halign: 'left' + valign: 'top' + +<RequestItem@CardItem> + address: '' + memo: '' + amount: '' + status: '' + date: '' + icon: '' + color: .699, .699, .699, 1 + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 if root.icon else 0 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.address + shorten: True + Widget + RequestLabel: + text: root.date + " " + root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.amount + halign: 'right' + font_size: '15sp' + Widget + RequestLabel: + text: root.status + halign: 'right' + font_size: '13sp' + color: .699, .699, .699, 1 + +AddressScreen: + id: addr_screen + name: 'address' + message: '' + addr_type: 'Receiving' + addr_status: 'New' + pr_status: 'Pending' + + on_message: + self.parent.generic_search() + + + BoxLayout + padding: '12dp', '70dp', '12dp', '12dp' + spacing: '12dp' + orientation: 'vertical' + size_hint: 1, 1.1 + + BoxLayout: + spacing: '6dp' + size_hint: 1, None + orientation: 'horizontal' + AddressFilter: + id: blue_bottom + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + AddressButton: + id: search + text: addr_screen.addr_type + on_release: Clock.schedule_once(lambda dt: app.address_screen.search(0)) + AddressFilter: + id: blue_bottom + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + AddressButton: + id: search + text: addr_screen.addr_status + on_release: Clock.schedule_once(lambda dt: app.address_screen.search(1)) + AddressFilter: + id: blue_bottom + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + AddressButton: + id: pending + text: addr_screen.pr_status + on_release: Clock.schedule_once(lambda dt: app.address_screen.search(2)) + AddressFilter: + id: blue_bottom + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + canvas.before: + Color: + rgba: 0.9, 0.9, 0.9, 1 + AddressButton: + id: change + text: addr_screen.message if addr_screen.message else _('Search') + on_release: Clock.schedule_once(lambda dt: app.description_dialog(addr_screen)) + + ScrollView: + GridLayout: + cols: 1 + id: search_container + size_hint_y: None + height: self.minimum_height + spacing: '2dp' DIR diff --git a/gui/kivy/uix/ui_screens/invoice.kv b/gui/kivy/uix/ui_screens/invoice.kv t@@ -11,6 +11,9 @@ Popup: description: '' status: '' signature: '' + isaddr: '' + fund: 0 + pk: '' title: _('Invoice') if popup.is_invoice else _('Request') tx_hash: '' BoxLayout: t@@ -28,10 +31,10 @@ Popup: height: self.minimum_height spacing: '10dp' BoxLabel: - text: (_('Status') if popup.amount or popup.is_invoice else _('Amount received')) if root.status else '' + text: (_('Status') if popup.amount or popup.is_invoice or popup.isaddr == 'y' else _('Amount received')) if root.status else '' value: root.status BoxLabel: - text: _('Amount') if root.amount else '' + text: _('Request amount') if root.amount else '' value: app.format_amount_and_units(root.amount) if root.amount else '' BoxLabel: text: _('Requestor') if popup.is_invoice else _('Address') t@@ -45,6 +48,15 @@ Popup: BoxLabel: text: _('Description') if root.description else '' value: root.description + BoxLabel: + text: _('Balance') if popup.fund else '' + value: app.format_amount_and_units(root.fund) if root.fund else '' + TopLabel: + text: _('Private Key') + RefLabel: + id: pk_label + touched: True if not self.touched else True + data: root.pk TopLabel: text: _('Outputs') if popup.is_invoice else '' t@@ -65,7 +77,13 @@ Popup: size_hint: 0.5, None height: '48dp' Button: - size_hint: 0.5, None + size_hint: 2, None height: '48dp' text: _('Close') on_release: popup.dismiss() + Button: + size_hint: 2, None + height: '48dp' + text: _('Hide private key') if pk_label.data else _('Export private key') + on_release: + setattr(pk_label, 'data', '') if pk_label.data else popup.export(pk_label, popup.requestor)