Define TreeWidget subclasses for lists

* move class code in separate files
* make menu column-dependent (fixes #1734)
This commit is contained in:
ThomasV 2016-05-27 09:56:53 +02:00
parent 0273936b07
commit 12dfccb3ab
7 changed files with 532 additions and 355 deletions

157
gui/qt/address_list.py Normal file
View File

@ -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))

93
gui/qt/contact_list.py Normal file
View File

@ -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)

View File

@ -32,13 +32,12 @@ from electrum.util import block_explorer_URL, format_satoshis, format_time
from electrum.plugins import run_hook
class HistoryWidget(MyTreeWidget):
class HistoryList(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'),
@ -113,18 +112,29 @@ class HistoryWidget(MyTreeWidget):
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 ID to Clipboard"), lambda: self.parent.app.clipboard().setText(tx_hash))
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))
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:

77
gui/qt/invoice_list.py Normal file
View File

@ -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))

View File

@ -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)

124
gui/qt/request_list.py Normal file
View File

@ -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))

View File

@ -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(),