Browse Source

Merge pull request #7133 from SomberNight/202103_qt_channel_features

qt channels list: add "features" column with icons (e.g. trampoline)
patch-4
ThomasV 4 years ago
committed by GitHub
parent
commit
57320c0304
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      electrum/gui/icons/kangaroo.png
  2. BIN
      electrum/gui/icons/nocloud.png
  3. 93
      electrum/gui/qt/channels_list.py
  4. 36
      electrum/gui/qt/util.py

BIN
electrum/gui/icons/kangaroo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
electrum/gui/icons/nocloud.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

93
electrum/gui/qt/channels_list.py

@ -2,12 +2,14 @@
import traceback
from enum import IntEnum
from typing import Sequence, Optional, Dict
from abc import abstractmethod, ABC
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QRect, QSize
from PyQt5.QtWidgets import (QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit,
QPushButton, QAbstractItemView, QComboBox, QCheckBox)
from PyQt5.QtGui import QFont, QStandardItem, QBrush
QPushButton, QAbstractItemView, QComboBox, QCheckBox,
QToolTip)
from PyQt5.QtGui import QFont, QStandardItem, QBrush, QPainter, QIcon, QHelpEvent
from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates
from electrum.i18n import _
@ -35,14 +37,16 @@ class ChannelsList(MyTreeView):
class Columns(IntEnum):
SHORT_CHANID = 0
NODE_ALIAS = 1
CAPACITY = 2
LOCAL_BALANCE = 3
REMOTE_BALANCE = 4
CHANNEL_STATUS = 5
FEATURES = 2
CAPACITY = 3
LOCAL_BALANCE = 4
REMOTE_BALANCE = 5
CHANNEL_STATUS = 6
headers = {
Columns.SHORT_CHANID: _('Short Channel ID'),
Columns.NODE_ALIAS: _('Node alias'),
Columns.FEATURES: _('Features'),
Columns.CAPACITY: _('Capacity'),
Columns.LOCAL_BALANCE: _('Can send'),
Columns.REMOTE_BALANCE: _('Can receive'),
@ -92,6 +96,7 @@ class ChannelsList(MyTreeView):
return {
self.Columns.SHORT_CHANID: chan.short_id_for_GUI(),
self.Columns.NODE_ALIAS: node_alias,
self.Columns.FEATURES: '',
self.Columns.CAPACITY: capacity_str,
self.Columns.LOCAL_BALANCE: '' if closed else labels[LOCAL],
self.Columns.REMOTE_BALANCE: '' if closed else labels[REMOTE],
@ -296,6 +301,7 @@ class ChannelsList(MyTreeView):
items[self.Columns.NODE_ALIAS].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.LOCAL_BALANCE].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.REMOTE_BALANCE].setFont(QFont(MONOSPACE_FONT))
items[self.Columns.FEATURES].setData(ChannelFeatureIcons.from_channel(chan), self.ROLE_CUSTOM_PAINT)
items[self.Columns.CAPACITY].setFont(QFont(MONOSPACE_FONT))
icon = "lightning" if not chan.is_backup() else "lightning_disconnected"
items[self.Columns.SHORT_CHANID].setIcon(read_QIcon(icon))
@ -486,3 +492,76 @@ class ChannelsList(MyTreeView):
from .swap_dialog import SwapDialog
d = SwapDialog(self.parent)
d.run()
class ChannelFeature(ABC):
def __init__(self):
self.rect = QRect()
@abstractmethod
def tooltip(self) -> str:
pass
@abstractmethod
def icon(self) -> QIcon:
pass
class ChanFeatTrampoline(ChannelFeature):
def tooltip(self) -> str:
return _("The channel peer can route Trampoline payments.")
def icon(self) -> QIcon:
return read_QIcon("kangaroo")
class ChanFeatNoOnchainBackup(ChannelFeature):
def tooltip(self) -> str:
return _("This channel cannot be recovered from your seed. You must back it up manually.")
def icon(self) -> QIcon:
return read_QIcon("nocloud")
class ChannelFeatureIcons:
ICON_SIZE = QSize(16, 16)
def __init__(self, features: Sequence['ChannelFeature']):
self.features = features
@classmethod
def from_channel(cls, chan: AbstractChannel) -> 'ChannelFeatureIcons':
if not isinstance(chan, Channel):
return ChannelFeatureIcons([])
feats = []
if chan.lnworker.is_trampoline_peer(chan.node_id):
feats.append(ChanFeatTrampoline())
if not chan.lnworker.has_recoverable_channels():
feats.append(ChanFeatNoOnchainBackup())
return ChannelFeatureIcons(feats)
def paint(self, painter: QPainter, rect: QRect) -> None:
painter.save()
cur_x = rect.x()
for feat in self.features:
icon_rect = QRect(cur_x, rect.y(), self.ICON_SIZE.width(), self.ICON_SIZE.height())
feat.rect = icon_rect
if rect.contains(icon_rect): # stay inside parent
painter.drawPixmap(icon_rect, feat.icon().pixmap(self.ICON_SIZE))
cur_x += self.ICON_SIZE.width() + 1
painter.restore()
def sizeHint(self, default_size: QSize) -> QSize:
if not self.features:
return default_size
width = len(self.features) * (self.ICON_SIZE.width() + 1)
return QSize(width, default_size.height())
def show_tooltip(self, evt: QHelpEvent) -> bool:
assert isinstance(evt, QHelpEvent)
for feat in self.features:
if feat.rect.contains(evt.pos()):
QToolTip.showText(evt.globalPos(), feat.tooltip())
break
else:
QToolTip.hideText()
evt.ignore()
return True

36
electrum/gui/qt/util.py

@ -13,16 +13,17 @@ from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List,
Sequence, Iterable)
from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem,
QPalette, QIcon, QFontMetrics, QShowEvent)
QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent)
from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
QCoreApplication, QItemSelectionModel, QThread,
QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel)
QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel,
QEvent)
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)
QMenu, QStyleOptionViewItem)
from electrum.i18n import _, languages
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
@ -518,9 +519,38 @@ class ElectrumItemDelegate(QStyledItemDelegate):
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
filter_columns: Iterable[int]

Loading…
Cancel
Save