Browse Source

qt: use QStandardItemModel

3.3.3.1
Janus 6 years ago
committed by ThomasV
parent
commit
5473320ce4
  1. 3
      electrum/contacts.py
  2. 82
      electrum/gui/qt/address_list.py
  3. 80
      electrum/gui/qt/contact_list.py
  4. 408
      electrum/gui/qt/history_list.py
  5. 28
      electrum/gui/qt/invoice_list.py
  6. 10
      electrum/gui/qt/main_window.py
  7. 3
      electrum/gui/qt/network_dialog.py
  8. 68
      electrum/gui/qt/request_list.py
  9. 232
      electrum/gui/qt/util.py
  10. 61
      electrum/gui/qt/utxo_list.py

3
electrum/contacts.py

@ -65,8 +65,9 @@ class Contacts(dict):
def pop(self, key): def pop(self, key):
if key in self.keys(): if key in self.keys():
dict.pop(self, key) res = dict.pop(self, key)
self.save() self.save()
return res
def resolve(self, k): def resolve(self, k):
if bitcoin.is_address(k): if bitcoin.is_address(k):

82
electrum/gui/qt/address_list.py

@ -31,13 +31,11 @@ from electrum.bitcoin import is_address
from .util import * from .util import *
class AddressList(MyTreeView):
class AddressList(MyTreeWidget):
filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 2) super().__init__(parent, self.create_menu, 2)
self.refresh_headers()
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.show_change = 0 self.show_change = 0
@ -50,6 +48,8 @@ class AddressList(MyTreeWidget):
self.used_button.currentIndexChanged.connect(self.toggle_used) self.used_button.currentIndexChanged.connect(self.toggle_used)
for t in [_('All'), _('Unused'), _('Funded'), _('Used')]: for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
self.used_button.addItem(t) self.used_button.addItem(t)
self.setModel(QStandardItemModel(self))
self.update()
def get_toolbar_buttons(self): def get_toolbar_buttons(self):
return QLabel(_("Filter:")), self.change_button, self.used_button return QLabel(_("Filter:")), self.change_button, self.used_button
@ -82,18 +82,19 @@ class AddressList(MyTreeWidget):
self.show_used = state self.show_used = state
self.update() self.update()
def on_update(self): def update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
item = self.currentItem() current_address = self.current_item_user_role(col=2)
current_address = item.data(0, Qt.UserRole) if item else None
if self.show_change == 1: if self.show_change == 1:
addr_list = self.wallet.get_receiving_addresses() addr_list = self.wallet.get_receiving_addresses()
elif self.show_change == 2: elif self.show_change == 2:
addr_list = self.wallet.get_change_addresses() addr_list = self.wallet.get_change_addresses()
else: else:
addr_list = self.wallet.get_addresses() addr_list = self.wallet.get_addresses()
self.clear() self.model().clear()
self.refresh_headers()
fx = self.parent.fx fx = self.parent.fx
set_address = None
for address in addr_list: for address in addr_list:
num = self.wallet.get_address_history_len(address) num = self.wallet.get_address_history_len(address)
label = self.wallet.labels.get(address, '') label = self.wallet.labels.get(address, '')
@ -111,61 +112,66 @@ class AddressList(MyTreeWidget):
if fx and fx.get_fiat_address_config(): if fx and fx.get_fiat_address_config():
rate = fx.exchange_rate() rate = fx.exchange_rate()
fiat_balance = fx.value_str(balance, rate) fiat_balance = fx.value_str(balance, rate)
address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num]) labels = ['', address, label, balance_text, fiat_balance, "%d"%num]
address_item = [QStandardItem(e) for e in labels]
else: else:
address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num]) labels = ['', address, label, balance_text, "%d"%num]
address_item = [QStandardItem(e) for e in labels]
# align text and set fonts # align text and set fonts
for i in range(address_item.columnCount()): for i, item in enumerate(address_item):
address_item.setTextAlignment(i, Qt.AlignVCenter) item.setTextAlignment(Qt.AlignVCenter)
if i not in (0, 2): if i not in (0, 2):
address_item.setFont(i, QFont(MONOSPACE_FONT)) item.setFont(QFont(MONOSPACE_FONT))
item.setEditable(i in self.editable_columns)
if fx and fx.get_fiat_address_config(): if fx and fx.get_fiat_address_config():
address_item.setTextAlignment(4, Qt.AlignRight | Qt.AlignVCenter) address_item[4].setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
# setup column 0 # setup column 0
if self.wallet.is_change(address): if self.wallet.is_change(address):
address_item.setText(0, _('change')) address_item[0].setText(_('change'))
address_item.setBackground(0, ColorScheme.YELLOW.as_color(True)) address_item[0].setBackground(ColorScheme.YELLOW.as_color(True))
else: else:
address_item.setText(0, _('receiving')) address_item[0].setText(_('receiving'))
address_item.setBackground(0, ColorScheme.GREEN.as_color(True)) address_item[0].setBackground(ColorScheme.GREEN.as_color(True))
address_item.setData(0, Qt.UserRole, address) # column 0; independent from address column address_item[2].setData(address, Qt.UserRole)
# setup column 1 # setup column 1
if self.wallet.is_frozen(address): if self.wallet.is_frozen(address):
address_item.setBackground(1, ColorScheme.BLUE.as_color(True)) address_item[1].setBackground(ColorScheme.BLUE.as_color(True))
if self.wallet.is_beyond_limit(address): if self.wallet.is_beyond_limit(address):
address_item.setBackground(1, ColorScheme.RED.as_color(True)) address_item[1].setBackground(ColorScheme.RED.as_color(True))
# add item # add item
self.addChild(address_item) count = self.model().rowCount()
self.model().insertRow(count, address_item)
address_idx = self.model().index(count, 2)
if address == current_address: if address == current_address:
self.setCurrentItem(address_item) set_address = QPersistentModelIndex(address_idx)
self.set_current_idx(set_address)
def create_menu(self, position): def create_menu(self, position):
from electrum.wallet import Multisig_Wallet from electrum.wallet import Multisig_Wallet
is_multisig = isinstance(self.wallet, Multisig_Wallet) is_multisig = isinstance(self.wallet, Multisig_Wallet)
can_delete = self.wallet.can_delete_address() can_delete = self.wallet.can_delete_address()
selected = self.selectedItems() selected = self.selected_in_column(1)
multi_select = len(selected) > 1 multi_select = len(selected) > 1
addrs = [item.text(1) for item in selected] addrs = [self.model().itemFromIndex(item).text() for item in selected]
if not addrs:
return
if not multi_select: if not multi_select:
item = self.itemAt(position) idx = self.indexAt(position)
col = self.currentColumn() col = idx.column()
item = self.model().itemFromIndex(idx)
if not item: if not item:
return return
addr = addrs[0] addr = addrs[0]
if not is_address(addr):
item.setExpanded(not item.isExpanded())
return
menu = QMenu() menu = QMenu()
if not multi_select: if not multi_select:
column_title = self.headerItem().text(col) addr_column_title = self.model().horizontalHeaderItem(2).text()
copy_text = item.text(col) addr_idx = idx.sibling(idx.row(), 2)
column_title = self.model().horizontalHeaderItem(col).text()
copy_text = self.model().itemFromIndex(idx).text()
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
if col in self.editable_columns: persistent = QPersistentModelIndex(addr_idx)
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col)) menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
if self.wallet.can_export(): if self.wallet.can_export():
menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))
@ -189,7 +195,3 @@ class AddressList(MyTreeWidget):
run_hook('receive_menu', menu, addrs, self.wallet) run_hook('receive_menu', menu, addrs, self.wallet)
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def on_permit_edit(self, item, column):
# labels for headings, e.g. "receiving" or "used" should not be editable
return item.childCount() == 0

80
electrum/gui/qt/contact_list.py

@ -34,67 +34,81 @@ from electrum.bitcoin import is_address
from electrum.util import block_explorer_URL from electrum.util import block_explorer_URL
from electrum.plugin import run_hook from electrum.plugin import run_hook
from .util import MyTreeWidget, import_meta_gui, export_meta_gui from .util import MyTreeView, import_meta_gui, export_meta_gui
class ContactList(MyTreeWidget): class ContactList(MyTreeView):
filter_columns = [0, 1] # Key, Value filter_columns = [0, 1] # Key, Value
def __init__(self, parent): def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Name'), _('Address')], 0, [0]) super().__init__(parent, self.create_menu, stretch_column=0, editable_columns=[0])
self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.update()
def on_permit_edit(self, item, column): def on_edited(self, idx, user_role, text):
# openalias items shouldn't be editable _type, prior_name = self.parent.contacts.pop(user_role)
return item.text(1) != "openalias"
def on_edited(self, item, column, prior): # TODO when min Qt >= 5.11, use siblingAtColumn
if column == 0: # Remove old contact if renamed col_1_sibling = idx.sibling(idx.row(), 1)
self.parent.contacts.pop(prior) col_1_item = self.model().itemFromIndex(col_1_sibling)
self.parent.set_contact(item.text(0), item.text(1))
self.parent.set_contact(text, col_1_item.text())
def import_contacts(self): def import_contacts(self):
import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update) import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.update)
def export_contacts(self): def export_contacts(self):
export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file) export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
def create_menu(self, position): def create_menu(self, position):
menu = QMenu() menu = QMenu()
selected = self.selectedItems() selected = self.selected_in_column(0)
if not selected: selected_keys = []
for idx in selected:
sel_key = self.model().itemFromIndex(idx).data(Qt.UserRole)
selected_keys.append(sel_key)
idx = self.indexAt(position)
if not selected or not idx.isValid():
menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
menu.addAction(_("Import file"), lambda: self.import_contacts()) menu.addAction(_("Import file"), lambda: self.import_contacts())
menu.addAction(_("Export file"), lambda: self.export_contacts()) menu.addAction(_("Export file"), lambda: self.export_contacts())
else: else:
names = [item.text(0) for item in selected] column = idx.column()
keys = [item.text(1) for item in selected] column_title = self.model().horizontalHeaderItem(column).text()
column = self.currentColumn() column_data = '\n'.join(self.model().itemFromIndex(idx).text() for idx in selected)
column_title = self.headerItem().text(column)
column_data = '\n'.join([item.text(column) for item in selected])
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
if column in self.editable_columns: if column in self.editable_columns:
item = self.currentItem() item = self.model().itemFromIndex(idx)
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) if item.isEditable():
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys)) # would not be editable if openalias
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys)) persistent = QPersistentModelIndex(idx)
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)] menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(selected_keys))
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(selected_keys))
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, selected_keys)]
if URLs: if URLs:
menu.addAction(_("View on block explorer"), lambda: map(webbrowser.open, URLs)) menu.addAction(_("View on block explorer"), lambda: map(webbrowser.open, URLs))
run_hook('create_contact_menu', menu, selected) run_hook('create_contact_menu', menu, selected_keys)
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def on_update(self): def update(self):
item = self.currentItem() current_key = self.current_item_user_role(col=0)
current_key = item.data(0, Qt.UserRole) if item else None self.model().clear()
self.clear() self.update_headers([_('Name'), _('Address')])
set_current = None
for key in sorted(self.parent.contacts.keys()): for key in sorted(self.parent.contacts.keys()):
_type, name = self.parent.contacts[key] contact_type, name = self.parent.contacts[key]
item = QTreeWidgetItem([name, key]) items = [QStandardItem(x) for x in (name, key)]
item.setData(0, Qt.UserRole, key) items[0].setEditable(contact_type != 'openalias')
self.addTopLevelItem(item) items[1].setEditable(False)
items[0].setData(key, Qt.UserRole)
row_count = self.model().rowCount()
self.model().insertRow(row_count, items)
if key == current_key: if key == current_key:
self.setCurrentItem(item) idx = self.model().index(row_count, 0)
set_current = QPersistentModelIndex(idx)
self.set_current_idx(set_current)
run_hook('update_contacts_tab', self) run_hook('update_contacts_tab', self)

408
electrum/gui/qt/history_list.py

@ -27,10 +27,11 @@ import webbrowser
import datetime import datetime
from datetime import date from datetime import date
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from collections import OrderedDict
from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus, Fiat
from .util import * from .util import *
@ -57,40 +58,111 @@ TX_ICONS = [
"confirmed.png", "confirmed.png",
] ]
class HistorySortModel(QSortFilterProxyModel):
class HistoryList(MyTreeWidget, AcceptFileDragDrop): def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
filter_columns = [2, 3, 4] # Date, Description, Amount item1 = self.sourceModel().itemFromIndex(source_left)
item2 = self.sourceModel().itemFromIndex(source_right)
data1 = item1.data(HistoryList.SORT_ROLE)
data2 = item2.data(HistoryList.SORT_ROLE)
if data1 is not None and data2 is not None:
return data1 < data2
return item1.text() < item2.text()
class HistoryList(MyTreeView, AcceptFileDragDrop):
filter_columns = [1, 2, 3] # Date, Description, Amount
TX_HASH_ROLE = Qt.UserRole TX_HASH_ROLE = Qt.UserRole
TX_VALUE_ROLE = Qt.UserRole + 1 SORT_ROLE = Qt.UserRole + 1
def should_hide(self, proxy_row):
if self.start_timestamp and self.end_timestamp:
source_idx = self.proxy.mapToSource(self.proxy.index(proxy_row, 0))
item = self.std_model.itemFromIndex(source_idx)
txid = item.data(self.TX_HASH_ROLE)
date = self.transactions[txid]['date']
if date:
in_interval = self.start_timestamp <= date <= self.end_timestamp
if not in_interval:
return True
return False
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) super().__init__(parent, self.create_menu, 2)
self.std_model = QStandardItemModel(self)
self.proxy = HistorySortModel(self)
self.proxy.setSourceModel(self.std_model)
self.setModel(self.proxy)
self.txid_to_items = {}
self.transactions = OrderedDict()
self.summary = {}
self.blue_brush = QBrush(QColor("#1E1EFF"))
self.red_brush = QBrush(QColor("#BC1E1E"))
self.monospace_font = QFont(MONOSPACE_FONT)
self.default_color = self.parent.app.palette().text().color()
self.config = parent.config
AcceptFileDragDrop.__init__(self, ".txn") AcceptFileDragDrop.__init__(self, ".txn")
self.refresh_headers()
self.setColumnHidden(1, True)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.sortByColumn(0, Qt.AscendingOrder)
self.start_timestamp = None self.start_timestamp = None
self.end_timestamp = None self.end_timestamp = None
self.years = [] self.years = []
self.create_toolbar_buttons() self.create_toolbar_buttons()
self.wallet = None self.wallet = None
root = self.std_model.invisibleRootItem()
self.wallet = self.parent.wallet # type: Abstract_Wallet
fx = self.parent.fx
r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx)
self.transactions.update([(x['txid'], x) for x in r['transactions']])
self.summary = r['summary']
if not self.years and self.transactions:
start_date = next(iter(self.transactions.values())).get('date') or date.today()
end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today()
self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
self.period_combo.insertItems(1, self.years)
if fx: fx.history_used_spot = False
self.refresh_headers()
for tx_item in self.transactions.values():
self.insert_tx(tx_item)
self.sortByColumn(0, Qt.AscendingOrder)
#def on_activated(self, idx: QModelIndex):
# # TODO use siblingAtColumn when min Qt version is >=5.11
# self.edit(idx.sibling(idx.row(), 2))
def format_date(self, d): def format_date(self, d):
return str(datetime.date(d.year, d.month, d.day)) if d else _('None') return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
def refresh_headers(self): def refresh_headers(self):
headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')] headers = ['', _('Date'), _('Description'), _('Amount'), _('Balance')]
fx = self.parent.fx fx = self.parent.fx
if fx and fx.show_history(): if fx and fx.show_history():
headers.extend(['%s '%fx.ccy + _('Value')]) headers.extend(['%s '%fx.ccy + _('Value')])
self.editable_columns |= {6} self.editable_columns |= {5}
if fx.get_history_capital_gains_config(): if fx.get_history_capital_gains_config():
headers.extend(['%s '%fx.ccy + _('Acquisition price')]) headers.extend(['%s '%fx.ccy + _('Acquisition price')])
headers.extend(['%s '%fx.ccy + _('Capital Gains')]) headers.extend(['%s '%fx.ccy + _('Capital Gains')])
else: else:
self.editable_columns -= {6} self.editable_columns -= {5}
self.update_headers(headers) col_count = self.std_model.columnCount()
diff = col_count-len(headers)
grew = False
if col_count > len(headers):
if diff == 2:
self.std_model.removeColumns(6, diff)
else:
assert diff in [1, 3]
self.std_model.removeColumns(5, diff)
for items in self.txid_to_items.values():
while len(items) > col_count:
items.pop()
elif col_count < len(headers):
grew = True
self.std_model.clear()
self.txid_to_items.clear()
self.transactions.clear()
self.summary.clear()
self.update_headers(headers, self.std_model)
def get_domain(self): def get_domain(self):
'''Replaced in address_dialog.py''' '''Replaced in address_dialog.py'''
@ -111,13 +183,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
year = int(s) year = int(s)
except: except:
return return
start_date = datetime.datetime(year, 1, 1) self.start_timestamp = start_date = datetime.datetime(year, 1, 1)
end_date = datetime.datetime(year+1, 1, 1) self.end_timestamp = end_date = datetime.datetime(year+1, 1, 1)
self.start_timestamp = time.mktime(start_date.timetuple())
self.end_timestamp = time.mktime(end_date.timetuple())
self.start_button.setText(_('From') + ' ' + self.format_date(start_date)) self.start_button.setText(_('From') + ' ' + self.format_date(start_date))
self.end_button.setText(_('To') + ' ' + self.format_date(end_date)) self.end_button.setText(_('To') + ' ' + self.format_date(end_date))
self.update() self.hide_rows()
def create_toolbar_buttons(self): def create_toolbar_buttons(self):
self.period_combo = QComboBox() self.period_combo = QComboBox()
@ -136,18 +206,18 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
def on_hide_toolbar(self): def on_hide_toolbar(self):
self.start_timestamp = None self.start_timestamp = None
self.end_timestamp = None self.end_timestamp = None
self.update() self.hide_rows()
def save_toolbar_state(self, state, config): def save_toolbar_state(self, state, config):
config.set_key('show_toolbar_history', state) config.set_key('show_toolbar_history', state)
def select_start_date(self): def select_start_date(self):
self.start_timestamp = self.select_date(self.start_button) self.start_timestamp = self.select_date(self.start_button)
self.update() self.hide_rows()
def select_end_date(self): def select_end_date(self):
self.end_timestamp = self.select_date(self.end_button) self.end_timestamp = self.select_date(self.end_button)
self.update() self.hide_rows()
def select_date(self, button): def select_date(self, button):
d = WindowModalDialog(self, _("Select date")) d = WindowModalDialog(self, _("Select date"))
@ -167,7 +237,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
return None return None
date = d.date.toPyDate() date = d.date.toPyDate()
button.setText(self.format_date(date)) button.setText(self.format_date(date))
return time.mktime(date.timetuple()) return datetime.datetime(date.year, date.month, date.day)
def show_summary(self): def show_summary(self):
h = self.summary h = self.summary
@ -215,104 +285,167 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
_("Perhaps some dependencies are missing...") + " (matplotlib?)") _("Perhaps some dependencies are missing...") + " (matplotlib?)")
return return
try: try:
plt = plot_history(self.transactions) plt = plot_history(list(self.transactions.values()))
plt.show() plt.show()
except NothingToPlotException as e: except NothingToPlotException as e:
self.parent.show_message(str(e)) self.parent.show_message(str(e))
def insert_tx(self, tx_item):
fx = self.parent.fx
tx_hash = tx_item['txid']
height = tx_item['height']
conf = tx_item['confirmations']
timestamp = tx_item['timestamp']
value = tx_item['value'].value
balance = tx_item['balance'].value
label = tx_item['label']
tx_mined_status = TxMinedStatus(height, conf, timestamp, None)
status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
has_invoice = self.wallet.invoices.paid.get(tx_hash)
icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True)
balance_str = self.parent.format_amount(balance, whitespaces=True)
entry = ['', status_str, label, v_str, balance_str]
fiat_value = None
item = [QStandardItem(e) for e in entry]
item[3].setData(value, self.SORT_ROLE)
item[4].setData(balance, self.SORT_ROLE)
if has_invoice:
item[2].setIcon(self.icon_cache.get(":icons/seal"))
for i in range(len(entry)):
self.set_item_properties(item[i], i, tx_hash)
if value and value < 0:
item[2].setForeground(self.red_brush)
item[3].setForeground(self.red_brush)
self.txid_to_items[tx_hash] = item
self.update_item(tx_hash, self.parent.wallet.get_tx_height(tx_hash))
source_row_idx = self.std_model.rowCount()
self.std_model.insertRow(source_row_idx, item)
new_idx = self.std_model.index(source_row_idx, 0)
history = self.parent.fx.show_history()
if history:
self.update_fiat(tx_hash, tx_item)
self.hide_row(self.proxy.mapFromSource(new_idx).row())
def set_item_properties(self, item, i, tx_hash):
if i>2:
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
if i!=1:
item.setFont(self.monospace_font)
item.setEditable(i in self.editable_columns)
item.setData(tx_hash, self.TX_HASH_ROLE)
def ensure_fields_available(self, items, idx, txid):
while len(items) < idx + 1:
row = list(self.transactions.keys()).index(txid)
qidx = self.std_model.index(row, len(items))
assert qidx.isValid(), (self.std_model.columnCount(), idx)
item = self.std_model.itemFromIndex(qidx)
self.set_item_properties(item, len(items), txid)
items.append(item)
@profiler @profiler
def on_update(self): def update(self):
self.wallet = self.parent.wallet # type: Abstract_Wallet self.wallet = self.parent.wallet # type: Abstract_Wallet
fx = self.parent.fx fx = self.parent.fx
r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx) r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx)
self.transactions = r['transactions'] seen = set()
self.summary = r['summary'] history = fx.show_history()
if not self.years and self.transactions: tx_list = list(self.transactions.values())
start_date = self.transactions[0].get('date') or date.today() if r['transactions'] == tx_list:
end_date = self.transactions[-1].get('date') or date.today() return
self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] if r['transactions'][:-1] == tx_list:
self.period_combo.insertItems(1, self.years) print_error('history_list: one new transaction')
item = self.currentItem() row = r['transactions'][-1]
current_tx = item.data(0, self.TX_HASH_ROLE) if item else None txid = row['txid']
self.clear() if txid not in self.transactions:
if fx: fx.history_used_spot = False self.transactions[txid] = row
blue_brush = QBrush(QColor("#1E1EFF")) self.transactions.move_to_end(txid, last=True)
red_brush = QBrush(QColor("#BC1E1E")) self.insert_tx(row)
monospace_font = QFont(MONOSPACE_FONT) return
for tx_item in self.transactions: else:
tx_hash = tx_item['txid'] print_error('history_list: tx added but txid is already in list (weird), txid: ', txid)
height = tx_item['height'] for idx, row in enumerate(r['transactions']):
conf = tx_item['confirmations'] txid = row['txid']
timestamp = tx_item['timestamp'] seen.add(txid)
value_sat = tx_item['value'].value if txid not in self.transactions:
balance = tx_item['balance'].value self.transactions[txid] = row
label = tx_item['label'] self.transactions.move_to_end(txid, last=True)
tx_mined_status = TxMinedStatus(height, conf, timestamp, None) self.insert_tx(row)
status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) continue
has_invoice = self.wallet.invoices.paid.get(tx_hash) old = self.transactions[txid]
icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) if old == row:
v_str = self.parent.format_amount(value_sat, is_diff=True, whitespaces=True) continue
balance_str = self.parent.format_amount(balance, whitespaces=True) self.update_item(txid, self.parent.wallet.get_tx_height(txid))
entry = ['', tx_hash, status_str, label, v_str, balance_str] if history:
fiat_value = None self.update_fiat(txid, row)
if value_sat is not None and fx and fx.show_history(): balance_str = self.parent.format_amount(row['balance'].value, whitespaces=True)
fiat_value = tx_item['fiat_value'].value self.txid_to_items[txid][4].setText(balance_str)
value_str = fx.format_fiat(fiat_value) self.txid_to_items[txid][4].setData(row['balance'].value, self.SORT_ROLE)
entry.append(value_str) old.clear()
# fixme: should use is_mine old.update(**row)
if value_sat < 0: removed = 0
entry.append(fx.format_fiat(tx_item['acquisition_price'].value)) l = list(enumerate(self.transactions.keys()))
entry.append(fx.format_fiat(tx_item['capital_gain'].value)) for idx, txid in l:
item = SortableTreeWidgetItem(entry) if txid not in seen:
item.setIcon(0, icon) del self.transactions[txid]
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) del self.txid_to_items[txid]
if has_invoice: items = self.std_model.takeRow(idx - removed)
item.setIcon(3, self.icon_cache.get(":icons/seal")) removed_txid = items[0].data(self.TX_HASH_ROLE)
for i in range(len(entry)): assert removed_txid == txid, (idx, removed)
if i>3: removed += 1
item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) self.apply_filter()
if i!=2:
item.setFont(i, monospace_font) def update_fiat(self, txid, row):
if value_sat and value_sat < 0: cap_gains = self.parent.fx.get_history_capital_gains_config()
item.setForeground(3, red_brush) items = self.txid_to_items[txid]
item.setForeground(4, red_brush) self.ensure_fields_available(items, 7 if cap_gains else 5, txid)
if fiat_value is not None and not tx_item['fiat_default']: items[5].setForeground(self.blue_brush if not row['fiat_default'] and row['fiat_value'] else self.default_color)
item.setForeground(6, blue_brush) value_str = self.parent.fx.format_fiat(row['fiat_value'].value)
# sort orders items[5].setText(value_str)
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) items[5].setData(row['fiat_value'].value, self.SORT_ROLE)
item.setData(4, SortableTreeWidgetItem.DataRole, value_sat) # fixme: should use is_mine
item.setData(5, SortableTreeWidgetItem.DataRole, balance) if row['value'].value < 0 and cap_gains:
if fiat_value is not None: acq = row['acquisition_price'].value
item.setData(6, SortableTreeWidgetItem.DataRole, fiat_value) items[6].setText(self.parent.fx.format_fiat(acq))
if value_sat < 0: items[6].setData(acq, self.SORT_ROLE)
item.setData(7, SortableTreeWidgetItem.DataRole, tx_item['acquisition_price'].value) cg = row['capital_gain'].value
item.setData(8, SortableTreeWidgetItem.DataRole, tx_item['capital_gain'].value) items[7].setText(self.parent.fx.format_fiat(cg))
if tx_hash: items[7].setData(cg, self.SORT_ROLE)
item.setData(0, self.TX_HASH_ROLE, tx_hash)
item.setData(0, self.TX_VALUE_ROLE, value_sat) def update_on_new_fee_histogram(self):
self.insertTopLevelItem(0, item) pass
if current_tx == tx_hash: # TODO update unconfirmed tx'es
self.setCurrentItem(item)
def on_edited(self, index, user_role, text):
def on_edited(self, item, column, prior): column = index.column()
'''Called only when the text actually changes''' index = self.proxy.mapToSource(index)
key = item.data(0, self.TX_HASH_ROLE) item = self.std_model.itemFromIndex(index)
value_sat = item.data(0, self.TX_VALUE_ROLE) key = item.data(self.TX_HASH_ROLE)
text = item.text(column)
# fixme # fixme
if column == 3: if column == 2:
self.parent.wallet.set_label(key, text) self.parent.wallet.set_label(key, text)
self.update_labels() self.update_labels()
self.parent.update_completions() self.parent.update_completions()
elif column == 6: elif column == 5:
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value_sat) tx_item = self.transactions[key]
self.on_update() self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value)
value = tx_item['value'].value
def on_doubleclick(self, item, column): if value is not None:
if self.permit_edit(item, column): fee = tx_item['fee']
super(HistoryList, self).on_doubleclick(item, column) fiat_fields = self.parent.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None)
tx_item.update(fiat_fields)
self.update_fiat(key, tx_item)
else: else:
tx_hash = item.data(0, self.TX_HASH_ROLE) assert False
def mouseDoubleClickEvent(self, event: QMouseEvent):
idx = self.indexAt(event.pos())
item = self.std_model.itemFromIndex(self.proxy.mapToSource(idx))
if not item or item.isEditable():
super().mouseDoubleClickEvent(event)
elif item:
tx_hash = item.data(self.TX_HASH_ROLE)
self.show_transaction(tx_hash) self.show_transaction(tx_hash)
def show_transaction(self, tx_hash): def show_transaction(self, tx_hash):
@ -323,13 +456,13 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
self.parent.show_transaction(tx, label) self.parent.show_transaction(tx, label)
def update_labels(self): def update_labels(self):
root = self.invisibleRootItem() root = self.std_model.invisibleRootItem()
child_count = root.childCount() child_count = root.rowCount()
for i in range(child_count): for i in range(child_count):
item = root.child(i) item = root.child(i, 2)
txid = item.data(0, self.TX_HASH_ROLE) txid = item.data(self.TX_HASH_ROLE)
label = self.wallet.get_label(txid) label = self.wallet.get_label(txid)
item.setText(3, label) item.setText(label)
def update_item(self, tx_hash, tx_mined_status): def update_item(self, tx_hash, tx_mined_status):
if self.wallet is None: if self.wallet is None:
@ -337,31 +470,30 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
conf = tx_mined_status.conf conf = tx_mined_status.conf
status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
items = self.findItems(tx_hash, Qt.MatchExactly, column=1) if tx_hash not in self.txid_to_items:
if items:
item = items[0]
item.setIcon(0, icon)
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
item.setText(2, status_str)
def create_menu(self, position):
self.selectedIndexes()
item = self.currentItem()
if not item:
return
column = self.currentColumn()
tx_hash = item.data(0, self.TX_HASH_ROLE)
if not tx_hash:
return return
items = self.txid_to_items[tx_hash]
items[0].setIcon(icon)
items[0].setToolTip(str(conf) + _(" confirmation" + ("s" if conf != 1 else "")))
items[0].setData((status, conf), self.SORT_ROLE)
items[1].setText(status_str)
def create_menu(self, position: QPoint):
org_idx: QModelIndex = self.indexAt(position)
idx = self.proxy.mapToSource(org_idx)
item: QStandardItem = self.std_model.itemFromIndex(idx)
assert item, 'create_menu: index not found in model'
tx_hash = idx.data(self.TX_HASH_ROLE)
column = idx.column()
assert tx_hash, "create_menu: no tx hash"
tx = self.wallet.transactions.get(tx_hash) tx = self.wallet.transactions.get(tx_hash)
if not tx: assert tx, "create_menu: no tx"
return if column == 0:
if column is 0: column_title = _('Transaction ID')
column_title = "ID"
column_data = tx_hash column_data = tx_hash
else: else:
column_title = self.headerItem().text(column) column_title = self.std_model.horizontalHeaderItem(column).text()
column_data = item.text(column) column_data = item.text()
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
height = self.wallet.get_tx_height(tx_hash).height height = self.wallet.get_tx_height(tx_hash).height
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
@ -372,8 +504,10 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
for c in self.editable_columns: for c in self.editable_columns:
menu.addAction(_("Edit {}").format(self.headerItem().text(c)), label = self.std_model.horizontalHeaderItem(c).text()
lambda bound_c=c: self.editItem(item, bound_c)) # TODO use siblingAtColumn when min Qt version is >=5.11
persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c))
menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p)))
menu.addAction(_("Details"), lambda: self.show_transaction(tx_hash)) menu.addAction(_("Details"), lambda: self.show_transaction(tx_hash))
if is_unconfirmed and tx: if is_unconfirmed and tx:
# note: the current implementation of RBF *needs* the old tx fee # note: the current implementation of RBF *needs* the old tx fee
@ -442,7 +576,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
self.parent.show_message(_("Your wallet history has been successfully exported.")) self.parent.show_message(_("Your wallet history has been successfully exported."))
def do_export_history(self, file_name, is_csv): def do_export_history(self, file_name, is_csv):
history = self.transactions history = self.transactions.values()
lines = [] lines = []
if is_csv: if is_csv:
for item in history: for item in history:

28
electrum/gui/qt/invoice_list.py

@ -29,36 +29,40 @@ from electrum.util import format_time
from .util import * from .util import *
class InvoiceList(MyTreeWidget): class InvoiceList(MyTreeView):
filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount
def __init__(self, parent): def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')], 2) super().__init__(parent, self.create_menu, 2)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.header().setSectionResizeMode(1, QHeaderView.Interactive)
self.setColumnWidth(1, 200) self.setColumnWidth(1, 200)
self.setModel(QStandardItemModel(self))
self.update()
def on_update(self): def update(self):
inv_list = self.parent.invoices.unpaid_invoices() inv_list = self.parent.invoices.unpaid_invoices()
self.clear() self.model().clear()
self.update_headers([_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')])
self.header().setSectionResizeMode(1, QHeaderView.Interactive)
for pr in inv_list: for pr in inv_list:
key = pr.get_id() key = pr.get_id()
status = self.parent.invoices.get_status(key) status = self.parent.invoices.get_status(key)
requestor = pr.get_requestor() requestor = pr.get_requestor()
exp = pr.get_expiration_date() exp = pr.get_expiration_date()
date_str = format_time(exp) if exp else _('Never') 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,'')]) labels = [date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]
item.setIcon(4, self.icon_cache.get(pr_icons.get(status))) item = [QStandardItem(e) for e in labels]
item.setData(0, Qt.UserRole, key) item[4].setIcon(self.icon_cache.get(pr_icons.get(status)))
item.setFont(1, QFont(MONOSPACE_FONT)) item[0].setData(Qt.UserRole, key)
item.setFont(3, QFont(MONOSPACE_FONT)) item[1].setFont(QFont(MONOSPACE_FONT))
item[3].setFont(QFont(MONOSPACE_FONT))
self.addTopLevelItem(item) self.addTopLevelItem(item)
self.setCurrentItem(self.topLevelItem(0)) self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent)
self.setVisible(len(inv_list)) self.setVisible(len(inv_list))
self.parent.invoices_label.setVisible(len(inv_list)) self.parent.invoices_label.setVisible(len(inv_list))
def import_invoices(self): def import_invoices(self):
import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update) import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.update)
def export_invoices(self): def export_invoices(self):
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)

10
electrum/gui/qt/main_window.py

@ -353,8 +353,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.config.is_dynfee(): if self.config.is_dynfee():
self.fee_slider.update() self.fee_slider.update()
self.do_update_fee() self.do_update_fee()
# todo: update only unconfirmed tx self.history_list.update_on_new_fee_histogram()
self.history_list.update()
else: else:
self.print_error("unexpected network_qt signal:", event, args) self.print_error("unexpected network_qt signal:", event, args)
@ -379,9 +378,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def load_wallet(self, wallet): def load_wallet(self, wallet):
wallet.thread = TaskThread(self, self.on_error) wallet.thread = TaskThread(self, self.on_error)
self.update_recently_visited(wallet.storage.path) self.update_recently_visited(wallet.storage.path)
# update(==init) all tabs; expensive for large wallets..
# so delay it somewhat, hence __init__ can finish and the window can appear sooner
QTimer.singleShot(50, self.update_tabs)
self.need_update.set() self.need_update.set()
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
# update menus # update menus
@ -1111,9 +1107,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.from_label = QLabel(_('From')) self.from_label = QLabel(_('From'))
grid.addWidget(self.from_label, 3, 0) grid.addWidget(self.from_label, 3, 0)
self.from_list = MyTreeWidget(self, self.from_list_menu, ['','']) self.from_list = FromList(self, self.from_list_menu)
self.from_list.setHeaderHidden(True)
self.from_list.setMaximumHeight(80)
grid.addWidget(self.from_list, 3, 1, 1, -1) grid.addWidget(self.from_list, 3, 1, 1, -1)
self.set_pay_from([]) self.set_pay_from([])

3
electrum/gui/qt/network_dialog.py

@ -100,7 +100,6 @@ class NodesListWidget(QTreeWidget):
def update(self, network: Network): def update(self, network: Network):
self.clear() self.clear()
self.addChild = self.addTopLevelItem
chains = network.get_blockchains() chains = network.get_blockchains()
n_chains = len(chains) n_chains = len(chains)
for chain_id, interfaces in chains.items(): for chain_id, interfaces in chains.items():
@ -118,7 +117,7 @@ class NodesListWidget(QTreeWidget):
item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) item = QTreeWidgetItem([i.host + star, '%d'%i.tip])
item.setData(0, Qt.UserRole, 0) item.setData(0, Qt.UserRole, 0)
item.setData(1, Qt.UserRole, i.server) item.setData(1, Qt.UserRole, i.server)
x.addChild(item) x.addTopLevelItem(item)
if n_chains > 1: if n_chains > 1:
self.addTopLevelItem(x) self.addTopLevelItem(x)
x.setExpanded(True) x.setExpanded(True)

68
electrum/gui/qt/request_list.py

@ -23,43 +23,39 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from PyQt5.QtGui import * from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import * from PyQt5.QtWidgets import QMenu
from PyQt5.QtWidgets import QTreeWidgetItem, QMenu from PyQt5.QtCore import Qt
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, age from electrum.util import format_time, age
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.paymentrequest import PR_UNKNOWN from electrum.paymentrequest import PR_UNKNOWN
from .util import MyTreeWidget, pr_tooltips, pr_icons from .util import MyTreeView, pr_tooltips, pr_icons
class RequestList(MyTreeView):
class RequestList(MyTreeWidget):
filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount
def __init__(self, parent): def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3) super().__init__(parent, self.create_menu, 3, editable_columns=[])
self.currentItemChanged.connect(self.item_changed) self.setModel(QStandardItemModel(self))
self.itemClicked.connect(self.item_changed)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setColumnWidth(0, 180) self.setColumnWidth(0, 180)
self.hideColumn(1) self.update()
self.selectionModel().currentRowChanged.connect(self.item_changed)
def item_changed(self, item): def item_changed(self, idx):
if item is None: # TODO use siblingAtColumn when min Qt version is >=5.11
return addr = self.model().itemFromIndex(idx.sibling(idx.row(), 1)).text()
if not item.isSelected():
return
addr = str(item.text(1))
req = self.wallet.receive_requests.get(addr) req = self.wallet.receive_requests.get(addr)
if req is None: if req is None:
self.update() self.update()
return return
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
amount = req['amount'] amount = req['amount']
message = self.wallet.labels.get(addr, '') message = req['memo']
self.parent.receive_address_e.setText(addr) self.parent.receive_address_e.setText(addr)
self.parent.receive_message_e.setText(message) self.parent.receive_message_e.setText(message)
self.parent.receive_amount_e.setAmount(amount) self.parent.receive_amount_e.setAmount(amount)
@ -68,7 +64,7 @@ class RequestList(MyTreeWidget):
self.parent.expires_label.setText(expires) self.parent.expires_label.setText(expires)
self.parent.new_request_button.setEnabled(True) self.parent.new_request_button.setEnabled(True)
def on_update(self): def update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
# hide receive tab if no receive requests available # hide receive tab if no receive requests available
b = len(self.wallet.receive_requests) > 0 b = len(self.wallet.receive_requests) > 0
@ -86,8 +82,9 @@ class RequestList(MyTreeWidget):
self.parent.set_receive_address(addr) self.parent.set_receive_address(addr)
self.parent.new_request_button.setEnabled(addr != current_address) self.parent.new_request_button.setEnabled(addr != current_address)
# clear the list and fill it again self.model().clear()
self.clear() self.update_headers([_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')])
self.hideColumn(1) # hide address column
for req in self.wallet.get_sorted_requests(self.config): for req in self.wallet.get_sorted_requests(self.config):
address = req['address'] address = req['address']
if address not in domain: if address not in domain:
@ -95,35 +92,40 @@ class RequestList(MyTreeWidget):
timestamp = req.get('time', 0) timestamp = req.get('time', 0)
amount = req.get('amount') amount = req.get('amount')
expiration = req.get('exp', None) expiration = req.get('exp', None)
message = req.get('memo', '') message = req['memo']
date = format_time(timestamp) date = format_time(timestamp)
status = req.get('status') status = req.get('status')
signature = req.get('sig') signature = req.get('sig')
requestor = req.get('name', '') requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else "" amount_str = self.parent.format_amount(amount) if amount else ""
item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')]) labels = [date, address, '', message, amount_str, pr_tooltips.get(status,'')]
items = [QStandardItem(e) for e in labels]
self.set_editability(items)
if signature is not None: if signature is not None:
item.setIcon(2, self.icon_cache.get(":icons/seal.png")) items[2].setIcon(self.icon_cache.get(":icons/seal.png"))
item.setToolTip(2, 'signed by '+ requestor) items[2].setToolTip('signed by '+ requestor)
if status is not PR_UNKNOWN: if status is not PR_UNKNOWN:
item.setIcon(6, self.icon_cache.get(pr_icons.get(status))) items[5].setIcon(self.icon_cache.get(pr_icons.get(status)))
self.addTopLevelItem(item) items[3].setData(address, Qt.UserRole)
self.model().insertRow(self.model().rowCount(), items)
def create_menu(self, position): def create_menu(self, position):
item = self.itemAt(position) idx = self.indexAt(position)
# TODO use siblingAtColumn when min Qt version is >=5.11
item = self.model().itemFromIndex(idx.sibling(idx.row(), 1))
if not item: if not item:
return return
addr = str(item.text(1)) addr = item.text()
req = self.wallet.receive_requests.get(addr) req = self.wallet.receive_requests.get(addr)
if req is None: if req is None:
self.update() self.update()
return return
column = self.currentColumn() column = idx.column()
column_title = self.headerItem().text(column) column_title = self.model().horizontalHeaderItem(column).text()
column_data = item.text(column) column_data = item.text()
menu = QMenu(self) menu = QMenu(self)
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) if column != 2:
menu.addAction(_("Copy {}").format(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(_("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(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))

232
electrum/gui/qt/util.py

@ -5,6 +5,7 @@ import platform
import queue import queue
from functools import partial from functools import partial
from typing import NamedTuple, Callable, Optional from typing import NamedTuple, Callable, Optional
from abc import abstractmethod
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtCore import * from PyQt5.QtCore import *
@ -398,20 +399,16 @@ class ElectrumItemDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
return self.parent().createEditor(parent, option, index) return self.parent().createEditor(parent, option, index)
class MyTreeWidget(QTreeWidget): class MyTreeView(QTreeView):
def __init__(self, parent, create_menu, headers, stretch_column=None, def __init__(self, parent, create_menu, stretch_column=None, editable_columns=None):
editable_columns=None): super().__init__(parent)
QTreeWidget.__init__(self, parent)
self.parent = parent self.parent = parent
self.config = self.parent.config 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)
self.setUniformRowHeights(True) self.setUniformRowHeights(True)
# extend the syntax for consistency
self.addChild = self.addTopLevelItem
self.insertChild = self.insertTopLevelItem
self.icon_cache = IconCache() self.icon_cache = IconCache()
@ -424,127 +421,143 @@ class MyTreeWidget(QTreeWidget):
editable_columns = set(editable_columns) editable_columns = set(editable_columns)
self.editable_columns = editable_columns self.editable_columns = editable_columns
self.setItemDelegate(ElectrumItemDelegate(self)) self.setItemDelegate(ElectrumItemDelegate(self))
self.itemDoubleClicked.connect(self.on_doubleclick)
self.update_headers(headers)
self.current_filter = "" self.current_filter = ""
self.setRootIsDecorated(False) # remove left margin self.setRootIsDecorated(False) # remove left margin
self.toolbar_shown = False self.toolbar_shown = False
def update_headers(self, headers): def set_editability(self, items):
self.setColumnCount(len(headers)) for idx, i in enumerate(items):
self.setHeaderLabels(headers) i.setEditable(idx in self.editable_columns)
def selected_in_column(self, column: int):
items = self.selectionModel().selectedIndexes()
return list(x for x in items if x.column() == column)
def current_item_user_role(self, col) -> Optional[QStandardItem]:
idx = self.selectionModel().currentIndex()
idx = idx.sibling(idx.row(), col)
item = self.model().itemFromIndex(idx)
if item:
return item.data(Qt.UserRole)
def set_current_idx(self, set_current: QPersistentModelIndex):
if set_current:
assert isinstance(set_current, QPersistentModelIndex)
assert set_current.isValid()
self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent)
def update_headers(self, headers, model=None):
if model is None:
model = self.model()
model.setHorizontalHeaderLabels(headers)
self.header().setStretchLastSection(False) self.header().setStretchLastSection(False)
for col in range(len(headers)): for col in range(len(headers)):
sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
self.header().setSectionResizeMode(col, sm) self.header().setSectionResizeMode(col, sm)
def editItem(self, item, column):
if column in self.editable_columns:
try:
self.editing_itemcol = (item, column, item.text(column))
# Calling setFlags causes on_changed events for some reason
item.setFlags(item.flags() | Qt.ItemIsEditable)
QTreeWidget.editItem(self, item, column)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
except RuntimeError:
# (item) wrapped C/C++ object has been deleted
pass
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: 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.selectionModel().currentIndex())
else: return
QTreeWidget.keyPressEvent(self, event) super().keyPressEvent(event)
def permit_edit(self, item, column):
return (column in self.editable_columns
and self.on_permit_edit(item, column))
def on_permit_edit(self, item, column):
return True
def on_doubleclick(self, item, column):
if self.permit_edit(item, column):
self.editItem(item, column)
def on_activated(self, item, column): def on_activated(self, idx):
# on 'enter' we show the menu # on 'enter' we show the menu
pt = self.visualItemRect(item).bottomLeft() pt = self.visualRect(idx).bottomLeft()
pt.setX(50) pt.setX(50)
self.customContextMenuRequested.emit(pt) self.customContextMenuRequested.emit(pt)
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),
parent, option, index) parent, option, index)
self.editor.editingFinished.connect(self.editing_finished) persistent = QPersistentModelIndex(index)
user_role = index.data(Qt.UserRole)
assert user_role is not None
idx = QModelIndex(persistent)
index = self.proxy.mapToSource(idx)
item = self.std_model.itemFromIndex(index)
prior_text = item.text()
def editing_finished():
# Long-time QT bug - pressing Enter to finish editing signals
# editingFinished twice. If the item changed the sequence is
# Enter key: editingFinished, on_change, editingFinished
# Mouse: on_change, editingFinished
# This mess is the cleanest way to ensure we make the
# on_edited callback with the updated item
if self.editor is None:
return
if self.editor.text() == prior_text:
self.editor = None # Unchanged - ignore any 2nd call
return
if item.text() == prior_text:
return # Buggy first call on Enter key, item not yet updated
if not idx.isValid():
return
self.on_edited(idx, user_role, self.editor.text())
self.editor = None
self.editor.editingFinished.connect(editing_finished)
return self.editor return self.editor
def editing_finished(self): def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None):
# Long-time QT bug - pressing Enter to finish editing signals """
# editingFinished twice. If the item changed the sequence is this is to prevent:
# Enter key: editingFinished, on_change, editingFinished edit: editing failed
# Mouse: on_change, editingFinished from inside qt
# This mess is the cleanest way to ensure we make the """
# on_edited callback with the updated item return super().edit(idx, trigger, event)
if self.editor:
(item, column, prior_text) = self.editing_itemcol def on_edited(self, idx: QModelIndex, user_role, text):
if self.editor.text() == prior_text: self.parent.wallet.set_label(user_role, text)
self.editor = None # Unchanged - ignore any 2nd call
elif item.text(column) == prior_text:
pass # Buggy first call on Enter key, item not yet updated
else:
# What we want - the updated item
self.on_edited(*self.editing_itemcol)
self.editor = None
# Now do any pending updates
if self.editor is None and self.pending_update:
self.pending_update = False
self.on_update()
def on_edited(self, item, column, prior):
'''Called only when the text actually changes'''
key = item.data(0, Qt.UserRole)
text = item.text(column)
self.parent.wallet.set_label(key, text)
self.parent.history_list.update_labels() self.parent.history_list.update_labels()
self.parent.update_completions() self.parent.update_completions()
def update(self): def apply_filter(self):
# Defer updates if editing
if self.editor:
self.pending_update = True
else:
self.setUpdatesEnabled(False)
scroll_pos = self.verticalScrollBar().value()
self.on_update()
self.setUpdatesEnabled(True)
# To paint the list before resetting the scroll position
self.parent.app.processEvents()
self.verticalScrollBar().setValue(scroll_pos)
if self.current_filter: if self.current_filter:
self.filter(self.current_filter) self.filter(self.current_filter)
def on_update(self): @abstractmethod
def should_hide(self, row):
"""
row_num is for self.model(). So if there is a proxy, it is the row number
in that!
"""
pass pass
def get_leaves(self, root): def hide_row(self, row_num):
child_count = root.childCount() """
if child_count == 0: row_num is for self.model(). So if there is a proxy, it is the row number
yield root in that!
for i in range(child_count): """
item = root.child(i) should_hide = self.should_hide(row_num)
for x in self.get_leaves(item): if not self.current_filter and should_hide is None:
yield x # no filters at all, neither date nor search
self.setRowHidden(row_num, QModelIndex(), False)
return
for column in self.filter_columns:
if isinstance(self.model(), QSortFilterProxyModel):
idx = self.model().mapToSource(self.model().index(row_num, column))
item = self.model().sourceModel().itemFromIndex(idx)
else:
idx = self.model().index(row_num, column)
item = self.model().itemFromIndex(idx)
txt = item.text().lower()
if self.current_filter in txt:
# the filter matched, but the date filter might apply
self.setRowHidden(row_num, QModelIndex(), bool(should_hide))
break
else:
# we did not find the filter in any columns, show the item
self.setRowHidden(row_num, QModelIndex(), True)
def filter(self, p): def filter(self, p):
columns = self.__class__.filter_columns
p = p.lower() p = p.lower()
self.current_filter = p self.current_filter = p
for item in self.get_leaves(self.invisibleRootItem()): self.hide_rows()
item.setHidden(all([item.text(column).lower().find(p) == -1
for column in columns])) def hide_rows(self):
for row in range(self.model().rowCount()):
self.hide_row(row)
def create_toolbar(self, config=None): def create_toolbar(self, config=None):
hbox = QHBoxLayout() hbox = QHBoxLayout()
@ -790,22 +803,6 @@ def get_parent_main_window(widget):
return widget return widget
return None return None
class SortableTreeWidgetItem(QTreeWidgetItem):
DataRole = Qt.UserRole + 100
def __lt__(self, other):
column = self.treeWidget().sortColumn()
if None not in [x.data(column, self.DataRole) for x in [self, other]]:
# We have set custom data to sort by
return self.data(column, self.DataRole) < other.data(column, self.DataRole)
try:
# Is the value something numeric?
return float(self.text(column)) < float(other.text(column))
except ValueError:
# If not, we will just do string comparison
return self.text(column) < other.text(column)
class IconCache: class IconCache:
def __init__(self): def __init__(self):
@ -821,6 +818,21 @@ def get_default_language():
name = QLocale.system().name() name = QLocale.system().name()
return name if name in languages else 'en_UK' return name if name in languages else 'en_UK'
class FromList(QTreeWidget):
def __init__(self, parent, create_menu):
super().__init__(parent)
self.setHeaderHidden(True)
self.setMaximumHeight(300)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(create_menu)
self.setUniformRowHeights(True)
# remove left margin
self.setRootIsDecorated(False)
self.setColumnCount(2)
self.header().setStretchLastSection(False)
sm = QHeaderView.ResizeToContents
self.header().setSectionResizeMode(0, sm)
self.header().setSectionResizeMode(1, sm)
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication([]) app = QApplication([])

61
electrum/gui/qt/utxo_list.py

@ -23,49 +23,60 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from typing import Optional, List
from electrum.i18n import _ from electrum.i18n import _
from .util import * from .util import *
class UTXOList(MyTreeView):
class UTXOList(MyTreeWidget): filter_columns = [0, 1] # Address, Label
filter_columns = [0, 2] # Address, Label
def __init__(self, parent=None): def __init__(self, parent=None):
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1) super().__init__(parent, self.create_menu, 1)
self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.update()
def get_name(self, x): def update(self):
return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
def on_update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
item = self.currentItem() utxos = self.wallet.get_utxos()
self.clear() self.utxo_dict = {}
self.utxos = self.wallet.get_utxos() self.model().clear()
for x in self.utxos: self.update_headers([ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')])
for idx, x in enumerate(utxos):
address = x.get('address') address = x.get('address')
height = x.get('height') height = x.get('height')
name = self.get_name(x) name = x.get('prevout_hash') + ":%d"%x.get('prevout_n')
self.utxo_dict[name] = x
label = self.wallet.get_label(x.get('prevout_hash')) label = self.wallet.get_label(x.get('prevout_hash'))
amount = self.parent.format_amount(x['value'], whitespaces=True) amount = self.parent.format_amount(x['value'], whitespaces=True)
utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) labels = [address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]
utxo_item.setFont(0, QFont(MONOSPACE_FONT)) utxo_item = [QStandardItem(x) for x in labels]
utxo_item.setFont(2, QFont(MONOSPACE_FONT)) self.set_editability(utxo_item)
utxo_item.setFont(4, QFont(MONOSPACE_FONT)) utxo_item[0].setFont(QFont(MONOSPACE_FONT))
utxo_item.setData(0, Qt.UserRole, name) utxo_item[2].setFont(QFont(MONOSPACE_FONT))
utxo_item[4].setFont(QFont(MONOSPACE_FONT))
utxo_item[0].setData(name, Qt.UserRole)
if self.wallet.is_frozen(address): if self.wallet.is_frozen(address):
utxo_item.setBackground(0, ColorScheme.BLUE.as_color(True)) utxo_item[0].setBackground(ColorScheme.BLUE.as_color(True))
self.addChild(utxo_item) self.model().insertRow(idx, utxo_item)
def selected_column_0_user_roles(self) -> Optional[List[str]]:
if not self.model():
return None
items = self.selected_in_column(0)
if not items:
return None
return [x.data(Qt.UserRole) for x in items]
def create_menu(self, position): def create_menu(self, position):
selected = [x.data(0, Qt.UserRole) for x in self.selectedItems()] selected = self.selected_column_0_user_roles()
if not selected: if not selected:
return return
menu = QMenu() menu = QMenu()
coins = filter(lambda x: self.get_name(x) in selected, self.utxos) coins = (self.utxo_dict[name] for name in selected)
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins)) menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
if len(selected) == 1: if len(selected) == 1:
txid = selected[0].split(':')[0] txid = selected[0].split(':')[0]
@ -75,7 +86,3 @@ class UTXOList(MyTreeWidget):
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def on_permit_edit(self, item, column):
# disable editing fields in this tab (labels)
return False

Loading…
Cancel
Save