Define TreeWidget subclasses for lists
* move class code in separate files * make menu column-dependent (fixes #1734)
This commit is contained in:
parent
0273936b07
commit
12dfccb3ab
|
@ -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))
|
|
@ -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)
|
|
@ -32,13 +32,12 @@ from electrum.util import block_explorer_URL, format_satoshis, format_time
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
|
|
||||||
|
|
||||||
class HistoryWidget(MyTreeWidget):
|
class HistoryList(MyTreeWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
||||||
self.refresh_headers()
|
self.refresh_headers()
|
||||||
self.setColumnHidden(1, True)
|
self.setColumnHidden(1, True)
|
||||||
self.config = self.parent.config
|
|
||||||
|
|
||||||
def refresh_headers(self):
|
def refresh_headers(self):
|
||||||
headers = ['', '', _('Date'), _('Description') , _('Amount'),
|
headers = ['', '', _('Date'), _('Description') , _('Amount'),
|
||||||
|
@ -113,18 +112,29 @@ class HistoryWidget(MyTreeWidget):
|
||||||
item = self.currentItem()
|
item = self.currentItem()
|
||||||
if not item:
|
if not item:
|
||||||
return
|
return
|
||||||
|
column = self.currentColumn()
|
||||||
tx_hash = str(item.data(0, Qt.UserRole).toString())
|
tx_hash = str(item.data(0, Qt.UserRole).toString())
|
||||||
if not tx_hash:
|
if not tx_hash:
|
||||||
return
|
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)
|
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
||||||
conf, timestamp = self.wallet.get_confirmations(tx_hash)
|
conf, timestamp = self.wallet.get_confirmations(tx_hash)
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
tx = self.wallet.transactions.get(tx_hash)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
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()
|
rbf = is_mine and (conf == 0) and tx and not tx.is_final()
|
||||||
menu = QMenu()
|
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(_("Details"), lambda: self.parent.show_transaction(tx))
|
||||||
menu.addAction(_("Edit description"), lambda: self.editItem(item, self.editable_columns[0]))
|
|
||||||
if rbf:
|
if rbf:
|
||||||
menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
|
menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
|
||||||
if tx_URL:
|
if tx_URL:
|
|
@ -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))
|
|
@ -88,26 +88,6 @@ class StatusBarButton(QPushButton):
|
||||||
|
|
||||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
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):
|
class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
|
@ -305,7 +285,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.update_buttons_on_seed()
|
self.update_buttons_on_seed()
|
||||||
self.update_console()
|
self.update_console()
|
||||||
self.clear_receive_tab()
|
self.clear_receive_tab()
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
self.tabs.show()
|
self.tabs.show()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -628,15 +608,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def update_tabs(self):
|
def update_tabs(self):
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
self.contacts_list.update()
|
self.contact_list.update()
|
||||||
self.invoices_list.update()
|
self.invoice_list.update()
|
||||||
self.update_completions()
|
self.update_completions()
|
||||||
|
|
||||||
def create_history_tab(self):
|
def create_history_tab(self):
|
||||||
from history_widget import HistoryWidget
|
from history_list import HistoryList
|
||||||
self.history_list = l = HistoryWidget(self)
|
self.history_list = l = HistoryList(self)
|
||||||
return l
|
return l
|
||||||
|
|
||||||
def show_address(self, addr):
|
def show_address(self, addr):
|
||||||
|
@ -711,14 +691,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
grid.addLayout(buttons, 4, 1, 1, 2)
|
grid.addLayout(buttons, 4, 1, 1, 2)
|
||||||
|
|
||||||
self.receive_requests_label = QLabel(_('Requests'))
|
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)
|
from request_list import RequestList
|
||||||
self.receive_list.itemClicked.connect(self.receive_item_changed)
|
self.request_list = RequestList(self)
|
||||||
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
|
|
||||||
|
|
||||||
# layout
|
# layout
|
||||||
vbox_g = QVBoxLayout()
|
vbox_g = QVBoxLayout()
|
||||||
|
@ -734,33 +709,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
vbox.addLayout(hbox)
|
vbox.addLayout(hbox)
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
vbox.addWidget(self.receive_requests_label)
|
vbox.addWidget(self.receive_requests_label)
|
||||||
vbox.addWidget(self.receive_list)
|
vbox.addWidget(self.request_list)
|
||||||
vbox.setStretchFactor(self.receive_list, 1000)
|
vbox.setStretchFactor(self.request_list, 1000)
|
||||||
|
|
||||||
return w
|
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):
|
def delete_payment_request(self, item):
|
||||||
addr = str(item.text(2))
|
addr = str(item.text(2))
|
||||||
self.wallet.remove_payment_request(addr, self.config)
|
self.wallet.remove_payment_request(addr, self.config)
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
self.clear_receive_tab()
|
self.clear_receive_tab()
|
||||||
|
|
||||||
def get_request_URI(self, addr):
|
def get_request_URI(self, addr):
|
||||||
|
@ -778,17 +736,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
URI += "&name=" + req['name'] + "&sig="+sig
|
URI += "&name=" + req['name'] + "&sig="+sig
|
||||||
return str(URI)
|
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):
|
def sign_payment_request(self, addr):
|
||||||
alias = self.config.get('alias')
|
alias = self.config.get('alias')
|
||||||
|
@ -823,7 +770,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
req = self.wallet.make_payment_request(addr, amount, message, expiration)
|
req = self.wallet.make_payment_request(addr, amount, message, expiration)
|
||||||
self.wallet.add_payment_request(req, self.config)
|
self.wallet.add_payment_request(req, self.config)
|
||||||
self.sign_payment_request(addr)
|
self.sign_payment_request(addr)
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
self.save_request_button.setEnabled(False)
|
self.save_request_button.setEnabled(False)
|
||||||
|
|
||||||
|
@ -901,52 +848,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.receive_address_e.setText(addr)
|
self.receive_address_e.setText(addr)
|
||||||
self.new_request_button.setEnabled(True)
|
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):
|
def update_receive_qr(self):
|
||||||
addr = str(self.receive_address_e.text())
|
addr = str(self.receive_address_e.text())
|
||||||
amount = self.receive_amount_e.get_amount()
|
amount = self.receive_amount_e.get_amount()
|
||||||
|
@ -1101,12 +1002,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.fee_e.textChanged.connect(entry_changed)
|
self.fee_e.textChanged.connect(entry_changed)
|
||||||
|
|
||||||
self.invoices_label = QLabel(_('Invoices'))
|
self.invoices_label = QLabel(_('Invoices'))
|
||||||
self.invoices_list = MyTreeWidget(self, self.invoices_list_menu,
|
from invoice_list import InvoiceList
|
||||||
[_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')], 2)
|
self.invoice_list = InvoiceList(self)
|
||||||
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
|
|
||||||
|
|
||||||
vbox0 = QVBoxLayout()
|
vbox0 = QVBoxLayout()
|
||||||
vbox0.addLayout(grid)
|
vbox0.addLayout(grid)
|
||||||
|
@ -1117,8 +1014,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
vbox.addLayout(hbox)
|
vbox.addLayout(hbox)
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
vbox.addWidget(self.invoices_label)
|
vbox.addWidget(self.invoices_label)
|
||||||
vbox.addWidget(self.invoices_list)
|
vbox.addWidget(self.invoice_list)
|
||||||
vbox.setStretchFactor(self.invoices_list, 1000)
|
vbox.setStretchFactor(self.invoice_list, 1000)
|
||||||
|
|
||||||
# Defer this until grid is parented to avoid ugly flash during startup
|
# Defer this until grid is parented to avoid ugly flash during startup
|
||||||
self.update_fee_edit()
|
self.update_fee_edit()
|
||||||
|
@ -1402,7 +1299,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if tx_desc is not None and tx.is_complete():
|
if tx_desc is not None and tx.is_complete():
|
||||||
self.wallet.set_label(tx.hash(), tx_desc)
|
self.wallet.set_label(tx.hash(), tx_desc)
|
||||||
parent.show_message(_('Payment sent.') + '\n' + msg)
|
parent.show_message(_('Payment sent.') + '\n' + msg)
|
||||||
self.invoices_list.update()
|
self.invoice_list.update()
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
else:
|
else:
|
||||||
parent.show_error(msg)
|
parent.show_error(msg)
|
||||||
|
@ -1428,23 +1325,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.payto_e.setText(_("please wait..."))
|
self.payto_e.setText(_("please wait..."))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def delete_invoice(self, key):
|
||||||
|
self.invoices.remove(key)
|
||||||
|
self.invoice_list.update()
|
||||||
|
|
||||||
def payment_request_ok(self):
|
def payment_request_ok(self):
|
||||||
pr = self.payment_request
|
pr = self.payment_request
|
||||||
key = self.invoices.add(pr)
|
key = self.invoices.add(pr)
|
||||||
status = self.invoices.get_status(key)
|
status = self.invoices.get_status(key)
|
||||||
self.invoices_list.update()
|
self.invoice_list.update()
|
||||||
if status == PR_PAID:
|
if status == PR_PAID:
|
||||||
self.show_message("invoice already paid")
|
self.show_message("invoice already paid")
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
return
|
return
|
||||||
|
|
||||||
self.payto_e.is_pr = True
|
self.payto_e.is_pr = True
|
||||||
if not pr.has_expired():
|
if not pr.has_expired():
|
||||||
self.payto_e.setGreen()
|
self.payto_e.setGreen()
|
||||||
else:
|
else:
|
||||||
self.payto_e.setExpired()
|
self.payto_e.setExpired()
|
||||||
|
|
||||||
self.payto_e.setText(pr.get_requestor())
|
self.payto_e.setText(pr.get_requestor())
|
||||||
self.amount_e.setText(format_satoshis_plain(pr.get_amount(), self.decimal_point))
|
self.amount_e.setText(format_satoshis_plain(pr.get_amount(), self.decimal_point))
|
||||||
self.message_e.setText(pr.get_memo())
|
self.message_e.setText(pr.get_memo())
|
||||||
|
@ -1524,42 +1423,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def create_addresses_tab(self):
|
def create_addresses_tab(self):
|
||||||
l = MyTreeWidget(self, self.create_address_menu, [ _('Address'), _('Label'), _('Balance'), _('Tx')], 1)
|
from address_list import AddressList
|
||||||
l.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.address_list = l = AddressList(self)
|
||||||
l.on_update = self.update_address_tab
|
|
||||||
self.address_list = l
|
|
||||||
return self.create_list_tab(l)
|
return self.create_list_tab(l)
|
||||||
|
|
||||||
def create_contacts_tab(self):
|
def create_contacts_tab(self):
|
||||||
l = MyTreeWidget(self, self.create_contact_menu, [_('Name'), _('Value'), _('Type')], 1, [0, 1])
|
from contact_list import ContactList
|
||||||
l.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.contact_list = l = ContactList(self)
|
||||||
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
|
|
||||||
return self.create_list_tab(l)
|
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):
|
def delete_imported_key(self, addr):
|
||||||
if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
|
if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
|
||||||
self.wallet.delete_imported_key(addr)
|
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.addAction(_("View details"), lambda: self.show_account_details(k))
|
||||||
menu.exec_(self.address_list.viewport().mapToGlobal(position))
|
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):
|
def get_coins(self):
|
||||||
if self.pay_from:
|
if self.pay_from:
|
||||||
|
@ -1669,22 +1494,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.payto_e.setText(text)
|
self.payto_e.setText(text)
|
||||||
self.payto_e.setFocus()
|
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):
|
def set_contact(self, label, address):
|
||||||
if not is_valid(address):
|
if not is_valid(address):
|
||||||
self.show_error(_('Invalid Address'))
|
self.show_error(_('Invalid Address'))
|
||||||
self.contacts_list.update() # Displays original unchanged value
|
self.contact_list.update() # Displays original unchanged value
|
||||||
return False
|
return False
|
||||||
self.contacts[label] = ('address', address)
|
self.contacts[label] = ('address', address)
|
||||||
self.contacts_list.update()
|
self.contact_list.update()
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.update_completions()
|
self.update_completions()
|
||||||
return True
|
return True
|
||||||
|
@ -1696,33 +1512,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
for label in labels:
|
for label in labels:
|
||||||
self.contacts.pop(label)
|
self.contacts.pop(label)
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.contacts_list.update()
|
self.contact_list.update()
|
||||||
self.update_completions()
|
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):
|
def show_invoice(self, key):
|
||||||
pr = self.invoices.get(key)
|
pr = self.invoices.get(key)
|
||||||
|
@ -1766,107 +1558,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
else:
|
else:
|
||||||
self.payment_request_error()
|
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):
|
def create_console_tab(self):
|
||||||
from console import Console
|
from console import Console
|
||||||
self.console = console = Console()
|
self.console = console = Console()
|
||||||
|
@ -1906,7 +1597,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
|
|
||||||
def create_status_bar(self):
|
def create_status_bar(self):
|
||||||
|
|
||||||
|
@ -1993,13 +1684,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
self.history_list.filter(t, [2, 3, 4]) # Date, Description, Amount
|
self.history_list.filter(t, [2, 3, 4]) # Date, Description, Amount
|
||||||
elif i == 1:
|
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:
|
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:
|
elif i == 3:
|
||||||
self.address_list.filter(t, [0,1, 2]) # Address, Label, Balance
|
self.address_list.filter(t, [0,1, 2]) # Address, Label, Balance
|
||||||
elif i == 4:
|
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):
|
def new_contact_dialog(self):
|
||||||
|
@ -2795,7 +2486,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
raise Exception('Unknown base unit')
|
raise Exception('Unknown base unit')
|
||||||
self.config.set_key('decimal_point', self.decimal_point, True)
|
self.config.set_key('decimal_point', self.decimal_point, True)
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.receive_list.update()
|
self.request_list.update()
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
for edit, amount in zip(edits, amounts):
|
for edit, amount in zip(edits, amounts):
|
||||||
edit.setAmount(amount)
|
edit.setAmount(amount)
|
||||||
|
|
|
@ -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))
|
|
@ -27,6 +27,28 @@ BLACK_FG = "QWidget {color:black;}"
|
||||||
|
|
||||||
dialogs = []
|
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):
|
class Timer(QThread):
|
||||||
stopped = False
|
stopped = False
|
||||||
|
|
||||||
|
@ -351,6 +373,7 @@ class MyTreeWidget(QTreeWidget):
|
||||||
editable_columns=None):
|
editable_columns=None):
|
||||||
QTreeWidget.__init__(self, parent)
|
QTreeWidget.__init__(self, parent)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
self.config = self.parent.config
|
||||||
self.stretch_column = stretch_column
|
self.stretch_column = stretch_column
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.customContextMenuRequested.connect(create_menu)
|
self.customContextMenuRequested.connect(create_menu)
|
||||||
|
@ -366,7 +389,7 @@ class MyTreeWidget(QTreeWidget):
|
||||||
editable_columns = [stretch_column]
|
editable_columns = [stretch_column]
|
||||||
self.editable_columns = editable_columns
|
self.editable_columns = editable_columns
|
||||||
self.setItemDelegate(ElectrumItemDelegate(self))
|
self.setItemDelegate(ElectrumItemDelegate(self))
|
||||||
self.itemActivated.connect(self.on_activated)
|
self.itemDoubleClicked.connect(self.on_doubleclick)
|
||||||
self.update_headers(headers)
|
self.update_headers(headers)
|
||||||
|
|
||||||
def update_headers(self, headers):
|
def update_headers(self, headers):
|
||||||
|
@ -386,7 +409,7 @@ class MyTreeWidget(QTreeWidget):
|
||||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
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())
|
self.on_activated(self.currentItem(), self.currentColumn())
|
||||||
else:
|
else:
|
||||||
QTreeWidget.keyPressEvent(self, event)
|
QTreeWidget.keyPressEvent(self, event)
|
||||||
|
@ -398,10 +421,12 @@ class MyTreeWidget(QTreeWidget):
|
||||||
def on_permit_edit(self, item, column):
|
def on_permit_edit(self, item, column):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def on_activated(self, item, column):
|
def on_doubleclick(self, item, column):
|
||||||
if self.permit_edit(item, column):
|
if self.permit_edit(item, column):
|
||||||
self.editItem(item, column)
|
self.editItem(item, column)
|
||||||
else:
|
|
||||||
|
def on_activated(self, item, column):
|
||||||
|
# on 'enter' we show the menu
|
||||||
pt = self.visualItemRect(item).bottomLeft()
|
pt = self.visualItemRect(item).bottomLeft()
|
||||||
pt.setX(50)
|
pt.setX(50)
|
||||||
self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), pt)
|
self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), pt)
|
||||||
|
|
Loading…
Reference in New Issue