move MyTreeView and related classes to own submodule
This commit is contained in:
@@ -37,7 +37,8 @@ from electrum.plugin import run_hook
|
|||||||
from electrum.bitcoin import is_address
|
from electrum.bitcoin import is_address
|
||||||
from electrum.wallet import InternalAddressCorruption
|
from electrum.wallet import InternalAddressCorruption
|
||||||
|
|
||||||
from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen, MySortModel
|
from .util import MONOSPACE_FONT, ColorScheme, webopen
|
||||||
|
from .my_treeview import MyTreeView, MySortModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id
|
|||||||
from electrum.lnworker import LNWallet
|
from electrum.lnworker import LNWallet
|
||||||
from electrum.gui import messages
|
from electrum.gui import messages
|
||||||
|
|
||||||
from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton,
|
from .util import (WindowModalDialog, Buttons, OkButton, CancelButton,
|
||||||
EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme)
|
EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme)
|
||||||
from .amountedit import BTCAmountEdit, FreezableLineEdit
|
from .amountedit import BTCAmountEdit, FreezableLineEdit
|
||||||
from .util import read_QIcon, font_height
|
from .util import read_QIcon, font_height
|
||||||
|
from .my_treeview import MyTreeView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ 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 MyTreeView, webopen
|
from .util import webopen
|
||||||
|
from .my_treeview import MyTreeView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|||||||
@@ -50,8 +50,9 @@ from electrum.logging import get_logger, Logger
|
|||||||
|
|
||||||
from .custom_model import CustomNode, CustomModel
|
from .custom_model import CustomNode, CustomModel
|
||||||
from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
|
from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
|
||||||
filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog,
|
filename_field, AcceptFileDragDrop, WindowModalDialog,
|
||||||
CloseButton, webopen, WWLabel)
|
CloseButton, webopen, WWLabel)
|
||||||
|
from .my_treeview import MyTreeView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from electrum.wallet import Abstract_Wallet
|
from electrum.wallet import Abstract_Wallet
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ from electrum.util import format_time
|
|||||||
from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
|
from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
|
||||||
from electrum.lnutil import HtlcLog
|
from electrum.lnutil import HtlcLog
|
||||||
|
|
||||||
from .util import MyTreeView, read_QIcon, MySortModel, pr_icons
|
from .util import read_QIcon, pr_icons
|
||||||
from .util import CloseButton, Buttons
|
from .util import CloseButton, Buttons
|
||||||
from .util import WindowModalDialog
|
from .util import WindowModalDialog
|
||||||
|
|
||||||
|
from .my_treeview import MyTreeView, MySortModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
from .send_tab import SendTab
|
from .send_tab import SendTab
|
||||||
|
|||||||
476
electrum/gui/qt/my_treeview.py
Normal file
476
electrum/gui/qt/my_treeview.py
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2023 The Electrum Developers
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import enum
|
||||||
|
import os.path
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
import queue
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import webbrowser
|
||||||
|
from decimal import Decimal
|
||||||
|
from functools import partial, lru_cache, wraps
|
||||||
|
from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any,
|
||||||
|
Sequence, Iterable, Tuple, Type)
|
||||||
|
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, QImage,
|
||||||
|
QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent, QMouseEvent)
|
||||||
|
from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
|
||||||
|
QCoreApplication, QItemSelectionModel, QThread,
|
||||||
|
QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel,
|
||||||
|
QEvent, QRect, QPoint, QObject)
|
||||||
|
from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
|
||||||
|
QAbstractItemView, QVBoxLayout, QLineEdit,
|
||||||
|
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
|
||||||
|
QFileDialog, QWidget, QToolButton, QTreeView, QPlainTextEdit,
|
||||||
|
QHeaderView, QApplication, QToolTip, QTreeWidget, QStyledItemDelegate,
|
||||||
|
QMenu, QStyleOptionViewItem, QLayout, QLayoutItem, QAbstractButton,
|
||||||
|
QGraphicsEffect, QGraphicsScene, QGraphicsPixmapItem, QSizePolicy)
|
||||||
|
|
||||||
|
from electrum.i18n import _, languages
|
||||||
|
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
|
||||||
|
from electrum.util import EventListener, event_listener
|
||||||
|
from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
|
||||||
|
from electrum.logging import Logger
|
||||||
|
from electrum.qrreader import MissingQrDetectionLib
|
||||||
|
|
||||||
|
from .util import read_QIcon
|
||||||
|
|
||||||
|
|
||||||
|
class MyMenu(QMenu):
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
QMenu.__init__(self)
|
||||||
|
self.setToolTipsVisible(True)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def addToggle(self, text: str, callback, *, tooltip=''):
|
||||||
|
m = self.addAction(text, callback)
|
||||||
|
m.setCheckable(True)
|
||||||
|
m.setToolTip(tooltip)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None):
|
||||||
|
b = self.config.get(name, default)
|
||||||
|
m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback))
|
||||||
|
m.setCheckable(True)
|
||||||
|
m.setChecked(b)
|
||||||
|
m.setToolTip(tooltip)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def _do_toggle_config(self, name, default, callback):
|
||||||
|
b = self.config.get(name, default)
|
||||||
|
self.config.set_key(name, not b)
|
||||||
|
if callback:
|
||||||
|
callback()
|
||||||
|
|
||||||
|
|
||||||
|
def create_toolbar_with_menu(config, title):
|
||||||
|
menu = MyMenu(config)
|
||||||
|
toolbar_button = QToolButton()
|
||||||
|
toolbar_button.setIcon(read_QIcon("preferences.png"))
|
||||||
|
toolbar_button.setMenu(menu)
|
||||||
|
toolbar_button.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
toolbar_button.setFocusPolicy(Qt.NoFocus)
|
||||||
|
toolbar = QHBoxLayout()
|
||||||
|
toolbar.addWidget(QLabel(title))
|
||||||
|
toolbar.addStretch()
|
||||||
|
toolbar.addWidget(toolbar_button)
|
||||||
|
return toolbar, menu
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 ElectrumItemDelegate(QStyledItemDelegate):
|
||||||
|
def __init__(self, tv: 'MyTreeView'):
|
||||||
|
super().__init__(tv)
|
||||||
|
self.tv = tv
|
||||||
|
self.opened = None
|
||||||
|
def on_closeEditor(editor: QLineEdit, hint):
|
||||||
|
self.opened = None
|
||||||
|
self.tv.is_editor_open = False
|
||||||
|
if self.tv._pending_update:
|
||||||
|
self.tv.update()
|
||||||
|
def on_commitData(editor: QLineEdit):
|
||||||
|
new_text = editor.text()
|
||||||
|
idx = QModelIndex(self.opened)
|
||||||
|
row, col = idx.row(), idx.column()
|
||||||
|
edit_key = self.tv.get_edit_key_from_coordinate(row, col)
|
||||||
|
assert edit_key is not None, (idx.row(), idx.column())
|
||||||
|
self.tv.on_edited(idx, edit_key=edit_key, text=new_text)
|
||||||
|
self.closeEditor.connect(on_closeEditor)
|
||||||
|
self.commitData.connect(on_commitData)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, idx):
|
||||||
|
self.opened = QPersistentModelIndex(idx)
|
||||||
|
self.tv.is_editor_open = True
|
||||||
|
return super().createEditor(parent, option, idx)
|
||||||
|
|
||||||
|
def paint(self, painter: QPainter, option: QStyleOptionViewItem, idx: QModelIndex) -> None:
|
||||||
|
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
||||||
|
if custom_data is None:
|
||||||
|
return super().paint(painter, option, idx)
|
||||||
|
else:
|
||||||
|
# let's call the default paint method first; to paint the background (e.g. selection)
|
||||||
|
super().paint(painter, option, idx)
|
||||||
|
# and now paint on top of that
|
||||||
|
custom_data.paint(painter, option.rect)
|
||||||
|
|
||||||
|
def helpEvent(self, evt: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, idx: QModelIndex) -> bool:
|
||||||
|
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
||||||
|
if custom_data is None:
|
||||||
|
return super().helpEvent(evt, view, option, idx)
|
||||||
|
else:
|
||||||
|
if evt.type() == QEvent.ToolTip:
|
||||||
|
if custom_data.show_tooltip(evt):
|
||||||
|
return True
|
||||||
|
return super().helpEvent(evt, view, option, idx)
|
||||||
|
|
||||||
|
def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize:
|
||||||
|
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
||||||
|
if custom_data is None:
|
||||||
|
return super().sizeHint(option, idx)
|
||||||
|
else:
|
||||||
|
default_size = super().sizeHint(option, idx)
|
||||||
|
return custom_data.sizeHint(default_size)
|
||||||
|
|
||||||
|
class MyTreeView(QTreeView):
|
||||||
|
|
||||||
|
ROLE_CLIPBOARD_DATA = Qt.UserRole + 100
|
||||||
|
ROLE_CUSTOM_PAINT = Qt.UserRole + 101
|
||||||
|
ROLE_EDIT_KEY = Qt.UserRole + 102
|
||||||
|
ROLE_FILTER_DATA = Qt.UserRole + 103
|
||||||
|
|
||||||
|
filter_columns: Iterable[int]
|
||||||
|
|
||||||
|
class BaseColumnsEnum(enum.IntEnum):
|
||||||
|
@staticmethod
|
||||||
|
def _generate_next_value_(name: str, start: int, count: int, last_values):
|
||||||
|
# this is overridden to get a 0-based counter
|
||||||
|
return count
|
||||||
|
|
||||||
|
Columns: Type[BaseColumnsEnum]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
parent: Optional[QWidget] = None,
|
||||||
|
main_window: Optional['ElectrumWindow'] = None,
|
||||||
|
stretch_column: Optional[int] = None,
|
||||||
|
editable_columns: Optional[Sequence[int]] = None,
|
||||||
|
):
|
||||||
|
parent = parent or main_window
|
||||||
|
super().__init__(parent)
|
||||||
|
self.main_window = main_window
|
||||||
|
self.config = self.main_window.config if self.main_window else None
|
||||||
|
self.stretch_column = stretch_column
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.customContextMenuRequested.connect(self.create_menu)
|
||||||
|
self.setUniformRowHeights(True)
|
||||||
|
|
||||||
|
# Control which columns are editable
|
||||||
|
if editable_columns is None:
|
||||||
|
editable_columns = []
|
||||||
|
self.editable_columns = set(editable_columns)
|
||||||
|
self.setItemDelegate(ElectrumItemDelegate(self))
|
||||||
|
self.current_filter = ""
|
||||||
|
self.is_editor_open = False
|
||||||
|
|
||||||
|
self.setRootIsDecorated(False) # remove left margin
|
||||||
|
self.toolbar_shown = False
|
||||||
|
|
||||||
|
# When figuring out the size of columns, Qt by default looks at
|
||||||
|
# the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents).
|
||||||
|
# This would be REALLY SLOW, and it's not perfect anyway.
|
||||||
|
# So to speed the UI up considerably, set it to
|
||||||
|
# only look at as many rows as currently visible.
|
||||||
|
self.header().setResizeContentsPrecision(0)
|
||||||
|
|
||||||
|
self._pending_update = False
|
||||||
|
self._forced_update = False
|
||||||
|
|
||||||
|
self._default_bg_brush = QStandardItem().background()
|
||||||
|
self.proxy = None # history, and address tabs use a proxy
|
||||||
|
|
||||||
|
def create_menu(self, position: QPoint) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_editability(self, items):
|
||||||
|
for idx, i in enumerate(items):
|
||||||
|
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 get_role_data_for_current_item(self, *, col, role) -> Any:
|
||||||
|
idx = self.selectionModel().currentIndex()
|
||||||
|
idx = idx.sibling(idx.row(), col)
|
||||||
|
item = self.item_from_index(idx)
|
||||||
|
if item:
|
||||||
|
return item.data(role)
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert set_current.isValid()
|
||||||
|
self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent)
|
||||||
|
|
||||||
|
def update_headers(self, headers: Union[List[str], Dict[int, str]]):
|
||||||
|
# headers is either a list of column names, or a dict: (col_idx->col_name)
|
||||||
|
if not isinstance(headers, dict): # convert to dict
|
||||||
|
headers = dict(enumerate(headers))
|
||||||
|
col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]
|
||||||
|
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
|
||||||
|
self.header().setSectionResizeMode(col_idx, sm)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
if self.itemDelegate().opened:
|
||||||
|
return
|
||||||
|
if event.key() in [Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter]:
|
||||||
|
self.on_activated(self.selectionModel().currentIndex())
|
||||||
|
return
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
||||||
|
idx: QModelIndex = self.indexAt(event.pos())
|
||||||
|
if self.proxy:
|
||||||
|
idx = self.proxy.mapToSource(idx)
|
||||||
|
if not idx.isValid():
|
||||||
|
# can happen e.g. before list is populated for the first time
|
||||||
|
return
|
||||||
|
self.on_double_click(idx)
|
||||||
|
|
||||||
|
def on_double_click(self, idx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_activated(self, idx):
|
||||||
|
# on 'enter' we show the menu
|
||||||
|
pt = self.visualRect(idx).bottomLeft()
|
||||||
|
pt.setX(50)
|
||||||
|
self.customContextMenuRequested.emit(pt)
|
||||||
|
|
||||||
|
def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None):
|
||||||
|
"""
|
||||||
|
this is to prevent:
|
||||||
|
edit: editing failed
|
||||||
|
from inside qt
|
||||||
|
"""
|
||||||
|
return super().edit(idx, trigger, event)
|
||||||
|
|
||||||
|
def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def should_hide(self, row):
|
||||||
|
"""
|
||||||
|
row_num is for self.model(). So if there is a proxy, it is the row number
|
||||||
|
in that!
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_text_from_coordinate(self, row, col) -> str:
|
||||||
|
idx = self.model().index(row, col)
|
||||||
|
item = self.item_from_index(idx)
|
||||||
|
return item.text()
|
||||||
|
|
||||||
|
def get_role_data_from_coordinate(self, row, col, *, role) -> Any:
|
||||||
|
idx = self.model().index(row, col)
|
||||||
|
item = self.item_from_index(idx)
|
||||||
|
role_data = item.data(role)
|
||||||
|
return role_data
|
||||||
|
|
||||||
|
def get_edit_key_from_coordinate(self, row, col) -> Any:
|
||||||
|
# overriding this might allow avoiding storing duplicate data
|
||||||
|
return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY)
|
||||||
|
|
||||||
|
def get_filter_data_from_coordinate(self, row, col) -> str:
|
||||||
|
filter_data = self.get_role_data_from_coordinate(row, col, role=self.ROLE_FILTER_DATA)
|
||||||
|
if filter_data:
|
||||||
|
return filter_data
|
||||||
|
txt = self.get_text_from_coordinate(row, col)
|
||||||
|
txt = txt.lower()
|
||||||
|
return txt
|
||||||
|
|
||||||
|
def hide_row(self, row_num):
|
||||||
|
"""
|
||||||
|
row_num is for self.model(). So if there is a proxy, it is the row number
|
||||||
|
in that!
|
||||||
|
"""
|
||||||
|
should_hide = self.should_hide(row_num)
|
||||||
|
if not self.current_filter and should_hide is None:
|
||||||
|
# no filters at all, neither date nor search
|
||||||
|
self.setRowHidden(row_num, QModelIndex(), False)
|
||||||
|
return
|
||||||
|
for column in self.filter_columns:
|
||||||
|
filter_data = self.get_filter_data_from_coordinate(row_num, column)
|
||||||
|
if self.current_filter in filter_data:
|
||||||
|
# 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, hide the item
|
||||||
|
self.setRowHidden(row_num, QModelIndex(), True)
|
||||||
|
|
||||||
|
def filter(self, p=None):
|
||||||
|
if p is not None:
|
||||||
|
p = p.lower()
|
||||||
|
self.current_filter = p
|
||||||
|
self.hide_rows()
|
||||||
|
|
||||||
|
def hide_rows(self):
|
||||||
|
for row in range(self.model().rowCount()):
|
||||||
|
self.hide_row(row)
|
||||||
|
|
||||||
|
def create_toolbar(self, config):
|
||||||
|
return
|
||||||
|
|
||||||
|
def create_toolbar_buttons(self):
|
||||||
|
hbox = QHBoxLayout()
|
||||||
|
buttons = self.get_toolbar_buttons()
|
||||||
|
for b in buttons:
|
||||||
|
b.setVisible(False)
|
||||||
|
hbox.addWidget(b)
|
||||||
|
self.toolbar_buttons = buttons
|
||||||
|
return hbox
|
||||||
|
|
||||||
|
def create_toolbar_with_menu(self, title):
|
||||||
|
return create_toolbar_with_menu(self.config, title)
|
||||||
|
|
||||||
|
def show_toolbar(self, state, config=None):
|
||||||
|
if state == self.toolbar_shown:
|
||||||
|
return
|
||||||
|
self.toolbar_shown = state
|
||||||
|
for b in self.toolbar_buttons:
|
||||||
|
b.setVisible(state)
|
||||||
|
if not state:
|
||||||
|
self.on_hide_toolbar()
|
||||||
|
|
||||||
|
def toggle_toolbar(self, config=None):
|
||||||
|
self.show_toolbar(not self.toolbar_shown, config)
|
||||||
|
|
||||||
|
def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
|
||||||
|
cc = menu.addMenu(_("Copy"))
|
||||||
|
for column in self.Columns:
|
||||||
|
if self.isColumnHidden(column):
|
||||||
|
continue
|
||||||
|
column_title = self.original_model().horizontalHeaderItem(column).text()
|
||||||
|
if not column_title:
|
||||||
|
continue
|
||||||
|
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()
|
||||||
|
cc.addAction(column_title,
|
||||||
|
lambda text=clipboard_data, title=column_title:
|
||||||
|
self.place_text_on_clipboard(text, title=title))
|
||||||
|
return cc
|
||||||
|
|
||||||
|
def place_text_on_clipboard(self, text: str, *, title: str = None) -> None:
|
||||||
|
self.main_window.do_copy(text, title=title)
|
||||||
|
|
||||||
|
def showEvent(self, e: 'QShowEvent'):
|
||||||
|
super().showEvent(e)
|
||||||
|
if e.isAccepted() and self._pending_update:
|
||||||
|
self._forced_update = True
|
||||||
|
self.update()
|
||||||
|
self._forced_update = False
|
||||||
|
|
||||||
|
def maybe_defer_update(self) -> bool:
|
||||||
|
"""Returns whether we should defer an update/refresh."""
|
||||||
|
defer = (not self._forced_update
|
||||||
|
and (not self.isVisible() or self.is_editor_open))
|
||||||
|
# side-effect: if we decide to defer update, the state will become stale:
|
||||||
|
self._pending_update = defer
|
||||||
|
return defer
|
||||||
|
|
||||||
|
def find_row_by_key(self, key) -> Optional[int]:
|
||||||
|
for row in range(0, self.std_model.rowCount()):
|
||||||
|
item = self.std_model.item(row, 0)
|
||||||
|
if item.data(self.key_role) == key:
|
||||||
|
return row
|
||||||
|
|
||||||
|
def refresh_all(self):
|
||||||
|
if self.maybe_defer_update():
|
||||||
|
return
|
||||||
|
for row in range(0, self.std_model.rowCount()):
|
||||||
|
item = self.std_model.item(row, 0)
|
||||||
|
key = item.data(self.key_role)
|
||||||
|
self.refresh_row(key, row)
|
||||||
|
|
||||||
|
def refresh_row(self, key: str, row: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def refresh_item(self, key):
|
||||||
|
row = self.find_row_by_key(key)
|
||||||
|
if row is not None:
|
||||||
|
self.refresh_row(key, row)
|
||||||
|
|
||||||
|
def delete_item(self, key):
|
||||||
|
row = self.find_row_by_key(key)
|
||||||
|
if row is not None:
|
||||||
|
self.std_model.takeRow(row)
|
||||||
|
self.hide_if_empty()
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from .util import (WindowModalDialog, Buttons, OkButton, CancelButton,
|
|||||||
EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel,
|
EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel,
|
||||||
char_width_in_lineedit)
|
char_width_in_lineedit)
|
||||||
from .amountedit import BTCAmountEdit
|
from .amountedit import BTCAmountEdit
|
||||||
|
from .my_treeview import create_toolbar_with_menu
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
@@ -34,7 +34,7 @@ class NewChannelDialog(WindowModalDialog):
|
|||||||
self.trampoline_names = list(self.trampolines.keys())
|
self.trampoline_names = list(self.trampolines.keys())
|
||||||
self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT
|
self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT
|
||||||
vbox = QVBoxLayout(self)
|
vbox = QVBoxLayout(self)
|
||||||
toolbar, menu = util.create_toolbar_with_menu(self.config, '')
|
toolbar, menu = create_toolbar_with_menu(self.config, '')
|
||||||
recov_tooltip = messages.to_rtf(_(messages.MSG_RECOVERABLE_CHANNELS))
|
recov_tooltip = messages.to_rtf(_(messages.MSG_RECOVERABLE_CHANNELS))
|
||||||
menu.addConfig(
|
menu.addConfig(
|
||||||
_("Create recoverable channels"), 'use_recoverable_channels', True,
|
_("Create recoverable channels"), 'use_recoverable_channels', True,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ from electrum.util import format_time
|
|||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
from electrum.invoices import Invoice
|
from electrum.invoices import Invoice
|
||||||
|
|
||||||
from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
|
from .util import pr_icons, read_QIcon, webopen
|
||||||
|
from .my_treeview import MyTreeView, MySortModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
|
|||||||
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
|
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
|
||||||
get_iconname_qrcode)
|
get_iconname_qrcode)
|
||||||
from .rate_limiter import rate_limited
|
from .rate_limiter import rate_limited
|
||||||
|
from .my_treeview import create_toolbar_with_menu
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
@@ -409,7 +409,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
self.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
toolbar, menu = util.create_toolbar_with_menu(self.config, '')
|
toolbar, menu = create_toolbar_with_menu(self.config, '')
|
||||||
menu.addConfig(
|
menu.addConfig(
|
||||||
_('Download missing data'), 'tx_dialog_fetch_txin_data', False,
|
_('Download missing data'), 'tx_dialog_fetch_txin_data', False,
|
||||||
tooltip=_(
|
tooltip=_(
|
||||||
|
|||||||
@@ -516,413 +516,7 @@ def filename_field(parent, config, defaultname, select_msg):
|
|||||||
return vbox, filename_e, b1
|
return vbox, filename_e, b1
|
||||||
|
|
||||||
|
|
||||||
class ElectrumItemDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, tv: 'MyTreeView'):
|
|
||||||
super().__init__(tv)
|
|
||||||
self.tv = tv
|
|
||||||
self.opened = None
|
|
||||||
def on_closeEditor(editor: QLineEdit, hint):
|
|
||||||
self.opened = None
|
|
||||||
self.tv.is_editor_open = False
|
|
||||||
if self.tv._pending_update:
|
|
||||||
self.tv.update()
|
|
||||||
def on_commitData(editor: QLineEdit):
|
|
||||||
new_text = editor.text()
|
|
||||||
idx = QModelIndex(self.opened)
|
|
||||||
row, col = idx.row(), idx.column()
|
|
||||||
edit_key = self.tv.get_edit_key_from_coordinate(row, col)
|
|
||||||
assert edit_key is not None, (idx.row(), idx.column())
|
|
||||||
self.tv.on_edited(idx, edit_key=edit_key, text=new_text)
|
|
||||||
self.closeEditor.connect(on_closeEditor)
|
|
||||||
self.commitData.connect(on_commitData)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, idx):
|
|
||||||
self.opened = QPersistentModelIndex(idx)
|
|
||||||
self.tv.is_editor_open = True
|
|
||||||
return super().createEditor(parent, option, idx)
|
|
||||||
|
|
||||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem, idx: QModelIndex) -> None:
|
|
||||||
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
|
||||||
if custom_data is None:
|
|
||||||
return super().paint(painter, option, idx)
|
|
||||||
else:
|
|
||||||
# let's call the default paint method first; to paint the background (e.g. selection)
|
|
||||||
super().paint(painter, option, idx)
|
|
||||||
# and now paint on top of that
|
|
||||||
custom_data.paint(painter, option.rect)
|
|
||||||
|
|
||||||
def helpEvent(self, evt: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, idx: QModelIndex) -> bool:
|
|
||||||
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
|
||||||
if custom_data is None:
|
|
||||||
return super().helpEvent(evt, view, option, idx)
|
|
||||||
else:
|
|
||||||
if evt.type() == QEvent.ToolTip:
|
|
||||||
if custom_data.show_tooltip(evt):
|
|
||||||
return True
|
|
||||||
return super().helpEvent(evt, view, option, idx)
|
|
||||||
|
|
||||||
def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize:
|
|
||||||
custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT)
|
|
||||||
if custom_data is None:
|
|
||||||
return super().sizeHint(option, idx)
|
|
||||||
else:
|
|
||||||
default_size = super().sizeHint(option, idx)
|
|
||||||
return custom_data.sizeHint(default_size)
|
|
||||||
|
|
||||||
|
|
||||||
class MyMenu(QMenu):
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
QMenu.__init__(self)
|
|
||||||
self.setToolTipsVisible(True)
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def addToggle(self, text: str, callback, *, tooltip=''):
|
|
||||||
m = self.addAction(text, callback)
|
|
||||||
m.setCheckable(True)
|
|
||||||
m.setToolTip(tooltip)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None):
|
|
||||||
b = self.config.get(name, default)
|
|
||||||
m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback))
|
|
||||||
m.setCheckable(True)
|
|
||||||
m.setChecked(b)
|
|
||||||
m.setToolTip(tooltip)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def _do_toggle_config(self, name, default, callback):
|
|
||||||
b = self.config.get(name, default)
|
|
||||||
self.config.set_key(name, not b)
|
|
||||||
if callback:
|
|
||||||
callback()
|
|
||||||
|
|
||||||
def create_toolbar_with_menu(config, title):
|
|
||||||
menu = MyMenu(config)
|
|
||||||
toolbar_button = QToolButton()
|
|
||||||
toolbar_button.setIcon(read_QIcon("preferences.png"))
|
|
||||||
toolbar_button.setMenu(menu)
|
|
||||||
toolbar_button.setPopupMode(QToolButton.InstantPopup)
|
|
||||||
toolbar_button.setFocusPolicy(Qt.NoFocus)
|
|
||||||
toolbar = QHBoxLayout()
|
|
||||||
toolbar.addWidget(QLabel(title))
|
|
||||||
toolbar.addStretch()
|
|
||||||
toolbar.addWidget(toolbar_button)
|
|
||||||
return toolbar, menu
|
|
||||||
|
|
||||||
class MyTreeView(QTreeView):
|
|
||||||
ROLE_CLIPBOARD_DATA = Qt.UserRole + 100
|
|
||||||
ROLE_CUSTOM_PAINT = Qt.UserRole + 101
|
|
||||||
ROLE_EDIT_KEY = Qt.UserRole + 102
|
|
||||||
ROLE_FILTER_DATA = Qt.UserRole + 103
|
|
||||||
|
|
||||||
filter_columns: Iterable[int]
|
|
||||||
|
|
||||||
class BaseColumnsEnum(enum.IntEnum):
|
|
||||||
@staticmethod
|
|
||||||
def _generate_next_value_(name: str, start: int, count: int, last_values):
|
|
||||||
# this is overridden to get a 0-based counter
|
|
||||||
return count
|
|
||||||
|
|
||||||
Columns: Type[BaseColumnsEnum]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
parent: Optional[QWidget] = None,
|
|
||||||
main_window: Optional['ElectrumWindow'] = None,
|
|
||||||
stretch_column: Optional[int] = None,
|
|
||||||
editable_columns: Optional[Sequence[int]] = None,
|
|
||||||
):
|
|
||||||
parent = parent or main_window
|
|
||||||
super().__init__(parent)
|
|
||||||
self.main_window = main_window
|
|
||||||
self.config = self.main_window.config if self.main_window else None
|
|
||||||
self.stretch_column = stretch_column
|
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
||||||
self.customContextMenuRequested.connect(self.create_menu)
|
|
||||||
self.setUniformRowHeights(True)
|
|
||||||
|
|
||||||
# Control which columns are editable
|
|
||||||
if editable_columns is None:
|
|
||||||
editable_columns = []
|
|
||||||
self.editable_columns = set(editable_columns)
|
|
||||||
self.setItemDelegate(ElectrumItemDelegate(self))
|
|
||||||
self.current_filter = ""
|
|
||||||
self.is_editor_open = False
|
|
||||||
|
|
||||||
self.setRootIsDecorated(False) # remove left margin
|
|
||||||
self.toolbar_shown = False
|
|
||||||
|
|
||||||
# When figuring out the size of columns, Qt by default looks at
|
|
||||||
# the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents).
|
|
||||||
# This would be REALLY SLOW, and it's not perfect anyway.
|
|
||||||
# So to speed the UI up considerably, set it to
|
|
||||||
# only look at as many rows as currently visible.
|
|
||||||
self.header().setResizeContentsPrecision(0)
|
|
||||||
|
|
||||||
self._pending_update = False
|
|
||||||
self._forced_update = False
|
|
||||||
|
|
||||||
self._default_bg_brush = QStandardItem().background()
|
|
||||||
self.proxy = None # history, and address tabs use a proxy
|
|
||||||
|
|
||||||
def create_menu(self, position: QPoint) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_editability(self, items):
|
|
||||||
for idx, i in enumerate(items):
|
|
||||||
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 get_role_data_for_current_item(self, *, col, role) -> Any:
|
|
||||||
idx = self.selectionModel().currentIndex()
|
|
||||||
idx = idx.sibling(idx.row(), col)
|
|
||||||
item = self.item_from_index(idx)
|
|
||||||
if item:
|
|
||||||
return item.data(role)
|
|
||||||
|
|
||||||
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)
|
|
||||||
assert set_current.isValid()
|
|
||||||
self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent)
|
|
||||||
|
|
||||||
def update_headers(self, headers: Union[List[str], Dict[int, str]]):
|
|
||||||
# headers is either a list of column names, or a dict: (col_idx->col_name)
|
|
||||||
if not isinstance(headers, dict): # convert to dict
|
|
||||||
headers = dict(enumerate(headers))
|
|
||||||
col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]
|
|
||||||
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
|
|
||||||
self.header().setSectionResizeMode(col_idx, sm)
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if self.itemDelegate().opened:
|
|
||||||
return
|
|
||||||
if event.key() in [Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter]:
|
|
||||||
self.on_activated(self.selectionModel().currentIndex())
|
|
||||||
return
|
|
||||||
super().keyPressEvent(event)
|
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
|
||||||
idx: QModelIndex = self.indexAt(event.pos())
|
|
||||||
if self.proxy:
|
|
||||||
idx = self.proxy.mapToSource(idx)
|
|
||||||
if not idx.isValid():
|
|
||||||
# can happen e.g. before list is populated for the first time
|
|
||||||
return
|
|
||||||
self.on_double_click(idx)
|
|
||||||
|
|
||||||
def on_double_click(self, idx):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_activated(self, idx):
|
|
||||||
# on 'enter' we show the menu
|
|
||||||
pt = self.visualRect(idx).bottomLeft()
|
|
||||||
pt.setX(50)
|
|
||||||
self.customContextMenuRequested.emit(pt)
|
|
||||||
|
|
||||||
def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None):
|
|
||||||
"""
|
|
||||||
this is to prevent:
|
|
||||||
edit: editing failed
|
|
||||||
from inside qt
|
|
||||||
"""
|
|
||||||
return super().edit(idx, trigger, event)
|
|
||||||
|
|
||||||
def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def should_hide(self, row):
|
|
||||||
"""
|
|
||||||
row_num is for self.model(). So if there is a proxy, it is the row number
|
|
||||||
in that!
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_text_from_coordinate(self, row, col) -> str:
|
|
||||||
idx = self.model().index(row, col)
|
|
||||||
item = self.item_from_index(idx)
|
|
||||||
return item.text()
|
|
||||||
|
|
||||||
def get_role_data_from_coordinate(self, row, col, *, role) -> Any:
|
|
||||||
idx = self.model().index(row, col)
|
|
||||||
item = self.item_from_index(idx)
|
|
||||||
role_data = item.data(role)
|
|
||||||
return role_data
|
|
||||||
|
|
||||||
def get_edit_key_from_coordinate(self, row, col) -> Any:
|
|
||||||
# overriding this might allow avoiding storing duplicate data
|
|
||||||
return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY)
|
|
||||||
|
|
||||||
def get_filter_data_from_coordinate(self, row, col) -> str:
|
|
||||||
filter_data = self.get_role_data_from_coordinate(row, col, role=self.ROLE_FILTER_DATA)
|
|
||||||
if filter_data:
|
|
||||||
return filter_data
|
|
||||||
txt = self.get_text_from_coordinate(row, col)
|
|
||||||
txt = txt.lower()
|
|
||||||
return txt
|
|
||||||
|
|
||||||
def hide_row(self, row_num):
|
|
||||||
"""
|
|
||||||
row_num is for self.model(). So if there is a proxy, it is the row number
|
|
||||||
in that!
|
|
||||||
"""
|
|
||||||
should_hide = self.should_hide(row_num)
|
|
||||||
if not self.current_filter and should_hide is None:
|
|
||||||
# no filters at all, neither date nor search
|
|
||||||
self.setRowHidden(row_num, QModelIndex(), False)
|
|
||||||
return
|
|
||||||
for column in self.filter_columns:
|
|
||||||
filter_data = self.get_filter_data_from_coordinate(row_num, column)
|
|
||||||
if self.current_filter in filter_data:
|
|
||||||
# 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, hide the item
|
|
||||||
self.setRowHidden(row_num, QModelIndex(), True)
|
|
||||||
|
|
||||||
def filter(self, p=None):
|
|
||||||
if p is not None:
|
|
||||||
p = p.lower()
|
|
||||||
self.current_filter = p
|
|
||||||
self.hide_rows()
|
|
||||||
|
|
||||||
def hide_rows(self):
|
|
||||||
for row in range(self.model().rowCount()):
|
|
||||||
self.hide_row(row)
|
|
||||||
|
|
||||||
def create_toolbar(self, config):
|
|
||||||
return
|
|
||||||
|
|
||||||
def create_toolbar_buttons(self):
|
|
||||||
hbox = QHBoxLayout()
|
|
||||||
buttons = self.get_toolbar_buttons()
|
|
||||||
for b in buttons:
|
|
||||||
b.setVisible(False)
|
|
||||||
hbox.addWidget(b)
|
|
||||||
self.toolbar_buttons = buttons
|
|
||||||
return hbox
|
|
||||||
|
|
||||||
def create_toolbar_with_menu(self, title):
|
|
||||||
return create_toolbar_with_menu(self.config, title)
|
|
||||||
|
|
||||||
def show_toolbar(self, state, config=None):
|
|
||||||
if state == self.toolbar_shown:
|
|
||||||
return
|
|
||||||
self.toolbar_shown = state
|
|
||||||
for b in self.toolbar_buttons:
|
|
||||||
b.setVisible(state)
|
|
||||||
if not state:
|
|
||||||
self.on_hide_toolbar()
|
|
||||||
|
|
||||||
def toggle_toolbar(self, config=None):
|
|
||||||
self.show_toolbar(not self.toolbar_shown, config)
|
|
||||||
|
|
||||||
def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
|
|
||||||
cc = menu.addMenu(_("Copy"))
|
|
||||||
for column in self.Columns:
|
|
||||||
if self.isColumnHidden(column):
|
|
||||||
continue
|
|
||||||
column_title = self.original_model().horizontalHeaderItem(column).text()
|
|
||||||
if not column_title:
|
|
||||||
continue
|
|
||||||
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()
|
|
||||||
cc.addAction(column_title,
|
|
||||||
lambda text=clipboard_data, title=column_title:
|
|
||||||
self.place_text_on_clipboard(text, title=title))
|
|
||||||
return cc
|
|
||||||
|
|
||||||
def place_text_on_clipboard(self, text: str, *, title: str = None) -> None:
|
|
||||||
self.main_window.do_copy(text, title=title)
|
|
||||||
|
|
||||||
def showEvent(self, e: 'QShowEvent'):
|
|
||||||
super().showEvent(e)
|
|
||||||
if e.isAccepted() and self._pending_update:
|
|
||||||
self._forced_update = True
|
|
||||||
self.update()
|
|
||||||
self._forced_update = False
|
|
||||||
|
|
||||||
def maybe_defer_update(self) -> bool:
|
|
||||||
"""Returns whether we should defer an update/refresh."""
|
|
||||||
defer = (not self._forced_update
|
|
||||||
and (not self.isVisible() or self.is_editor_open))
|
|
||||||
# side-effect: if we decide to defer update, the state will become stale:
|
|
||||||
self._pending_update = defer
|
|
||||||
return defer
|
|
||||||
|
|
||||||
def find_row_by_key(self, key) -> Optional[int]:
|
|
||||||
for row in range(0, self.std_model.rowCount()):
|
|
||||||
item = self.std_model.item(row, 0)
|
|
||||||
if item.data(self.key_role) == key:
|
|
||||||
return row
|
|
||||||
|
|
||||||
def refresh_all(self):
|
|
||||||
if self.maybe_defer_update():
|
|
||||||
return
|
|
||||||
for row in range(0, self.std_model.rowCount()):
|
|
||||||
item = self.std_model.item(row, 0)
|
|
||||||
key = item.data(self.key_role)
|
|
||||||
self.refresh_row(key, row)
|
|
||||||
|
|
||||||
def refresh_row(self, key: str, row: int) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def refresh_item(self, key):
|
|
||||||
row = self.find_row_by_key(key)
|
|
||||||
if row is not None:
|
|
||||||
self.refresh_row(key, row)
|
|
||||||
|
|
||||||
def delete_item(self, key):
|
|
||||||
row = self.find_row_by_key(key)
|
|
||||||
if row is not None:
|
|
||||||
self.std_model.takeRow(row)
|
|
||||||
self.hide_if_empty()
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_iconname_qrcode() -> str:
|
def get_iconname_qrcode() -> str:
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ from electrum.bitcoin import is_address
|
|||||||
from electrum.transaction import PartialTxInput, PartialTxOutput
|
from electrum.transaction import PartialTxInput, PartialTxOutput
|
||||||
from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT
|
from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT
|
||||||
|
|
||||||
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton
|
from .util import ColorScheme, MONOSPACE_FONT, EnterButton
|
||||||
|
from .my_treeview import MyTreeView
|
||||||
from .new_channel_dialog import NewChannelDialog
|
from .new_channel_dialog import NewChannelDialog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ from PyQt5.QtCore import Qt
|
|||||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QPushButton, QLabel)
|
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QPushButton, QLabel)
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from .util import MyTreeView, Buttons
|
from .util import Buttons
|
||||||
|
from .my_treeview import MyTreeView
|
||||||
|
|
||||||
|
|
||||||
class WatcherList(MyTreeView):
|
class WatcherList(MyTreeView):
|
||||||
|
|||||||
Reference in New Issue
Block a user