######################################################################################################################
# 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/>.
######################################################################################################################
"""
Custom QTableView classes that support copy-paste and the like.
:author: M. Marin (KTH)
:date: 18.5.2018
"""
from PySide2.QtCore import Qt, Signal, Slot
from PySide2.QtWidgets import QAction, QTableView, QMenu
from PySide2.QtGui import QKeySequence
from ...widgets.report_plotting_failure import report_plotting_failure
from ...widgets.plot_widget import PlotWidget, _prepare_plot_in_window_menu
from ...widgets.custom_qtableview import CopyPasteTableView, AutoFilterCopyPasteTableView
from ...widgets.custom_qwidgets import TitleWidgetAction
from ...plotting import plot_selection, PlottingError, ParameterTablePlottingHints, PivotTablePlottingHints
from .pivot_table_header_view import PivotTableHeaderView
from .tabular_view_header_widget import TabularViewHeaderWidget
from .custom_delegates import (
DatabaseNameDelegate,
ParameterDefaultValueDelegate,
TagListDelegate,
ValueListDelegate,
ParameterValueDelegate,
ParameterNameDelegate,
ObjectClassNameDelegate,
ObjectNameDelegate,
RelationshipClassNameDelegate,
ObjectNameListDelegate,
AlternativeNameDelegate,
)
[docs]class ParameterTableView(AutoFilterCopyPasteTableView):
def __init__(self, parent):
"""Initialize the view."""
super().__init__(parent=parent)
self._menu = QMenu(self)
self._spine_db_editor = None
self._open_in_editor_action = None
self._plot_action = None
self._plot_separator = None
@property
[docs] def value_column_header(self):
"""Either "default value" or "value". Used to identifiy the value column for advanced editting and plotting.
"""
raise NotImplementedError()
[docs] def connect_spine_db_editor(self, spine_db_editor):
"""Connects a Spine db editor to work with this view.
Args:
spine_db_editor (SpineDBEditor)
"""
self._spine_db_editor = spine_db_editor
self.populate_context_menu()
self.create_delegates()
[docs] def _make_delegate(self, column_name, delegate_class):
"""Creates a delegate for the given column and returns it.
Args:
column_name (str)
delegate_class (ParameterDelegate)
Returns:
ParameterDelegate
"""
column = self.model().header.index(column_name)
delegate = delegate_class(self._spine_db_editor, self._spine_db_editor.db_mngr)
self.setItemDelegateForColumn(column, delegate)
delegate.data_committed.connect(self._spine_db_editor.set_parameter_data)
return delegate
[docs] def create_delegates(self):
"""Creates delegates for this view"""
self._make_delegate("database", DatabaseNameDelegate)
[docs] def open_in_editor(self):
"""Opens the current index in a parameter_value editor using the connected Spine db editor."""
index = self.currentIndex()
self._spine_db_editor.show_parameter_value_editor(index)
@Slot(bool)
[docs] def plot(self, checked=False):
"""Plots current index."""
selection = self.selectedIndexes()
try:
hints = ParameterTablePlottingHints()
plot_widget = plot_selection(self.model(), selection, hints)
except PlottingError as error:
report_plotting_failure(error, self._spine_db_editor)
else:
plot_widget.use_as_window(self.window(), self.value_column_header)
plot_widget.show()
@Slot(QAction)
[docs] def plot_in_window(self, action):
"""Plots current index in the window given by action's name."""
plot_window_name = action.text()
plot_window = PlotWidget.plot_windows.get(plot_window_name)
selection = self.selectedIndexes()
try:
hints = ParameterTablePlottingHints()
plot_selection(self.model(), selection, hints, plot_window)
plot_window.raise_()
except PlottingError as error:
report_plotting_failure(error, self._spine_db_editor)
[docs] def _selected_rows_per_column(self):
"""Computes selected rows per column.
Returns:
dict: Mapping columns to selected rows in that column.
"""
selection = self.selectionModel().selection()
if not selection:
return {}
v_header = self.verticalHeader()
h_header = self.horizontalHeader()
rows_per_column = {}
for rng in sorted(selection, key=lambda x: h_header.visualIndex(x.left())):
for j in range(rng.left(), rng.right() + 1):
if h_header.isSectionHidden(j):
continue
rows = rows_per_column.setdefault(j, set())
for i in range(rng.top(), rng.bottom() + 1):
if v_header.isSectionHidden(i):
continue
rows.add(i)
return rows_per_column
@Slot(bool)
[docs] def filter_by_selection(self, checked=False):
rows_per_column = self._selected_rows_per_column()
self.model().filter_by(rows_per_column)
@Slot(bool)
[docs] def filter_excluding_selection(self, checked=False):
rows_per_column = self._selected_rows_per_column()
self.model().filter_excluding(rows_per_column)
[docs] def remove_selected(self):
"""Removes selected indexes."""
selection = self.selectionModel().selection()
rows = list()
while not selection.isEmpty():
current = selection.takeFirst()
top = current.top()
bottom = current.bottom()
rows += range(top, bottom + 1)
# Get parameter data grouped by db_map
db_map_typed_data = dict()
model = self.model()
for row in sorted(rows, reverse=True):
db_map = model.sub_model_at_row(row).db_map
if db_map is None:
# It's an empty model, just remove the row
_, sub_row = model._row_map[row]
model.empty_model.removeRow(sub_row)
continue
id_ = model.item_at_row(row)
db_map_typed_data.setdefault(db_map, {}).setdefault(model.item_type, []).append(id_)
model.db_mngr.remove_items(db_map_typed_data)
self.selectionModel().clearSelection()
[docs]class ObjectParameterTableMixin:
[docs] def create_delegates(self):
super().create_delegates()
self._make_delegate("object_class_name", ObjectClassNameDelegate)
[docs]class RelationshipParameterTableMixin:
[docs] def create_delegates(self):
super().create_delegates()
self._make_delegate("relationship_class_name", RelationshipClassNameDelegate)
[docs]class ParameterDefinitionTableView(ParameterTableView):
@property
[docs] def value_column_header(self):
return "default_value"
[docs] def create_delegates(self):
super().create_delegates()
self._make_delegate("parameter_tag_list", TagListDelegate)
self._make_delegate("value_list_name", ValueListDelegate)
delegate = self._make_delegate("default_value", ParameterDefaultValueDelegate)
delegate.parameter_value_editor_requested.connect(self._spine_db_editor.show_parameter_value_editor)
[docs]class ParameterValueTableView(ParameterTableView):
def __init__(self, parent):
"""Initialize the view."""
super().__init__(parent=parent)
self._show_value_metadata_action = None
@property
[docs] def value_column_header(self):
return "value"
[docs] def create_delegates(self):
super().create_delegates()
self._make_delegate("parameter_name", ParameterNameDelegate)
self._make_delegate("alternative_name", AlternativeNameDelegate)
delegate = self._make_delegate("value", ParameterValueDelegate)
delegate.parameter_value_editor_requested.connect(self._spine_db_editor.show_parameter_value_editor)
[docs]class ObjectParameterDefinitionTableView(ObjectParameterTableMixin, ParameterDefinitionTableView):
"""A custom QTableView for the object parameter_definition pane in Spine db editor."""
[docs]class RelationshipParameterDefinitionTableView(RelationshipParameterTableMixin, ParameterDefinitionTableView):
"""A custom QTableView for the relationship parameter_definition pane in Spine db editor."""
[docs]class ObjectParameterValueTableView(ObjectParameterTableMixin, ParameterValueTableView):
"""A custom QTableView for the object parameter_value pane in Spine db editor."""
[docs] def create_delegates(self):
super().create_delegates()
self._make_delegate("object_name", ObjectNameDelegate)
[docs]class RelationshipParameterValueTableView(RelationshipParameterTableMixin, ParameterValueTableView):
"""A custom QTableView for the relationship parameter_value pane in Spine db editor."""
[docs] def create_delegates(self):
super().create_delegates()
delegate = self._make_delegate("object_name_list", ObjectNameListDelegate)
delegate.object_name_list_editor_requested.connect(self._spine_db_editor.show_object_name_list_editor)
[docs]class PivotTableView(CopyPasteTableView):
"""Custom QTableView class with pivot capabilities.
"""
[docs] _REMOVE_OBJECT = "Remove objects"
[docs] _REMOVE_RELATIONSHIP = "Remove relationships"
[docs] _REMOVE_PARAMETER = "Remove parameter definitions"
[docs] _REMOVE_ALTERNATIVE = "Remove alternatives"
[docs] _REMOVE_SCENARIO = "Remove scenarios"
def __init__(self, parent=None):
"""Initialize the class."""
super().__init__(parent)
self._spine_db_editor = None
self._menu = QMenu(self)
self._selected_value_indexes = list()
self._selected_entity_indexes = list()
self._selected_parameter_indexes = list()
self._selected_alternative_indexes = list()
self._selected_scenario_indexes = list()
self._open_in_editor_action = None
self._plot_action = None
self._plot_in_window_menu = None
self._remove_values_action = None
self._remove_objects_action = None
self._remove_relationships_action = None
self._remove_parameters_action = None
self._remove_alternatives_action = None
self._remove_scenarios_action = None
@property
[docs] def source_model(self):
return self.model().sourceModel()
@property
[docs] def db_mngr(self):
return self.source_model.db_mngr
[docs] def connect_spine_db_editor(self, spine_db_editor):
self._spine_db_editor = spine_db_editor
self.populate_context_menu()
h_header = PivotTableHeaderView(Qt.Horizontal, "columns", self)
h_header.setContextMenuPolicy(Qt.DefaultContextMenu)
h_header.setResizeContentsPrecision(spine_db_editor.visible_rows)
v_header = PivotTableHeaderView(Qt.Vertical, "rows", self)
v_header.setContextMenuPolicy(Qt.NoContextMenu)
v_header.setDefaultSectionSize(spine_db_editor.default_row_height)
self.setHorizontalHeader(h_header)
self.setVerticalHeader(v_header)
[docs] def remove_selected(self):
self.remove_values()
if self._can_remove_relationships():
self.remove_relationships()
self.remove_objects()
self.remove_parameters()
self.remove_alternatives()
self.remove_scenarios()
[docs] def remove_values(self):
row_mask = set()
column_mask = set()
for index in self._selected_value_indexes:
row, column = self.source_model.map_to_pivot(index)
row_mask.add(row)
column_mask.add(column)
data = self.source_model.model.get_pivoted_data(row_mask, column_mask)
items = (item for row in data for item in row)
db_map_typed_data = {}
for item in items:
if item is None:
continue
db_map, id_ = item
db_map_typed_data.setdefault(db_map, {}).setdefault("parameter_value", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def remove_objects(self):
db_map_typed_data = {}
for index in self._selected_entity_indexes:
db_map, id_ = self.source_model._header_id(index)
db_map_typed_data.setdefault(db_map, {}).setdefault("object", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def remove_relationships(self):
db_map_relationship_lookup = {
db_map: {rel["object_id_list"]: rel["id"] for rel in rels}
for db_map, rels in self._spine_db_editor._get_db_map_entities().items()
}
db_map_typed_data = {}
for index in self._selected_entity_indexes:
db_map, object_ids = self.source_model.db_map_object_ids(index)
object_id_list = ",".join([str(id_) for id_ in object_ids])
id_ = db_map_relationship_lookup.get(db_map, {}).get(object_id_list)
if id_:
db_map_typed_data.setdefault(db_map, {}).setdefault("relationship", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def remove_parameters(self):
db_map_typed_data = {}
for index in self._selected_parameter_indexes:
db_map, id_ = self.source_model._header_id(index)
db_map_typed_data.setdefault(db_map, {}).setdefault("parameter_definition", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def remove_alternatives(self):
db_map_typed_data = {}
for index in self._selected_alternative_indexes:
db_map, id_ = self.source_model._header_id(index)
db_map_typed_data.setdefault(db_map, {}).setdefault("alternative", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def remove_scenarios(self):
db_map_typed_data = {}
for index in self._selected_scenario_indexes:
db_map, id_ = self.source_model._header_id(index)
db_map_typed_data.setdefault(db_map, {}).setdefault("scenario", set()).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
[docs] def open_in_editor(self):
"""Opens the parameter_value editor for the first selected cell."""
index = self._selected_value_indexes[0]
self._spine_db_editor.show_parameter_value_editor(index)
[docs] def plot(self):
"""Plots the selected cells in the pivot table."""
selected_indexes = self.selectedIndexes()
hints = PivotTablePlottingHints()
try:
plot_window = plot_selection(self.model(), selected_indexes, hints)
except PlottingError as error:
report_plotting_failure(error, self)
return
plotted_column_names = {
hints.column_label(self.model(), index.column())
for index in selected_indexes
if hints.is_index_in_data(self.model(), index)
}
plot_window.use_as_window(self.parentWidget(), ", ".join(plotted_column_names))
plot_window.show()
[docs] def _refresh_selected_indexes(self):
self._selected_value_indexes = list()
self._selected_entity_indexes = list()
self._selected_parameter_indexes = list()
self._selected_alternative_indexes = list()
self._selected_scenario_indexes = list()
indexes = [self.model().mapToSource(ind) for ind in self.selectedIndexes()]
for index in indexes:
if self.source_model.index_in_data(index):
self._selected_value_indexes.append(index)
elif self.source_model.index_in_headers(index):
top_left_id = self.source_model.top_left_id(index)
header_type = self.source_model.top_left_headers[top_left_id].header_type
if header_type == "parameter":
self._selected_parameter_indexes.append(index)
elif header_type == "object":
self._selected_entity_indexes.append(index)
elif header_type == "alternative":
self._selected_alternative_indexes.append(index)
elif header_type == "scenario":
self._selected_scenario_indexes.append(index)
[docs] def _update_actions_availability(self):
self._open_in_editor_action.setEnabled(len(self._selected_value_indexes) == 1)
self._plot_action.setEnabled(len(self._selected_value_indexes) > 0)
self._remove_values_action.setEnabled(bool(self._selected_value_indexes))
self._remove_objects_action.setEnabled(bool(self._selected_entity_indexes))
self._remove_relationships_action.setEnabled(
bool(self._selected_entity_indexes) and self._can_remove_relationships()
)
self._remove_parameters_action.setEnabled(bool(self._selected_parameter_indexes))
self._remove_alternatives_action.setEnabled(bool(self._selected_alternative_indexes))
self._remove_scenarios_action.setEnabled(bool(self._selected_scenario_indexes))
[docs] def _can_remove_relationships(self):
return (
self.model().sourceModel().item_type == "parameter_value"
and self._spine_db_editor.current_class_type == "relationship_class"
)
@Slot(QAction)
[docs] def _plot_in_window(self, action):
window_id = action.text()
plot_window = PlotWidget.plot_windows.get(window_id)
if plot_window is None:
self.plot()
return
selected_indexes = self.selectedIndexes()
hints = PivotTablePlottingHints()
try:
plot_selection(self.model(), selected_indexes, hints, plot_window)
plot_window.raise_()
except PlottingError as error:
report_plotting_failure(error, self)
[docs]class FrozenTableView(QTableView):
@property
[docs] def area(self):
return "frozen"
[docs] def dragEnterEvent(self, event):
if isinstance(event.source(), TabularViewHeaderWidget):
event.accept()
[docs] def dragMoveEvent(self, event):
if isinstance(event.source(), TabularViewHeaderWidget):
event.accept()
[docs] def dropEvent(self, event):
self.header_dropped.emit(event.source(), self)