zapret-kvn/xray_fluent/ui/nodes_view.py
loop-uh 120bbc5613
Some checks failed
Windows project source guards / test (push) Failing after 44s
fix: refresh managed DNS and cache server table rendering
2026-09-06 14:51:07 +03:00

219 lines
9.7 KiB
Python

"""Interactive virtual table with fixed rows and inexpensive group headers."""
from PyQt6.QtCore import QItemSelectionModel, Qt, pyqtSignal, QRect, QPoint, QSignalBlocker
from PyQt6.QtGui import QColor, QPainter, QPolygon
from PyQt6.QtWidgets import QHeaderView
from qfluentwidgets import TableView, themeColor, isDarkTheme
from .nodes_table_delegate import NodesActivityDelegate
from .nodes_table_model import ACTIVE_ROLE, NODE_ID_ROLE
from .nodes_group_model import GROUP_KEY_ROLE
class NodesDelegate(NodesActivityDelegate):
def _paint_active_row_fill(self, painter, option, index, color):
first, last = self._row_fill_span(index)
rect = option.rect.adjusted(4 if first else 0, self.margin, -4 if last else 0, -self.margin)
painter.fillRect(rect, color)
def _drawBackground(self, painter, option, index):
color = painter.brush().color()
if color.alpha():
first, last = self._row_fill_span(index)
rect = option.rect.adjusted(4 if first else 0, 0, -4 if last else 0, 0)
painter.fillRect(rect, color)
def _row_fill_span(self, index):
edges = getattr(self.parent(), "_row_edges", None)
if edges is not None:
return index.column() == edges[0], index.column() == edges[1]
return super()._row_fill_span(index)
def row_fill_color(self, index):
if index.data(ACTIVE_ROLE) or index.row() in self.selectedRows:
color = themeColor()
color.setAlpha(95 if isDarkTheme() else 60)
return color
if index.data(NODE_ID_ROLE):
return QColor(255, 255, 255, 6) if isDarkTheme() else QColor(0, 0, 0, 6)
return None
def paint(self, painter, option, index):
super().paint(painter, option, index)
if index.column() == 0 and index.data(GROUP_KEY_ROLE):
painter.save()
painter.setPen(QColor(210, 210, 210) if isDarkTheme() else QColor(75, 75, 75))
x, y = option.rect.left() + 8, option.rect.center().y()
points = [(x, y-3), (x+4, y), (x, y+3)] if not self.parent().isExpanded(index) else [(x-2,y-2),(x+1,y+2),(x+4,y-2)]
painter.drawPolyline(QPolygon([QPoint(*point) for point in points]))
painter.restore()
class NodesHeader(QHeaderView):
def paintSection(self, painter, rect, logical_index):
super().paintSection(painter, rect, logical_index)
painter.save()
painter.setPen(QColor(255, 255, 255, 28) if isDarkTheme() else QColor(0, 0, 0, 28))
painter.drawLine(rect.right(), rect.top()+4, rect.right(), rect.bottom()-4)
painter.restore()
class NodesView(TableView):
collapsed = pyqtSignal(object)
expanded = pyqtSignal(object)
def __init__(self, parent=None):
super().__init__(parent)
self.setHorizontalHeader(NodesHeader(Qt.Orientation.Horizontal, self))
self.setShowGrid(False)
self._collapsed_groups = set()
self._hidden_rows = set()
def setModel(self, model):
super().setModel(model)
model.modelAboutToBeReset.connect(self._remember_selection)
model.modelReset.connect(self._restore_selection)
def _remember_selection(self):
# Read identities directly: the source may already have removed rows.
self._saved_selection = [
self.model().index(row, 0).internalPointer().key
for selected_range in self.selectionModel().selection()
for row in range(selected_range.top(), selected_range.bottom() + 1)
]
current = self.currentIndex()
self._saved_current = current.internalPointer().key if current.isValid() else None
def _restore_selection(self):
self._hidden_rows.clear()
selection = self.selectionModel()
with QSignalBlocker(selection):
for key in self._saved_selection:
entry = self.model()._entries.get(key)
if entry is not None:
selection.select(self.model().index(entry.row, 0),
QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows)
entry = self.model()._entries.get(self._saved_current)
if entry is not None:
selection.setCurrentIndex(self.model().index(entry.row, 0), QItemSelectionModel.SelectionFlag.NoUpdate)
self.apply_collapsed_groups(self._collapsed_groups)
self.updateSelectedRows()
def header(self):
return self.horizontalHeader()
def apply_collapsed_groups(self, keys):
self._collapsed_groups = set(keys)
hidden = set()
hidden_ids = set()
self.model().collapsed_groups = self._collapsed_groups
for index in self.model().group_indexes():
if index.data(GROUP_KEY_ROLE) in self._collapsed_groups:
hidden.update(child.row for child in index.internalPointer().children)
hidden_ids.update(child.node_id for child in index.internalPointer().children)
for row in self._hidden_rows - hidden:
self.setRowHidden(row, False)
for row in hidden - self._hidden_rows:
self.setRowHidden(row, True)
self._hidden_rows = hidden
self.model().sourceModel().set_deferred_nodes(hidden_ids)
self.viewport().update()
def isExpanded(self, index):
return index.data(GROUP_KEY_ROLE) not in self._collapsed_groups
def setExpanded(self, index, expanded):
key = index.data(GROUP_KEY_ROLE)
if not key or expanded == self.isExpanded(index):
return
keys = self._collapsed_groups - {key} if expanded else self._collapsed_groups | {key}
self.apply_collapsed_groups(keys)
(self.expanded if expanded else self.collapsed).emit(index)
def expand(self, index):
self.setExpanded(index, True)
def collapse(self, index):
self.setExpanded(index, False)
def _set_all_expanded(self, expanded):
groups = self.model().group_indexes()
changed = [index for index in groups if self.isExpanded(index) != expanded]
keys = set() if expanded else {index.data(GROUP_KEY_ROLE) for index in groups}
self.apply_collapsed_groups(keys)
signal = self.expanded if expanded else self.collapsed
for index in changed:
signal.emit(index)
def expandAll(self):
self._set_all_expanded(True)
def collapseAll(self):
self._set_all_expanded(False)
def mousePressEvent(self, event):
index = self.indexAt(event.pos())
if event.button() == Qt.MouseButton.LeftButton and index.data(GROUP_KEY_ROLE):
self.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectionFlag.NoUpdate)
self.setExpanded(index, not self.isExpanded(index))
return
super().mousePressEvent(event)
def keyPressEvent(self, event):
index = self.currentIndex()
key = event.key()
if index.isValid() and not event.modifiers():
group = index if index.data(GROUP_KEY_ROLE) else None
if group is None and key == Qt.Key.Key_Left:
parent = index.internalPointer().parent
if parent is not None:
group = self.model().index(parent.row, 0)
self.selectionModel().setCurrentIndex(group, QItemSelectionModel.SelectionFlag.NoUpdate)
self.scrollTo(group)
return
if group is not None and key in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Space):
expanded = not self.isExpanded(group) if key == Qt.Key.Key_Space else key == Qt.Key.Key_Right
self.setExpanded(group, expanded)
return
super().keyPressEvent(event)
def select_index(self, index):
if not index.isValid():
return
group = index.internalPointer().parent
if group is not None:
self.expand(self.model().index(group.row, 0))
self.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
self.updateSelectedRows()
def paintEvent(self, event):
theme = (isDarkTheme(), themeColor().rgba(), self.font().toString())
if theme != getattr(self, "_paint_theme", None):
self._paint_theme = theme
if self.model() is not None:
self.model().clear_display_cache()
header = self.header()
columns = [header.logicalIndex(i) for i in range(header.count()) if not header.isSectionHidden(header.logicalIndex(i))]
self._row_edges = (columns[0], columns[-1]) if columns else None
super().paintEvent(event)
painter = QPainter(self.viewport())
base = 255 if isDarkTheme() else 0
painter.setPen(QColor(base, base, base, 20))
for col in columns[:-1]:
x = header.sectionViewportPosition(col) + header.sectionSize(col) - 1
painter.drawLine(x, 0, x, self.viewport().height())
# Paint borders only for visible rows, never scan the server catalogue.
y = 0
while y < self.viewport().height():
row = self.rowAt(y)
if row < 0:
break
top, height = self.rowViewportPosition(row), self.rowHeight(row)
if self.model().index(row, 0).data(NODE_ID_ROLE):
border = QColor(base, base, base, 20)
width = self.viewport().width()-7
painter.fillRect(QRect(3, top+1, width, 1), border)
painter.fillRect(QRect(3, top+height-2, width, 1), border)
painter.fillRect(QRect(3, top+2, 1, height-4), border)
painter.fillRect(QRect(3+width-1, top+2, 1, height-4), border)
y = max(y+1, top+height)
painter.end()