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)
SAMPLED_ROWS = 200
class Column(typing.NamedTuple):
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)

Column( title: str, text: Callable[[Any], str], value: Callable[[Any], Any] | None = None, tip: str = '')

Create new instance of Column(title, text, value, tip)

title: str

Alias for field number 0

text: Callable[[typing.Any], str]

Alias for field number 1

value: Callable[[Any], Any] | None

Alias for field number 2

tip: str

Alias for field number 3

class RowStyle(typing.NamedTuple):
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.

RowStyle( color: Callable[[Any, int], QColor | None] = <function RowStyle.<lambda>>, bold: Callable[[Any, int], bool] = <function RowStyle.<lambda>>, icon: Callable[[Any], Any] | None = None, tip: Callable[[Any], str] | None = None, data: Callable[[Any, int], Any] | None = None, icon_column: int = 0)

Create new instance of RowStyle(color, bold, icon, tip, data, icon_column)

color: Callable[[typing.Any, int], PySide6.QtGui.QColor | None]

Alias for field number 0

bold: Callable[[typing.Any, int], bool]

Alias for field number 1

icon: Callable[[Any], Any] | None

Alias for field number 2

tip: Callable[[Any], str] | None

Alias for field number 3

data: Callable[[Any, int], Any] | None

Alias for field number 4

icon_column: int

Alias for field number 5

class RowModel(PySide6.QtCore.QAbstractTableModel):
 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

RowModel( columns: list[Column], style: RowStyle, parent=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.

columns
style
rows: list
bold_font
def set_rows(self, rows: list):
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()
def rowCount(self, parent=<PySide6.QtCore.QModelIndex(-1,-1,0x0,QObject(0x0))>):
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

def columnCount(self, parent=<PySide6.QtCore.QModelIndex(-1,-1,0x0,QObject(0x0))>):
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

def headerData(self, section, orientation, role=0):
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

def data(self, index, role=0):
 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

def sort(self, column, order=<SortOrder.AscendingOrder: 0>):
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

staticMetaObject = PySide6.QtCore.QMetaObject("RowModel" inherits "QAbstractTableModel": )
class Cell:
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).

Cell(model: RowModel, index)
128    def __init__(self, model: RowModel, index):
129        self.model, self.index = model, index
def text(self) -> str:
131    def text(self) -> str:
132        return self.model.data(self.index, DISPLAY) or ""
def data(self, role):
134    def data(self, role):
135        return self.model.data(self.index, role)
def foreground(self) -> PySide6.QtGui.QBrush:
137    def foreground(self) -> QBrush:
138        return self.model.data(self.index, FOREGROUND) or QBrush()
def toolTip(self) -> str:
140    def toolTip(self) -> str:
141        return self.model.data(self.index, TOOLTIP) or ""
class MetricsTable(PySide6.QtWidgets.QTableView):
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.

MetricsTable( columns: list[Column], style: RowStyle, parent=None)
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.

def cellClicked(unknown):

The type of the None singleton.

def currentCellChanged(unknown):

The type of the None singleton.

row_model
def set_rows(self, rows: list):
171    def set_rows(self, rows: list):
172        self.row_model.set_rows(rows)
def rows(self) -> list:
174    def rows(self) -> list:
175        """The rows, in the order shown."""
176        return self.row_model.rows

The rows, in the order shown.

def rowCount(self) -> int:
178    def rowCount(self) -> int:
179        return len(self.row_model.rows)
def item(self, row: int, column: int) -> Cell | None:
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
def currentRow(self) -> int:
185    def currentRow(self) -> int:
186        return self.currentIndex().row()
def setCurrentCell(self, row: int, column: int):
188    def setCurrentCell(self, row: int, column: int):
189        self.setCurrentIndex(self.row_model.index(row, column) if row >= 0 else QModelIndex())
def horizontalHeaderItem(self, column: int) -> vibefpmgui.metrics_table._Title:
191    def horizontalHeaderItem(self, column: int) -> _Title:
192        return _Title(self.row_model.columns[column].title)
def visualItemRect(self, cell: Cell) -> PySide6.QtCore.QRect:
194    def visualItemRect(self, cell: Cell) -> QRect:
195        return self.visualRect(cell.index)
staticMetaObject = PySide6.QtCore.QMetaObject("MetricsTable" inherits "QTableView": Methods: #87 type=Signal, signature=cellClicked(int,int), parameters=int, int #88 type=Signal, signature=currentCellChanged(int,int,int,int), parameters=int, int, int, int )