mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 17:31:36 +00:00
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)
This commit is contained in:
parent
7d0703fc4a
commit
93c90a30f0
4 changed files with 84 additions and 38 deletions
|
@ -755,7 +755,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
|
|||
from electrum.util import json_encode
|
||||
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))
|
||||
tx_item = self.hm.transactions.value_from_pos(idx.row())
|
||||
return self.hm.data(idx, Qt.DisplayRole).value(), get_item_key(tx_item)
|
||||
|
|
|
@ -37,7 +37,7 @@ from electrum.util import get_request_status
|
|||
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
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)
|
||||
from .util import CloseButton, Buttons
|
||||
from .util import WindowModalDialog
|
||||
|
@ -46,6 +46,7 @@ from .util import WindowModalDialog
|
|||
|
||||
ROLE_REQUEST_TYPE = Qt.UserRole
|
||||
ROLE_REQUEST_ID = Qt.UserRole + 1
|
||||
ROLE_SORT_ORDER = Qt.UserRole + 2
|
||||
|
||||
|
||||
class InvoiceList(MyTreeView):
|
||||
|
@ -68,8 +69,11 @@ class InvoiceList(MyTreeView):
|
|||
super().__init__(parent, self.create_menu,
|
||||
stretch_column=self.Columns.DESCRIPTION,
|
||||
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.setModel(QStandardItemModel(self))
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.update()
|
||||
|
||||
|
@ -92,7 +96,8 @@ class InvoiceList(MyTreeView):
|
|||
|
||||
def update(self):
|
||||
# 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)
|
||||
for idx, item in enumerate(self.parent.wallet.get_invoices()):
|
||||
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.DATE].setData(key, role=ROLE_REQUEST_ID)
|
||||
items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE)
|
||||
self.model().insertRow(idx, items)
|
||||
|
||||
self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent)
|
||||
items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)
|
||||
self.std_model.insertRow(idx, items)
|
||||
self.filter()
|
||||
self.proxy.setDynamicSortFilter(True)
|
||||
# sort requests by date
|
||||
self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder)
|
||||
# hide list if empty
|
||||
if self.parent.isVisible():
|
||||
b = self.model().rowCount() > 0
|
||||
b = self.std_model.rowCount() > 0
|
||||
self.setVisible(b)
|
||||
self.parent.invoices_label.setVisible(b)
|
||||
self.filter()
|
||||
|
||||
def import_invoices(self):
|
||||
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))
|
||||
return
|
||||
idx = self.indexAt(position)
|
||||
item = self.model().itemFromIndex(idx)
|
||||
item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
||||
item = self.item_from_index(idx)
|
||||
item_col0 = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
|
||||
if not item or not item_col0:
|
||||
return
|
||||
key = item_col0.data(ROLE_REQUEST_ID)
|
||||
request_type = item_col0.data(ROLE_REQUEST_TYPE)
|
||||
menu = QMenu(self)
|
||||
self.add_copy_menu(menu, idx)
|
||||
invoice = self.parent.wallet.get_invoice(key)
|
||||
|
|
|
@ -27,21 +27,21 @@ from enum import IntEnum
|
|||
from typing import Optional
|
||||
|
||||
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.QtWidgets import QAbstractItemView
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.util import format_time, get_request_status
|
||||
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
from electrum.util import PR_PAID
|
||||
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_KEY = Qt.UserRole + 1
|
||||
ROLE_SORT_ORDER = Qt.UserRole + 2
|
||||
|
||||
|
||||
class RequestList(MyTreeView):
|
||||
|
||||
|
@ -64,7 +64,10 @@ class RequestList(MyTreeView):
|
|||
stretch_column=self.Columns.DESCRIPTION,
|
||||
editable_columns=[])
|
||||
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.selectionModel().currentRowChanged.connect(self.item_changed)
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
|
@ -86,7 +89,7 @@ class RequestList(MyTreeView):
|
|||
if not idx.isValid():
|
||||
return
|
||||
# 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)
|
||||
key = item.data(ROLE_KEY)
|
||||
req = self.wallet.get_request(key)
|
||||
|
@ -107,14 +110,13 @@ class RequestList(MyTreeView):
|
|||
self.selectionModel().clearCurrentIndex()
|
||||
|
||||
def refresh_status(self):
|
||||
m = self.model()
|
||||
m = self.std_model
|
||||
for r in range(m.rowCount()):
|
||||
idx = m.index(r, self.Columns.STATUS)
|
||||
date_idx = idx.sibling(idx.row(), self.Columns.DATE)
|
||||
date_item = m.itemFromIndex(date_idx)
|
||||
status_item = m.itemFromIndex(idx)
|
||||
key = date_item.data(ROLE_KEY)
|
||||
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == PR_TYPE_LN
|
||||
req = self.wallet.get_request(key)
|
||||
if req:
|
||||
status, status_str = get_request_status(req)
|
||||
|
@ -124,7 +126,8 @@ class RequestList(MyTreeView):
|
|||
def update(self):
|
||||
# not calling maybe_defer_update() as it interferes with conditional-visibility
|
||||
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)
|
||||
for req in self.wallet.get_sorted_requests():
|
||||
status, status_str = get_request_status(req)
|
||||
|
@ -147,16 +150,18 @@ class RequestList(MyTreeView):
|
|||
self.set_editability(items)
|
||||
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
|
||||
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.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
||||
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.proxy.setDynamicSortFilter(True)
|
||||
# sort requests by date
|
||||
self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder)
|
||||
# hide list if empty
|
||||
if self.parent.isVisible():
|
||||
b = self.model().rowCount() > 0
|
||||
b = self.std_model.rowCount() > 0
|
||||
self.setVisible(b)
|
||||
self.parent.receive_requests_label.setVisible(b)
|
||||
if not b:
|
||||
|
@ -172,9 +177,8 @@ class RequestList(MyTreeView):
|
|||
menu.exec_(self.viewport().mapToGlobal(position))
|
||||
return
|
||||
idx = self.indexAt(position)
|
||||
item = self.model().itemFromIndex(idx)
|
||||
# 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:
|
||||
return
|
||||
key = item.data(ROLE_KEY)
|
||||
|
|
|
@ -7,15 +7,16 @@ import queue
|
|||
import traceback
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
from decimal import Decimal
|
||||
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,
|
||||
QPalette, QIcon, QFontMetrics, QShowEvent)
|
||||
from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
|
||||
QCoreApplication, QItemSelectionModel, QThread,
|
||||
QSortFilterProxyModel, QSize, QLocale)
|
||||
QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel)
|
||||
from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
|
||||
QAbstractItemView, QVBoxLayout, QLineEdit,
|
||||
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
|
||||
|
@ -466,6 +467,7 @@ def filename_field(parent, config, defaultname, select_msg):
|
|||
|
||||
return vbox, filename_e, b1
|
||||
|
||||
|
||||
class ElectrumItemDelegate(QStyledItemDelegate):
|
||||
def __init__(self, tv: 'MyTreeView'):
|
||||
super().__init__(tv)
|
||||
|
@ -477,7 +479,7 @@ class ElectrumItemDelegate(QStyledItemDelegate):
|
|||
new_text = editor.text()
|
||||
idx = QModelIndex(self.opened)
|
||||
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
|
||||
assert user_role is not None, (row, col)
|
||||
self.tv.on_edited(idx, user_role, new_text)
|
||||
|
@ -488,9 +490,12 @@ class ElectrumItemDelegate(QStyledItemDelegate):
|
|||
self.opened = QPersistentModelIndex(idx)
|
||||
return super().createEditor(parent, option, idx)
|
||||
|
||||
|
||||
class MyTreeView(QTreeView):
|
||||
ROLE_CLIPBOARD_DATA = Qt.UserRole + 100
|
||||
|
||||
filter_columns: Iterable[int]
|
||||
|
||||
def __init__(self, parent: 'ElectrumWindow', create_menu, *,
|
||||
stretch_column=None, editable_columns=None):
|
||||
super().__init__(parent)
|
||||
|
@ -536,10 +541,25 @@ class MyTreeView(QTreeView):
|
|||
def current_item_user_role(self, col) -> Any:
|
||||
idx = self.selectionModel().currentIndex()
|
||||
idx = idx.sibling(idx.row(), col)
|
||||
item = self.model().itemFromIndex(idx)
|
||||
item = self.item_from_index(idx)
|
||||
if item:
|
||||
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):
|
||||
if set_current:
|
||||
assert isinstance(set_current, QPersistentModelIndex)
|
||||
|
@ -551,8 +571,7 @@ class MyTreeView(QTreeView):
|
|||
if not isinstance(headers, dict): # convert to dict
|
||||
headers = dict(enumerate(headers))
|
||||
col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]
|
||||
model = self.model()
|
||||
model.setHorizontalHeaderLabels(col_names)
|
||||
self.original_model().setHorizontalHeaderLabels(col_names)
|
||||
self.header().setStretchLastSection(False)
|
||||
for col_idx in headers:
|
||||
sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents
|
||||
|
@ -592,10 +611,9 @@ class MyTreeView(QTreeView):
|
|||
"""
|
||||
return False
|
||||
|
||||
def text_txid_from_coordinate(self, row_num, column):
|
||||
assert not isinstance(self.model(), QSortFilterProxyModel)
|
||||
def get_text_and_userrole_from_coordinate(self, 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)
|
||||
return item.text(), user_role
|
||||
|
||||
|
@ -610,7 +628,7 @@ class MyTreeView(QTreeView):
|
|||
self.setRowHidden(row_num, QModelIndex(), False)
|
||||
return
|
||||
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()
|
||||
if self.current_filter in txt:
|
||||
# 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:
|
||||
cc = menu.addMenu(_("Copy"))
|
||||
for column in self.Columns:
|
||||
column_title = self.model().horizontalHeaderItem(column).text()
|
||||
item_col = self.model().itemFromIndex(idx.sibling(idx.row(), column))
|
||||
column_title = self.original_model().horizontalHeaderItem(column).text()
|
||||
item_col = self.item_from_index(idx.sibling(idx.row(), column))
|
||||
clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA)
|
||||
if clipboard_data is None:
|
||||
clipboard_data = item_col.text().strip()
|
||||
|
@ -692,6 +710,26 @@ class MyTreeView(QTreeView):
|
|||
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):
|
||||
|
||||
def __init__(self):
|
||||
|
|
Loading…
Add table
Reference in a new issue