# Copyright 2025–2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Module for managing item models in the radioviz application.
This module provides the :class:`ItemModel` class which implements the
:class:`QAbstractTableModel` interface to display items from an
:class:`~radioviz.models.item_store.ItemStore` in a Qt table view.
"""
from __future__ import annotations
from typing import Any, Generic, Optional, TypeVar
from PySide6.QtCore import QAbstractTableModel, QModelIndex, QObject, QPersistentModelIndex, Qt
from radioviz.models.item_store import IdentifiableItem, ItemStore, StoreEvent
T = TypeVar('T', bound=IdentifiableItem)
[docs]
class ItemModel(QAbstractTableModel, Generic[T]):
"""
Abstract table model for displaying items from an item store.
This class provides a generic implementation of a Qt table model that
displays items from an :class:`~radioviz.models.item_store.ItemStore`.
It automatically updates the view when the underlying store changes.
:ivar headers: List of column headers for the table model
:vartype headers: list[str]
"""
@classmethod
def __class_getitem__(cls, item: T) -> 'type[ItemModel[T]]':
return cls
def __init__(self, store: ItemStore[T], parent: Optional[QObject] = None) -> None:
"""
Initialize the item model with a data store.
:param store: The item store to display in the model
:type store: ItemStore[T]
:param parent: Parent object for the model
:type parent: QObject or None
"""
super().__init__(parent)
self._store: ItemStore[T] = store
self._store.add_listener(self.on_store_change)
self.headers: list[str] = [] # user must overload this
[docs]
def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
"""
Return the number of rows in the model.
:param parent: Parent index (unused)
:type parent: QModelIndex or QPersistentModelIndex
:return: Number of items in the store
:rtype: int
"""
return len(self._store)
[docs]
def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
"""
Return the number of columns in the model.
:param parent: Parent index (unused)
:type parent: QModelIndex or QPersistentModelIndex
:return: Number of headers
:rtype: int
"""
return len(self.headers)
[docs]
def data(
self,
index: QModelIndex | QPersistentModelIndex,
role: int = Qt.ItemDataRole.DisplayRole,
) -> Any:
"""
Return the data for the given index and role.
:param index: The model index
:type index: QModelIndex or QPersistentModelIndex
:param role: The data role
:type role: int
:return: Data for the cell or None
:rtype: Any
"""
if not index.isValid() or index.row() >= len(self._store):
return None
item = self._store[index.row()]
return self.data_for(item, index.column(), role)
[docs]
def data_for(self, item: T, column: int, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
"""
Return the data for a specific item and column.
This method must be implemented by subclasses to provide the actual
data display logic for each cell.
:param item: The item to get data for
:type item: T
:param column: The column index
:type column: int
:param role: The data role
:type role: int
:return: Data for the cell
:rtype: Any
:raise NotImplementedError: Always raised as this is an abstract method
"""
raise NotImplementedError
[docs]
def setData(
self,
index: QModelIndex | QPersistentModelIndex,
value: Any,
role: int = Qt.ItemDataRole.DisplayRole,
) -> bool:
"""
Set the data for the given index and role.
:param index: The model index
:type index: QModelIndex or QPersistentModelIndex
:param value: The value to set
:type value: Any
:param role: The data role
:type role: int
:return: True if the data was set successfully, False otherwise
:rtype: bool
"""
if not index.isValid() or index.row() >= len(self._store):
return False
item = self._store[index.row()]
return self.set_data_for(item, index.column(), value, role)
[docs]
def set_data_for(self, item: T, column: int, value: Any, role: int = Qt.ItemDataRole.DisplayRole) -> bool:
"""
Set the data for a specific item and column.
This method must be implemented by subclasses to provide the actual
data setting logic for each cell.
:param item: The item to set data for
:type item: T
:param column: The column index
:type column: int
:param value: The value to set
:type value: Any
:param role: The data role
:type role: int
:return: True if the data was set successfully, False otherwise
:rtype: bool
:raise NotImplementedError: Always raised as this is an abstract method
"""
raise NotImplementedError
[docs]
def on_store_change(self, event: StoreEvent, data: Optional[T] = None, index: int = 0) -> None:
"""
Handle changes in the underlying store.
This method is called when the store emits a change event and updates
the model accordingly.
:param event: The type of store event
:type event: StoreEvent
:param data: The data associated with the event
:type data: T
:param index: The index where the event occurred
:type index: int
"""
if event == StoreEvent.ADDED:
self.beginInsertRows(QModelIndex(), index, index)
self.endInsertRows()
elif event == StoreEvent.REMOVED:
self.beginRemoveRows(QModelIndex(), index, index)
self.endRemoveRows()
elif event == StoreEvent.CLEARED:
self.beginResetModel()
self.endResetModel()
elif event == StoreEvent.UPDATED:
if index < 0 or index >= self.rowCount():
return
top_left = self.index(index, 0)
bottom_right = self.index(index, self.columnCount() - 1)
if not top_left.isValid():
return
self.dataChanged.emit(top_left, bottom_right)