From bbee0ff0783c7d360cb9f2008479ed054490ec25 Mon Sep 17 00:00:00 2001
From: Manuel
Date: Tue, 19 Sep 2023 08:05:46 +0200
Subject: [PATCH] Support to save and load graph view states
---
.../spine_db_editor/graphics_items.py | 12 +-
.../ui/spine_db_editor_window.ui | 6 +
.../widgets/custom_qgraphicsviews.py | 329 +++++++++++-------
.../widgets/graph_view_mixin.py | 127 +++++--
spinetoolbox/ui/settings.py | 133 +++----
spinetoolbox/ui/settings.ui | 180 +++++-----
spinetoolbox/widgets/custom_qwidgets.py | 6 +
spinetoolbox/widgets/settings_widget.py | 45 ++-
8 files changed, 518 insertions(+), 320 deletions(-)
diff --git a/spinetoolbox/spine_db_editor/graphics_items.py b/spinetoolbox/spine_db_editor/graphics_items.py
index f92fdc0ad..ce7b2fb87 100644
--- a/spinetoolbox/spine_db_editor/graphics_items.py
+++ b/spinetoolbox/spine_db_editor/graphics_items.py
@@ -13,7 +13,7 @@
Classes for drawing graphics items on graph view's QGraphicsScene.
"""
from enum import Enum, auto
-from PySide6.QtCore import Qt, Signal, Slot, QLineF, QRectF, QPointF, QObject
+from PySide6.QtCore import Qt, Signal, Slot, QLineF, QRectF, QPointF, QObject, QByteArray
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from PySide6.QtWidgets import (
QGraphicsItem,
@@ -26,7 +26,7 @@
QMenu,
)
from PySide6.QtSvg import QSvgRenderer
-from PySide6.QtGui import QPen, QBrush, QPainterPath, QPalette, QGuiApplication, QAction, QColor
+from PySide6.QtGui import QPen, QBrush, QPainterPath, QPalette, QGuiApplication, QAction
from spinetoolbox.helpers import DB_ITEM_SEPARATOR, color_from_index
from spinetoolbox.widgets.custom_qwidgets import TitleWidgetAction
@@ -882,11 +882,12 @@ class Anchor(Enum):
Anchor.BR: Qt.SizeFDiagCursor,
}
- def __init__(self, file_path, parent=None):
+ def __init__(self, svg, parent=None):
super().__init__(parent)
self._renderer = QSvgRenderer()
self._svg_item = _ResizableQGraphicsSvgItem(self)
- _loading_ok = self._renderer.load(file_path)
+ self.svg = svg
+ _loading_ok = self._renderer.load(QByteArray(self.svg))
self._svg_item.setCacheMode(QGraphicsItem.CacheMode.NoCache) # Needed for the exported pdf to be vector
self._svg_item.setSharedRenderer(self._renderer)
self._scaling_factor = 1
@@ -945,9 +946,12 @@ def _do_resize(self, rect, strong):
self._place_resizers()
def fit_rect(self, rect):
+ if not isinstance(rect, QRectF):
+ rect = QRectF(*rect)
self._do_resize(rect, True)
def fit_coordinates(self, p1, p2, scen1, scen2):
+ # NOTE: not in use at the moment
size = self._renderer.defaultSize()
x1, y1 = p1
x2, y2 = p2
diff --git a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui
index a02da47d3..e60025278 100644
--- a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui
+++ b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui
@@ -1060,6 +1060,12 @@
spinetoolbox/spine_db_editor/widgets/custom_qwidgets.h
1
+
+ ProgressBarWidget
+ QWidget
+ spinetoolbox/spine_db_editor/widgets/custom_qwidgets.h
+ 1
+
diff --git a/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py b/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py
index fa91a6235..d0010edd6 100644
--- a/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py
+++ b/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py
@@ -29,6 +29,83 @@
from .select_graph_parameters_dialog import SelectGraphParametersDialog
+class _GraphProperty:
+ def __init__(self, name, settings_name):
+ self._name = name
+ self._settings_name = "appSettings/" + settings_name
+ self._spine_db_editor = None
+ self._value = None
+
+ @property
+ def value(self):
+ return self._value
+
+ def connect_spine_db_editor(self, spine_db_editor):
+ self._spine_db_editor = spine_db_editor
+
+
+class _GraphBoolProperty(_GraphProperty):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._action = None
+
+ @Slot(bool)
+ def _set_value(self, _checked=False, save_setting=True):
+ checked = self._action.isChecked()
+ if checked == self._value:
+ return
+ self._value = checked
+ if save_setting:
+ self._spine_db_editor.qsettings.setValue(self._settings_name, "true" if checked else "false")
+ self._spine_db_editor.build_graph()
+
+ def set_value(self, checked):
+ self._action.setChecked(checked)
+ self._set_value(save_setting=False)
+
+ def update(self, menu):
+ self._value = self._spine_db_editor.qsettings.value(self._settings_name, defaultValue="true") == "true"
+ self._action = menu.addAction(self._name)
+ self._action.setCheckable(True)
+ self._action.setChecked(self._value)
+ self._action.triggered.connect(self._set_value)
+
+
+class _GraphIntProperty(_GraphProperty):
+ def __init__(self, min_value, max_value, default_value, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._min_value, self._max_value, self._default_value = min_value, max_value, default_value
+ self._spin_box = None
+
+ @Slot(int)
+ def _set_value(self, _value=None, save_setting=True):
+ value = self._spin_box.value()
+ if value == self._value:
+ return
+ self._value = value
+ if save_setting:
+ self._spine_db_editor.qsettings.setValue(self._settings_name, str(value))
+ self._spine_db_editor.build_graph()
+
+ def set_value(self, value):
+ self._spin_box.setValue(value)
+ self._set_value(save_setting=False)
+
+ def update(self, menu):
+ self._value = int(
+ self._spine_db_editor.qsettings.value(self._settings_name, defaultValue=str(self._default_value))
+ )
+ action = ToolBarWidgetAction(self._name, menu, compact=True)
+ self._spin_box = HorizontalSpinBox(menu)
+ self._spin_box.setMinimum(self._min_value)
+ if self._max_value is not None:
+ self._spin_box.setMaximum(self._max_value)
+ self._spin_box.setValue(self._value)
+ self._spin_box.valueChanged.connect(self._set_value)
+ action.tool_bar.addWidget(self._spin_box)
+ menu.addAction(action)
+
+
class EntityQGraphicsView(CustomQGraphicsView):
"""QGraphicsView for the Entity Graph View."""
@@ -50,7 +127,6 @@ def __init__(self, parent):
self.arc_width_parameter = ""
self._margin = 0.05
self._bg_item = None
- self._bg_entity_coordinates = None
self.selected_items = []
self.removed_items = set()
self.hidden_items = {}
@@ -58,15 +134,21 @@ def __init__(self, parent):
self._hovered_ent_item = None
self.entity_class = None
self.cross_hairs_items = []
- self.auto_expand_entities = None
- self.merge_dbs = None
- self.max_entity_dimension = None
- self.disable_max_relationship_dimension = None
- self._auto_expand_entities_action = None
- self._merge_dbs_action = None
- self._max_ent_dim_action = None
- self._disable_max_ent_dim_action = None
- self._max_ent_dim_spin_box = None
+ self._properties = {
+ "auto_expand_entities": _GraphBoolProperty("Auto-expand entities", "autoExpandEntities"),
+ "merge_dbs": _GraphBoolProperty("Merge databases", "mergeDBs"),
+ "snap_entities": _GraphBoolProperty("Snap entities to grid", "snapEntities"),
+ "max_entity_dimension_count": _GraphIntProperty(
+ 2, None, 5, "Max. entity dimension count", "maxEntityDimensionCount"
+ ),
+ "build_iters": _GraphIntProperty(3, None, 12, "Number of build iterations", "layoutAlgoBuildIterations"),
+ "spread_factor": _GraphIntProperty(
+ 1, 100, 100, "Minimum distance between nodes (%)", "layoutAlgoSpreadFactor"
+ ),
+ "neg_weight_exp": _GraphIntProperty(
+ 1, 100, 2, "Decay rate of attraction with distance", "layoutAlgoNegWeightExp"
+ ),
+ }
self._add_entities_action = None
self._select_graph_params_action = None
self._save_pos_action = None
@@ -83,13 +165,44 @@ def __init__(self, parent):
self._arc_length_action = None
self._find_action = None
self._select_bg_image_action = None
+ self._save_state_action = None
self._previous_mouse_pos = None
self._context_menu_pos = None
self._hide_classes_menu = None
self._show_hidden_menu = None
self._prune_classes_menu = None
self._restore_pruned_menu = None
+ self._load_state_menu = None
+ self._remove_state_menu = None
self._items_per_class = {}
+ self._db_map_graph_data_by_name = {}
+
+ def get_property(self, name):
+ return self._properties[name].value
+
+ def set_property(self, name, value):
+ return self._properties[name].set_value(value)
+
+ def get_all_properties(self):
+ return {name: prop.value for name, prop in self._properties.items()}
+
+ def set_many_properties(self, props):
+ for name, value in props.items():
+ self.set_property(name, value)
+
+ def set_pruned_entity_ids(self, db_map, ids):
+ self.pruned_db_map_entity_ids = {"pruned from saved view": {(db_map, id_) for id_ in ids}}
+
+ def get_pruned_entity_ids(self, db_map):
+ return [
+ id_
+ for db_map_ids in self.pruned_db_map_entity_ids.values()
+ for db_map_, id_ in db_map_ids
+ if db_map_ is db_map
+ ]
+
+ def get_pruned_db_map_entity_ids(self):
+ return [db_map_id for db_map_ids in self.pruned_db_map_entity_ids.values() for db_map_id in db_map_ids]
@property
def _qsettings(self):
@@ -116,50 +229,14 @@ def handle_scene_selection_changed(self):
def connect_spine_db_editor(self, spine_db_editor):
self._spine_db_editor = spine_db_editor
+ for prop in self._properties.values():
+ prop.connect_spine_db_editor(spine_db_editor)
self.populate_context_menu()
def populate_context_menu(self):
- self.auto_expand_entities = (
- self._qsettings.value("appSettings/autoExpandObjects", defaultValue="true") == "true"
- )
- self.merge_dbs = self._qsettings.value("appSettings/mergeDBs", defaultValue="true") == "true"
- self.max_entity_dimension = int(self._qsettings.value("appSettings/maxRelationshipDimension", defaultValue="2"))
- self.disable_max_relationship_dimension = (
- self._qsettings.value("appSettings/disableMaxRelationshipDimension", defaultValue="true") == "true"
- )
- self._auto_expand_entities_action = self._menu.addAction("Auto-expand entities")
- self._auto_expand_entities_action.setCheckable(True)
- self._auto_expand_entities_action.setChecked(self.auto_expand_entities)
- self._auto_expand_entities_action.triggered.connect(self._set_auto_expand_entities)
- self._merge_dbs_action = self._menu.addAction("Merge databases")
- self._merge_dbs_action.setCheckable(True)
- self._merge_dbs_action.setChecked(self.merge_dbs)
- self._merge_dbs_action.triggered.connect(self._set_merge_dbs)
- self._max_ent_dim_action = ToolBarWidgetAction("Max entity dimension", self._menu, compact=True)
- self._max_ent_dim_spin_box = HorizontalSpinBox(self)
- self._max_ent_dim_spin_box.setMinimum(2)
- self._max_ent_dim_spin_box.setValue(self.max_entity_dimension)
- self._max_ent_dim_spin_box.valueChanged.connect(self._set_max_relationship_dimension)
- self._max_ent_dim_action.tool_bar.addWidget(self._max_ent_dim_spin_box)
- self._max_ent_dim_action.tool_bar.addSeparator()
- self._disable_max_ent_dim_action = self._max_ent_dim_action.tool_bar.addAction("\u221E")
- self._disable_max_ent_dim_action.setCheckable(True)
- self._disable_max_ent_dim_action.toggled.connect(self._set_disable_max_relationship_dimension)
- self._disable_max_ent_dim_action.toggled.connect(self._max_ent_dim_spin_box.setDisabled)
- self._disable_max_ent_dim_action.setToolTip("No limit")
- self._disable_max_ent_dim_action.setChecked(self.disable_max_relationship_dimension)
- self._menu.addAction(self._max_ent_dim_action)
- self._menu.addSeparator()
self._add_entities_action = self._menu.addAction("Add entities...", self.add_entities_at_position)
self._menu.addSeparator()
- self._find_action = self._menu.addAction("Find...", self._find)
- self._menu.addAction(self._find_action)
- self._menu.addSeparator()
- self._select_graph_params_action = self._menu.addAction(
- "Select graph parameters...", self.select_graph_parameters
- )
- self._save_pos_action = self._menu.addAction("Save positions", self.save_positions)
- self._clear_pos_action = self._menu.addAction("Clear saved positions", self.clear_saved_positions)
+ self._find_action = self._menu.addAction("Search...", self._find)
self._menu.addSeparator()
self._hide_selected_action = self._menu.addAction("Hide selected", self.hide_selected_items)
self._hide_classes_menu = self._menu.addMenu("Hide classes")
@@ -175,12 +252,6 @@ def populate_context_menu(self):
self._restore_pruned_menu.triggered.connect(self.restore_pruned_items)
self._restore_all_pruned_action = self._menu.addAction("Restore all", self.restore_all_pruned_items)
self._menu.addSeparator()
- self._select_bg_image_action = self._menu.addAction("Select background image...", self._select_bg_image)
- self._menu.addSeparator()
- self._rebuild_action = self._menu.addAction("Rebuild", self._spine_db_editor.rebuild_graph)
- self._export_as_image_action = self._menu.addAction("Export as image...", self.export_as_image)
- self._export_as_video_action = self._menu.addAction("Export as video...", self.export_as_video)
- self._menu.addSeparator()
self._zoom_action = ToolBarWidgetAction("Zoom", self._menu, compact=True)
self._zoom_action.tool_bar.addAction("-", self.zoom_out).setToolTip("Zoom out")
self._zoom_action.tool_bar.addAction("Reset", self.reset_zoom).setToolTip("Reset zoom")
@@ -201,8 +272,50 @@ def populate_context_menu(self):
self._menu.addAction(self._zoom_action)
self._menu.addAction(self._arc_length_action)
self._menu.addAction(self._rotate_action)
+ self._menu.addSeparator()
+ for prop in self._properties.values():
+ prop.update(self._menu)
+ self._menu.addSeparator()
+ self._select_graph_params_action = self._menu.addAction(
+ "Select graph parameters...", self.select_graph_parameters
+ )
+ self._select_bg_image_action = self._menu.addAction("Select background image...", self._select_bg_image)
+ self._menu.addSeparator()
+ self._save_pos_action = self._menu.addAction("Save positions", self.save_positions)
+ self._clear_pos_action = self._menu.addAction("Clear saved positions", self.clear_saved_positions)
+ self._menu.addSeparator()
+ self._save_state_action = self._menu.addAction("Save state...", self._save_state)
+ self._load_state_menu = self._menu.addMenu("Load state")
+ self._load_state_menu.triggered.connect(self._load_state)
+ self._remove_state_menu = self._menu.addMenu("Remove state")
+ self._remove_state_menu.triggered.connect(self._remove_state)
+ self._menu.addSeparator()
+ self._export_as_image_action = self._menu.addAction("Export as image...", self.export_as_image)
+ self._export_as_video_action = self._menu.addAction("Export as video...", self.export_as_video)
+ self._menu.addSeparator()
+ self._rebuild_action = self._menu.addAction("Rebuild", self._spine_db_editor.rebuild_graph)
self._menu.aboutToShow.connect(self._update_actions_visibility)
+ def _save_state(self):
+ name, ok = QInputDialog.getText(self, "Save state...", "Enter a name for the state.")
+ if not ok:
+ return
+ if name in self._db_map_graph_data_by_name:
+ self._spine_db_editor.msg_error.emit(f"State {name} already exists")
+ return
+ self._spine_db_editor.save_graph_data(name)
+
+ @Slot(QAction)
+ def _load_state(self, action):
+ name = action.text()
+ db_map_graph_data = self._db_map_graph_data_by_name.get(name)
+ self._spine_db_editor.load_graph_data(db_map_graph_data)
+
+ @Slot(QAction)
+ def _remove_state(self, action):
+ name = action.text()
+ self._spine_db_editor.remove_graph_data(name)
+
def _find(self):
expr, ok = QInputDialog.getText(self, "Find in graph...", "Enter entity names to find separated by comma.")
if not ok:
@@ -229,6 +342,11 @@ def decrease_arc_length(self):
def _update_actions_visibility(self):
"""Enables or disables actions according to current selection in the graph."""
has_graph = bool(self.items())
+ self._items_per_class = {}
+ for item in self.entity_items:
+ key = f"{item.entity_class_name}"
+ self._items_per_class.setdefault(key, list()).append(item)
+ self._db_map_graph_data_by_name = self._spine_db_editor.get_db_map_graph_data_by_name()
self._save_pos_action.setEnabled(bool(self.selected_items))
self._clear_pos_action.setEnabled(bool(self.selected_items))
self._hide_selected_action.setEnabled(bool(self.selected_items))
@@ -244,18 +362,22 @@ def _update_actions_visibility(self):
self._find_action.setEnabled(has_graph)
self._export_as_image_action.setEnabled(has_graph)
self._export_as_video_action.setEnabled(has_graph and self._spine_db_editor.ui.time_line_widget.isVisible())
- self._items_per_class = {}
- for item in self.entity_items:
- key = f"{item.entity_class_name}"
- self._items_per_class.setdefault(key, list()).append(item)
self._hide_classes_menu.clear()
self._hide_classes_menu.setEnabled(bool(self._items_per_class))
- self._prune_classes_menu.clear()
- self._prune_classes_menu.setEnabled(bool(self._items_per_class))
for key in sorted(self._items_per_class.keys() - self.hidden_items.keys()):
self._hide_classes_menu.addAction(key)
+ self._prune_classes_menu.clear()
+ self._prune_classes_menu.setEnabled(bool(self._items_per_class))
for key in sorted(self._items_per_class.keys() - self.pruned_db_map_entity_ids.keys()):
self._prune_classes_menu.addAction(key)
+ self._save_state_action.setEnabled(has_graph)
+ self._load_state_menu.clear()
+ self._load_state_menu.setEnabled(bool(self._db_map_graph_data_by_name))
+ self._remove_state_menu.clear()
+ self._remove_state_menu.setEnabled(bool(self._db_map_graph_data_by_name))
+ for key in sorted(self._db_map_graph_data_by_name.keys()):
+ self._load_state_menu.addAction(key)
+ self._remove_state_menu.addAction(key)
def make_items_menu(self):
menu = QMenu(self)
@@ -270,58 +392,6 @@ def make_items_menu(self):
menu.aboutToShow.connect(self._update_actions_visibility)
return menu
- @Slot(bool)
- def _set_auto_expand_entities(self, _checked=False, save_setting=True):
- checked = self._auto_expand_entities_action.isChecked()
- if checked == self.auto_expand_entities:
- return
- if save_setting:
- self._qsettings.setValue("appSettings/autoExpandObjects", "true" if checked else "false")
- self.auto_expand_entities = checked
- self._spine_db_editor.build_graph()
-
- def set_auto_expand_entities(self, checked):
- self._auto_expand_entities_action.setChecked(checked)
- self._set_auto_expand_entities(save_setting=False)
-
- @Slot(bool)
- def _set_merge_dbs(self, _checked=False, save_setting=True):
- checked = self._merge_dbs_action.isChecked()
- if checked == self.merge_dbs:
- return
- if save_setting:
- self._qsettings.setValue("appSettings/mergeDBs", "true" if checked else "false")
- self.merge_dbs = checked
- self._spine_db_editor.build_graph()
-
- def set_merge_dbs(self, checked):
- self._merge_dbs_action.setChecked(checked)
- self._set_merge_dbs(save_setting=False)
-
- @Slot(bool)
- def _set_disable_max_relationship_dimension(self, _checked=False, save_setting=True):
- checked = self._disable_max_ent_dim_action.isChecked()
- if checked == self.disable_max_relationship_dimension:
- return
- if save_setting:
- self._qsettings.setValue("appSettings/disableMaxRelationshipDimension", "true" if checked else "false")
- self.disable_max_relationship_dimension = checked
- self._spine_db_editor.build_graph()
-
- def set_disable_max_relationship_dimension(self, checked):
- self._disable_max_ent_dim_action.setChecked(checked)
- self._set_disable_max_relationship_dimension(save_setting=False)
-
- @Slot(int)
- def _set_max_relationship_dimension(self, _value=None, save_setting=True):
- value = self._max_ent_dim_spin_box.value()
- if value == self.max_entity_dimension:
- return
- if save_setting:
- self._qsettings.setValue("appSettings/maxRelationshipDimension", str(value))
- self.max_entity_dimension = value
- self._spine_db_editor.build_graph()
-
@Slot(bool)
def add_entities_at_position(self, checked=False):
self._spine_db_editor.add_entities_at_position(self._context_menu_pos)
@@ -392,9 +462,8 @@ def show_hidden_items(self, action):
@Slot(bool)
def prune_selected_items(self, checked=False):
"""Prunes selected items."""
- entity_ids = {db_map_id for x in self.selected_items for db_map_id in x.db_map_ids}
key = self._get_selected_entity_names()
- self.pruned_db_map_entity_ids[key] = entity_ids
+ self.pruned_db_map_entity_ids[key] = {db_map_id for x in self.selected_items for db_map_id in x.db_map_ids}
self._restore_pruned_menu.addAction(key)
self._spine_db_editor.build_graph()
@@ -512,30 +581,30 @@ def _select_bg_image(self, _checked=False):
)
if not file_path:
return
- self.set_bg_image(file_path)
+ with open(file_path, "r") as fh:
+ svg = fh.read().rstrip()
+ self.set_bg_svg(svg)
rect = self._get_viewport_scene_rect()
self._bg_item.fit_rect(rect)
self._bg_item.apply_zoom(self.zoom_factor)
- def set_bg_image(self, file_path, transform=None):
+ def set_bg_svg(self, svg):
if self._bg_item is not None:
self.scene().removeItem(self._bg_item)
- self._bg_item = BgItem(file_path)
+ self._bg_item = BgItem(svg)
self.scene().addItem(self._bg_item)
- def set_bg_entity_coordinates(self, entity_coordinates):
- self._bg_entity_coordinates = {
- ent: self._spine_db_editor.convert_position(*p) for ent, p in entity_coordinates.items()
- }
- self.fit_bg_coordinates()
+ def get_bg_svg(self):
+ return self._bg_item.svg if self._bg_item else ""
- def fit_bg_coordinates(self):
- (ent1, p1), (ent2, p2), *_ignored = self._bg_entity_coordinates.items()
- pos_by_name = {item.display_data: item.pos() for item in self.entity_items}
- scen1, scen2 = pos_by_name.get(ent1), pos_by_name.get(ent2)
- if None in (scen1, scen2):
- return
- self._bg_item.fit_coordinates(p1, p2, (scen1.x(), scen1.y()), (scen2.x(), scen2.y()))
+ def set_bg_rect(self, rect):
+ if self._bg_item is not None and rect:
+ self._bg_item.fit_rect(rect)
+
+ def get_bg_rect(self):
+ if self._bg_item is not None:
+ rect = self._bg_item.rect()
+ return rect.x(), rect.y(), rect.width(), rect.height()
def clear_scene(self):
for item in self.scene().items():
diff --git a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
index 8384f892b..fbc846161 100644
--- a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
+++ b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
@@ -15,6 +15,7 @@
import sys
import itertools
+import json
from time import monotonic
from PySide6.QtCore import Slot, QTimer, QThreadPool
from spinedb_api import from_database
@@ -83,7 +84,7 @@ def __init__(self, *args, **kwargs):
self._time_line_index = None
self.entity_items = []
self.arc_items = []
- self.selected_tree_inds = {}
+ self._selected_item_type_db_map_ids = {}
self.db_map_entity_id_sets = []
self.entity_inds = []
self.element_inds = []
@@ -329,7 +330,7 @@ def _handle_entity_graph_visibility_changed(self, visible):
@Slot(dict)
def _handle_entity_tree_selection_changed_in_graph(self, selected):
"""Stores the given selection of entity tree indexes and builds graph."""
- self.selected_tree_inds = selected
+ self._selected_item_type_db_map_ids = self._get_selected_item_type_db_map_ids(selected)
self.added_db_map_entity_ids.clear()
self._extending_graph = True
self.build_graph()
@@ -339,8 +340,71 @@ def rebuild_graph(self, _checked=False):
self.db_map_entity_id_sets.clear()
self.build_graph()
+ def save_graph_data(self, name):
+ db_map_data = {}
+ for db_map in self.db_maps:
+ graph_data = {
+ "type": "graph_data",
+ "selected_item_type_ids": {
+ item_type: db_map_ids.get(db_map, [])
+ for item_type, db_map_ids in self._selected_item_type_db_map_ids.items()
+ },
+ "pruned_entity_ids": self.ui.graphicsView.get_pruned_entity_ids(db_map),
+ "pos_x_parameter": self.ui.graphicsView.pos_x_parameter,
+ "pos_y_parameter": self.ui.graphicsView.pos_y_parameter,
+ "name_parameter": self.ui.graphicsView.name_parameter,
+ "color_parameter": self.ui.graphicsView.color_parameter,
+ "arc_width_parameter": self.ui.graphicsView.arc_width_parameter,
+ "bg_svg": self.ui.graphicsView.get_bg_svg(),
+ "bg_rect": self.ui.graphicsView.get_bg_rect(),
+ "properties": self.ui.graphicsView.get_all_properties(),
+ }
+ db_map_data[db_map] = [{"name": name, "value": json.dumps(graph_data)}]
+ self.db_mngr.add_metadata(db_map_data)
+ # TODO: also add entity_metadata so it sticks
+
+ def get_db_map_graph_data_by_name(self):
+ db_map_graph_data_by_name = {}
+ for db_map in self.db_maps:
+ for metadata_item in self.db_mngr.get_items(db_map, "metadata"):
+ try:
+ graph_data = json.loads(metadata_item["value"])
+ except json.decoder.JSONDecodeError:
+ continue
+ if isinstance(graph_data, dict) and graph_data.get("type") == "graph_data":
+ db_map_graph_data_by_name.setdefault(metadata_item["name"], {})[db_map] = graph_data
+ return db_map_graph_data_by_name
+
+ def load_graph_data(self, db_map_graph_data):
+ if not db_map_graph_data:
+ self.msg_error.emit("Invalid graph data")
+ self._selected_item_type_db_map_ids = {}
+ for db_map, gd in db_map_graph_data.items():
+ for item_type, ids in gd["selected_item_type_ids"].items():
+ self._selected_item_type_db_map_ids.setdefault(item_type, {})[db_map] = ids
+ self.ui.graphicsView.set_pruned_entity_ids(db_map, gd["pruned_entity_ids"])
+ graph_data = db_map_graph_data[self.first_db_map]
+ self.ui.graphicsView.pos_x_parameter = graph_data["pos_x_parameter"]
+ self.ui.graphicsView.pos_y_parameter = graph_data["pos_y_parameter"]
+ self.ui.graphicsView.name_parameter = graph_data["name_parameter"]
+ self.ui.graphicsView.color_parameter = graph_data["color_parameter"]
+ self.ui.graphicsView.arc_width_parameter = graph_data["arc_width_parameter"]
+ self.ui.graphicsView.set_bg_svg(graph_data["bg_svg"])
+ self.ui.graphicsView.set_bg_rect(graph_data["bg_rect"])
+ self.ui.graphicsView.set_many_properties(graph_data["properties"])
+ self.build_graph()
+
+ def remove_graph_data(self, name):
+ db_map_typed_ids = {}
+ for db_map in self.db_maps:
+ metadata_item = next((x for x in self.db_mngr.get_items(db_map, "metadata") if x["name"] == name), None)
+ if metadata_item is None:
+ continue
+ db_map_typed_ids[db_map] = {"metadata": {metadata_item["id"]}}
+ self.db_mngr.remove_items(db_map_typed_ids)
+
def build_graph(self, persistent=False):
- """Builds the graph.
+ """Builds graph from selection in the entity tree.
Args:
persistent (bool, optional): If True, elements in the current graph (if any) retain their position
@@ -388,7 +452,23 @@ def _complete_graph(self, layout_gen_id, x, y):
self.ui.graphicsView.reset_zoom()
else:
self.ui.graphicsView.apply_zoom()
- self.ui.graphicsView.fit_bg_coordinates()
+
+ @staticmethod
+ def _get_selected_item_type_db_map_ids(selected_tree_inds):
+ """Returns a dict mapping item type to db_map to selected ids-
+
+ Returns:
+ dict
+ """
+ if "root" in selected_tree_inds:
+ return {"root": None}
+ item_type_db_map_ids = {}
+ for item_type, indexes in selected_tree_inds.items():
+ for index in indexes:
+ item = index.model().item_from_index(index)
+ for db_map, id_ in item.db_map_ids.items():
+ item_type_db_map_ids.setdefault(item_type, {}).setdefault(db_map, []).append(id_)
+ return item_type_db_map_ids
def _get_selected_db_map_entity_ids(self):
"""Returns a set of ids corresponding to selected entities in the trees.
@@ -397,24 +477,19 @@ def _get_selected_db_map_entity_ids(self):
set: selected object ids
set: selected relationship ids
"""
- if "root" in self.selected_tree_inds:
+ if "root" in self._selected_item_type_db_map_ids:
return set((db_map, x["id"]) for db_map in self.db_maps for x in self.db_mngr.get_items(db_map, "entity"))
db_map_entity_ids = set()
- for index in self.selected_tree_inds.get("entity", {}):
- item = index.model().item_from_index(index)
- db_map_entity_ids |= set(item.db_map_ids.items())
- for index in self.selected_tree_inds.get("entity_class", {}):
- item = index.model().item_from_index(index)
+ for db_map, ids in self._selected_item_type_db_map_ids.get("entity", {}).items():
+ db_map_entity_ids |= {(db_map, id_) for id_ in ids}
+ for db_map, ids in self._selected_item_type_db_map_ids.get("entity_class", {}).items():
db_map_entity_ids |= set(
- (db_map, x["id"])
- for db_map, id_ in item.db_map_ids.items()
- for x in self.db_mngr.get_items(db_map, "entity")
- if x["class_id"] == id_
+ (db_map, x["id"]) for x in self.db_mngr.get_items(db_map, "entity") if x["class_id"] in ids
)
return db_map_entity_ids
def _get_db_map_entities_for_graph(self, db_map_entity_ids):
- cond = any if self.ui.graphicsView.auto_expand_entities else all
+ cond = any if self.ui.graphicsView.get_property("auto_expand_entities") else all
return [
(db_map, x)
for db_map in self.db_maps
@@ -427,18 +502,12 @@ def _get_db_map_entities_for_graph(self, db_map_entity_ids):
def _update_graph_data(self):
"""Updates data for graph according to selection in trees."""
- pruned_db_map_entity_ids = {
- id_ for ids in self.ui.graphicsView.pruned_db_map_entity_ids.values() for id_ in ids
- }
+ pruned_db_map_entity_ids = set(self.ui.graphicsView.get_pruned_db_map_entity_ids())
db_map_entity_ids = self._get_selected_db_map_entity_ids()
db_map_entity_ids |= self.added_db_map_entity_ids
db_map_entity_ids -= pruned_db_map_entity_ids
db_map_entities = self._get_db_map_entities_for_graph(db_map_entity_ids)
- max_ent_dim = (
- self.ui.graphicsView.max_entity_dimension
- if not self.ui.graphicsView.disable_max_relationship_dimension
- else sys.maxsize
- )
+ max_ent_dim_count = self.ui.graphicsView.get_property("max_entity_dimension_count")
db_map_element_id_lists = {}
for db_map, entity in db_map_entities:
if (db_map, entity["id"]) in pruned_db_map_entity_ids:
@@ -447,7 +516,7 @@ def _update_graph_data(self):
(db_map, id_) for id_ in entity["element_id_list"] if (db_map, id_) not in pruned_db_map_entity_ids
]
el_count = len(db_map_element_id_list)
- if el_count != 0 and (el_count < 2 or el_count > max_ent_dim):
+ if el_count != 0 and (el_count < 2 or el_count > max_ent_dim_count):
continue
db_map_entity_ids.add((db_map, entity["id"]))
db_map_entity_ids.update(db_map_element_id_list)
@@ -468,7 +537,7 @@ def get_entity_key(self, db_map_entity_id):
db_map, entity_id = db_map_entity_id
entity = self.db_mngr.get_item(db_map, "entity", entity_id)
key = (entity["class_name"], entity["dimension_name_list"], entity["byname"])
- if not self.ui.graphicsView.merge_dbs:
+ if not self.ui.graphicsView.get_property("merge_dbs"):
key += (db_map.codename,)
return key
@@ -588,9 +657,9 @@ def _make_layout_generator(self):
for db_map_entity_id in db_map_entity_ids
if fixed_positions[db_map_entity_id]
}
- spread_factor = int(self.qsettings.value("appSettings/layoutAlgoSpreadFactor", defaultValue="100")) / 100
- neg_weight_exp = int(self.qsettings.value("appSettings/layoutAlgoNegWeightExp", defaultValue="2"))
- max_iters = int(self.qsettings.value("appSettings/layoutAlgoMaxIterations", defaultValue="12"))
+ spread_factor = self.ui.graphicsView.get_property("spread_factor") / 100
+ build_iters = self.ui.graphicsView.get_property("build_iters")
+ neg_weight_exp = self.ui.graphicsView.get_property("neg_weight_exp")
return GraphLayoutGeneratorRunnable(
self._layout_gen_id,
len(self.db_map_entity_id_sets),
@@ -599,7 +668,7 @@ def _make_layout_generator(self):
spread=spread_factor * self._ARC_LENGTH_HINT,
heavy_positions=heavy_positions,
weight_exp=-neg_weight_exp,
- max_iters=max_iters,
+ max_iters=build_iters,
)
@staticmethod
diff --git a/spinetoolbox/ui/settings.py b/spinetoolbox/ui/settings.py
index 41c549e9e..49cddc636 100644
--- a/spinetoolbox/ui/settings.py
+++ b/spinetoolbox/ui/settings.py
@@ -658,82 +658,87 @@ def setupUi(self, SettingsForm):
self.groupBox_entity_graph = QGroupBox(self.SpineDBEditor)
self.groupBox_entity_graph.setObjectName(u"groupBox_entity_graph")
- self.verticalLayout_10 = QVBoxLayout(self.groupBox_entity_graph)
- self.verticalLayout_10.setObjectName(u"verticalLayout_10")
- self.checkBox_auto_expand_entities = QCheckBox(self.groupBox_entity_graph)
- self.checkBox_auto_expand_entities.setObjectName(u"checkBox_auto_expand_entities")
-
- self.verticalLayout_10.addWidget(self.checkBox_auto_expand_entities)
-
- self.checkBox_merge_dbs = QCheckBox(self.groupBox_entity_graph)
- self.checkBox_merge_dbs.setObjectName(u"checkBox_merge_dbs")
-
- self.verticalLayout_10.addWidget(self.checkBox_merge_dbs)
-
- self.checkBox_snap_entities = QCheckBox(self.groupBox_entity_graph)
- self.checkBox_snap_entities.setObjectName(u"checkBox_snap_entities")
+ self.gridLayout_5 = QGridLayout(self.groupBox_entity_graph)
+ self.gridLayout_5.setObjectName(u"gridLayout_5")
+ self.checkBox_smooth_entity_graph_rotation = QCheckBox(self.groupBox_entity_graph)
+ self.checkBox_smooth_entity_graph_rotation.setObjectName(u"checkBox_smooth_entity_graph_rotation")
- self.verticalLayout_10.addWidget(self.checkBox_snap_entities)
+ self.gridLayout_5.addWidget(self.checkBox_smooth_entity_graph_rotation, 4, 0, 1, 1)
self.checkBox_smooth_entity_graph_zoom = QCheckBox(self.groupBox_entity_graph)
self.checkBox_smooth_entity_graph_zoom.setObjectName(u"checkBox_smooth_entity_graph_zoom")
- self.verticalLayout_10.addWidget(self.checkBox_smooth_entity_graph_zoom)
+ self.gridLayout_5.addWidget(self.checkBox_smooth_entity_graph_zoom, 3, 0, 1, 1)
- self.checkBox_smooth_entity_graph_rotation = QCheckBox(self.groupBox_entity_graph)
- self.checkBox_smooth_entity_graph_rotation.setObjectName(u"checkBox_smooth_entity_graph_rotation")
+ self.spinBox_layout_algo_max_iterations = QSpinBox(self.groupBox_entity_graph)
+ self.spinBox_layout_algo_max_iterations.setObjectName(u"spinBox_layout_algo_max_iterations")
+ self.spinBox_layout_algo_max_iterations.setMinimum(1)
+ self.spinBox_layout_algo_max_iterations.setMaximum(100)
+ self.spinBox_layout_algo_max_iterations.setValue(12)
- self.verticalLayout_10.addWidget(self.checkBox_smooth_entity_graph_rotation)
+ self.gridLayout_5.addWidget(self.spinBox_layout_algo_max_iterations, 6, 1, 1, 1)
- self.groupBox = QGroupBox(self.groupBox_entity_graph)
- self.groupBox.setObjectName(u"groupBox")
- self.gridLayout_6 = QGridLayout(self.groupBox)
- self.gridLayout_6.setObjectName(u"gridLayout_6")
- self.spinBox_layout_algo_neg_weight_exp = QSpinBox(self.groupBox)
- self.spinBox_layout_algo_neg_weight_exp.setObjectName(u"spinBox_layout_algo_neg_weight_exp")
- self.spinBox_layout_algo_neg_weight_exp.setMinimum(1)
- self.spinBox_layout_algo_neg_weight_exp.setMaximum(100)
+ self.label_10 = QLabel(self.groupBox_entity_graph)
+ self.label_10.setObjectName(u"label_10")
- self.gridLayout_6.addWidget(self.spinBox_layout_algo_neg_weight_exp, 3, 1, 1, 1)
+ self.gridLayout_5.addWidget(self.label_10, 7, 0, 1, 1)
- self.label_6 = QLabel(self.groupBox)
+ self.spinBox_layout_algo_spread_factor = QSpinBox(self.groupBox_entity_graph)
+ self.spinBox_layout_algo_spread_factor.setObjectName(u"spinBox_layout_algo_spread_factor")
+ self.spinBox_layout_algo_spread_factor.setMinimum(1)
+ self.spinBox_layout_algo_spread_factor.setMaximum(100)
+ self.spinBox_layout_algo_spread_factor.setValue(100)
+
+ self.gridLayout_5.addWidget(self.spinBox_layout_algo_spread_factor, 7, 1, 1, 1)
+
+ self.label_6 = QLabel(self.groupBox_entity_graph)
self.label_6.setObjectName(u"label_6")
sizePolicy8 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
- sizePolicy8.setHorizontalStretch(0)
+ sizePolicy8.setHorizontalStretch(2)
sizePolicy8.setVerticalStretch(0)
sizePolicy8.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth())
self.label_6.setSizePolicy(sizePolicy8)
- self.gridLayout_6.addWidget(self.label_6, 0, 0, 1, 1)
+ self.gridLayout_5.addWidget(self.label_6, 6, 0, 1, 1)
- self.label_10 = QLabel(self.groupBox)
- self.label_10.setObjectName(u"label_10")
+ self.checkBox_auto_expand_entities = QCheckBox(self.groupBox_entity_graph)
+ self.checkBox_auto_expand_entities.setObjectName(u"checkBox_auto_expand_entities")
- self.gridLayout_6.addWidget(self.label_10, 1, 0, 1, 1)
+ self.gridLayout_5.addWidget(self.checkBox_auto_expand_entities, 0, 0, 1, 1)
- self.label_16 = QLabel(self.groupBox)
+ self.label_16 = QLabel(self.groupBox_entity_graph)
self.label_16.setObjectName(u"label_16")
- self.gridLayout_6.addWidget(self.label_16, 3, 0, 1, 1)
+ self.gridLayout_5.addWidget(self.label_16, 8, 0, 1, 1)
- self.spinBox_layout_algo_max_iterations = QSpinBox(self.groupBox)
- self.spinBox_layout_algo_max_iterations.setObjectName(u"spinBox_layout_algo_max_iterations")
- self.spinBox_layout_algo_max_iterations.setMinimum(1)
- self.spinBox_layout_algo_max_iterations.setMaximum(100)
- self.spinBox_layout_algo_max_iterations.setValue(12)
+ self.checkBox_snap_entities = QCheckBox(self.groupBox_entity_graph)
+ self.checkBox_snap_entities.setObjectName(u"checkBox_snap_entities")
- self.gridLayout_6.addWidget(self.spinBox_layout_algo_max_iterations, 0, 1, 1, 1)
+ self.gridLayout_5.addWidget(self.checkBox_snap_entities, 2, 0, 1, 1)
- self.spinBox_layout_algo_spread_factor = QSpinBox(self.groupBox)
- self.spinBox_layout_algo_spread_factor.setObjectName(u"spinBox_layout_algo_spread_factor")
- self.spinBox_layout_algo_spread_factor.setMinimum(1)
- self.spinBox_layout_algo_spread_factor.setMaximum(100)
- self.spinBox_layout_algo_spread_factor.setValue(100)
+ self.checkBox_merge_dbs = QCheckBox(self.groupBox_entity_graph)
+ self.checkBox_merge_dbs.setObjectName(u"checkBox_merge_dbs")
- self.gridLayout_6.addWidget(self.spinBox_layout_algo_spread_factor, 1, 1, 1, 1)
+ self.gridLayout_5.addWidget(self.checkBox_merge_dbs, 1, 0, 1, 1)
+ self.spinBox_layout_algo_neg_weight_exp = QSpinBox(self.groupBox_entity_graph)
+ self.spinBox_layout_algo_neg_weight_exp.setObjectName(u"spinBox_layout_algo_neg_weight_exp")
+ self.spinBox_layout_algo_neg_weight_exp.setMinimum(1)
+ self.spinBox_layout_algo_neg_weight_exp.setMaximum(100)
- self.verticalLayout_10.addWidget(self.groupBox)
+ self.gridLayout_5.addWidget(self.spinBox_layout_algo_neg_weight_exp, 8, 1, 1, 1)
+
+ self.label_3 = QLabel(self.groupBox_entity_graph)
+ self.label_3.setObjectName(u"label_3")
+
+ self.gridLayout_5.addWidget(self.label_3, 5, 0, 1, 1)
+
+ self.spinBox_max_ent_dim_count = QSpinBox(self.groupBox_entity_graph)
+ self.spinBox_max_ent_dim_count.setObjectName(u"spinBox_max_ent_dim_count")
+ self.spinBox_max_ent_dim_count.setMinimum(2)
+ self.spinBox_max_ent_dim_count.setValue(5)
+
+ self.gridLayout_5.addWidget(self.spinBox_max_ent_dim_count, 5, 1, 1, 1)
self.verticalLayout_9.addWidget(self.groupBox_entity_graph)
@@ -747,23 +752,23 @@ def setupUi(self, SettingsForm):
self.SpecificationEditors.setObjectName(u"SpecificationEditors")
self.verticalLayout_11 = QVBoxLayout(self.SpecificationEditors)
self.verticalLayout_11.setObjectName(u"verticalLayout_11")
- self.groupBox1 = QGroupBox(self.SpecificationEditors)
- self.groupBox1.setObjectName(u"groupBox1")
- self.verticalLayout_12 = QVBoxLayout(self.groupBox1)
+ self.groupBox = QGroupBox(self.SpecificationEditors)
+ self.groupBox.setObjectName(u"groupBox")
+ self.verticalLayout_12 = QVBoxLayout(self.groupBox)
self.verticalLayout_12.setObjectName(u"verticalLayout_12")
- self.checkBox_save_spec_before_closing = QCheckBox(self.groupBox1)
+ self.checkBox_save_spec_before_closing = QCheckBox(self.groupBox)
self.checkBox_save_spec_before_closing.setObjectName(u"checkBox_save_spec_before_closing")
self.checkBox_save_spec_before_closing.setTristate(True)
self.verticalLayout_12.addWidget(self.checkBox_save_spec_before_closing)
- self.checkBox_spec_show_undo = QCheckBox(self.groupBox1)
+ self.checkBox_spec_show_undo = QCheckBox(self.groupBox)
self.checkBox_spec_show_undo.setObjectName(u"checkBox_spec_show_undo")
self.verticalLayout_12.addWidget(self.checkBox_spec_show_undo)
- self.verticalLayout_11.addWidget(self.groupBox1)
+ self.verticalLayout_11.addWidget(self.groupBox)
self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
@@ -1199,20 +1204,20 @@ def retranslateUi(self, SettingsForm):
self.checkBox_entity_tree_sticky_selection.setText(QCoreApplication.translate("SettingsForm", u"Sticky selection", None))
self.checkBox_hide_empty_classes.setText(QCoreApplication.translate("SettingsForm", u"Hide empty classes", None))
self.groupBox_entity_graph.setTitle(QCoreApplication.translate("SettingsForm", u"Entity graph", None))
+ self.checkBox_smooth_entity_graph_rotation.setText(QCoreApplication.translate("SettingsForm", u"Smooth rotation", None))
+ self.checkBox_smooth_entity_graph_zoom.setText(QCoreApplication.translate("SettingsForm", u"Smooth zoom", None))
+ self.spinBox_layout_algo_max_iterations.setSuffix("")
+ self.label_10.setText(QCoreApplication.translate("SettingsForm", u"Minimum distance between nodes (%)", None))
+ self.label_6.setText(QCoreApplication.translate("SettingsForm", u"Number of build iterations", None))
#if QT_CONFIG(tooltip)
self.checkBox_auto_expand_entities.setToolTip(QCoreApplication.translate("SettingsForm", u"Checked: Whenever an object is included in the Entity graph, the graph automatically includes all its relationships.
Unchecked: Whenever all the objects in a relationship are included in the Entity graph, the graph automatically includes the relationship.
Note: This setting is a global default, but can be locally overriden in every Spine DB editor session.
", None))
#endif // QT_CONFIG(tooltip)
self.checkBox_auto_expand_entities.setText(QCoreApplication.translate("SettingsForm", u"Auto-expand entities", None))
- self.checkBox_merge_dbs.setText(QCoreApplication.translate("SettingsForm", u"Merge databases", None))
- self.checkBox_snap_entities.setText(QCoreApplication.translate("SettingsForm", u"Snap entities to grid", None))
- self.checkBox_smooth_entity_graph_zoom.setText(QCoreApplication.translate("SettingsForm", u"Smooth zoom", None))
- self.checkBox_smooth_entity_graph_rotation.setText(QCoreApplication.translate("SettingsForm", u"Smooth rotation", None))
- self.groupBox.setTitle(QCoreApplication.translate("SettingsForm", u"Layout generation algorithm", None))
- self.label_6.setText(QCoreApplication.translate("SettingsForm", u"Number of iterations", None))
- self.label_10.setText(QCoreApplication.translate("SettingsForm", u"Distance between nodes (as percentage of node size)", None))
self.label_16.setText(QCoreApplication.translate("SettingsForm", u"Decay rate of attraction with distance", None))
- self.spinBox_layout_algo_max_iterations.setSuffix("")
- self.groupBox1.setTitle(QCoreApplication.translate("SettingsForm", u"Specification editors", None))
+ self.checkBox_snap_entities.setText(QCoreApplication.translate("SettingsForm", u"Snap entities to grid", None))
+ self.checkBox_merge_dbs.setText(QCoreApplication.translate("SettingsForm", u"Merge databases", None))
+ self.label_3.setText(QCoreApplication.translate("SettingsForm", u"Max. entity dimension count", None))
+ self.groupBox.setTitle(QCoreApplication.translate("SettingsForm", u"Specification editors", None))
#if QT_CONFIG(tooltip)
self.checkBox_save_spec_before_closing.setToolTip(QCoreApplication.translate("SettingsForm", u"
Unchecked: Don't save specification and don't show message box
Partially checked: Show message box (default)
Checked: Save specification and don't show message box
", None))
#endif // QT_CONFIG(tooltip)
diff --git a/spinetoolbox/ui/settings.ui b/spinetoolbox/ui/settings.ui
index b8b89374d..0bbb5e9e8 100644
--- a/spinetoolbox/ui/settings.ui
+++ b/spinetoolbox/ui/settings.ui
@@ -1120,8 +1120,71 @@
Entity graph
-
- -
+
+
-
+
+
+ Smooth rotation
+
+
+
+ -
+
+
+ Smooth zoom
+
+
+
+ -
+
+
+
+
+
+ 1
+
+
+ 100
+
+
+ 12
+
+
+
+ -
+
+
+ Minimum distance between nodes (%)
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+ 100
+
+
+
+ -
+
+
+
+ 2
+ 0
+
+
+
+ Number of build iterations
+
+
+
+ -
<html><head/><body><p><span style=" font-weight:600;">Checked</span>: Whenever an object is included in the Entity graph, the graph automatically includes <span style=" font-style:italic;">all</span> its relationships.</p><p><span style=" font-weight:600;">Unchecked</span>: Whenever <span style=" font-style:italic;">all</span> the objects in a relationship are included in the Entity graph, the graph automatically includes the relationship.</p><p>Note: This setting is a global default, but can be locally overriden in every Spine DB editor session.</p></body></html>
@@ -1131,107 +1194,52 @@
- -
-
+
-
+
- Merge databases
+ Decay rate of attraction with distance
- -
+
-
Snap entities to grid
- -
-
+
-
+
- Smooth zoom
+ Merge databases
- -
-
+
-
+
+
+ 1
+
+
+ 100
+
+
+
+ -
+
- Smooth rotation
+ Max. entity dimension count
- -
-
-
- Layout generation algorithm
-
-
-
-
-
-
- 1
-
-
- 100
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Number of iterations
-
-
-
- -
-
-
- Distance between nodes (as percentage of node size)
-
-
-
- -
-
-
- Decay rate of attraction with distance
-
-
-
- -
-
-
-
-
-
- 1
-
-
- 100
-
-
- 12
-
-
-
- -
-
-
- 1
-
-
- 100
-
-
- 100
-
-
-
-
+ -
+
+
+ 2
+
+
+ 5
+
@@ -1714,7 +1722,7 @@
-
+
diff --git a/spinetoolbox/widgets/custom_qwidgets.py b/spinetoolbox/widgets/custom_qwidgets.py
index 469c1ea76..bacd3552e 100644
--- a/spinetoolbox/widgets/custom_qwidgets.py
+++ b/spinetoolbox/widgets/custom_qwidgets.py
@@ -676,6 +676,12 @@ def setMinimum(self, minimum):
except TypeError:
pass
+ def setMaximum(self, maximum):
+ try:
+ self._validator.setTop(maximum)
+ except TypeError:
+ pass
+
@Slot(str)
def setValue(self, value, strict=False):
try:
diff --git a/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py
index ea43bb47f..a27df6f84 100644
--- a/spinetoolbox/widgets/settings_widget.py
+++ b/spinetoolbox/widgets/settings_widget.py
@@ -156,6 +156,11 @@ def connect_signals(self):
self.ui.checkBox_hide_empty_classes.clicked.connect(self.set_hide_empty_classes)
self.ui.checkBox_auto_expand_entities.clicked.connect(self.set_auto_expand_entities)
self.ui.checkBox_merge_dbs.clicked.connect(self.set_merge_dbs)
+ self.ui.checkBox_snap_entities.clicked.connect(self.set_snap_entities)
+ self.ui.spinBox_max_ent_dim_count.valueChanged.connect(self.set_max_entity_dimension_count)
+ self.ui.spinBox_layout_algo_max_iterations.valueChanged.connect(self.set_build_iters)
+ self.ui.spinBox_layout_algo_spread_factor.valueChanged.connect(self.set_spread_factor)
+ self.ui.spinBox_layout_algo_neg_weight_exp.valueChanged.connect(self.set_neg_weight_exp)
def read_settings(self):
"""Read saved settings from app QSettings instance and update UI to display them."""
@@ -168,7 +173,8 @@ def read_settings(self):
snap_entities = self._qsettings.value("appSettings/snapEntities", defaultValue="false")
merge_dbs = self._qsettings.value("appSettings/mergeDBs", defaultValue="true")
db_editor_show_undo = int(self._qsettings.value("appSettings/dbEditorShowUndo", defaultValue="2"))
- max_iters = int(self.qsettings.value("appSettings/layoutAlgoMaxIterations", defaultValue="12"))
+ max_ent_dim_count = int(self.qsettings.value("appSettings/maxEntityDimensionCount", defaultValue="5"))
+ build_iters = int(self.qsettings.value("appSettings/layoutAlgoBuildIterations", defaultValue="12"))
spread_factor = int(self.qsettings.value("appSettings/layoutAlgoSpreadFactor", defaultValue="100"))
neg_weight_exp = int(self.qsettings.value("appSettings/layoutAlgoNegWeightExp", defaultValue="2"))
if commit_at_exit == 0: # Not needed but makes the code more readable.
@@ -186,7 +192,8 @@ def read_settings(self):
self.ui.checkBox_merge_dbs.setChecked(merge_dbs == "true")
if db_editor_show_undo == 2:
self.ui.checkBox_db_editor_show_undo.setChecked(True)
- self.ui.spinBox_layout_algo_max_iterations.setValue(max_iters)
+ self.ui.spinBox_max_ent_dim_count.setValue(max_ent_dim_count)
+ self.ui.spinBox_layout_algo_max_iterations.setValue(build_iters)
self.ui.spinBox_layout_algo_spread_factor.setValue(spread_factor)
self.ui.spinBox_layout_algo_neg_weight_exp.setValue(neg_weight_exp)
@@ -212,8 +219,10 @@ def save_settings(self):
self._qsettings.setValue("appSettings/mergeDBs", merge_dbs)
db_editor_show_undo = str(self.ui.checkBox_db_editor_show_undo.checkState().value)
self._qsettings.setValue("appSettings/dbEditorShowUndo", db_editor_show_undo)
- max_iters = str(self.ui.spinBox_layout_algo_max_iterations.value())
- self._qsettings.setValue("appSettings/layoutAlgoMaxIterations", max_iters)
+ max_ent_dim_count = str(self.ui.spinBox_layout_algo_max_iterations.value())
+ self._qsettings.setValue("appSettings/maxEntityDimensionCount", max_ent_dim_count)
+ build_iters = str(self.ui.spinBox_layout_algo_max_iterations.value())
+ self._qsettings.setValue("appSettings/layoutAlgoBuildIterations", build_iters)
spread_factor = str(self.ui.spinBox_layout_algo_spread_factor.value())
self._qsettings.setValue("appSettings/layoutAlgoSpreadFactor", spread_factor)
neg_weight_exp = str(self.ui.spinBox_layout_algo_neg_weight_exp.value())
@@ -236,13 +245,35 @@ def set_hide_empty_classes(self, checked=False):
@Slot(bool)
def set_auto_expand_entities(self, checked=False):
- for db_editor in self.db_mngr.get_all_spine_db_editors():
- db_editor.ui.graphicsView.set_auto_expand_entities(checked)
+ self._set_graph_property("auto_expand_entities", checked)
@Slot(bool)
def set_merge_dbs(self, checked=False):
+ self._set_graph_property("merge_dbs", checked)
+
+ @Slot(bool)
+ def set_snap_entities(self, checked=False):
+ self._set_graph_property("snap_entities", checked)
+
+ @Slot(int)
+ def set_max_entity_dimension_count(self, value=None):
+ self._set_graph_property("max_entity_dimension_count", value)
+
+ @Slot(int)
+ def set_build_iters(self, value=None):
+ self._set_graph_property("build_iters", value)
+
+ @Slot(int)
+ def set_spread_factor(self, value=None):
+ self._set_graph_property("spread_factor", value)
+
+ @Slot(int)
+ def set_neg_weight_exp(self, value=None):
+ self._set_graph_property("neg_weight_exp", value)
+
+ def _set_graph_property(self, name, value):
for db_editor in self.db_mngr.get_all_spine_db_editors():
- db_editor.ui.graphicsView.set_merge_dbs(checked)
+ db_editor.ui.graphicsView.set_property(name, value)
class SpineDBEditorSettingsWidget(SpineDBEditorSettingsMixin, SettingsWidgetBase):