qt: use QStandardItemModel
This commit is contained in:
parent
9350709f13
commit
5473320ce4
@ -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):
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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):
|
||||||
|
def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
|
||||||
|
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(MyTreeWidget, AcceptFileDragDrop):
|
class HistoryList(MyTreeView, AcceptFileDragDrop):
|
||||||
filter_columns = [2, 3, 4] # Date, Description, Amount
|
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)
|
|
||||||
if value_sat and value_sat < 0:
|
|
||||||
item.setForeground(3, red_brush)
|
|
||||||
item.setForeground(4, red_brush)
|
|
||||||
if fiat_value is not None and not tx_item['fiat_default']:
|
|
||||||
item.setForeground(6, blue_brush)
|
|
||||||
# sort orders
|
|
||||||
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
|
||||||
item.setData(4, SortableTreeWidgetItem.DataRole, value_sat)
|
|
||||||
item.setData(5, SortableTreeWidgetItem.DataRole, balance)
|
|
||||||
if fiat_value is not None:
|
|
||||||
item.setData(6, SortableTreeWidgetItem.DataRole, fiat_value)
|
|
||||||
if value_sat < 0:
|
|
||||||
item.setData(7, SortableTreeWidgetItem.DataRole, tx_item['acquisition_price'].value)
|
|
||||||
item.setData(8, SortableTreeWidgetItem.DataRole, tx_item['capital_gain'].value)
|
|
||||||
if tx_hash:
|
|
||||||
item.setData(0, self.TX_HASH_ROLE, tx_hash)
|
|
||||||
item.setData(0, self.TX_VALUE_ROLE, value_sat)
|
|
||||||
self.insertTopLevelItem(0, item)
|
|
||||||
if current_tx == tx_hash:
|
|
||||||
self.setCurrentItem(item)
|
|
||||||
|
|
||||||
def on_edited(self, item, column, prior):
|
def update_fiat(self, txid, row):
|
||||||
'''Called only when the text actually changes'''
|
cap_gains = self.parent.fx.get_history_capital_gains_config()
|
||||||
key = item.data(0, self.TX_HASH_ROLE)
|
items = self.txid_to_items[txid]
|
||||||
value_sat = item.data(0, self.TX_VALUE_ROLE)
|
self.ensure_fields_available(items, 7 if cap_gains else 5, txid)
|
||||||
text = item.text(column)
|
items[5].setForeground(self.blue_brush if not row['fiat_default'] and row['fiat_value'] else self.default_color)
|
||||||
|
value_str = self.parent.fx.format_fiat(row['fiat_value'].value)
|
||||||
|
items[5].setText(value_str)
|
||||||
|
items[5].setData(row['fiat_value'].value, self.SORT_ROLE)
|
||||||
|
# fixme: should use is_mine
|
||||||
|
if row['value'].value < 0 and cap_gains:
|
||||||
|
acq = row['acquisition_price'].value
|
||||||
|
items[6].setText(self.parent.fx.format_fiat(acq))
|
||||||
|
items[6].setData(acq, self.SORT_ROLE)
|
||||||
|
cg = row['capital_gain'].value
|
||||||
|
items[7].setText(self.parent.fx.format_fiat(cg))
|
||||||
|
items[7].setData(cg, self.SORT_ROLE)
|
||||||
|
|
||||||
|
def update_on_new_fee_histogram(self):
|
||||||
|
pass
|
||||||
|
# TODO update unconfirmed tx'es
|
||||||
|
|
||||||
|
def on_edited(self, index, user_role, text):
|
||||||
|
column = index.column()
|
||||||
|
index = self.proxy.mapToSource(index)
|
||||||
|
item = self.std_model.itemFromIndex(index)
|
||||||
|
key = item.data(self.TX_HASH_ROLE)
|
||||||
# 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:
|
return
|
||||||
item = items[0]
|
items = self.txid_to_items[tx_hash]
|
||||||
item.setIcon(0, icon)
|
items[0].setIcon(icon)
|
||||||
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
items[0].setToolTip(str(conf) + _(" confirmation" + ("s" if conf != 1 else "")))
|
||||||
item.setText(2, status_str)
|
items[0].setData((status, conf), self.SORT_ROLE)
|
||||||
|
items[1].setText(status_str)
|
||||||
|
|
||||||
def create_menu(self, position):
|
def create_menu(self, position: QPoint):
|
||||||
self.selectedIndexes()
|
org_idx: QModelIndex = self.indexAt(position)
|
||||||
item = self.currentItem()
|
idx = self.proxy.mapToSource(org_idx)
|
||||||
if not item:
|
item: QStandardItem = self.std_model.itemFromIndex(idx)
|
||||||
return
|
assert item, 'create_menu: index not found in model'
|
||||||
column = self.currentColumn()
|
tx_hash = idx.data(self.TX_HASH_ROLE)
|
||||||
tx_hash = item.data(0, self.TX_HASH_ROLE)
|
column = idx.column()
|
||||||
if not tx_hash:
|
assert tx_hash, "create_menu: no tx hash"
|
||||||
return
|
|
||||||
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:
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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([])
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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):
|
def on_activated(self, idx):
|
||||||
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):
|
|
||||||
# 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
|
|
||||||
if self.editor.text() == prior_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
|
def on_edited(self, idx: QModelIndex, user_role, text):
|
||||||
if self.editor is None and self.pending_update:
|
self.parent.wallet.set_label(user_role, text)
|
||||||
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([])
|
||||||
|
|||||||
@ -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…
Reference in New Issue
Block a user