Browse Source

qt MyTreeView: impl custom sort order framework, and use for invoices

sort invoices and payreqs (for Date column) based on timestamps
(timestamps have second resolution while the displayed date has minute resolution)
master
SomberNight 5 years ago
parent
commit
93c90a30f0
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 2
      electrum/gui/qt/history_list.py
  2. 26
      electrum/gui/qt/invoice_list.py
  3. 30
      electrum/gui/qt/request_list.py
  4. 64
      electrum/gui/qt/util.py

2
electrum/gui/qt/history_list.py

@ -755,7 +755,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
from electrum.util import json_encode from electrum.util import json_encode
f.write(json_encode(txns)) f.write(json_encode(txns))
def text_txid_from_coordinate(self, row, col): def get_text_and_userrole_from_coordinate(self, row, col):
idx = self.model().mapToSource(self.model().index(row, col)) idx = self.model().mapToSource(self.model().index(row, col))
tx_item = self.hm.transactions.value_from_pos(idx.row()) tx_item = self.hm.transactions.value_from_pos(idx.row())
return self.hm.data(idx, Qt.DisplayRole).value(), get_item_key(tx_item) return self.hm.data(idx, Qt.DisplayRole).value(), get_item_key(tx_item)

26
electrum/gui/qt/invoice_list.py

@ -37,7 +37,7 @@ from electrum.util import get_request_status
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import PaymentAttemptLog from electrum.lnutil import PaymentAttemptLog
from .util import (MyTreeView, read_QIcon, from .util import (MyTreeView, read_QIcon, MySortModel,
import_meta_gui, export_meta_gui, pr_icons) import_meta_gui, export_meta_gui, pr_icons)
from .util import CloseButton, Buttons from .util import CloseButton, Buttons
from .util import WindowModalDialog from .util import WindowModalDialog
@ -46,6 +46,7 @@ from .util import WindowModalDialog
ROLE_REQUEST_TYPE = Qt.UserRole ROLE_REQUEST_TYPE = Qt.UserRole
ROLE_REQUEST_ID = Qt.UserRole + 1 ROLE_REQUEST_ID = Qt.UserRole + 1
ROLE_SORT_ORDER = Qt.UserRole + 2
class InvoiceList(MyTreeView): class InvoiceList(MyTreeView):
@ -68,8 +69,11 @@ class InvoiceList(MyTreeView):
super().__init__(parent, self.create_menu, super().__init__(parent, self.create_menu,
stretch_column=self.Columns.DESCRIPTION, stretch_column=self.Columns.DESCRIPTION,
editable_columns=[]) editable_columns=[])
self.std_model = QStandardItemModel(self)
self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER)
self.proxy.setSourceModel(self.std_model)
self.setModel(self.proxy)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.update() self.update()
@ -92,7 +96,8 @@ class InvoiceList(MyTreeView):
def update(self): def update(self):
# not calling maybe_defer_update() as it interferes with conditional-visibility # not calling maybe_defer_update() as it interferes with conditional-visibility
self.model().clear() self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for idx, item in enumerate(self.parent.wallet.get_invoices()): for idx, item in enumerate(self.parent.wallet.get_invoices()):
invoice_type = item['type'] invoice_type = item['type']
@ -119,17 +124,17 @@ class InvoiceList(MyTreeView):
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID) items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID)
items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE)
self.model().insertRow(idx, items) items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)
self.std_model.insertRow(idx, items)
self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent) self.filter()
self.proxy.setDynamicSortFilter(True)
# sort requests by date # sort requests by date
self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder) self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder)
# hide list if empty # hide list if empty
if self.parent.isVisible(): if self.parent.isVisible():
b = self.model().rowCount() > 0 b = self.std_model.rowCount() > 0
self.setVisible(b) self.setVisible(b)
self.parent.invoices_label.setVisible(b) self.parent.invoices_label.setVisible(b)
self.filter()
def import_invoices(self): def import_invoices(self):
import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.update) import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.update)
@ -150,12 +155,11 @@ class InvoiceList(MyTreeView):
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
return return
idx = self.indexAt(position) idx = self.indexAt(position)
item = self.model().itemFromIndex(idx) item = self.item_from_index(idx)
item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) item_col0 = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
if not item or not item_col0: if not item or not item_col0:
return return
key = item_col0.data(ROLE_REQUEST_ID) key = item_col0.data(ROLE_REQUEST_ID)
request_type = item_col0.data(ROLE_REQUEST_TYPE)
menu = QMenu(self) menu = QMenu(self)
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
invoice = self.parent.wallet.get_invoice(key) invoice = self.parent.wallet.get_invoice(key)

30
electrum/gui/qt/request_list.py

@ -27,21 +27,21 @@ from enum import IntEnum
from typing import Optional from typing import Optional
from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QMenu from PyQt5.QtWidgets import QMenu, QAbstractItemView
from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
from PyQt5.QtWidgets import QAbstractItemView
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, get_request_status from electrum.util import format_time, get_request_status
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.util import PR_PAID
from electrum.plugin import run_hook from electrum.plugin import run_hook
from .util import MyTreeView, pr_icons, read_QIcon, webopen from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
ROLE_REQUEST_TYPE = Qt.UserRole ROLE_REQUEST_TYPE = Qt.UserRole
ROLE_KEY = Qt.UserRole + 1 ROLE_KEY = Qt.UserRole + 1
ROLE_SORT_ORDER = Qt.UserRole + 2
class RequestList(MyTreeView): class RequestList(MyTreeView):
@ -64,7 +64,10 @@ class RequestList(MyTreeView):
stretch_column=self.Columns.DESCRIPTION, stretch_column=self.Columns.DESCRIPTION,
editable_columns=[]) editable_columns=[])
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
self.setModel(QStandardItemModel(self)) self.std_model = QStandardItemModel(self)
self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER)
self.proxy.setSourceModel(self.std_model)
self.setModel(self.proxy)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.selectionModel().currentRowChanged.connect(self.item_changed) self.selectionModel().currentRowChanged.connect(self.item_changed)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
@ -86,7 +89,7 @@ class RequestList(MyTreeView):
if not idx.isValid(): if not idx.isValid():
return return
# TODO use siblingAtColumn when min Qt version is >=5.11 # TODO use siblingAtColumn when min Qt version is >=5.11
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
request_type = item.data(ROLE_REQUEST_TYPE) request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_KEY) key = item.data(ROLE_KEY)
req = self.wallet.get_request(key) req = self.wallet.get_request(key)
@ -107,14 +110,13 @@ class RequestList(MyTreeView):
self.selectionModel().clearCurrentIndex() self.selectionModel().clearCurrentIndex()
def refresh_status(self): def refresh_status(self):
m = self.model() m = self.std_model
for r in range(m.rowCount()): for r in range(m.rowCount()):
idx = m.index(r, self.Columns.STATUS) idx = m.index(r, self.Columns.STATUS)
date_idx = idx.sibling(idx.row(), self.Columns.DATE) date_idx = idx.sibling(idx.row(), self.Columns.DATE)
date_item = m.itemFromIndex(date_idx) date_item = m.itemFromIndex(date_idx)
status_item = m.itemFromIndex(idx) status_item = m.itemFromIndex(idx)
key = date_item.data(ROLE_KEY) key = date_item.data(ROLE_KEY)
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == PR_TYPE_LN
req = self.wallet.get_request(key) req = self.wallet.get_request(key)
if req: if req:
status, status_str = get_request_status(req) status, status_str = get_request_status(req)
@ -124,7 +126,8 @@ class RequestList(MyTreeView):
def update(self): def update(self):
# not calling maybe_defer_update() as it interferes with conditional-visibility # not calling maybe_defer_update() as it interferes with conditional-visibility
self.parent.update_receive_address_styling() self.parent.update_receive_address_styling()
self.model().clear() self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests(): for req in self.wallet.get_sorted_requests():
status, status_str = get_request_status(req) status, status_str = get_request_status(req)
@ -147,16 +150,18 @@ class RequestList(MyTreeView):
self.set_editability(items) self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
items[self.Columns.DATE].setData(key, ROLE_KEY) items[self.Columns.DATE].setData(key, ROLE_KEY)
items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER)
items[self.Columns.DATE].setIcon(icon) items[self.Columns.DATE].setIcon(icon)
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
items[self.Columns.DATE].setToolTip(tooltip) items[self.Columns.DATE].setToolTip(tooltip)
self.model().insertRow(self.model().rowCount(), items) self.std_model.insertRow(self.std_model.rowCount(), items)
self.filter() self.filter()
self.proxy.setDynamicSortFilter(True)
# sort requests by date # sort requests by date
self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder) self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder)
# hide list if empty # hide list if empty
if self.parent.isVisible(): if self.parent.isVisible():
b = self.model().rowCount() > 0 b = self.std_model.rowCount() > 0
self.setVisible(b) self.setVisible(b)
self.parent.receive_requests_label.setVisible(b) self.parent.receive_requests_label.setVisible(b)
if not b: if not b:
@ -172,9 +177,8 @@ class RequestList(MyTreeView):
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
return return
idx = self.indexAt(position) idx = self.indexAt(position)
item = self.model().itemFromIndex(idx)
# TODO use siblingAtColumn when min Qt version is >=5.11 # TODO use siblingAtColumn when min Qt version is >=5.11
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
if not item: if not item:
return return
key = item.data(ROLE_KEY) key = item.data(ROLE_KEY)

64
electrum/gui/qt/util.py

@ -7,15 +7,16 @@ import queue
import traceback import traceback
import os import os
import webbrowser import webbrowser
from decimal import Decimal
from functools import partial, lru_cache from functools import partial, lru_cache
from typing import NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any,
Sequence, Iterable)
from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem,
QPalette, QIcon, QFontMetrics, QShowEvent) QPalette, QIcon, QFontMetrics, QShowEvent)
from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal, from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
QCoreApplication, QItemSelectionModel, QThread, QCoreApplication, QItemSelectionModel, QThread,
QSortFilterProxyModel, QSize, QLocale) QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel)
from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
QAbstractItemView, QVBoxLayout, QLineEdit, QAbstractItemView, QVBoxLayout, QLineEdit,
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton, QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
@ -466,6 +467,7 @@ def filename_field(parent, config, defaultname, select_msg):
return vbox, filename_e, b1 return vbox, filename_e, b1
class ElectrumItemDelegate(QStyledItemDelegate): class ElectrumItemDelegate(QStyledItemDelegate):
def __init__(self, tv: 'MyTreeView'): def __init__(self, tv: 'MyTreeView'):
super().__init__(tv) super().__init__(tv)
@ -477,7 +479,7 @@ class ElectrumItemDelegate(QStyledItemDelegate):
new_text = editor.text() new_text = editor.text()
idx = QModelIndex(self.opened) idx = QModelIndex(self.opened)
row, col = idx.row(), idx.column() row, col = idx.row(), idx.column()
_prior_text, user_role = self.tv.text_txid_from_coordinate(row, col) _prior_text, user_role = self.tv.get_text_and_userrole_from_coordinate(row, col)
# check that we didn't forget to set UserRole on an editable field # check that we didn't forget to set UserRole on an editable field
assert user_role is not None, (row, col) assert user_role is not None, (row, col)
self.tv.on_edited(idx, user_role, new_text) self.tv.on_edited(idx, user_role, new_text)
@ -488,9 +490,12 @@ class ElectrumItemDelegate(QStyledItemDelegate):
self.opened = QPersistentModelIndex(idx) self.opened = QPersistentModelIndex(idx)
return super().createEditor(parent, option, idx) return super().createEditor(parent, option, idx)
class MyTreeView(QTreeView): class MyTreeView(QTreeView):
ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 ROLE_CLIPBOARD_DATA = Qt.UserRole + 100
filter_columns: Iterable[int]
def __init__(self, parent: 'ElectrumWindow', create_menu, *, def __init__(self, parent: 'ElectrumWindow', create_menu, *,
stretch_column=None, editable_columns=None): stretch_column=None, editable_columns=None):
super().__init__(parent) super().__init__(parent)
@ -536,10 +541,25 @@ class MyTreeView(QTreeView):
def current_item_user_role(self, col) -> Any: def current_item_user_role(self, col) -> Any:
idx = self.selectionModel().currentIndex() idx = self.selectionModel().currentIndex()
idx = idx.sibling(idx.row(), col) idx = idx.sibling(idx.row(), col)
item = self.model().itemFromIndex(idx) item = self.item_from_index(idx)
if item: if item:
return item.data(Qt.UserRole) return item.data(Qt.UserRole)
def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]:
model = self.model()
if isinstance(model, QSortFilterProxyModel):
idx = model.mapToSource(idx)
return model.sourceModel().itemFromIndex(idx)
else:
return model.itemFromIndex(idx)
def original_model(self) -> QAbstractItemModel:
model = self.model()
if isinstance(model, QSortFilterProxyModel):
return model.sourceModel()
else:
return model
def set_current_idx(self, set_current: QPersistentModelIndex): def set_current_idx(self, set_current: QPersistentModelIndex):
if set_current: if set_current:
assert isinstance(set_current, QPersistentModelIndex) assert isinstance(set_current, QPersistentModelIndex)
@ -551,8 +571,7 @@ class MyTreeView(QTreeView):
if not isinstance(headers, dict): # convert to dict if not isinstance(headers, dict): # convert to dict
headers = dict(enumerate(headers)) headers = dict(enumerate(headers))
col_names = [headers[col_idx] for col_idx in sorted(headers.keys())] col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]
model = self.model() self.original_model().setHorizontalHeaderLabels(col_names)
model.setHorizontalHeaderLabels(col_names)
self.header().setStretchLastSection(False) self.header().setStretchLastSection(False)
for col_idx in headers: for col_idx in headers:
sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents
@ -592,10 +611,9 @@ class MyTreeView(QTreeView):
""" """
return False return False
def text_txid_from_coordinate(self, row_num, column): def get_text_and_userrole_from_coordinate(self, row_num, column):
assert not isinstance(self.model(), QSortFilterProxyModel)
idx = self.model().index(row_num, column) idx = self.model().index(row_num, column)
item = self.model().itemFromIndex(idx) item = self.item_from_index(idx)
user_role = item.data(Qt.UserRole) user_role = item.data(Qt.UserRole)
return item.text(), user_role return item.text(), user_role
@ -610,7 +628,7 @@ class MyTreeView(QTreeView):
self.setRowHidden(row_num, QModelIndex(), False) self.setRowHidden(row_num, QModelIndex(), False)
return return
for column in self.filter_columns: for column in self.filter_columns:
txt, _ = self.text_txid_from_coordinate(row_num, column) txt, _ = self.get_text_and_userrole_from_coordinate(row_num, column)
txt = txt.lower() txt = txt.lower()
if self.current_filter in txt: if self.current_filter in txt:
# the filter matched, but the date filter might apply # the filter matched, but the date filter might apply
@ -664,8 +682,8 @@ class MyTreeView(QTreeView):
def add_copy_menu(self, menu: QMenu, idx) -> QMenu: def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
cc = menu.addMenu(_("Copy")) cc = menu.addMenu(_("Copy"))
for column in self.Columns: for column in self.Columns:
column_title = self.model().horizontalHeaderItem(column).text() column_title = self.original_model().horizontalHeaderItem(column).text()
item_col = self.model().itemFromIndex(idx.sibling(idx.row(), column)) item_col = self.item_from_index(idx.sibling(idx.row(), column))
clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA) clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA)
if clipboard_data is None: if clipboard_data is None:
clipboard_data = item_col.text().strip() clipboard_data = item_col.text().strip()
@ -692,6 +710,26 @@ class MyTreeView(QTreeView):
return defer return defer
class MySortModel(QSortFilterProxyModel):
def __init__(self, parent, *, sort_role):
super().__init__(parent)
self._sort_role = sort_role
def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
item1 = self.sourceModel().itemFromIndex(source_left)
item2 = self.sourceModel().itemFromIndex(source_right)
data1 = item1.data(self._sort_role)
data2 = item2.data(self._sort_role)
if data1 is not None and data2 is not None:
return data1 < data2
v1 = item1.text()
v2 = item2.text()
try:
return Decimal(v1) < Decimal(v2)
except:
return v1 < v2
class ButtonsWidget(QWidget): class ButtonsWidget(QWidget):
def __init__(self): def __init__(self):

Loading…
Cancel
Save