Source code for radioviz.views.item_model

#  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 headerData( self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole, ) -> Any: """ Return the header data for the given section and orientation. :param section: The section index :type section: int :param orientation: The orientation of the header :type orientation: Qt.Orientation :param role: The data role :type role: int :return: Header data or None :rtype: str or None """ if role != Qt.ItemDataRole.DisplayRole: return None if orientation == Qt.Orientation.Horizontal: try: return self.headers[section] except IndexError: return None return None
[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)