pyqt6-scaffold/pyqt6_scaffold/core/models.py
2026-03-06 16:05:24 +03:00

248 lines
No EOL
7.6 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-later
from abc import ABC
from typing import List, Sequence, Any
from PyQt6.QtGui import QColor, QBrush, QFont
from PyQt6.QtCore import (
Qt,
QAbstractTableModel,
QAbstractListModel,
QModelIndex,
QVariant
)
class DataMixin(ABC):
"""
Mixin for Qt data models providing common data storage and display logic.
Intended to be used alongside QAbstractTableModel or QAbstractListModel.
The refresh() method relies on beginResetModel() and endResetModel()
provided by the Qt base class.
"""
def __init__(self):
super().__init__()
self._data: List[Sequence[Any]] = list()
def row_background(self, data: tuple) -> QColor | None:
"""
Return a background color for the given row, or None for default.
Override in subclass to apply conditional row highlighting.
Args:
data: The full row tuple from the dataset.
"""
pass
def row_foreground(self, data: tuple) -> QColor | None:
"""
Return a foreground (text) color for the given row, or None for default.
Override in subclass to apply conditional text coloring.
Args:
data: The full row tuple from the dataset.
"""
pass
def row_font(self, data: tuple) -> QFont | None:
"""
Return a QFont for the given row, or None for default.
Override in subclass to apply conditional font styling,
such as strikethrough or bold.
Args:
data: The full row tuple from the dataset.
"""
pass
def refresh(self, data: List[Sequence[Any]]):
"""
Replace the model data and notify the view.
Args:
data: New dataset as a list of sequences.
"""
self.beginResetModel()
self._data = data
self.endResetModel()
def row_data(self, row: int) -> tuple | None:
"""
Return the raw row tuple at the given index, or None if out of range.
"""
if 0 <= row < len(self._data):
return self._data[row]
return None
def row_display(self, data: Sequence[Any]):
"""
Return the string representation of a row for DisplayRole.
Override in subclass to customize how rows appear in the view.
Args:
data: The full row tuple from the dataset.
"""
return str(data[0]) if data else ""
class BaseTableModel(DataMixin, QAbstractTableModel):
"""
Table model backed by a list of row tuples.
Subclasses must define the headers class attribute.
Override row_display(), row_background(), row_foreground(),
and row_font() to customize cell appearance.
Attributes:
headers: Column header labels. Must be defined in subclass.
"""
# Column header labels displayed in the table view.
# Define this in your subclass: headers = ["ID", "Name", "Price"]
headers: List[str] = []
def rowCount(self, parent = QModelIndex()) -> int:
return len(self._data)
def columnCount(self, parent = QModelIndex()) -> int:
return len(self.headers)
def row_display(self, data):
"""
Return the string representation of a single cell value.
Args:
data: A single cell value from the dataset.
"""
return str(data) if data else ""
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return QVariant()
match role:
case Qt.ItemDataRole.DisplayRole:
value = self._data[index.row()][index.column()]
if value is not None:
return self.row_display(value)
else:
return ""
case Qt.ItemDataRole.UserRole:
return self._data[index.row()]
case Qt.ItemDataRole.BackgroundRole:
color = self.row_background(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.ForegroundRole:
color = self.row_foreground(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.FontRole:
font = self.row_font(self._data[index.row()])
if font is None:
return QVariant()
return font
return QVariant()
class BaseListModel(DataMixin, QAbstractListModel):
"""
List model backed by a list of row tuples.
Each row is treated as a single list item. DisplayRole
is rendered via row_display() which returns the first
element by default.
Override row_display() to customize how items appear in the view.
"""
def rowCount(self, parent = QModelIndex()) -> int:
return len(self._data)
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return QVariant()
match role:
case Qt.ItemDataRole.DisplayRole:
value = self._data[index.row()]
if value is not None:
return self.row_display(value)
return ""
case Qt.ItemDataRole.UserRole:
return self._data[index.row()]
case Qt.ItemDataRole.BackgroundRole:
color = self.row_background(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.ForegroundRole:
color = self.row_foreground(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.FontRole:
font = self.row_font(self._data[index.row()])
if font is None:
return QVariant()
return font
return QVariant()
class BaseCardModel(DataMixin, QAbstractListModel):
"""
List model for card-based views using a custom QStyledItemDelegate.
Does not implement DisplayRole — visual rendering is handled
entirely by a delegate. Raw row data is available via UserRole.
Note:
You must provide a QStyledItemDelegate subclass to render items.
Without a delegate, the view will appear empty.
"""
def rowCount(self, parent = QModelIndex()) -> int:
return len(self._data)
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return QVariant()
match role:
case Qt.ItemDataRole.UserRole:
return self._data[index.row()]
case Qt.ItemDataRole.BackgroundRole:
color = self.row_background(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.ForegroundRole:
color = self.row_foreground(self._data[index.row()])
if color is None:
return QVariant()
brush = QBrush(color)
return brush
case Qt.ItemDataRole.FontRole:
font = self.row_font(self._data[index.row()])
if font is None:
return QVariant()
return font
return QVariant()