Source code for spinetoolbox.spine_db_editor.mvcmodels.tree_item_utility

######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# 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/>.
######################################################################################################################

"""A tree model for parameter_value lists."""
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QFont, QIcon, QGuiApplication
from spinetoolbox.mvcmodels.minimal_tree_model import TreeItem
from spinetoolbox.helpers import CharIconEngine, bisect_chunks, plain_to_tool_tip
from spinetoolbox.fetch_parent import FlexibleFetchParent


[docs]class StandardTreeItem(TreeItem): """A tree item that fetches their children as they are inserted.""" @property
[docs] def item_type(self): return None
@property
[docs] def db_mngr(self): return self.model.db_mngr
@property
[docs] def display_data(self): return None
@property
[docs] def icon_code(self): return None
[docs] def tool_tip(self, column): return None
@property
[docs] def display_icon(self): if self.icon_code is None: return None engine = CharIconEngine(self.icon_code, 0) return QIcon(engine.pixmap())
[docs] def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.ToolTipRole: return self.tool_tip(column) if column != 0: return None if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return self.display_data if role == Qt.ItemDataRole.DecorationRole: return self.display_icon return super().data(0, role)
[docs] def set_data(self, column, value, role=Qt.ItemDataRole.DisplayRole): return False
@property
[docs] def non_empty_children(self): return self.children
@property
[docs] def children_ids(self): for child in self.non_empty_children: try: yield child.id except AttributeError: pass
[docs]class EditableMixin:
[docs] def flags(self, column): """Makes items editable.""" return Qt.ItemIsEditable | super().flags(column)
[docs]class GrayIfLastMixin: """Paints the item gray if it's the last."""
[docs] def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ForegroundRole and self.child_number() == self.parent_item.row_count() - 1: gray_color = QGuiApplication.palette().text().color() gray_color.setAlpha(128) gray_brush = QBrush(gray_color) return gray_brush return super().data(column, role)
[docs]class BoldTextMixin: """Bolds text."""
[docs] def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.FontRole: bold_font = QFont() bold_font.setBold(True) return bold_font return super().data(column, role)
[docs]class EmptyChildMixin: """Guarantees there's always an empty child.""" @property
[docs] def non_empty_children(self): return self.children[:-1]
[docs] def empty_child(self): raise NotImplementedError()
[docs] def _do_set_up(self): super()._do_set_up() empty_child = self.empty_child() self.append_children([empty_child])
[docs]class SortChildrenMixin:
[docs] def _children_sort_key(self, child): return child.data(0)
[docs] def insert_children_sorted(self, children): for child in children: child.parent_item = self for chunk, pos in bisect_chunks(self.non_empty_children, children, key=self._children_sort_key): if not super().insert_children(pos, chunk): return False return True
[docs]class FetchMoreMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._natural_fetch_parent = FlexibleFetchParent( self.fetch_item_type, handle_items_added=self.handle_items_added, handle_items_removed=self.handle_items_removed, handle_items_updated=self.handle_items_updated, accepts_item=self.accepts_item, )
[docs] def tear_down(self): super().tear_down() self._natural_fetch_parent.set_obsolete(True) self._natural_fetch_parent.deleteLater()
@property
[docs] def fetch_item_type(self): return self.item_type
[docs] def _fetch_parents(self): yield self._natural_fetch_parent
[docs] def can_fetch_more(self): result = False for parent in self._fetch_parents(): result |= self.db_mngr.can_fetch_more(self.db_map, parent) return result
[docs] def fetch_more(self): for parent in self._fetch_parents(): self.db_mngr.fetch_more(self.db_map, parent)
[docs] def _make_child(self, id_): raise NotImplementedError()
[docs] def _do_make_child(self, id_): child = self._created_children.get(id_) if child is None: child = self._created_children[id_] = self._make_child(id_) return child
[docs] def accepts_item(self, item, db_map): return True
[docs] def handle_items_added(self, db_map_data): """Inserts items at right positions. Items with commit_id are kept sorted. Items without a commit_id are put at the end. Args: db_map_data (dict): mapping db_map to list of dict corresponding to db items """ db_items = db_map_data.get(self.db_map, []) ids_committed = [] ids_uncommitted = [] for item in db_items: if item["id"] in self.children_ids: continue ids = ids_committed if item.get("commit_id") is not None else ids_uncommitted ids.append(item["id"]) children_committed = [self._do_make_child(id_) for id_ in ids_committed] children_uncommitted = [self._do_make_child(id_) for id_ in ids_uncommitted] self.insert_children_sorted(children_committed) self.insert_children(len(self.non_empty_children), children_uncommitted)
[docs] def handle_items_removed(self, db_map_data): ids = {x["id"] for x in db_map_data.get(self.db_map, [])} removed_rows = [] for row, leaf_item in enumerate(self.children): if leaf_item.id and leaf_item.id in ids: removed_rows.append(row) for row in sorted(removed_rows, reverse=True): self.remove_children(row, 1)
[docs] def handle_items_updated(self, db_map_data): leaf_items = {leaf_item.id: leaf_item for leaf_item in self.children if leaf_item.id} ids = {x["id"] for x in db_map_data.get(self.db_map, [])} for id_ in set(ids).intersection(leaf_items): leaf_item = leaf_items[id_] leaf_item.handle_updated_in_db() index = self.model.index_from_item(leaf_item) self.model.dataChanged.emit(index, index) if leaf_item.children: top_left = self.model.index_from_item(leaf_item.child(0)) bottom_right = self.model.index_from_item(leaf_item.child(leaf_item.child_count() - 1)) self.model.dataChanged.emit(top_left, bottom_right)
[docs]class StandardDBItem(SortChildrenMixin, StandardTreeItem): """An item representing a db.""" def __init__(self, model, db_map): """Init class. Args: model (MinimalTreeModel) db_map (DatabaseMapping) """ super().__init__(model) self.db_map = db_map @property
[docs] def item_type(self): return "db"
[docs] def data(self, column, role=Qt.ItemDataRole.DisplayRole): """Shows Spine icon for fun.""" if column != 0: return None if role == Qt.ItemDataRole.DecorationRole: return QIcon(":/symbols/Spine_symbol.png") if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return self.db_map.codename
[docs]class LeafItem(StandardTreeItem): def __init__(self, model, identifier=None): """ Args: model (MinimalTreeModel) identifier (int, optional): item's database id """ super().__init__(model) self._id = identifier
[docs] def _make_item_data(self): return {"name": f"Type new {self.item_type} name here...", "description": ""}
@property
[docs] def item_type(self): raise NotImplementedError()
@property
[docs] def db_map(self): return self.parent_item.db_map
@property
[docs] def id(self): return self._id
@property
[docs] def item_data(self): if not self.id: return self._make_item_data() return self.db_mngr.get_item(self.db_map, self.item_type, self.id)
@property
[docs] def name(self): return self.item_data["name"]
[docs] def tool_tip(self, column): if column != 0 and (header_data := self.header_data(column)) == "description": return plain_to_tool_tip(self.item_data.get(header_data)) return super().tool_tip(column)
[docs] def add_item_to_db(self, db_item): raise NotImplementedError()
[docs] def update_item_in_db(self, db_item): raise NotImplementedError()
[docs] def header_data(self, column): return self.model.headerData(column, Qt.Orientation.Horizontal)
[docs] def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): data = self.item_data.get(self.header_data(column)) if data is None: data = "" return data return super().data(column, role)
[docs] def set_data(self, column, value, role=Qt.ItemDataRole.EditRole): if role != Qt.ItemDataRole.EditRole or value == self.data(column, role): return False if self.id: db_item = self._make_item_to_update(column, value) self.update_item_in_db(db_item) return True if column == 0: db_item = self._make_item_to_add(value) self.add_item_to_db(db_item) return True
[docs] def _make_item_to_add(self, value): return dict(name=value, description=self.item_data["description"])
[docs] def _make_item_to_update(self, column, value): field = self.header_data(column) return {"id": self.id, field: value}
[docs] def handle_updated_in_db(self): index = self.index() sibling = self.index().sibling(self.index().row(), 1) self.model.dataChanged.emit(index, sibling)
[docs] def can_fetch_more(self): return False