vibefpmgui.metrics_table
A sortable table of rows of objects, drawn on demand (Qt asks for the cells on view, not all of them): the Complexity tab's procedures and files.
A QTableWidget makes an item for each cell: a big project's 9000
procedures were 90 000 items, made again at each refill (more than half a
second). Here a row is an object (a procedure, a file) and each column says
how to show it; tooltips are made when asked for.
It has the few QTableWidget calls the panel uses (item, rowCount,
currentRow, setCurrentCell, cellClicked, currentCellChanged…).
1"""A sortable table of rows of objects, drawn on demand (Qt asks for the 2cells on view, not all of them): the Complexity tab's procedures and files. 3 4A `QTableWidget` makes an item for each cell: a big project's 9000 5procedures were 90 000 items, made again at each refill (more than half a 6second). Here a row is an object (a procedure, a file) and each column says 7how to show it; tooltips are made when asked for. 8 9It has the few `QTableWidget` calls the panel uses (`item`, `rowCount`, 10`currentRow`, `setCurrentCell`, `cellClicked`, `currentCellChanged`…). 11""" 12 13from __future__ import annotations 14 15from collections.abc import Callable 16from typing import Any, NamedTuple 17 18from qtpy.QtCore import QAbstractTableModel, QModelIndex, QRect, Qt, Signal 19from qtpy.QtGui import QBrush, QColor 20from qtpy.QtWidgets import QTableView 21 22 23def _role(role) -> int: 24 return int(getattr(role, "value", role)) 25 26 27# The roles as plain numbers: data() is asked for every cell and role, and 28# comparing a number with one of Qt's enums is ~100 times slower (a Python Enum). 29DISPLAY, FOREGROUND, FONT, DECORATION, TOOLTIP, USER = ( 30 _role(r) for r in (Qt.DisplayRole, Qt.ForegroundRole, Qt.FontRole, Qt.DecorationRole, Qt.ToolTipRole, 31 Qt.UserRole)) 32SAMPLED_ROWS = 200 # rows looked at to size a column to its contents (Qt's default: 1000) 33 34 35class Column(NamedTuple): 36 title: str 37 text: Callable[[Any], str] # a row's text in it 38 value: Callable[[Any], Any] | None = None # what it's sorted by (None: its text) 39 tip: str = "" # the title's tooltip 40 41 42class RowStyle(NamedTuple): 43 """How rows look and what they hold, apart from their text.""" 44 color: Callable[[Any, int], QColor | None] = lambda row, column: None 45 bold: Callable[[Any, int], bool] = lambda row, column: False 46 icon: Callable[[Any], Any] | None = None # the first column's icon (see icon_column) 47 tip: Callable[[Any], str] | None = None 48 data: Callable[[Any, int], Any] | None = None # (row, role) -> a value for that role, or None 49 icon_column: int = 0 50 51 52class RowModel(QAbstractTableModel): 53 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 54 super().__init__(parent) 55 self.columns = columns 56 self.style = style 57 self.rows: list = [] 58 self._sorted: tuple[int, Qt.SortOrder] | None = None 59 self.bold_font = None # (set by the table) 60 61 def set_rows(self, rows: list): 62 self.beginResetModel() 63 self.rows = list(rows) 64 if self._sorted is not None: 65 self._sort_rows(*self._sorted) 66 self.endResetModel() 67 68 def rowCount(self, parent=QModelIndex()): # noqa: B008 - Qt's signature 69 return 0 if parent.isValid() else len(self.rows) 70 71 def columnCount(self, parent=QModelIndex()): # noqa: B008 72 return 0 if parent.isValid() else len(self.columns) 73 74 def headerData(self, section, orientation, role=DISPLAY): 75 role = _role(role) 76 if orientation != Qt.Horizontal or not 0 <= section < len(self.columns): 77 return None 78 if role == DISPLAY: 79 return self.columns[section].title 80 if role == TOOLTIP: 81 return self.columns[section].tip or None 82 return None 83 84 def data(self, index, role=DISPLAY): 85 role = _role(role) 86 if role not in (DISPLAY, FOREGROUND, FONT, DECORATION, TOOLTIP) and role < USER: 87 return None # (alignment, size hints…: Qt's defaults) 88 if not index.isValid(): 89 return None 90 row, column = self.rows[index.row()], index.column() 91 style = self.style 92 if role == DISPLAY: 93 return self.columns[column].text(row) 94 if role == FOREGROUND: 95 color = style.color(row, column) 96 return QBrush(color) if color is not None else None 97 if role == FONT: 98 return self.bold_font if style.bold(row, column) else None 99 if role == DECORATION: 100 return style.icon(row) if style.icon is not None and column == style.icon_column else None 101 if role == TOOLTIP: 102 return style.tip(row) if style.tip is not None else None 103 if style.data is not None: 104 return style.data(row, role) 105 return None 106 107 def sort(self, column, order=Qt.AscendingOrder): 108 self._sorted = (column, order) 109 self.layoutAboutToBeChanged.emit() 110 old = self.persistentIndexList() # (the current row, the selection: they follow their rows) 111 old_rows = [self.rows[i.row()] for i in old] 112 self._sort_rows(column, order) 113 place = {id(row): n for n, row in enumerate(self.rows)} 114 self.changePersistentIndexList(old, [self.index(place[id(row)], i.column()) for row, i in zip(old_rows, old, 115 strict=True)]) 116 self.layoutChanged.emit() 117 118 def _sort_rows(self, column: int, order): 119 spec = self.columns[column] 120 key = spec.value or spec.text 121 self.rows.sort(key=key, reverse=order == Qt.DescendingOrder) # (stable: ties keep their order) 122 123 124class Cell: 125 """A cell, as `QTableWidget.item` gives it (what the panel and its tests ask of one).""" 126 127 def __init__(self, model: RowModel, index): 128 self.model, self.index = model, index 129 130 def text(self) -> str: 131 return self.model.data(self.index, DISPLAY) or "" 132 133 def data(self, role): 134 return self.model.data(self.index, role) 135 136 def foreground(self) -> QBrush: 137 return self.model.data(self.index, FOREGROUND) or QBrush() 138 139 def toolTip(self) -> str: 140 return self.model.data(self.index, TOOLTIP) or "" 141 142 143class _Title: 144 def __init__(self, text: str): 145 self._text = text 146 147 def text(self) -> str: 148 return self._text 149 150 151class MetricsTable(QTableView): 152 """A `QTableView` of a `RowModel`, with `QTableWidget`'s cell signals.""" 153 154 cellClicked = Signal(int, int) 155 currentCellChanged = Signal(int, int, int, int) # row, column, previous row, previous column 156 157 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 158 super().__init__(parent) 159 self.row_model = RowModel(columns, style, self) 160 self.setModel(self.row_model) 161 self.verticalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 162 self.horizontalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 163 font = self.font() 164 font.setBold(True) 165 self.row_model.bold_font = font 166 self.clicked.connect(lambda index: self.cellClicked.emit(index.row(), index.column())) 167 self.selectionModel().currentChanged.connect( 168 lambda now, before: self.currentCellChanged.emit(now.row(), now.column(), before.row(), before.column())) 169 170 def set_rows(self, rows: list): 171 self.row_model.set_rows(rows) 172 173 def rows(self) -> list: 174 """The rows, in the order shown.""" 175 return self.row_model.rows 176 177 def rowCount(self) -> int: 178 return len(self.row_model.rows) 179 180 def item(self, row: int, column: int) -> Cell | None: 181 index = self.row_model.index(row, column) 182 return Cell(self.row_model, index) if index.isValid() else None 183 184 def currentRow(self) -> int: 185 return self.currentIndex().row() 186 187 def setCurrentCell(self, row: int, column: int): 188 self.setCurrentIndex(self.row_model.index(row, column) if row >= 0 else QModelIndex()) 189 190 def horizontalHeaderItem(self, column: int) -> _Title: 191 return _Title(self.row_model.columns[column].title) 192 193 def visualItemRect(self, cell: Cell) -> QRect: 194 return self.visualRect(cell.index)
36class Column(NamedTuple): 37 title: str 38 text: Callable[[Any], str] # a row's text in it 39 value: Callable[[Any], Any] | None = None # what it's sorted by (None: its text) 40 tip: str = "" # the title's tooltip
Column(title, text, value, tip)
43class RowStyle(NamedTuple): 44 """How rows look and what they hold, apart from their text.""" 45 color: Callable[[Any, int], QColor | None] = lambda row, column: None 46 bold: Callable[[Any, int], bool] = lambda row, column: False 47 icon: Callable[[Any], Any] | None = None # the first column's icon (see icon_column) 48 tip: Callable[[Any], str] | None = None 49 data: Callable[[Any, int], Any] | None = None # (row, role) -> a value for that role, or None 50 icon_column: int = 0
How rows look and what they hold, apart from their text.
Create new instance of RowStyle(color, bold, icon, tip, data, icon_column)
53class RowModel(QAbstractTableModel): 54 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 55 super().__init__(parent) 56 self.columns = columns 57 self.style = style 58 self.rows: list = [] 59 self._sorted: tuple[int, Qt.SortOrder] | None = None 60 self.bold_font = None # (set by the table) 61 62 def set_rows(self, rows: list): 63 self.beginResetModel() 64 self.rows = list(rows) 65 if self._sorted is not None: 66 self._sort_rows(*self._sorted) 67 self.endResetModel() 68 69 def rowCount(self, parent=QModelIndex()): # noqa: B008 - Qt's signature 70 return 0 if parent.isValid() else len(self.rows) 71 72 def columnCount(self, parent=QModelIndex()): # noqa: B008 73 return 0 if parent.isValid() else len(self.columns) 74 75 def headerData(self, section, orientation, role=DISPLAY): 76 role = _role(role) 77 if orientation != Qt.Horizontal or not 0 <= section < len(self.columns): 78 return None 79 if role == DISPLAY: 80 return self.columns[section].title 81 if role == TOOLTIP: 82 return self.columns[section].tip or None 83 return None 84 85 def data(self, index, role=DISPLAY): 86 role = _role(role) 87 if role not in (DISPLAY, FOREGROUND, FONT, DECORATION, TOOLTIP) and role < USER: 88 return None # (alignment, size hints…: Qt's defaults) 89 if not index.isValid(): 90 return None 91 row, column = self.rows[index.row()], index.column() 92 style = self.style 93 if role == DISPLAY: 94 return self.columns[column].text(row) 95 if role == FOREGROUND: 96 color = style.color(row, column) 97 return QBrush(color) if color is not None else None 98 if role == FONT: 99 return self.bold_font if style.bold(row, column) else None 100 if role == DECORATION: 101 return style.icon(row) if style.icon is not None and column == style.icon_column else None 102 if role == TOOLTIP: 103 return style.tip(row) if style.tip is not None else None 104 if style.data is not None: 105 return style.data(row, role) 106 return None 107 108 def sort(self, column, order=Qt.AscendingOrder): 109 self._sorted = (column, order) 110 self.layoutAboutToBeChanged.emit() 111 old = self.persistentIndexList() # (the current row, the selection: they follow their rows) 112 old_rows = [self.rows[i.row()] for i in old] 113 self._sort_rows(column, order) 114 place = {id(row): n for n, row in enumerate(self.rows)} 115 self.changePersistentIndexList(old, [self.index(place[id(row)], i.column()) for row, i in zip(old_rows, old, 116 strict=True)]) 117 self.layoutChanged.emit() 118 119 def _sort_rows(self, column: int, order): 120 spec = self.columns[column] 121 key = spec.value or spec.text 122 self.rows.sort(key=key, reverse=order == Qt.DescendingOrder) # (stable: ties keep their order)
QAbstractTableModel(self, /, parent: PySide6.QtCore.QObject | None = None) -> None
54 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 55 super().__init__(parent) 56 self.columns = columns 57 self.style = style 58 self.rows: list = [] 59 self._sorted: tuple[int, Qt.SortOrder] | None = None 60 self.bold_font = None # (set by the table)
__init__(self, /, parent: PySide6.QtCore.QObject | None = None) -> None
Initialize self. See help(type(self)) for accurate signature.
69 def rowCount(self, parent=QModelIndex()): # noqa: B008 - Qt's signature 70 return 0 if parent.isValid() else len(self.rows)
rowCount(self, /, parent: PySide6.QtCore.QModelIndex | PySide6.QtCore.QPersistentModelIndex = Invalid(PySide6.QtCore.QModelIndex)) -> int
72 def columnCount(self, parent=QModelIndex()): # noqa: B008 73 return 0 if parent.isValid() else len(self.columns)
columnCount(self, /, parent: PySide6.QtCore.QModelIndex | PySide6.QtCore.QPersistentModelIndex = Invalid(PySide6.QtCore.QModelIndex)) -> int
75 def headerData(self, section, orientation, role=DISPLAY): 76 role = _role(role) 77 if orientation != Qt.Horizontal or not 0 <= section < len(self.columns): 78 return None 79 if role == DISPLAY: 80 return self.columns[section].title 81 if role == TOOLTIP: 82 return self.columns[section].tip or None 83 return None
headerData(self, section: int, orientation: PySide6.QtCore.Qt.Orientation, /, role: int = Instance(Qt.DisplayRole)) -> Any
85 def data(self, index, role=DISPLAY): 86 role = _role(role) 87 if role not in (DISPLAY, FOREGROUND, FONT, DECORATION, TOOLTIP) and role < USER: 88 return None # (alignment, size hints…: Qt's defaults) 89 if not index.isValid(): 90 return None 91 row, column = self.rows[index.row()], index.column() 92 style = self.style 93 if role == DISPLAY: 94 return self.columns[column].text(row) 95 if role == FOREGROUND: 96 color = style.color(row, column) 97 return QBrush(color) if color is not None else None 98 if role == FONT: 99 return self.bold_font if style.bold(row, column) else None 100 if role == DECORATION: 101 return style.icon(row) if style.icon is not None and column == style.icon_column else None 102 if role == TOOLTIP: 103 return style.tip(row) if style.tip is not None else None 104 if style.data is not None: 105 return style.data(row, role) 106 return None
data(self, index: PySide6.QtCore.QModelIndex | PySide6.QtCore.QPersistentModelIndex, /, role: int = Instance(Qt.DisplayRole)) -> Any
108 def sort(self, column, order=Qt.AscendingOrder): 109 self._sorted = (column, order) 110 self.layoutAboutToBeChanged.emit() 111 old = self.persistentIndexList() # (the current row, the selection: they follow their rows) 112 old_rows = [self.rows[i.row()] for i in old] 113 self._sort_rows(column, order) 114 place = {id(row): n for n, row in enumerate(self.rows)} 115 self.changePersistentIndexList(old, [self.index(place[id(row)], i.column()) for row, i in zip(old_rows, old, 116 strict=True)]) 117 self.layoutChanged.emit()
sort(self, column: int, /, order: PySide6.QtCore.Qt.SortOrder = Instance(Qt.AscendingOrder)) -> None
125class Cell: 126 """A cell, as `QTableWidget.item` gives it (what the panel and its tests ask of one).""" 127 128 def __init__(self, model: RowModel, index): 129 self.model, self.index = model, index 130 131 def text(self) -> str: 132 return self.model.data(self.index, DISPLAY) or "" 133 134 def data(self, role): 135 return self.model.data(self.index, role) 136 137 def foreground(self) -> QBrush: 138 return self.model.data(self.index, FOREGROUND) or QBrush() 139 140 def toolTip(self) -> str: 141 return self.model.data(self.index, TOOLTIP) or ""
A cell, as QTableWidget.item gives it (what the panel and its tests ask of one).
152class MetricsTable(QTableView): 153 """A `QTableView` of a `RowModel`, with `QTableWidget`'s cell signals.""" 154 155 cellClicked = Signal(int, int) 156 currentCellChanged = Signal(int, int, int, int) # row, column, previous row, previous column 157 158 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 159 super().__init__(parent) 160 self.row_model = RowModel(columns, style, self) 161 self.setModel(self.row_model) 162 self.verticalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 163 self.horizontalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 164 font = self.font() 165 font.setBold(True) 166 self.row_model.bold_font = font 167 self.clicked.connect(lambda index: self.cellClicked.emit(index.row(), index.column())) 168 self.selectionModel().currentChanged.connect( 169 lambda now, before: self.currentCellChanged.emit(now.row(), now.column(), before.row(), before.column())) 170 171 def set_rows(self, rows: list): 172 self.row_model.set_rows(rows) 173 174 def rows(self) -> list: 175 """The rows, in the order shown.""" 176 return self.row_model.rows 177 178 def rowCount(self) -> int: 179 return len(self.row_model.rows) 180 181 def item(self, row: int, column: int) -> Cell | None: 182 index = self.row_model.index(row, column) 183 return Cell(self.row_model, index) if index.isValid() else None 184 185 def currentRow(self) -> int: 186 return self.currentIndex().row() 187 188 def setCurrentCell(self, row: int, column: int): 189 self.setCurrentIndex(self.row_model.index(row, column) if row >= 0 else QModelIndex()) 190 191 def horizontalHeaderItem(self, column: int) -> _Title: 192 return _Title(self.row_model.columns[column].title) 193 194 def visualItemRect(self, cell: Cell) -> QRect: 195 return self.visualRect(cell.index)
A QTableView of a RowModel, with QTableWidget's cell signals.
158 def __init__(self, columns: list[Column], style: RowStyle, parent=None): 159 super().__init__(parent) 160 self.row_model = RowModel(columns, style, self) 161 self.setModel(self.row_model) 162 self.verticalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 163 self.horizontalHeader().setResizeContentsPrecision(SAMPLED_ROWS) 164 font = self.font() 165 font.setBold(True) 166 self.row_model.bold_font = font 167 self.clicked.connect(lambda index: self.cellClicked.emit(index.row(), index.column())) 168 self.selectionModel().currentChanged.connect( 169 lambda now, before: self.currentCellChanged.emit(now.row(), now.column(), before.row(), before.column()))
__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, *, showGrid: bool | None = None, gridStyle: PySide6.QtCore.Qt.PenStyle | None = None, sortingEnabled: bool | None = None, wordWrap: bool | None = None, cornerButtonEnabled: bool | None = None) -> None
Initialize self. See help(type(self)) for accurate signature.