electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 12dfccb3ab199c59d767f729f332c24f0ba1dde9
parent 0273936b07d21f09be7c91bb3b6640320fdd340c
Author: ThomasV <thomasv@electrum.org>
Date:   Fri, 27 May 2016 09:56:53 +0200

Define TreeWidget subclasses for lists
* move class code in separate files
* make menu column-dependent (fixes #1734)

Diffstat:
Agui/qt/address_list.py | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/qt/contact_list.py | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/qt/history_list.py | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgui/qt/history_widget.py | 132-------------------------------------------------------------------------------
Agui/qt/invoice_list.py | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgui/qt/main_window.py | 379++++++++-----------------------------------------------------------------------
Agui/qt/request_list.py | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgui/qt/util.py | 39++++++++++++++++++++++++++++++++-------
8 files changed, 660 insertions(+), 483 deletions(-)

diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import webbrowser + +from util import * +from electrum.i18n import _ +from electrum.util import block_explorer_URL, format_satoshis, format_time +from electrum.plugins import run_hook +from electrum.bitcoin import is_address + + +class AddressList(MyTreeWidget): + + def __init__(self, parent=None): + MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Balance'), _('Tx')], 1) + self.setSelectionMode(QAbstractItemView.ExtendedSelection) + + def on_update(self): + self.wallet = self.parent.wallet + item = self.currentItem() + current_address = item.data(0, Qt.UserRole).toString() if item else None + self.clear() + accounts = self.wallet.get_accounts() + if self.parent.current_account is None: + account_items = sorted(accounts.items()) + else: + account_items = [(self.parent.current_account, accounts.get(self.parent.current_account))] + for k, account in account_items: + if len(accounts) > 1: + name = self.wallet.get_account_name(k) + c, u, x = self.wallet.get_account_balance(k) + account_item = QTreeWidgetItem([ name, '', self.parent.format_amount(c + u + x), '']) + account_item.setExpanded(self.accounts_expanded.get(k, True)) + account_item.setData(0, Qt.UserRole, k) + self.addTopLevelItem(account_item) + else: + account_item = self + sequences = [0,1] if account.has_change() else [0] + for is_change in sequences: + if len(sequences) > 1: + name = _("Receiving") if not is_change else _("Change") + seq_item = QTreeWidgetItem( [ name, '', '', '', ''] ) + account_item.addChild(seq_item) + if not is_change: + seq_item.setExpanded(True) + else: + seq_item = account_item + used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] ) + used_flag = False + addr_list = account.get_addresses(is_change) + for address in addr_list: + num = len(self.wallet.history.get(address,[])) + is_used = self.wallet.is_used(address) + label = self.wallet.labels.get(address,'') + c, u, x = self.wallet.get_addr_balance(address) + balance = self.parent.format_amount(c + u + x) + address_item = QTreeWidgetItem([address, label, balance, "%d"%num]) + address_item.setFont(0, QFont(MONOSPACE_FONT)) + address_item.setData(0, Qt.UserRole, address) + address_item.setData(0, Qt.UserRole+1, True) # label can be edited + if self.wallet.is_frozen(address): + address_item.setBackgroundColor(0, QColor('lightblue')) + if self.wallet.is_beyond_limit(address, account, is_change): + address_item.setBackgroundColor(0, QColor('red')) + if is_used: + if not used_flag: + seq_item.insertChild(0, used_item) + used_flag = True + used_item.addChild(address_item) + else: + seq_item.addChild(address_item) + if address == current_address: + self.setCurrentItem(address_item) + # add utxos + utxos = self.wallet.get_addr_utxo(address) + for x in utxos: + h = x.get('prevout_hash') + s = h + ":%d"%x.get('prevout_n') + label = self.wallet.get_label(h) + utxo_item = QTreeWidgetItem([s, label, self.parent.format_amount(x['value'])]) + utxo_item.setFont(0, QFont(MONOSPACE_FONT)) + address_item.addChild(utxo_item) + + def create_menu(self, position): + selected = self.selectedItems() + multi_select = len(selected) > 1 + addrs = [unicode(item.text(0)) for item in selected] + if not multi_select: + item = self.itemAt(position) + col = self.currentColumn() + if not item: + return + addr = addrs[0] + if not is_address(addr): + k = str(item.data(0,32).toString()) + if k: + self.create_account_menu(position, k, item) + else: + item.setExpanded(not item.isExpanded()) + return + + menu = QMenu() + if not multi_select: + column_title = self.headerItem().text(col) + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(item.text(col))) + if col in self.editable_columns: + menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, col)) + menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) + menu.addAction(_('History'), lambda: self.parent.show_address(addr)) + menu.addAction(_('Public Keys'), lambda: self.parent.show_public_keys(addr)) + if self.wallet.can_export(): + menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) + if not self.wallet.is_watching_only(): + menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr)) + menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr)) + if self.wallet.is_imported(addr): + menu.addAction(_("Remove from wallet"), lambda: self.parent.delete_imported_key(addr)) + addr_URL = block_explorer_URL(self.config, 'addr', addr) + if addr_URL: + menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL)) + + if any(not self.wallet.is_frozen(addr) for addr in addrs): + menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state(addrs, True)) + if any(self.wallet.is_frozen(addr) for addr in addrs): + menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state(addrs, False)) + + def can_send(addr): + return not self.wallet.is_frozen(addr) and sum(self.wallet.get_addr_balance(addr)[:2]) + if any(can_send(addr) for addr in addrs): + menu.addAction(_("Send From"), lambda: self.parent.send_from_addresses(addrs)) + + run_hook('receive_menu', menu, addrs, self.wallet) + menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from electrum.i18n import _ +from electrum.util import block_explorer_URL, format_satoshis, format_time, age +from electrum.plugins import run_hook +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from util import MyTreeWidget, pr_tooltips, pr_icons + + +class ContactList(MyTreeWidget): + + def __init__(self, parent): + MyTreeWidget.__init__(self, parent, self.create_menu, [_('Name'), _('Value'), _('Type')], 1, [0, 1]) + self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.setSortingEnabled(True) + + def on_permit_edit(self, item, column): + # openalias items shouldn't be editable + return item.text(2) != "openalias" + + def on_edited(self, item, column, prior): + if column == 0: # Remove old contact if renamed + self.parent.contacts.pop(prior) + self.parent.set_contact(unicode(item.text(0)), unicode(item.text(1))) + + def create_menu(self, position): + menu = QMenu() + selected = self.selectedItems() + if not selected: + menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) + else: + labels = [unicode(item.text(0)) for item in selected] + addrs = [unicode(item.text(1)) for item in selected] + types = [unicode(item.text(2)) for item in selected] + column = self.currentColumn() + column_title = self.headerItem().text(column) + column_data = '\n'.join([unicode(item.text(column)) for item in selected]) + + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + if column in self.editable_columns: + menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) + + menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(labels)) + menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(labels)) + URLs = [] + for (addr, _type) in zip(addrs, types): + if _type == 'address': + URLs.append(block_explorer_URL(self.config, 'addr', addr)) + if URLs: + menu.addAction(_("View on block explorer"), + lambda: map(webbrowser.open, URLs)) + + run_hook('create_contact_menu', menu, selected) + menu.exec_(self.viewport().mapToGlobal(position)) + + def on_update(self): + item = self.currentItem() + current_key = item.data(0, Qt.UserRole).toString() if item else None + self.clear() + for key in sorted(self.parent.contacts.keys()): + _type, value = self.parent.contacts[key] + item = QTreeWidgetItem([key, value, _type]) + item.setData(0, Qt.UserRole, key) + self.addTopLevelItem(item) + if key == current_key: + self.setCurrentItem(item) + run_hook('update_contacts_tab', self) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import webbrowser + +from util import * +from electrum.i18n import _ +from electrum.util import block_explorer_URL, format_satoshis, format_time +from electrum.plugins import run_hook + + +class HistoryList(MyTreeWidget): + + def __init__(self, parent=None): + MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) + self.refresh_headers() + self.setColumnHidden(1, True) + + def refresh_headers(self): + headers = ['', '', _('Date'), _('Description') , _('Amount'), + _('Balance')] + run_hook('history_tab_headers', headers) + self.update_headers(headers) + + def get_icon(self, conf, timestamp): + time_str = _("unknown") + if conf > 0: + time_str = format_time(timestamp) + if conf == -1: + time_str = _('Not Verified') + icon = QIcon(":icons/unconfirmed.png") + elif conf == 0: + time_str = _('Unconfirmed') + icon = QIcon(":icons/unconfirmed.png") + elif conf < 6: + icon = QIcon(":icons/clock%d.png"%conf) + else: + icon = QIcon(":icons/confirmed.png") + return icon, time_str + + def get_domain(self): + '''Replaced in address_dialog.py''' + return self.wallet.get_account_addresses(self.parent.current_account) + + def on_update(self): + self.wallet = self.parent.wallet + h = self.wallet.get_history(self.get_domain()) + + item = self.currentItem() + current_tx = item.data(0, Qt.UserRole).toString() if item else None + self.clear() + run_hook('history_tab_update_begin') + for tx in h: + tx_hash, conf, value, timestamp, balance = tx + if conf is None and timestamp is None: + continue # skip history in offline mode + icon, time_str = self.get_icon(conf, timestamp) + v_str = self.parent.format_amount(value, True, whitespaces=True) + balance_str = self.parent.format_amount(balance, whitespaces=True) + label = self.wallet.get_label(tx_hash) + entry = ['', tx_hash, time_str, label, v_str, balance_str] + run_hook('history_tab_update', tx, entry) + item = QTreeWidgetItem(entry) + item.setIcon(0, icon) + for i in range(len(entry)): + if i>3: + item.setTextAlignment(i, Qt.AlignRight) + if i!=2: + item.setFont(i, QFont(MONOSPACE_FONT)) + if value < 0: + item.setForeground(3, QBrush(QColor("#BC1E1E"))) + item.setForeground(4, QBrush(QColor("#BC1E1E"))) + if tx_hash: + item.setData(0, Qt.UserRole, tx_hash) + self.insertTopLevelItem(0, item) + if current_tx == tx_hash: + self.setCurrentItem(item) + + def update_item(self, tx_hash, conf, timestamp): + icon, time_str = self.get_icon(conf, timestamp) + items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1) + if items: + item = items[0] + item.setIcon(0, icon) + item.setText(2, time_str) + + def create_menu(self, position): + self.selectedIndexes() + item = self.currentItem() + if not item: + return + column = self.currentColumn() + tx_hash = str(item.data(0, Qt.UserRole).toString()) + if not tx_hash: + return + if column is 0: + column_title = "ID" + column_data = tx_hash + else: + column_title = self.headerItem().text(column) + column_data = item.text(column) + + tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) + conf, timestamp = self.wallet.get_confirmations(tx_hash) + tx = self.wallet.transactions.get(tx_hash) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) + rbf = is_mine and (conf == 0) and tx and not tx.is_final() + menu = QMenu() + + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + if column in self.editable_columns: + menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) + + menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) + if rbf: + menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) + if tx_URL: + menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) + menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/history_widget.py b/gui/qt/history_widget.py @@ -1,132 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2015 Thomas Voegtlin -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -import webbrowser - -from util import * -from electrum.i18n import _ -from electrum.util import block_explorer_URL, format_satoshis, format_time -from electrum.plugins import run_hook - - -class HistoryWidget(MyTreeWidget): - - def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) - self.refresh_headers() - self.setColumnHidden(1, True) - self.config = self.parent.config - - def refresh_headers(self): - headers = ['', '', _('Date'), _('Description') , _('Amount'), - _('Balance')] - run_hook('history_tab_headers', headers) - self.update_headers(headers) - - def get_icon(self, conf, timestamp): - time_str = _("unknown") - if conf > 0: - time_str = format_time(timestamp) - if conf == -1: - time_str = _('Not Verified') - icon = QIcon(":icons/unconfirmed.png") - elif conf == 0: - time_str = _('Unconfirmed') - icon = QIcon(":icons/unconfirmed.png") - elif conf < 6: - icon = QIcon(":icons/clock%d.png"%conf) - else: - icon = QIcon(":icons/confirmed.png") - return icon, time_str - - def get_domain(self): - '''Replaced in address_dialog.py''' - return self.wallet.get_account_addresses(self.parent.current_account) - - def on_update(self): - self.wallet = self.parent.wallet - h = self.wallet.get_history(self.get_domain()) - - item = self.currentItem() - current_tx = item.data(0, Qt.UserRole).toString() if item else None - self.clear() - run_hook('history_tab_update_begin') - for tx in h: - tx_hash, conf, value, timestamp, balance = tx - if conf is None and timestamp is None: - continue # skip history in offline mode - icon, time_str = self.get_icon(conf, timestamp) - v_str = self.parent.format_amount(value, True, whitespaces=True) - balance_str = self.parent.format_amount(balance, whitespaces=True) - label = self.wallet.get_label(tx_hash) - entry = ['', tx_hash, time_str, label, v_str, balance_str] - run_hook('history_tab_update', tx, entry) - item = QTreeWidgetItem(entry) - item.setIcon(0, icon) - for i in range(len(entry)): - if i>3: - item.setTextAlignment(i, Qt.AlignRight) - if i!=2: - item.setFont(i, QFont(MONOSPACE_FONT)) - if value < 0: - item.setForeground(3, QBrush(QColor("#BC1E1E"))) - item.setForeground(4, QBrush(QColor("#BC1E1E"))) - if tx_hash: - item.setData(0, Qt.UserRole, tx_hash) - self.insertTopLevelItem(0, item) - if current_tx == tx_hash: - self.setCurrentItem(item) - - def update_item(self, tx_hash, conf, timestamp): - icon, time_str = self.get_icon(conf, timestamp) - items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1) - if items: - item = items[0] - item.setIcon(0, icon) - item.setText(2, time_str) - - def create_menu(self, position): - self.selectedIndexes() - item = self.currentItem() - if not item: - return - tx_hash = str(item.data(0, Qt.UserRole).toString()) - if not tx_hash: - return - tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) - conf, timestamp = self.wallet.get_confirmations(tx_hash) - tx = self.wallet.transactions.get(tx_hash) - is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) - rbf = is_mine and (conf == 0) and tx and not tx.is_final() - menu = QMenu() - menu.addAction(_("Copy ID to Clipboard"), lambda: self.parent.app.clipboard().setText(tx_hash)) - menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) - menu.addAction(_("Edit description"), lambda: self.editItem(item, self.editable_columns[0])) - if rbf: - menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) - if tx_URL: - menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) - menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from util import * +from electrum.i18n import _ +from electrum.util import block_explorer_URL, format_satoshis, format_time +from electrum.plugins import run_hook + + +class InvoiceList(MyTreeWidget): + + def __init__(self, parent): + MyTreeWidget.__init__(self, parent, self.create_menu, [_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')], 2) + self.setSortingEnabled(True) + self.header().setResizeMode(1, QHeaderView.Interactive) + self.setColumnWidth(1, 200) + + def on_update(self): + inv_list = self.parent.invoices.sorted_list() + self.clear() + for pr in inv_list: + key = pr.get_id() + status = self.parent.invoices.get_status(key) + requestor = pr.get_requestor() + exp = pr.get_expiration_date() + date_str = format_time(exp) if exp else _('Never') + item = QTreeWidgetItem([date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]) + item.setIcon(4, QIcon(pr_icons.get(status))) + item.setData(0, Qt.UserRole, key) + item.setFont(1, QFont(MONOSPACE_FONT)) + item.setFont(3, QFont(MONOSPACE_FONT)) + self.addTopLevelItem(item) + self.setCurrentItem(self.topLevelItem(0)) + self.setVisible(len(inv_list)) + self.parent.invoices_label.setVisible(len(inv_list)) + + def create_menu(self, position): + item = self.itemAt(position) + if not item: + return + key = str(item.data(0, 32).toString()) + column = self.currentColumn() + column_title = self.headerItem().text(column) + column_data = item.text(column) + pr = self.parent.invoices.get(key) + status = self.parent.invoices.get_status(key) + menu = QMenu() + if column_data: + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) + if status == PR_UNPAID: + menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key)) + menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key)) + menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -88,26 +88,6 @@ class StatusBarButton(QPushButton): from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED -pr_icons = { - PR_UNPAID:":icons/unpaid.png", - PR_PAID:":icons/confirmed.png", - PR_EXPIRED:":icons/expired.png" -} - -pr_tooltips = { - PR_UNPAID:_('Pending'), - PR_PAID:_('Paid'), - PR_EXPIRED:_('Expired') -} - -expiration_values = [ - (_('1 hour'), 60*60), - (_('1 day'), 24*60*60), - (_('1 week'), 7*24*60*60), - (_('Never'), None) -] - - class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): @@ -305,7 +285,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.update_buttons_on_seed() self.update_console() self.clear_receive_tab() - self.receive_list.update() + self.request_list.update() self.tabs.show() try: @@ -628,15 +608,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def update_tabs(self): self.history_list.update() - self.receive_list.update() + self.request_list.update() self.address_list.update() - self.contacts_list.update() - self.invoices_list.update() + self.contact_list.update() + self.invoice_list.update() self.update_completions() def create_history_tab(self): - from history_widget import HistoryWidget - self.history_list = l = HistoryWidget(self) + from history_list import HistoryList + self.history_list = l = HistoryList(self) return l def show_address(self, addr): @@ -711,14 +691,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.addLayout(buttons, 4, 1, 1, 2) self.receive_requests_label = QLabel(_('Requests')) - self.receive_list = MyTreeWidget(self, self.receive_list_menu, [_('Date'), _('Account'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 4) - self.receive_list.currentItemChanged.connect(self.receive_item_changed) - self.receive_list.itemClicked.connect(self.receive_item_changed) - self.receive_list.setSortingEnabled(True) - self.receive_list.setColumnWidth(0, 180) - self.receive_list.hideColumn(1) - self.receive_list.hideColumn(2) - self.receive_list.on_update = self.update_receive_tab + + from request_list import RequestList + self.request_list = RequestList(self) # layout vbox_g = QVBoxLayout() @@ -734,33 +709,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): vbox.addLayout(hbox) vbox.addStretch(1) vbox.addWidget(self.receive_requests_label) - vbox.addWidget(self.receive_list) - vbox.setStretchFactor(self.receive_list, 1000) + vbox.addWidget(self.request_list) + vbox.setStretchFactor(self.request_list, 1000) return w - def receive_item_changed(self, item): - if item is None: - return - if not self.receive_list.isItemSelected(item): - return - addr = str(item.text(2)) - req = self.wallet.receive_requests[addr] - expires = util.age(req['time'] + req['exp']) if req.get('exp') else _('Never') - amount = req['amount'] - message = self.wallet.labels.get(addr, '') - self.receive_address_e.setText(addr) - self.receive_message_e.setText(message) - self.receive_amount_e.setAmount(amount) - self.expires_combo.hide() - self.expires_label.show() - self.expires_label.setText(expires) - self.new_request_button.setEnabled(True) def delete_payment_request(self, item): addr = str(item.text(2)) self.wallet.remove_payment_request(addr, self.config) - self.receive_list.update() + self.request_list.update() self.clear_receive_tab() def get_request_URI(self, addr): @@ -778,17 +736,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): URI += "&name=" + req['name'] + "&sig="+sig return str(URI) - def receive_list_menu(self, position): - item = self.receive_list.itemAt(position) - addr = str(item.text(2)) - req = self.wallet.receive_requests[addr] - menu = QMenu(self) - menu.addAction(_("Copy Address"), lambda: self.view_and_paste(_('Address'), '', addr)) - menu.addAction(_("Copy URI"), lambda: self.view_and_paste('URI', '', self.get_request_URI(addr))) - menu.addAction(_("Save as BIP70 file"), lambda: self.export_payment_request(addr)) - menu.addAction(_("Delete"), lambda: self.delete_payment_request(item)) - run_hook('receive_list_menu', menu, addr) - menu.exec_(self.receive_list.viewport().mapToGlobal(position)) def sign_payment_request(self, addr): alias = self.config.get('alias') @@ -823,7 +770,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): req = self.wallet.make_payment_request(addr, amount, message, expiration) self.wallet.add_payment_request(req, self.config) self.sign_payment_request(addr) - self.receive_list.update() + self.request_list.update() self.address_list.update() self.save_request_button.setEnabled(False) @@ -901,52 +848,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.receive_address_e.setText(addr) self.new_request_button.setEnabled(True) - def update_receive_tab(self): - - # hide receive tab if no receive requests available - b = len(self.wallet.receive_requests) > 0 - self.receive_list.setVisible(b) - self.receive_requests_label.setVisible(b) - if not b: - self.expires_label.hide() - self.expires_combo.show() - - # check if it is necessary to show the account - self.receive_list.setColumnHidden(1, len(self.wallet.get_accounts()) == 1) - - # update the receive address if necessary - current_address = self.receive_address_e.text() - domain = self.wallet.get_account_addresses(self.current_account, include_change=False) - addr = self.wallet.get_unused_address(self.current_account) - if not current_address in domain and addr: - self.set_receive_address(addr) - self.new_request_button.setEnabled(addr != current_address) - - # clear the list and fill it again - self.receive_list.clear() - for req in self.wallet.get_sorted_requests(self.config): - address = req['address'] - if address not in domain: - continue - timestamp = req.get('time', 0) - amount = req.get('amount') - expiration = req.get('exp', None) - message = req.get('memo', '') - date = format_time(timestamp) - status = req.get('status') - signature = req.get('sig') - requestor = req.get('name', '') - amount_str = self.format_amount(amount) if amount else "" - account = '' - item = QTreeWidgetItem([date, account, address, '', message, amount_str, pr_tooltips.get(status,'')]) - if signature is not None: - item.setIcon(3, QIcon(":icons/seal.png")) - item.setToolTip(3, 'signed by '+ requestor) - if status is not PR_UNKNOWN: - item.setIcon(6, QIcon(pr_icons.get(status))) - self.receive_list.addTopLevelItem(item) - - def update_receive_qr(self): addr = str(self.receive_address_e.text()) amount = self.receive_amount_e.get_amount() @@ -1101,12 +1002,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.fee_e.textChanged.connect(entry_changed) self.invoices_label = QLabel(_('Invoices')) - self.invoices_list = MyTreeWidget(self, self.invoices_list_menu, - [_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')], 2) - self.invoices_list.setSortingEnabled(True) - self.invoices_list.header().setResizeMode(1, QHeaderView.Interactive) - self.invoices_list.setColumnWidth(1, 200) - self.invoices_list.on_update = self.update_invoices_list + from invoice_list import InvoiceList + self.invoice_list = InvoiceList(self) vbox0 = QVBoxLayout() vbox0.addLayout(grid) @@ -1117,8 +1014,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): vbox.addLayout(hbox) vbox.addStretch(1) vbox.addWidget(self.invoices_label) - vbox.addWidget(self.invoices_list) - vbox.setStretchFactor(self.invoices_list, 1000) + vbox.addWidget(self.invoice_list) + vbox.setStretchFactor(self.invoice_list, 1000) # Defer this until grid is parented to avoid ugly flash during startup self.update_fee_edit() @@ -1402,7 +1299,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if tx_desc is not None and tx.is_complete(): self.wallet.set_label(tx.hash(), tx_desc) parent.show_message(_('Payment sent.') + '\n' + msg) - self.invoices_list.update() + self.invoice_list.update() self.do_clear() else: parent.show_error(msg) @@ -1428,23 +1325,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.payto_e.setText(_("please wait...")) return True + def delete_invoice(self, key): + self.invoices.remove(key) + self.invoice_list.update() + def payment_request_ok(self): pr = self.payment_request key = self.invoices.add(pr) status = self.invoices.get_status(key) - self.invoices_list.update() + self.invoice_list.update() if status == PR_PAID: self.show_message("invoice already paid") self.do_clear() self.payment_request = None return - self.payto_e.is_pr = True if not pr.has_expired(): self.payto_e.setGreen() else: self.payto_e.setExpired() - self.payto_e.setText(pr.get_requestor()) self.amount_e.setText(format_satoshis_plain(pr.get_amount(), self.decimal_point)) self.message_e.setText(pr.get_memo()) @@ -1524,42 +1423,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): return w def create_addresses_tab(self): - l = MyTreeWidget(self, self.create_address_menu, [ _('Address'), _('Label'), _('Balance'), _('Tx')], 1) - l.setSelectionMode(QAbstractItemView.ExtendedSelection) - l.on_update = self.update_address_tab - self.address_list = l + from address_list import AddressList + self.address_list = l = AddressList(self) return self.create_list_tab(l) def create_contacts_tab(self): - l = MyTreeWidget(self, self.create_contact_menu, [_('Name'), _('Value'), _('Type')], 1, [0, 1]) - l.setSelectionMode(QAbstractItemView.ExtendedSelection) - l.setSortingEnabled(True) - l.on_edited = self.on_contact_edited - l.on_permit_edit = self.on_permit_contact_edit - l.on_update = self.update_contacts_tab - self.contacts_list = l + from contact_list import ContactList + self.contact_list = l = ContactList(self) return self.create_list_tab(l) - def update_invoices_list(self): - inv_list = self.invoices.sorted_list() - l = self.invoices_list - l.clear() - for pr in inv_list: - key = pr.get_id() - status = self.invoices.get_status(key) - requestor = pr.get_requestor() - exp = pr.get_expiration_date() - date_str = util.format_time(exp) if exp else _('Never') - item = QTreeWidgetItem([date_str, requestor, pr.memo, self.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]) - item.setIcon(4, QIcon(pr_icons.get(status))) - item.setData(0, Qt.UserRole, key) - item.setFont(1, QFont(MONOSPACE_FONT)) - item.setFont(3, QFont(MONOSPACE_FONT)) - l.addTopLevelItem(item) - l.setCurrentItem(l.topLevelItem(0)) - self.invoices_list.setVisible(len(inv_list)) - self.invoices_label.setVisible(len(inv_list)) - def delete_imported_key(self, addr): if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")): self.wallet.delete_imported_key(addr) @@ -1586,53 +1458,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): menu.addAction(_("View details"), lambda: self.show_account_details(k)) menu.exec_(self.address_list.viewport().mapToGlobal(position)) - def create_address_menu(self, position): - selected = self.address_list.selectedItems() - multi_select = len(selected) > 1 - addrs = [unicode(item.text(0)) for item in selected] - if not multi_select: - item = self.address_list.itemAt(position) - if not item: - return - addr = addrs[0] - if not is_valid(addr): - k = str(item.data(0,32).toString()) - if k: - self.create_account_menu(position, k, item) - else: - item.setExpanded(not item.isExpanded()) - return - menu = QMenu() - if not multi_select: - menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr)) - menu.addAction(_("Request payment"), lambda: self.receive_at(addr)) - menu.addAction(_("Edit label"), lambda: self.address_list.editItem(item, self.address_list.editable_columns[0])) - menu.addAction(_('History'), lambda: self.show_address(addr)) - menu.addAction(_('Public Keys'), lambda: self.show_public_keys(addr)) - if self.wallet.can_export(): - menu.addAction(_("Private key"), lambda: self.show_private_key(addr)) - if not self.wallet.is_watching_only(): - menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr)) - menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr)) - if self.wallet.is_imported(addr): - menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr)) - addr_URL = block_explorer_URL(self.config, 'addr', addr) - if addr_URL: - menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL)) - - if any(not self.wallet.is_frozen(addr) for addr in addrs): - menu.addAction(_("Freeze"), lambda: self.set_frozen_state(addrs, True)) - if any(self.wallet.is_frozen(addr) for addr in addrs): - menu.addAction(_("Unfreeze"), lambda: self.set_frozen_state(addrs, False)) - - def can_send(addr): - return not self.wallet.is_frozen(addr) and sum(self.wallet.get_addr_balance(addr)[:2]) - if any(can_send(addr) for addr in addrs): - menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs)) - - run_hook('receive_menu', menu, addrs, self.wallet) - menu.exec_(self.address_list.viewport().mapToGlobal(position)) - def get_coins(self): if self.pay_from: @@ -1669,22 +1494,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.payto_e.setText(text) self.payto_e.setFocus() - def on_permit_contact_edit(self, item, column): - # openalias items shouldn't be editable - return item.text(2) != "openalias" - - def on_contact_edited(self, item, column, prior): - if column == 0: # Remove old contact if renamed - self.contacts.pop(prior) - self.set_contact(unicode(item.text(0)), unicode(item.text(1))) - def set_contact(self, label, address): if not is_valid(address): self.show_error(_('Invalid Address')) - self.contacts_list.update() # Displays original unchanged value + self.contact_list.update() # Displays original unchanged value return False self.contacts[label] = ('address', address) - self.contacts_list.update() + self.contact_list.update() self.history_list.update() self.update_completions() return True @@ -1696,33 +1512,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): for label in labels: self.contacts.pop(label) self.history_list.update() - self.contacts_list.update() + self.contact_list.update() self.update_completions() - def create_contact_menu(self, position): - menu = QMenu() - selected = self.contacts_list.selectedItems() - if not selected: - menu.addAction(_("New contact"), lambda: self.new_contact_dialog()) - else: - labels = [unicode(item.text(0)) for item in selected] - addrs = [unicode(item.text(1)) for item in selected] - types = [unicode(item.text(2)) for item in selected] - menu.addAction(_("Copy to Clipboard"), lambda: - self.app.clipboard().setText('\n'.join(labels))) - menu.addAction(_("Pay to"), lambda: self.payto_contacts(labels)) - menu.addAction(_("Delete"), lambda: self.delete_contacts(labels)) - URLs = [] - for (addr, _type) in zip(addrs, types): - if _type == 'address': - URLs.append(block_explorer_URL(self.config, 'addr', addr)) - if URLs: - menu.addAction(_("View on block explorer"), - lambda: map(webbrowser.open, URLs)) - - run_hook('create_contact_menu', menu, selected) - menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) - def show_invoice(self, key): pr = self.invoices.get(key) @@ -1766,107 +1558,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): else: self.payment_request_error() - - def invoices_list_menu(self, position): - item = self.invoices_list.itemAt(position) - if not item: - return - key = str(item.data(0, 32).toString()) - pr = self.invoices.get(key) - status = self.invoices.get_status(key) - menu = QMenu() - menu.addAction(_("Details"), lambda: self.show_invoice(key)) - if status == PR_UNPAID: - menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key)) - def delete_invoice(key): - self.invoices.remove(key) - self.invoices_list.update() - menu.addAction(_("Delete"), lambda: delete_invoice(key)) - menu.exec_(self.invoices_list.viewport().mapToGlobal(position)) - - - def update_address_tab(self): - l = self.address_list - item = l.currentItem() - current_address = item.data(0, Qt.UserRole).toString() if item else None - l.clear() - accounts = self.wallet.get_accounts() - if self.current_account is None: - account_items = sorted(accounts.items()) - else: - account_items = [(self.current_account, accounts.get(self.current_account))] - for k, account in account_items: - if len(accounts) > 1: - name = self.wallet.get_account_name(k) - c, u, x = self.wallet.get_account_balance(k) - account_item = QTreeWidgetItem([ name, '', self.format_amount(c + u + x), '']) - account_item.setExpanded(self.accounts_expanded.get(k, True)) - account_item.setData(0, Qt.UserRole, k) - l.addTopLevelItem(account_item) - else: - account_item = l - sequences = [0,1] if account.has_change() else [0] - for is_change in sequences: - if len(sequences) > 1: - name = _("Receiving") if not is_change else _("Change") - seq_item = QTreeWidgetItem( [ name, '', '', '', ''] ) - account_item.addChild(seq_item) - if not is_change: - seq_item.setExpanded(True) - else: - seq_item = account_item - used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] ) - used_flag = False - addr_list = account.get_addresses(is_change) - for address in addr_list: - num = len(self.wallet.history.get(address,[])) - is_used = self.wallet.is_used(address) - label = self.wallet.labels.get(address,'') - c, u, x = self.wallet.get_addr_balance(address) - balance = self.format_amount(c + u + x) - address_item = QTreeWidgetItem([address, label, balance, "%d"%num]) - address_item.setFont(0, QFont(MONOSPACE_FONT)) - address_item.setData(0, Qt.UserRole, address) - address_item.setData(0, Qt.UserRole+1, True) # label can be edited - if self.wallet.is_frozen(address): - address_item.setBackgroundColor(0, QColor('lightblue')) - if self.wallet.is_beyond_limit(address, account, is_change): - address_item.setBackgroundColor(0, QColor('red')) - if is_used: - if not used_flag: - seq_item.insertChild(0, used_item) - used_flag = True - used_item.addChild(address_item) - else: - seq_item.addChild(address_item) - if address == current_address: - l.setCurrentItem(address_item) - # add utxos - utxos = self.wallet.get_addr_utxo(address) - for x in utxos: - h = x.get('prevout_hash') - s = h + ":%d"%x.get('prevout_n') - label = self.wallet.get_label(h) - utxo_item = QTreeWidgetItem([s, label, self.format_amount(x['value'])]) - utxo_item.setFont(0, QFont(MONOSPACE_FONT)) - address_item.addChild(utxo_item) - - - def update_contacts_tab(self): - l = self.contacts_list - item = l.currentItem() - current_key = item.data(0, Qt.UserRole).toString() if item else None - l.clear() - for key in sorted(self.contacts.keys()): - _type, value = self.contacts[key] - item = QTreeWidgetItem([key, value, _type]) - item.setData(0, Qt.UserRole, key) - l.addTopLevelItem(item) - if key == current_key: - l.setCurrentItem(item) - run_hook('update_contacts_tab', l) - - def create_console_tab(self): from console import Console self.console = console = Console() @@ -1906,7 +1597,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.history_list.update() self.update_status() self.address_list.update() - self.receive_list.update() + self.request_list.update() def create_status_bar(self): @@ -1993,13 +1684,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if i == 0: self.history_list.filter(t, [2, 3, 4]) # Date, Description, Amount elif i == 1: - self.invoices_list.filter(t, [0, 1, 2, 3]) # Date, Requestor, Description, Amount + self.invoice_list.filter(t, [0, 1, 2, 3]) # Date, Requestor, Description, Amount elif i == 2: - self.receive_list.filter(t, [0, 1, 2, 3, 4]) # Date, Account, Address, Description, Amount + self.request_list.filter(t, [0, 1, 2, 3, 4]) # Date, Account, Address, Description, Amount elif i == 3: self.address_list.filter(t, [0,1, 2]) # Address, Label, Balance elif i == 4: - self.contacts_list.filter(t, [0, 1]) # Key, Value + self.contact_list.filter(t, [0, 1]) # Key, Value def new_contact_dialog(self): @@ -2795,7 +2486,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): raise Exception('Unknown base unit') self.config.set_key('decimal_point', self.decimal_point, True) self.history_list.update() - self.receive_list.update() + self.request_list.update() self.address_list.update() for edit, amount in zip(edits, amounts): edit.setAmount(amount) diff --git a/gui/qt/request_list.py b/gui/qt/request_list.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from electrum.i18n import _ +from electrum.util import block_explorer_URL, format_satoshis, format_time, age +from electrum.plugins import run_hook +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from util import MyTreeWidget, pr_tooltips, pr_icons + + +class RequestList(MyTreeWidget): + + def __init__(self, parent): + MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Account'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 4) + self.currentItemChanged.connect(self.item_changed) + self.itemClicked.connect(self.item_changed) + self.setSortingEnabled(True) + self.setColumnWidth(0, 180) + self.hideColumn(1) + self.hideColumn(2) + + def item_changed(self, item): + if item is None: + return + if not self.isItemSelected(item): + return + addr = str(item.text(2)) + req = self.wallet.receive_requests[addr] + expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') + amount = req['amount'] + message = self.wallet.labels.get(addr, '') + self.parent.receive_address_e.setText(addr) + self.parent.receive_message_e.setText(message) + self.parent.receive_amount_e.setAmount(amount) + self.parent.expires_combo.hide() + self.parent.expires_label.show() + self.parent.expires_label.setText(expires) + self.parent.new_request_button.setEnabled(True) + + def on_update(self): + self.wallet = self.parent.wallet + # hide receive tab if no receive requests available + b = len(self.wallet.receive_requests) > 0 + self.setVisible(b) + self.parent.receive_requests_label.setVisible(b) + if not b: + self.parent.expires_label.hide() + self.parent.expires_combo.show() + + # check if it is necessary to show the account + self.setColumnHidden(1, len(self.wallet.get_accounts()) == 1) + + # update the receive address if necessary + current_address = self.parent.receive_address_e.text() + domain = self.wallet.get_account_addresses(self.parent.current_account, include_change=False) + addr = self.wallet.get_unused_address(self.parent.current_account) + if not current_address in domain and addr: + self.parent.set_receive_address(addr) + self.parent.new_request_button.setEnabled(addr != current_address) + + # clear the list and fill it again + self.clear() + for req in self.wallet.get_sorted_requests(self.config): + address = req['address'] + if address not in domain: + continue + timestamp = req.get('time', 0) + amount = req.get('amount') + expiration = req.get('exp', None) + message = req.get('memo', '') + date = format_time(timestamp) + status = req.get('status') + signature = req.get('sig') + requestor = req.get('name', '') + amount_str = self.parent.format_amount(amount) if amount else "" + account = '' + item = QTreeWidgetItem([date, account, address, '', message, amount_str, pr_tooltips.get(status,'')]) + if signature is not None: + item.setIcon(3, QIcon(":icons/seal.png")) + item.setToolTip(3, 'signed by '+ requestor) + if status is not PR_UNKNOWN: + item.setIcon(6, QIcon(pr_icons.get(status))) + self.addTopLevelItem(item) + + + def create_menu(self, position): + item = self.itemAt(position) + addr = str(item.text(2)) + req = self.wallet.receive_requests[addr] + column = self.currentColumn() + column_title = self.headerItem().text(column) + column_data = item.text(column) + menu = QMenu(self) + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr))) + menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr)) + menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(item)) + run_hook('receive_list_menu', menu, addr) + menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/util.py b/gui/qt/util.py @@ -27,6 +27,28 @@ BLACK_FG = "QWidget {color:black;}" dialogs = [] +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED + +pr_icons = { + PR_UNPAID:":icons/unpaid.png", + PR_PAID:":icons/confirmed.png", + PR_EXPIRED:":icons/expired.png" +} + +pr_tooltips = { + PR_UNPAID:_('Pending'), + PR_PAID:_('Paid'), + PR_EXPIRED:_('Expired') +} + +expiration_values = [ + (_('1 hour'), 60*60), + (_('1 day'), 24*60*60), + (_('1 week'), 7*24*60*60), + (_('Never'), None) +] + + class Timer(QThread): stopped = False @@ -351,6 +373,7 @@ class MyTreeWidget(QTreeWidget): editable_columns=None): QTreeWidget.__init__(self, parent) self.parent = parent + self.config = self.parent.config self.stretch_column = stretch_column self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(create_menu) @@ -366,7 +389,7 @@ class MyTreeWidget(QTreeWidget): editable_columns = [stretch_column] self.editable_columns = editable_columns self.setItemDelegate(ElectrumItemDelegate(self)) - self.itemActivated.connect(self.on_activated) + self.itemDoubleClicked.connect(self.on_doubleclick) self.update_headers(headers) def update_headers(self, headers): @@ -386,7 +409,7 @@ class MyTreeWidget(QTreeWidget): item.setFlags(item.flags() & ~Qt.ItemIsEditable) def keyPressEvent(self, event): - if event.key() == Qt.Key_F2: + if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: self.on_activated(self.currentItem(), self.currentColumn()) else: QTreeWidget.keyPressEvent(self, event) @@ -398,13 +421,15 @@ class MyTreeWidget(QTreeWidget): def on_permit_edit(self, item, column): return True - def on_activated(self, item, column): + def on_doubleclick(self, item, column): if self.permit_edit(item, column): self.editItem(item, column) - else: - pt = self.visualItemRect(item).bottomLeft() - pt.setX(50) - self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), pt) + + def on_activated(self, item, column): + # on 'enter' we show the menu + pt = self.visualItemRect(item).bottomLeft() + pt.setX(50) + self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), pt) def createEditor(self, parent, option, index): self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),