######################################################################################################################
# Copyright (C) 2017-2020 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/>.
######################################################################################################################
"""
A tree model for parameter value lists.
:authors: M. Marin (KTH)
:date: 28.6.2019
"""
from PySide2.QtCore import Qt, Signal, QModelIndex
from PySide2.QtGui import QBrush, QFont, QIcon, QGuiApplication
from spinedb_api import to_database, from_database
from .minimal_tree_model import MinimalTreeModel, TreeItem
[docs]class EditableMixin:
[docs] def flags(self, column):
"""Makes items editable."""
return Qt.ItemIsEditable | super().flags(column)
[docs]class GrayFontMixin:
"""Paints the text gray."""
[docs] def data(self, column, role=Qt.DisplayRole):
if role == Qt.ForegroundRole and self.child_number() == self.parent_item.child_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 BoldFontMixin:
"""Bolds text."""
[docs] def data(self, column, role=Qt.DisplayRole):
if role == Qt.FontRole:
bold_font = QFont()
bold_font.setBold(True)
return bold_font
return super().data(column, role)
[docs]class AppendEmptyChildMixin:
"""Provides a method to append an empty child if needed."""
[docs] def append_empty_child(self, row):
"""Append empty child if the row is the last one."""
if row == self.child_count() - 1:
empty_child = self.empty_child()
self.append_children(empty_child)
[docs]class DBItem(AppendEmptyChildMixin, TreeItem):
"""An item representing a db."""
def __init__(self, db_map):
"""Init class.
Args
db_mngr (SpineDBManager)
db_map (DiffDatabaseMapping)
"""
super().__init__()
self.db_map = db_map
@property
[docs] def db_mngr(self):
return self.model.db_mngr
[docs] def fetch_more(self):
children = [
ListItem(self.db_map, value_list["id"], value_list["name"], value_list["value_list"].split(","))
for value_list in self.db_mngr.get_parameter_value_lists(self.db_map)
]
empty_child = self.empty_child()
self.append_children(*children, empty_child)
self._fetched = True
[docs] def empty_child(self):
return ListItem(self.db_map)
[docs] def data(self, column, role=Qt.DisplayRole):
"""Shows Spine icon for fun."""
if role == Qt.DecorationRole:
return QIcon(":/symbols/Spine_symbol.png")
if role == Qt.DisplayRole:
return f"root ({self.db_map.codename})"
[docs]class ListItem(GrayFontMixin, BoldFontMixin, AppendEmptyChildMixin, EditableMixin, TreeItem):
"""A list item."""
def __init__(self, db_map, identifier=None, name=None, value_list=()):
super().__init__()
self.db_map = db_map
self.id = identifier
self.name = name or "Type new list name here..."
self.value_list = value_list
@property
[docs] def db_mngr(self):
return self.model.db_mngr
[docs] def fetch_more(self):
children = [ValueItem(from_database(value)) for value in self.value_list]
empty_child = self.empty_child()
self.append_children(*children, empty_child)
self._fetched = True
[docs] def compile_value_list(self):
return [to_database(child.value) for child in self.children[:-1]]
# pylint: disable=no-self-use
[docs] def empty_child(self):
return ValueItem("Type new list value here...")
[docs] def data(self, column, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self.name
return super().data(column, role)
[docs] def set_data(self, column, name):
if name == self.name:
return False
if self.id:
self.update_name_in_db(name)
return False
self.name = name
self.parent_item.append_empty_child(self.child_number())
self.add_to_db()
return True
[docs] def set_child_data(self, child, value):
if value == child.value:
return False
if self.id:
self.update_value_list_in_db(child, value)
return False
child.value = value
self.append_empty_child(child.child_number())
self.add_to_db()
return True
[docs] def update_name_in_db(self, name):
db_item = dict(id=self.id, name=name)
self.db_mngr.update_parameter_value_lists({self.db_map: [db_item]})
[docs] def update_value_list_in_db(self, child, value):
value_list = self.compile_value_list()
value = to_database(value)
try:
value_list[child.child_number()] = value
except IndexError:
value_list.append(value)
db_item = dict(id=self.id, value_list=value_list)
self.db_mngr.update_parameter_value_lists({self.db_map: [db_item]})
[docs] def add_to_db(self):
"""Add item to db."""
value_list = self.compile_value_list()
db_item = dict(name=self.name, value_list=value_list)
self.db_mngr.add_parameter_value_lists({self.db_map: [db_item]})
[docs] def handle_updated_in_db(self, name, value_list):
"""Runs when an item with this id has been updated in the db."""
self.name = name
self.reset_value_list(value_list)
[docs] def handle_added_to_db(self, identifier, value_list):
"""Runs when the item with this name has been added to the db."""
self.id = identifier
self.reset_value_list(value_list)
[docs] def reset_value_list(self, value_list):
curr_value_list = self.compile_value_list()
if value_list == curr_value_list:
return
value_count = len(value_list)
curr_value_count = len(curr_value_list)
if value_count > curr_value_count:
added_count = value_count - curr_value_count
children = [ValueItem() for _ in range(added_count)]
self.insert_children(curr_value_count, *children)
elif curr_value_count > value_count:
removed_count = curr_value_count - value_count
self.remove_children(value_count, removed_count)
for child, value in zip(self.children, value_list):
child.value = from_database(value)
[docs]class ValueItem(GrayFontMixin, EditableMixin, TreeItem):
"""A value item."""
def __init__(self, value=None):
super().__init__()
self.value = value
self._fetched = True
[docs] def data(self, column, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self.value
return super().data(column, role)
[docs] def set_data(self, column, value):
return self.parent_item.set_child_data(self, value)
[docs]class ParameterValueListModel(MinimalTreeModel):
"""A model to display parameter value list data in a tree view.
Args:
parent (DataStoreForm)
db_mngr (SpineDBManager)
db_maps (iter): DiffDatabaseMapping instances
"""
[docs] remove_selection_requested = Signal()
[docs] remove_icon = QIcon(":/icons/minus.png")
def __init__(self, parent, db_mngr, *db_maps):
"""Initialize class"""
super().__init__(parent)
self.db_mngr = db_mngr
self.db_maps = db_maps
[docs] def receive_parameter_value_lists_added(self, db_map_data):
self.layoutAboutToBeChanged.emit()
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
items = {x["name"]: x for x in items}
for list_item in db_item.children[:-1]:
item = items.pop(list_item.name, None)
if not item:
continue
list_item.handle_added_to_db(identifier=item["id"], value_list=item["value_list"].split(","))
# Now append remaining items
children = [
ListItem(db_item.db_map, item["id"], item["name"], item["value_list"].split(","))
for item in items.values()
]
db_item.insert_children(db_item.child_count() - 1, *children)
self.layoutChanged.emit()
[docs] def receive_parameter_value_lists_updated(self, db_map_data):
self.layoutAboutToBeChanged.emit()
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
items = {x["id"]: x for x in items}
for list_item in db_item.children[:-1]:
item = items.get(list_item.id)
if not item:
continue
list_item.handle_updated_in_db(name=item["name"], value_list=item["value_list"].split(","))
self.layoutChanged.emit()
[docs] def receive_parameter_value_lists_removed(self, db_map_data):
self.layoutAboutToBeChanged.emit()
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
ids = {x["id"] for x in items}
removed_rows = []
for row, list_item in enumerate(db_item.children[:-1]):
if list_item.id in ids:
removed_rows.append(row)
for row in sorted(removed_rows, reverse=True):
db_item.remove_children(row, 1)
self.layoutChanged.emit()
[docs] def build_tree(self):
"""Initialize the internal data structure of the model."""
self.beginResetModel()
self._invisible_root_item = TreeItem(self)
self.endResetModel()
db_items = [DBItem(db_map) for db_map in self.db_maps]
self._invisible_root_item.append_children(*db_items)
for item in self.visit_all():
item.fetch_more()
[docs] def columnCount(self, parent=QModelIndex()):
"""Returns the number of columns under the given parent. Always 1.
"""
return 1