Source code for spinetoolbox.mvcmodels.compound_table_model

######################################################################################################################
# Copyright (C) 2017-2021 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Models that vertically concatenate two or more table models.

:authors: M. Marin (KTH)
:date:   9.10.2019
"""

from PySide2.QtCore import Qt, Signal, Slot, QModelIndex
from ..mvcmodels.minimal_table_model import MinimalTableModel


[docs]class CompoundTableModel(MinimalTableModel): """A model that concatenates several sub table models vertically."""
[docs] refreshed = Signal()
def __init__(self, parent=None, header=None): """Initializes model. Args: parent (QObject): the parent object """ super().__init__(parent=parent, header=header) self.sub_models = [] self._row_map = [] # Maps compound row to tuple (sub_model, sub_row) self._inv_row_map = {} # Maps tuple (sub_model, sub_row) to compound row self._fetch_sub_model = None
[docs] def map_to_sub(self, index): """Returns an equivalent submodel index. Args: index (QModelIndex): the compound model index. Returns: QModelIndex: the equivalent index in one of the submodels """ if not index.isValid(): return QModelIndex() row = index.row() column = index.column() try: sub_model, sub_row = self._row_map[row] except IndexError: return QModelIndex() return sub_model.index(sub_row, column)
[docs] def map_from_sub(self, sub_model, sub_index): """Returns an equivalent compound model index. Args: sub_model (MinimalTableModel): the submodel sub_index (QModelIndex): the submodel index. Returns: QModelIndex: the equivalent index in the compound model """ try: row = self._inv_row_map[sub_model, sub_index.row()] except KeyError: return QModelIndex() return self.index(row, sub_index.column())
[docs] def item_at_row(self, row): """Returns the item at given row. Args: row (int) Returns: object """ sub_model, sub_row = self._row_map[row] return sub_model._main_data[sub_row]
[docs] def sub_model_at_row(self, row): """Returns the submodel corresponding to the given row in the compound model. Args: row (int): Returns: MinimalTableModel """ sub_model, _ = self._row_map[row] return sub_model
[docs] def refresh(self): """Refreshes the layout by computing a new row map.""" self.layoutAboutToBeChanged.emit() self.do_refresh() self.layoutChanged.emit()
[docs] def do_refresh(self): """Recomputes the row and inverse row maps.""" self._row_map.clear() self._inv_row_map.clear() for model in self.sub_models: row_map = self._row_map_for_model(model) self._append_row_map(row_map) self.refreshed.emit()
[docs] def _append_row_map(self, row_map): """Appends given row map to the tail of the model. Args: row_map (list): tuples (model, row number) """ for model_row_tup in row_map: self._inv_row_map[model_row_tup] = self.rowCount() self._row_map.append(model_row_tup)
@staticmethod
[docs] def _row_map_for_model(model): """Returns row map for given model. The base class implementation just returns all model rows. Args: model (MinimalTableModel) Returns: list: tuples (model, row number) """ return [(model, i) for i in range(model.rowCount())]
[docs] def canFetchMore(self, parent=QModelIndex()): """Returns True if any of the submodels that haven't been fetched yet can fetch more.""" for self._fetch_sub_model in self.sub_models: if self._fetch_sub_model.canFetchMore(self.map_to_sub(parent)): return True return False
[docs] def fetchMore(self, parent=QModelIndex()): """Fetches the next sub model and increments the fetched counter.""" self._fetch_sub_model.fetchMore(self.map_to_sub(parent)) if not self._fetch_sub_model.rowCount(): self.sub_models.remove(self._fetch_sub_model) self.layoutChanged.emit()
[docs] def flags(self, index): return self.map_to_sub(index).flags()
[docs] def data(self, index, role=Qt.DisplayRole): return self.map_to_sub(index).data(role)
[docs] def rowCount(self, parent=QModelIndex()): """Returns the sum of rows in all models.""" return len(self._row_map)
[docs] def batch_set_data(self, indexes, data): """Sets data for indexes in batch. Distributes indexes and values among the different submodels and calls batch_set_data on each of them.""" if not indexes or not data: return False d = {} # Maps models to (index, value) tuples rows = [] columns = [] for index, value in zip(indexes, data): if not index.isValid(): continue rows.append(index.row()) columns.append(index.column()) sub_model, _ = self._row_map[index.row()] sub_index = self.map_to_sub(index) d.setdefault(sub_model, list()).append((sub_index, value)) for model, index_value_tuples in d.items(): indexes, values = zip(*index_value_tuples) model.batch_set_data(list(indexes), list(values)) # Find square envelope of indexes to emit dataChanged top = min(rows) bottom = max(rows) left = min(columns) right = max(columns) self.dataChanged.emit(self.index(top, left), self.index(bottom, right)) return True
[docs] def insertRows(self, row, count, parent=QModelIndex()): """Inserts count rows after the given row under the given parent. Localizes the appropriate submodel and calls insertRows on it. """ if row < 0 or row > self.rowCount(): return False if count < 1: return False try: sub_model, sub_row = self._row_map[row] except IndexError: sub_model, sub_row = self._row_map[-1] self.beginInsertRows(parent, row, row + count - 1) sub_model.insertRows(sub_row, count, self.map_to_sub(parent)) self.endInsertRows() self.refresh() return True
[docs] def removeRows(self, row, count, parent=QModelIndex()): """Removes count rows starting with the given row under parent. Localizes the appropriate submodels and calls removeRows on it. """ if row < 0 or row > self.rowCount(): return False if count < 1: return False first = row last = row + count - 1 self.beginRemoveRows(parent, first, last) while first <= last: try: sub_model, sub_row = self._row_map[first] sub_count = min(sub_model.rowCount(), count) first += sub_count count -= sub_count except IndexError: sub_model, sub_row = self._row_map[-1] sub_count = min(sub_model.rowCount(), count) break finally: sub_model.removeRows(sub_row, sub_count, self.map_to_sub(parent)) self.endRemoveRows() self.refresh() return True
[docs]class CompoundWithEmptyTableModel(CompoundTableModel): """A compound parameter table model where the last model is an empty row model.""" @property
[docs] def single_models(self): return self.sub_models[:-1]
@property
[docs] def empty_model(self): return self.sub_models[-1]
[docs] def _create_single_models(self): """Returns a list of single models.""" raise NotImplementedError()
[docs] def _create_empty_model(self): """Returns an empty model.""" raise NotImplementedError()
[docs] def init_model(self): """Initializes the compound model. Basically populates the sub_models list attribute with the result of _create_single_models and _create_empty_model. """ self.clear_model() self.sub_models = self._create_single_models() self.sub_models.append(self._create_empty_model()) self.connect_model_signals()
[docs] def connect_model_signals(self): """Connects signals so changes in the submodels are acknowledge by the compound.""" self.empty_model.rowsRemoved.connect(self._handle_empty_rows_removed) self.empty_model.rowsInserted.connect(self._handle_empty_rows_inserted) for model in self.single_models: model.modelReset.connect(lambda model=model: self._handle_single_model_reset(model)) for model in self.sub_models: model.dataChanged.connect( lambda top_left, bottom_right, roles, model=model: self.dataChanged.emit( self.map_from_sub(model, top_left), self.map_from_sub(model, bottom_right), roles
) )
[docs] def _recompute_empty_row_map(self): """Recomputeds the part of the row map corresponding to the empty model.""" empty_row_map = self._row_map_for_model(self.empty_model) try: row = self._inv_row_map[self.empty_model, 0] self._row_map = self._row_map[:row] except KeyError: pass self._append_row_map(empty_row_map)
@Slot("QModelIndex", "int", "int")
[docs] def _handle_empty_rows_removed(self, parent, empty_first, empty_last): """Runs when rows are removed from the empty model. Updates row_map, then emits rowsRemoved so the removed rows are no longer visible. """ first = self._inv_row_map[self.empty_model, empty_first] last = self._inv_row_map[self.empty_model, empty_last] self._recompute_empty_row_map() self.rowsRemoved.emit(QModelIndex(), first, last)
@Slot("QModelIndex", "int", "int")
[docs] def _handle_empty_rows_inserted(self, parent, empty_first, empty_last): """Runs when rows are inserted to the empty model. Updates row_map, then emits rowsInserted so the new rows become visible. """ self._recompute_empty_row_map() first = self._inv_row_map[self.empty_model, empty_first] last = self._inv_row_map[self.empty_model, empty_last] self.rowsInserted.emit(QModelIndex(), first, last)
[docs] def _handle_single_model_reset(self, single_model): """Runs when one of the single models is reset. Updates row_map, then emits rowsInserted so the new rows become visible. """ single_row_map = self._row_map_for_model(single_model) self._insert_single_row_map(single_row_map)
[docs] def _insert_single_row_map(self, single_row_map): """Inserts given row map just before the empty model's.""" if not single_row_map: return try: row = self._inv_row_map[self.empty_model, 0] self._row_map, empty_row_map = self._row_map[:row], self._row_map[row:] except KeyError: row = self.rowCount() empty_row_map = [] self._append_row_map(single_row_map) self._append_row_map(empty_row_map) first = row last = row + len(single_row_map) - 1 self.rowsInserted.emit(QModelIndex(), first, last)
[docs] def clear_model(self): """Clears the model.""" if self._row_map: self.beginResetModel() self._row_map.clear() self.endResetModel() for m in self.sub_models: m.deleteLater() self.sub_models.clear() self._inv_row_map.clear()