Skip to content

Commit

Permalink
Add box for auto-build and rebuild buttons into graph
Browse files Browse the repository at this point in the history
  • Loading branch information
Henrik Koski committed Jul 8, 2024
1 parent 53be2c2 commit 28fec9e
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 13 deletions.
4 changes: 4 additions & 0 deletions spinetoolbox/spine_db_editor/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
######################################################################################################################

"""Helpers and utilities for Spine Database editor."""
from PySide6.QtGui import QColor


def string_to_display_icon(x):
Expand Down Expand Up @@ -53,3 +54,6 @@ def table_name_from_item_type(item_type):
"parameter_definition": "Parameter definition",
"entity_alternative": "Entity alternative",
}.get(item_type)


GRAPH_OVERLAY_COLOR = QColor(210, 210, 210, 211)
77 changes: 68 additions & 9 deletions spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,28 @@
from contextlib import contextmanager
import numpy as np
from PySide6.QtCore import Qt, QTimeLine, Signal, Slot, QRectF, QRunnable, QThreadPool
from PySide6.QtWidgets import QMenu, QInputDialog, QColorDialog, QMessageBox, QLineEdit, QGraphicsScene
from PySide6.QtGui import QCursor, QPainter, QIcon, QAction, QPageSize, QPixmap
from PySide6.QtWidgets import (
QMenu,
QInputDialog,
QColorDialog,
QMessageBox,
QLineEdit,
QGraphicsScene,
QWidget,
QVBoxLayout,
QPushButton,
QRadioButton,
)
from PySide6.QtGui import QCursor, QPainter, QIcon, QAction, QPageSize, QPixmap, QKeySequence
from PySide6.QtPrintSupport import QPrinter
from PySide6.QtSvg import QSvgGenerator
from .custom_qwidgets import ExportAsVideoDialog
from .select_graph_parameters_dialog import SelectGraphParametersDialog
from ..helpers import GRAPH_OVERLAY_COLOR
from ..graphics_items import EntityItem, CrossHairsArcItem, BgItem, ArcItem
from ...helpers import CharIconEngine, remove_first
from ...widgets.custom_qgraphicsviews import CustomQGraphicsView
from ...widgets.custom_qwidgets import ToolBarWidgetAction, HorizontalSpinBox
from ..graphics_items import EntityItem, CrossHairsArcItem, BgItem, ArcItem
from .custom_qwidgets import ExportAsVideoDialog
from .select_graph_parameters_dialog import SelectGraphParametersDialog


class _GraphProperty:
Expand Down Expand Up @@ -59,9 +71,9 @@ def _set_value(self, _checked=False, save_setting=True):
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):
def set_value(self, checked, save=False):
self._action.setChecked(checked)
self._set_value(save_setting=False)
self._set_value(save_setting=save)

def update(self, menu):
self._value = self._spine_db_editor.qsettings.value(self._settings_name, defaultValue="true") == "true"
Expand Down Expand Up @@ -106,6 +118,46 @@ def update(self, menu):
menu.addAction(action)


class GraphOptionsOverlay(QWidget):
"""Widget that holds buttons for rebuild and toggling auto-build."""

def __init__(self, parent=None):
super().__init__(parent)
overlay = QWidget(self)
overlay.setFixedSize(100, 100)
layout = QVBoxLayout()
self._auto_build_button = QRadioButton("Auto-build")
self._auto_build_button.setToolTip("<p>Whether to build the graph when the tree selections change.</p>")
self._rebuild_button = QPushButton("Rebuild (F5)")
layout.addWidget(self._auto_build_button)
layout.addWidget(self._rebuild_button)
overlay.setLayout(layout)
self._rebuild_action = QAction("Rebuild", self)
self._rebuild_action.setShortcut(QKeySequence.Refresh)
self._rebuild_action.triggered.connect(lambda: self.parent()._spine_db_editor.rebuild_graph(force=True))
self.addAction(self._rebuild_action)
self._connect_button_signals()

def _connect_button_signals(self):
"""Connects signals. Makes sure that auto-build is synced between context menu and the overlay."""
self._auto_build_button.setChecked(self.parent()._properties.get("auto_build").value)
self._auto_build_button.toggled.connect(
lambda x: self.parent()._properties.get("auto_build").set_value(x, save=True)
)
self._auto_build_button.toggled.connect(lambda x: self.parent()._spine_db_editor.build_graph() if x else None)
self.parent()._properties.get("auto_build")._action.triggered.connect(
lambda x: self._auto_build_button.setChecked(x)
)
self.parent()._properties["auto_build"]._action.triggered.connect(lambda x: print("skaga", x))
self._rebuild_button.pressed.connect(lambda: self.parent()._spine_db_editor.rebuild_graph(force=True))

def paintEvent(self, event):
"""Gives a colored background"""
painter = QPainter(self)
painter.setBrush(GRAPH_OVERLAY_COLOR)
painter.drawRect(self.rect())

Check warning on line 158 in spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py#L156-L158

Added lines #L156 - L158 were not covered by tests


class EntityQGraphicsView(CustomQGraphicsView):
"""QGraphicsView for the Entity Graph View."""

Expand Down Expand Up @@ -137,6 +189,7 @@ def __init__(self, parent):
self._properties = {
"auto_expand_entities": _GraphBoolProperty("Auto-expand entities", "autoExpandEntities"),
"merge_dbs": _GraphBoolProperty("Merge databases", "mergeDBs"),
"auto_build": _GraphBoolProperty("Auto-build", "autoBuild"),
"snap_entities": _GraphBoolProperty("Snap entities to grid", "snapEntities"),
"max_entity_dimension_count": _GraphIntProperty(
2, None, 5, "Max. entity dimension count", "maxEntityDimensionCount"
Expand Down Expand Up @@ -174,6 +227,7 @@ def __init__(self, parent):
self._items_per_class = {}
self._db_map_graph_data_by_name = {}
self._thread_pool = QThreadPool()
self._options_overlay = None

@property
def _qsettings(self):
Expand Down Expand Up @@ -216,6 +270,7 @@ def connect_spine_db_editor(self, spine_db_editor):
for prop in self._properties.values():
prop.connect_spine_db_editor(spine_db_editor)
self.populate_context_menu()
self._options_overlay = GraphOptionsOverlay(self)

def populate_context_menu(self):
self._add_entities_action = self._menu.addAction("Add entities...", self.add_entities_at_position)
Expand Down Expand Up @@ -275,21 +330,25 @@ def populate_context_menu(self):
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._rebuild_action = self._menu.addAction("Rebuild", lambda: self._spine_db_editor.rebuild_graph(force=True))
self._rebuild_action.setShortcuts(QKeySequence.Refresh)
self._menu.aboutToShow.connect(self._update_actions_visibility)

@Slot()
def _update_actions_visibility(self):
"""Enables or disables actions according to current selection in the graph."""
has_graph = bool(self.items())
has_selections = self._spine_db_editor.ui.treeView_entity.selectionModel().hasSelection()
has_selections |= self._spine_db_editor.ui.alternative_tree_view.selectionModel().hasSelection()
has_selections |= self._spine_db_editor.ui.scenario_tree_view.selectionModel().hasSelection()

Check warning on line 343 in spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py#L341-L343

Added lines #L341 - L343 were not covered by tests
self._items_per_class = {}
for item in self.entity_items:
key = f"{item.entity_class_name}"
self._items_per_class.setdefault(key, []).append(item)
self._db_map_graph_data_by_name = self._spine_db_editor.get_db_map_graph_data_by_name()
self._show_all_hidden_action.setEnabled(bool(self.hidden_items))
self._restore_all_pruned_action.setEnabled(any(self._spine_db_editor.pruned_db_map_entity_ids.values()))
self._rebuild_action.setEnabled(has_graph)
self._rebuild_action.setEnabled(has_selections or has_graph)

Check warning on line 351 in spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py#L351

Added line #L351 was not covered by tests
self._zoom_action.setEnabled(has_graph)
self._rotate_action.setEnabled(has_graph)
self._find_action.setEnabled(has_graph)
Expand Down
12 changes: 8 additions & 4 deletions spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,25 +527,29 @@ def remove_graph_data(self, name):
self.db_mngr.remove_items(db_map_typed_ids)

@Slot(bool)
def rebuild_graph(self, _checked=False):
def rebuild_graph(self, _checked=False, force=False):
self.db_map_entity_id_sets.clear()
self.expanded_db_map_entity_ids.clear()
self.collapsed_db_map_entity_ids.clear()
self.build_graph()
self.build_graph(force=force)

Check warning on line 534 in spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py#L534

Added line #L534 was not covered by tests

def build_graph(self, persistent=False):
def build_graph(self, persistent=False, force=False):
"""Builds graph from current selection of items.
Args:
persistent (bool, optional): If True, elements in the current graph (if any) retain their position
in the new one.
force (bool, optional): If True, graph will be built no matter what as long as its visible.
"""
self._persisted_positions.clear()
if persistent:
for item in self.entity_items:
x, y = self.convert_position(item.pos().x(), item.pos().y())
self._persisted_positions[item.first_db_map, item.first_id] = {"x": x, "y": y}
if not self.ui.dockWidget_entity_graph.isVisible():
if (
not (force or self.ui.graphicsView.get_property("auto_build"))
or not self.ui.dockWidget_entity_graph.isVisible()
):
self._owes_graph = True
return
self._owes_graph = False
Expand Down
55 changes: 55 additions & 0 deletions tests/spine_db_editor/widgets/test_custom_qgraphicsviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""Unit tests for ``custom_qgraphicsviews`` module."""
import unittest
from unittest import mock
from PySide6.QtWidgets import QWidget, QApplication
from spinetoolbox.spine_db_editor.widgets.custom_qgraphicsviews import EntityQGraphicsView


class TestGraphOptionsOverlay(unittest.TestCase):
"""Tests for the class GraphOptionsOverlay"""

@classmethod
def setUpClass(cls):
if not QApplication.instance():
QApplication()

def setUp(self):
self._parent = QWidget()
self.graph = EntityQGraphicsView(self._parent)
self.mock_editor = mock.MagicMock()
self.graph.connect_spine_db_editor(self.mock_editor)

def test_auto_build_consistent_between_overlay_and_context_menu(self):
"""Check that the overlay widget and the context menu are synced."""
self.assertFalse(self.graph._options_overlay._auto_build_button.isChecked())
self.assertFalse(self.graph.get_property("auto_build"))
# Toggle from context menu.
self.graph._options_overlay._auto_build_button.toggle()
self.assertTrue(self.graph._options_overlay._auto_build_button.isChecked())
self.assertTrue(self.graph.get_property("auto_build"))
# Toggle from overlay.
self.graph._properties["auto_build"]._action.trigger()
self.assertFalse(self.graph.get_property("auto_build"))
self.assertFalse(self.graph._options_overlay._auto_build_button.isChecked())

def test_rebuild(self):
"""Test that the graph is rebuilt when the button is pressed."""
self.mock_editor.rebuild_graph.assert_not_called()
self.graph._options_overlay._rebuild_button.click()
self.mock_editor.rebuild_graph.assert_called_once_with(force=True)


if __name__ == "__main__":
unittest.main()

0 comments on commit 28fec9e

Please sign in to comment.