Source code for spinetoolbox.mvcmodels.minimal_tree_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 to represent items in a tree.

:authors: P. Vennström (VTT), M. Marin (KTH)
:date:   11.3.2019
"""
from PySide2.QtCore import Qt, QAbstractItemModel, QModelIndex


[docs]class TreeItem: """A tree item that can fetch its children.""" def __init__(self, model=None): """Initializes item. Args: model (MinimalTreeModel, NoneType): The model where the item belongs. """ self._model = model self._children = None self._parent_item = None self._fetched = False self.children = [] @property
[docs] def model(self): return self._model
@property
[docs] def child_item_type(self): """Returns the type of child items. Reimplement in subclasses to return something more meaningful.""" return TreeItem
@property
[docs] def children(self): return self._children
@children.setter def children(self, children): bad_types = [type(child) for child in children if not isinstance(child, TreeItem)] if bad_types: raise TypeError(f"Cand't set children of type {bad_types} for an item of type {type(self)}") for child in children: child.parent_item = self self._children = children @property
[docs] def parent_item(self): return self._parent_item
@parent_item.setter def parent_item(self, parent_item): if not isinstance(parent_item, TreeItem) and parent_item is not None: raise ValueError("Parent must be instance of TreeItem or None") self._parent_item = parent_item self._model = parent_item.model
[docs] def child(self, row): """Returns the child at given row or None if out of bounds.""" try: return self._children[row] except IndexError: return None
[docs] def last_child(self): """Returns the last child.""" return self.child(-1)
[docs] def child_count(self): """Returns the number of children.""" return len(self._children)
[docs] def child_number(self): """Returns the rank of this item within its parent or -1 if it's an orphan.""" if self.parent_item: return self.parent_item.children.index(self) return -1
[docs] def find_children(self, cond=lambda child: True): """Returns children that meet condition expressed as a lambda function.""" for child in self.children: if cond(child): yield child
[docs] def find_child(self, cond=lambda child: True): """Returns first child that meet condition expressed as a lambda function or None.""" return next(self.find_children(cond), None)
[docs] def next_sibling(self): """Returns the next sibling or None if it's the last.""" return self.parent_item.child(self.child_number() + 1)
[docs] def previous_sibling(self): """Returns the previous sibling or None if it's the first.""" if self.child_number() <= 0: return None return self.parent_item.child(self.child_number() - 1)
[docs] def index(self): return self.model.index_from_item(self)
[docs] def insert_children(self, position, *children): """Insert new children at given position. Returns a boolean depending on how it went. Args: position (int): insert new items here children (iter): insert items from this iterable """ bad_types = [type(child) for child in children if not isinstance(child, TreeItem)] if bad_types: raise TypeError(f"Cand't insert children of type {bad_types} to an item of type {type(self)}") if position < 0 or position > self.child_count(): return False children = list(children) for child in children: child.parent_item = self self.model.beginInsertRows(self.index(), position, position + len(children) - 1) self._children[position:position] = children self.model.endInsertRows() return True
[docs] def append_children(self, *children): """Append children at the end.""" return self.insert_children(self.child_count(), *children)
[docs] def remove_children(self, position, count): """Removes count children starting from the given position.""" first = position last = position + count - 1 if first >= self.child_count() or first < 0: return False if last >= self.child_count(): last = self.child_count() - 1 self.model.beginRemoveRows(self.index(), first, last) del self._children[first : last + 1] self.model.endRemoveRows() return True
[docs] def clear_children(self): """Clear children list.""" self.children.clear()
# pylint: disable=no-self-use
[docs] def flags(self, column): """Enables the item and makes it selectable.""" return Qt.ItemIsSelectable | Qt.ItemIsEnabled
# pylint: disable=no-self-use
[docs] def data(self, column, role=Qt.DisplayRole): """Returns data for given column and role.""" return None
[docs] def has_children(self): """Returns whether or not this item has or could have children.""" if self.child_count() or self.can_fetch_more(): return True return False
[docs] def can_fetch_more(self): """Returns whether or not this item can fetch more.""" return not self._fetched
[docs] def fetch_more(self): """Fetches more children.""" self._fetched = True
@property
[docs] def display_data(self): return "unnamed"
@property
[docs] def edit_data(self): return self.display_data
[docs] def set_data(self, column, value, role): """ Sets data for this item. Args: column (int): column index value (object): a new value role (int): role of the new value Returns: bool: True if data was set successfully, False otherwise """ raise NotImplementedError()
[docs]class MinimalTreeModel(QAbstractItemModel): """Base class for all tree models.""" def __init__(self, parent): """Init class. Args: parent (SpineDBEditor) """ super().__init__(parent) self._parent = parent self._invisible_root_item = TreeItem(self)
[docs] def visit_all(self, index=QModelIndex()): """Iterates all items in the model including and below the given index. Iterative implementation so we don't need to worry about Python recursion limits. """ if index.isValid(): ancient_one = self.item_from_index(index) else: ancient_one = self._invisible_root_item yield ancient_one child = ancient_one.last_child() if not child: return current = child visit_children = True while True: if visit_children: yield current child = current.last_child() if child: current = child continue sibling = current.previous_sibling() if sibling: visit_children = True current = sibling continue parent_item = current.parent_item if parent_item == ancient_one: break visit_children = False # To make sure we don't visit children again current = parent_item
[docs] def item_from_index(self, index): """Return the item corresponding to the given index.""" if index.isValid(): return index.internalPointer() return self._invisible_root_item
[docs] def index_from_item(self, item): """Return a model index corresponding to the given item.""" return self.createIndex(item.child_number(), 0, item)
[docs] def index(self, row, column, parent=QModelIndex()): """Returns the index of the item in the model specified by the given row, column and parent index.""" if not self.hasIndex(row, column, parent): return QModelIndex() parent_item = self.item_from_index(parent) item = parent_item.child(row) return self.createIndex(row, column, item)
[docs] def parent(self, index): """Returns the parent of the model item with the given index.""" if not index.isValid(): return QModelIndex() item = self.item_from_index(index) parent_item = item.parent_item if parent_item is None or parent_item == self._invisible_root_item: return QModelIndex() return self.createIndex(parent_item.child_number(), 0, parent_item)
[docs] def columnCount(self, parent=QModelIndex()): return 1
[docs] def rowCount(self, parent=QModelIndex()): if parent.column() > 0: return 0 parent_item = self.item_from_index(parent) return parent_item.child_count()
[docs] def data(self, index, role=Qt.DisplayRole): """Returns the data stored under the given role for the index.""" if not index.isValid(): return None item = self.item_from_index(index) return item.data(index.column(), role)
[docs] def setData(self, index, value, role=Qt.EditRole): """Sets data for given index and role. Returns True if successful; otherwise returns False. """ if not index.isValid(): return False item = self.item_from_index(index) if not item.set_data(index.column(), value, role): return False self.dataChanged.emit(index, index, []) return True
[docs] def flags(self, index): """Returns the item flags for the given index. """ item = self.item_from_index(index) return item.flags(index.column())
[docs] def hasChildren(self, parent): parent_item = self.item_from_index(parent) return parent_item.has_children()
[docs] def canFetchMore(self, parent): parent_item = self.item_from_index(parent) return parent_item.can_fetch_more()
[docs] def fetchMore(self, parent): parent_item = self.item_from_index(parent) parent_item.fetch_more()