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):