From db8a7b055489643dd0c4ad16efc9413072efc368 Mon Sep 17 00:00:00 2001
From: Pekka T Savolainen
Date: Wed, 11 Sep 2024 12:26:20 +0300
Subject: [PATCH] New feature: Groups
---
spinetoolbox/group.py | 234 ++++++++++++++++++
spinetoolbox/link.py | 36 +--
spinetoolbox/project.py | 74 +++++-
spinetoolbox/project_commands.py | 119 ++++++++-
spinetoolbox/project_item/project_item.py | 7 +
spinetoolbox/project_item_icon.py | 43 ++--
spinetoolbox/resources_icons_rc.py | 117 ++++++---
spinetoolbox/spine_engine_worker.py | 10 +-
spinetoolbox/ui/datetime_editor.py | 2 +-
spinetoolbox/ui/mainwindow.py | 6 +-
spinetoolbox/ui/mainwindow.ui | 16 +-
spinetoolbox/ui/mainwindowlite.py | 74 ++----
spinetoolbox/ui/mainwindowlite.ui | 56 +----
.../ui/resources/menu_icons/object-group.svg | 1 +
spinetoolbox/ui/resources/resources_icons.qrc | 1 +
spinetoolbox/ui/select_database_items.py | 2 +-
spinetoolbox/ui_main.py | 123 +++++----
spinetoolbox/ui_main_base.py | 27 +-
spinetoolbox/ui_main_lite.py | 213 ++++++++++++++--
.../widgets/add_project_item_widget.py | 2 +-
spinetoolbox/widgets/custom_qgraphicsscene.py | 14 +-
spinetoolbox/widgets/custom_qgraphicsviews.py | 145 +++++++++--
.../widgets/set_description_dialog.py | 2 +-
tests/test_SpineToolboxProject.py | 2 +-
tests/test_project_item_icon.py | 4 +-
25 files changed, 1013 insertions(+), 317 deletions(-)
create mode 100644 spinetoolbox/group.py
create mode 100644 spinetoolbox/ui/resources/menu_icons/object-group.svg
diff --git a/spinetoolbox/group.py b/spinetoolbox/group.py
new file mode 100644
index 000000000..1bef17795
--- /dev/null
+++ b/spinetoolbox/group.py
@@ -0,0 +1,234 @@
+######################################################################################################################
+# 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 .
+######################################################################################################################
+
+"""Class for drawing an item group on QGraphicsScene."""
+from PySide6.QtCore import Qt, QMarginsF, QRectF
+from PySide6.QtGui import QBrush, QPen, QAction, QPainterPath, QTransform
+from PySide6.QtWidgets import (
+ QGraphicsItem,
+ QGraphicsRectItem,
+ QGraphicsTextItem,
+ QGraphicsDropShadowEffect,
+ QStyle
+)
+from .project_item_icon import ProjectItemIcon
+
+
+class Group(QGraphicsRectItem):
+
+ FONT_SIZE_PIXELS = 12 # pixel size to prevent font scaling by system
+
+ def __init__(self, toolbox, name, item_names):
+ super().__init__()
+ print(f"toolbox:{toolbox}")
+ self._toolbox = toolbox
+ self._name = name
+ self._item_names = item_names # strings
+ self._items = dict() # QGraphicsItems
+ conns = self._toolbox.project.connections + self._toolbox.project.jumps
+ for name in item_names:
+ try:
+ icon = self._toolbox.project.get_item(name).get_icon()
+ self._items[name] = icon
+ except KeyError: # name refers to a link or to a jump
+ link_icons = [link.graphics_item for link in conns if link.name == name]
+ self._items[name] = link_icons[0]
+ for item_icon in self._items.values():
+ item_icon.my_groups.add(self)
+ self._n_items = len(self._items)
+ disband_action = QAction("Ungroup items")
+ disband_action.triggered.connect(lambda checked=False, group_name=self.name: self._toolbox.ui.graphicsView.push_disband_group_command(checked, group_name))
+ rename_group_action = QAction("Rename group...")
+ rename_group_action.triggered.connect(lambda checked=False, group_name=self.name: self._toolbox.rename_group(checked, group_name))
+ self._actions = [disband_action, rename_group_action]
+ self.margins = QMarginsF(0, 0, 0, 10.0) # left, top, right, bottom
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=True)
+ self.setAcceptHoverEvents(True)
+ self.setZValue(-10)
+ self.name_item = QGraphicsTextItem(self._name, parent=self)
+ self.set_name_attributes()
+ self.setRect(self.rect())
+ self._reposition_name_item()
+ self.setBrush(self._toolbox.ui.graphicsView.scene().bg_color.lighter(107))
+ self.normal_pen = QPen(QBrush("gray"), 1, Qt.PenStyle.SolidLine)
+ self.selected_pen_for_ui_lite = QPen(QBrush("gray"), 5, Qt.PenStyle.DashLine)
+ self.selected_pen = QPen(QBrush("black"), 1, Qt.PenStyle.DashLine)
+ self.setPen(self.normal_pen)
+ self.set_graphics_effects()
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, new_name):
+ self._name = new_name
+ self.name_item.setPlainText(new_name)
+
+ @property
+ def items(self):
+ return self._items.values()
+
+ @property
+ def item_names(self):
+ return list(self._items.keys())
+
+ @property
+ def project_items(self):
+ return [item for item in self.items if isinstance(item, ProjectItemIcon)]
+
+ @property
+ def n_items(self):
+ return len(self._items)
+
+ def actions(self):
+ return self._actions
+
+ def set_name_attributes(self):
+ """Sets name item attributes (font, size, style, alignment)."""
+ self.name_item.setZValue(100)
+ font = self.name_item.font()
+ font.setPixelSize(self.FONT_SIZE_PIXELS)
+ font.setBold(True)
+ self.name_item.setFont(font)
+ option = self.name_item.document().defaultTextOption()
+ option.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.name_item.document().setDefaultTextOption(option)
+
+ def _reposition_name_item(self):
+ """Sets name item position (left side on top of the group icon)."""
+ main_rect = self.boundingRect()
+ name_rect = self.name_item.sceneBoundingRect()
+ self.name_item.setPos(main_rect.left(), main_rect.y() - name_rect.height() - 4)
+
+ def add_item(self, name):
+ """Adds item to this Group.
+
+ Args:
+ name (str): Project item or Link name
+ """
+ try:
+ icon = self._toolbox.project.get_item(name).get_icon()
+ except KeyError: # name refers to a link or to a jump
+ conns = self._toolbox.project.connections + self._toolbox.project.jumps
+ link_icons = [link.graphics_item for link in conns if link.name == name]
+ icon = link_icons[0]
+ icon.my_groups.add(self)
+ self._items[name] = icon
+ self.update_group_rect()
+
+ def remove_item(self, name):
+ """Removes item from this Group.
+
+ Args:
+ name (str): Project item name
+ """
+ item = self._items.pop(name)
+ for conn in item.connectors.values():
+ for link in conn.outgoing_links():
+ if link.name in self._items.keys():
+ self._items.pop(link.name)
+ link.my_groups.remove(self)
+ for link in conn.incoming_links():
+ if link.name in self._items.keys():
+ self._items.pop(link.name)
+ link.my_groups.remove(self)
+ item.my_groups.remove(self)
+ self.update_group_rect()
+
+ def remove_all_items(self):
+ """Removes all items (ProjectItemIcons) from this group."""
+ for item in self.project_items:
+ self.remove_item(item.name)
+
+ def update_group_rect(self):
+ """Updates group rectangle and it's attributes when group member(s) is/are moved."""
+ self.setRect(self.rect())
+ self._reposition_name_item()
+
+ def rect(self):
+ """Calculates the size of the rectangle for this group."""
+ united_rect = QRectF()
+ for item in self.items:
+ if isinstance(item, ProjectItemIcon):
+ united_rect = united_rect.united(item.name_item.sceneBoundingRect().united(item.sceneBoundingRect()))
+ else:
+ united_rect = united_rect.united(item.sceneBoundingRect())
+ rect_with_margins = united_rect.marginsAdded(self.margins)
+ return rect_with_margins
+
+ def set_graphics_effects(self):
+ shadow_effect = QGraphicsDropShadowEffect()
+ shadow_effect.setOffset(1)
+ shadow_effect.setEnabled(False)
+ self.setGraphicsEffect(shadow_effect)
+
+ def mousePressEvent(self, event):
+ """Sets all items belonging to this group selected.
+
+ Args:
+ event (QMousePressEvent): Event
+ """
+ event.accept()
+ path = QPainterPath()
+ path.addRect(self.rect())
+ self._toolbox.toolboxuibase.active_ui_window.ui.graphicsView.scene().setSelectionArea(path, QTransform())
+
+ def mouseReleaseEvent(self, event):
+ """Accepts the event to prevent graphics view's mouseReleaseEvent from clearing the selections."""
+ event.accept()
+
+ def contextMenuEvent(self, event):
+ """Opens context-menu in design mode."""
+ if self._toolbox.active_ui_mode == "toolboxuilite":
+ event.ignore() # Send context-menu request to graphics view
+ return
+ event.accept()
+ self.scene().clearSelection()
+ self.setSelected(True)
+ self._toolbox.show_project_or_item_context_menu(event.screenPos(), self)
+
+ def hoverEnterEvent(self, event):
+ """Sets a drop shadow effect to icon when mouse enters group boundaries.
+
+ Args:
+ event (QGraphicsSceneMouseEvent): Event
+ """
+ self.prepareGeometryChange()
+ self.graphicsEffect().setEnabled(True)
+ event.accept()
+
+ def hoverLeaveEvent(self, event):
+ """Disables the drop shadow when mouse leaves group boundaries.
+
+ Args:
+ event (QGraphicsSceneMouseEvent): Event
+ """
+ self.prepareGeometryChange()
+ self.graphicsEffect().setEnabled(False)
+ event.accept()
+
+ def to_dict(self):
+ """Returns a dictionary mapping group name to item names."""
+ return {self.name: self.item_names}
+
+ def paint(self, painter, option, widget=None):
+ """Sets a dashline pen when selected."""
+ if option.state & QStyle.StateFlag.State_Selected:
+ option.state &= ~QStyle.StateFlag.State_Selected
+ if self._toolbox.active_ui_mode == "toolboxui":
+ self.setPen(self.selected_pen)
+ elif self._toolbox.active_ui_mode == "toolboxuilite":
+ self.setPen(self.selected_pen_for_ui_lite)
+ else:
+ self.setPen(self.normal_pen)
+ super().paint(painter, option, widget)
diff --git a/spinetoolbox/link.py b/spinetoolbox/link.py
index 9e4f9f414..2762d6ee9 100644
--- a/spinetoolbox/link.py
+++ b/spinetoolbox/link.py
@@ -40,6 +40,8 @@ class LinkBase(QGraphicsPathItem):
"""
_COLOR = QColor(0, 0, 0, 0)
+ DEFAULT_LINK_SELECTION_PEN_W = 2
+ USER_MODE_LINK_SELECTION_PEN_W = 5
def __init__(self, toolbox, src_connector, dst_connector):
"""
@@ -53,20 +55,20 @@ def __init__(self, toolbox, src_connector, dst_connector):
self.src_connector = src_connector
self.dst_connector = dst_connector
self.arrow_angle = pi / 4
- self.setCursor(Qt.PointingHandCursor)
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
self._guide_path = None
self._pen = QPen(self._COLOR)
self._pen.setWidthF(self.magic_number)
- self._pen.setJoinStyle(Qt.MiterJoin)
+ self._pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)
self.setPen(self._pen)
- self.selected_pen = QPen(self.outline_color, 2, Qt.DotLine)
+ self.selected_pen = QPen(self.outline_color, self.DEFAULT_LINK_SELECTION_PEN_W, Qt.PenStyle.DotLine)
self.normal_pen = QPen(self.outline_color, 1)
self._outline = QGraphicsPathItem(self)
- self._outline.setFlag(QGraphicsPathItem.ItemStacksBehindParent)
+ self._outline.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemStacksBehindParent)
self._outline.setPen(self.normal_pen)
self._stroker = QPainterPathStroker()
self._stroker.setWidth(self.magic_number)
- self._stroker.setJoinStyle(Qt.MiterJoin)
+ self._stroker.setJoinStyle(Qt.PenJoinStyle.MiterJoin)
self._shape = QPainterPath()
def shape(self):
@@ -100,6 +102,10 @@ def dst_center(self):
"""Returns the center point of the destination rectangle."""
return self.dst_rect.center()
+ def set_link_selection_pen_w(self, pen_width):
+ """Sets selected links dash line width."""
+ self.selected_pen = QPen(self.outline_color, pen_width, Qt.PenStyle.DotLine)
+
def moveBy(self, _dx, _dy):
"""Does nothing. This item is not moved the regular way, but follows the ConnectorButtons it connects."""
@@ -131,7 +137,7 @@ def _add_ellipse_path(self, path):
"""Adds an ellipse for the link's base.
Args:
- QPainterPath
+ path (QPainterPath)
"""
radius = 0.5 * self.magic_number
rect = QRectF(0, 0, radius, radius)
@@ -145,7 +151,7 @@ def _add_arrow_path(self, path):
"""Returns an arrow path for the link's tip.
Args:
- QPainterPath
+ path (QPainterPath)
"""
angle = self._get_joint_angle()
arrow_p0 = self.dst_center + 0.5 * self.magic_number * self._get_dst_offset()
@@ -275,7 +281,7 @@ def __init__(self, x, y, w, h, parent, tooltip=None, active=True):
if tooltip:
self.setToolTip(tooltip)
self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=False)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)
self.setBrush(palette.window())
def hoverEnterEvent(self, event):
@@ -300,7 +306,7 @@ def __init__(self, parent, extent, path, tooltip=None, active=False):
scale = 0.8 * self.rect().width() / self._renderer.defaultSize().width()
self._svg_item.setScale(scale)
self._svg_item.setPos(self.sceneBoundingRect().center() - self._svg_item.sceneBoundingRect().center())
- self.setPen(Qt.NoPen)
+ self.setPen(Qt.PenStyle.NoPen)
def wipe_out(self):
"""Cleans up icon's resources."""
@@ -315,12 +321,12 @@ class _TextIcon(_IconBase):
def __init__(self, parent, extent, char, tooltip=None, active=False):
super().__init__(0, 0, extent, extent, parent, tooltip=tooltip, active=active)
self._text_item = QGraphicsTextItem(self)
- font = QFont("Font Awesome 5 Free Solid", weight=QFont.Bold)
+ font = QFont("Font Awesome 5 Free Solid", weight=QFont.Weight.Bold)
self._text_item.setFont(font)
self._text_item.setDefaultTextColor(self._fg_color)
self._text_item.setPlainText(char)
self._text_item.setPos(self.sceneBoundingRect().center() - self._text_item.sceneBoundingRect().center())
- self.setPen(Qt.NoPen)
+ self.setPen(Qt.PenStyle.NoPen)
def wipe_out(self):
"""Cleans up icon's resources."""
@@ -342,11 +348,12 @@ class JumpOrLink(LinkBase):
def __init__(self, toolbox, src_connector, dst_connector):
super().__init__(toolbox, src_connector, dst_connector)
- self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True)
- self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=True)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, enabled=True)
self._icon_extent = 3 * self.magic_number
self._icons = []
self._anim = self._make_execution_animation()
+ self.my_groups = set()
self.update_geometry()
@property
@@ -385,6 +392,7 @@ def mousePressEvent(self, e):
"""
if any(isinstance(x, ConnectorButton) for x in self.scene().items(e.scenePos())):
e.ignore()
+ return
def contextMenuEvent(self, e):
"""Selects the link and shows context menu.
@@ -396,7 +404,7 @@ def contextMenuEvent(self, e):
self._toolbox.show_link_context_menu(e.screenPos(), self)
def paint(self, painter, option, widget=None):
- """Sets a dashed pen if selected."""
+ """Sets a dotted pen when selected."""
if option.state & QStyle.StateFlag.State_Selected:
option.state &= ~QStyle.StateFlag.State_Selected
self._outline.setPen(self.selected_pen)
diff --git a/spinetoolbox/project.py b/spinetoolbox/project.py
index f286d50de..3805d6e23 100644
--- a/spinetoolbox/project.py
+++ b/spinetoolbox/project.py
@@ -17,7 +17,7 @@
import os
from pathlib import Path
import networkx as nx
-from PySide6.QtCore import QCoreApplication, Signal
+from PySide6.QtCore import QCoreApplication, Signal, Slot
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QMessageBox
from spine_engine.exception import EngineInitFailed, RemoteEngineInitFailed
@@ -62,6 +62,8 @@
from .project_upgrader import ProjectUpgrader
from .server.engine_client import EngineClient
from .spine_engine_worker import SpineEngineWorker
+from .ui_main_lite import ToolboxUILite
+from .group import Group
@unique
@@ -107,6 +109,10 @@ class SpineToolboxProject(MetaObject):
"""Emitted after a specification has been replaced."""
specification_saved = Signal(str, str)
"""Emitted after a specification has been saved."""
+ group_added = Signal(object)
+ """Emitted after new Group has been added to project."""
+ group_disbanded = Signal(object)
+ """Emitted after a Group has been removed from project."""
LOCAL_EXECUTION_JOB_ID = "1"
@@ -132,6 +138,8 @@ def __init__(self, toolbox, p_dir, plugin_specs, app_settings, settings, logger)
self._settings = settings
self._engine_workers = []
self._execution_in_progress = False
+ # self._execution_groups = {}
+ self._groups = dict()
self.project_dir = None # Full path to project directory
self.config_dir = None # Full path to .spinetoolbox directory
self.items_dir = None # Full path to items directory
@@ -173,6 +181,55 @@ def jumps(self):
def app_settings(self):
return self._app_settings
+ @property
+ def groups(self):
+ return self._groups
+
+ def make_new_group_name(self):
+ """Returns a unique name for a new Group."""
+ group_number = 1
+ group_name_taken = True
+ group_name = "NA"
+ while group_name_taken:
+ group_name = "Group " + str(group_number)
+ if group_name in self.groups.keys():
+ group_name_taken = True
+ group_number += 1
+ else:
+ group_name_taken = False
+ return group_name
+
+ def make_group(self, group_name, item_names):
+ """Adds a Group with the given name and given item names to project.
+
+ Args:
+ group_name (str): Group name
+ item_names (list): List of item names to group
+ """
+ group = Group(self.toolbox().active_ui_window, group_name, item_names)
+ self._groups[group_name] = group
+ self.group_added.emit(group)
+
+ def add_item_to_group(self, item_name, group_name):
+ """Adds item with given name to Group with given name."""
+ self.groups[group_name].add_item(item_name)
+
+ def remove_item_from_group(self, item_name, group_name):
+ """Removes item with given name from given group. If only
+ one item remains in the group, destroys the whole group."""
+ self.groups[group_name].remove_item(item_name)
+ if len(self.groups[group_name].project_items) == 1:
+ self.disband_group(False, group_name)
+
+ @Slot(bool, str)
+ def disband_group(self, _, group_name):
+ """Removes all items from a given group and destroys the group."""
+ self.groups[group_name].remove_all_items()
+ self.groups[group_name].prepareGeometryChange()
+ self.groups[group_name].setGraphicsEffect(None)
+ group = self.groups.pop(group_name)
+ self.group_disbanded.emit(group)
+
def has_items(self):
"""Returns True if project has project items.
@@ -260,6 +317,7 @@ def save(self):
"specifications": serialized_spec_paths,
"connections": [connection.to_dict() for connection in self._connections],
"jumps": [jump.to_dict() for jump in self._jumps],
+ "groups": [group.to_dict() for group in self.groups.values()],
}
items_dict = {name: item.item_dict() for name, item in self._project_items.items()}
local_items_data = self._pop_local_data_from_items_dict(items_dict)
@@ -370,8 +428,9 @@ def load(self, spec_factories, item_factories):
if not items_dict:
self._logger.msg_warning.emit("Project has no items")
self.restore_project_items(items_dict, item_factories)
- self._logger.msg.emit("Restoring connections...")
connection_dicts = project_info["project"]["connections"]
+ if len(connection_dicts) > 0:
+ self._logger.msg.emit("Restoring connections...")
connections = list(map(self.connection_from_dict, connection_dicts))
for connection in connections:
self.add_connection(connection, silent=True, notify_resource_changes=False)
@@ -381,10 +440,17 @@ def load(self, spec_factories, item_factories):
self._notify_rsrc_changes(destination, source)
for connection in connections:
connection.link.update_icons()
- self._logger.msg.emit("Restoring jumps...")
jump_dicts = project_info["project"].get("jumps", [])
+ if len(jump_dicts) > 0:
+ self._logger.msg.emit("Restoring jumps...")
for jump in map(self.jump_from_dict, jump_dicts):
self.add_jump(jump, silent=True)
+ groups_list = project_info["project"].get("groups", [])
+ if len(groups_list) > 0:
+ self._logger.msg.emit("Restoring groups...")
+ for group_dict in groups_list:
+ for group_name, item_names in group_dict.items():
+ self.make_group(group_name, item_names)
return True
@staticmethod
@@ -668,7 +734,7 @@ def rename_item(self, previous_name, new_name, rename_data_dir_message):
self._logger.error_box.emit("Invalid name", msg)
return False
item = self._project_items.pop(previous_name, None)
- if item is None:
+ if not item:
# Happens when renaming an item, removing, and then closing the project.
# We try to undo the renaming because it's critical, but the item doesn't exist anymore so it's fine.
return True
diff --git a/spinetoolbox/project_commands.py b/spinetoolbox/project_commands.py
index d6f818797..40948a58d 100644
--- a/spinetoolbox/project_commands.py
+++ b/spinetoolbox/project_commands.py
@@ -71,10 +71,10 @@ def __init__(self, icon, project):
self._representative = next(iter(icon_group), None)
if self._representative is None:
self.setObsolete(True)
- self._previous_pos = {x.name(): x.previous_pos for x in icon_group}
- self._current_pos = {x.name(): x.scenePos() for x in icon_group}
+ self._previous_pos = {x.name: x.previous_pos for x in icon_group}
+ self._current_pos = {x.name: x.scenePos() for x in icon_group}
if len(icon_group) == 1:
- self.setText(f"move {self._representative.name()}")
+ self.setText(f"move {self._representative.name}")
else:
self.setText("move multiple items")
@@ -244,6 +244,119 @@ def is_critical(self):
return True
+class MakeGroupCommand(SpineToolboxCommand):
+ """Command to add a group of project items to project."""
+
+ def __init__(self, project, item_names):
+ """
+ Args:
+ project (SpineToolboxProject): project
+ item_names (list): List of item names to group
+ """
+ super().__init__()
+ self._project = project
+ self._item_names = item_names
+ self._group_name = self._project.make_new_group_name()
+ self.setText(f"make {self._group_name}")
+
+ def redo(self):
+ self._project.make_group(self._group_name, self._item_names)
+
+ def undo(self):
+ self._project.disband_group(False, self._group_name)
+
+
+class RenameGroupCommand(SpineToolboxCommand):
+ """Command to rename groups."""
+
+ def __init__(self, project, previous_name, new_name):
+ """
+ Args:
+ project (SpineToolboxProject): The project
+ previous_name (str): Groups previous name
+ new_name (str): New Group name
+ """
+ super().__init__()
+ self._project = project
+ self._previous_name = previous_name
+ self._new_name = new_name
+ self.setText(f"rename Group {self._previous_name} to {self._new_name}")
+
+ def redo(self):
+ if self._new_name in self._project.groups.keys():
+ self.setObsolete(True)
+ group = self._project.groups.pop(self._previous_name)
+ group.name = self._new_name
+ self._project.groups[self._new_name] = group
+
+ def undo(self):
+ group = self._project.groups.pop(self._new_name)
+ group.name = self._previous_name
+ self._project.groups[self._previous_name] = group
+
+
+class RemoveItemFromGroupCommand(SpineToolboxCommand):
+ """Command to remove an item from a group. If only one item
+ remains in the group after the operation, disbands the group."""
+
+ def __init__(self, project, item_name, group_name):
+ """
+ Args:
+ project (SpineToolboxProject): Project
+ item_name (str): Item name to remove from group
+ group_name (str): Group to edit
+ """
+ super().__init__()
+ self._project = project
+ self._item_name = item_name
+ self._group_name = group_name
+ self._item_names = self._project.groups[group_name].item_names
+ self._links_removed = []
+ self._remake_group = False
+ if len(self._project.groups[group_name].project_items) == 2:
+ self._remake_group = True
+ self.setText(f"disband {self._group_name}")
+
+ def redo(self):
+ self._project.remove_item_from_group(self._item_name, self._group_name)
+ if not self._remake_group:
+ self._links_removed = [i for i in self._item_names if i not in self._project.groups[self._group_name].item_names]
+ self._links_removed.remove(self._item_name)
+
+ def undo(self):
+ if self._remake_group:
+ # Redo removed the whole group
+ self._project.make_group(self._group_name, self._item_names)
+ return
+ # First, add the project item icon back into group
+ self._project.add_item_to_group(self._item_name, self._group_name)
+ # Then, add link icons back
+ for link_name in self._links_removed:
+ self._project.add_item_to_group(link_name, self._group_name)
+
+
+class DisbandGroupCommand(SpineToolboxCommand):
+ """Command to disband a group of project items."""
+
+ def __init__(self, project, group_name):
+ """
+ Args:
+ project (SpineToolboxProject): project
+ group_name (Group): Name of Group to disband
+ """
+ super().__init__()
+ self._project = project
+ self._group_name = group_name
+ self._item_names = self._project.groups[group_name].item_names
+ self.setText(f"disband {self._group_name}")
+
+ def redo(self):
+ self._project.disband_group(False, self._group_name)
+
+ def undo(self):
+ self._project.make_group(self._group_name, self._item_names)
+
+
class AddConnectionCommand(SpineToolboxCommand):
"""Command to add connection between project items."""
diff --git a/spinetoolbox/project_item/project_item.py b/spinetoolbox/project_item/project_item.py
index 15acd0f07..d04f99f6b 100644
--- a/spinetoolbox/project_item/project_item.py
+++ b/spinetoolbox/project_item/project_item.py
@@ -19,6 +19,7 @@
from ..log_mixin import LogMixin
from ..metaobject import MetaObject
from ..project_commands import SetItemSpecificationCommand
+from ..ui_main_lite import ToolboxUILite
class ProjectItem(LogMixin, MetaObject):
@@ -211,6 +212,12 @@ def set_rank(self, rank):
else:
self.get_icon().rank_icon.set_rank("X")
+ def update_progress_bar(self):
+ # print(f"{self._project.active_toolboxui}")
+ if self._toolbox.active_ui_mode == "toolboxuilite":
+ n_selected = len(self._toolbox.ui.graphicsView.scene().selectedItems())
+ print(f"[{n_selected}] started: {self.name}")
+
@property
def executable_class(self):
raise NotImplementedError()
diff --git a/spinetoolbox/project_item_icon.py b/spinetoolbox/project_item_icon.py
index 041e348ee..23790db6a 100644
--- a/spinetoolbox/project_item_icon.py
+++ b/spinetoolbox/project_item_icon.py
@@ -36,7 +36,8 @@ class ProjectItemIcon(QGraphicsPathItem):
ITEM_EXTENT = 64
FONT_SIZE_PIXELS = 12 # pixel size to prevent font scaling by system
- DEFAULT_SELECTION_PEN_WIDTH = 1
+ DEFAULT_ICON_SELECTION_PEN_W = 1
+ USER_MODE_ICON_SELECTION_PEN_W = 5
def __init__(self, toolbox, icon_file, icon_color):
"""
@@ -55,6 +56,7 @@ def __init__(self, toolbox, icon_file, icon_color):
self._moved_on_scene = False
self.previous_pos = QPointF()
self.icon_group = {self}
+ self.my_groups = set()
self.renderer = QSvgRenderer()
self.svg_item = QGraphicsSvgItem(self)
self.svg_item.setZValue(100)
@@ -68,7 +70,7 @@ def __init__(self, toolbox, icon_file, icon_color):
self.rank_icon = RankIcon(self)
# Make item name graphics item.
self._name = ""
- self.name_item = QGraphicsTextItem(self._name)
+ self.name_item = QGraphicsTextItem(self._name, parent=self)
self.name_item.setZValue(100)
self.set_name_attributes() # Set font, size, position, etc.
self.spec_item = None # For displaying Tool Spec icon
@@ -137,9 +139,9 @@ def _do_update_path(self, rounded):
path = QPainterPath()
path.addRoundedRect(self._rect.adjusted(-margin, -margin, margin, margin), radius + margin, radius + margin)
self._selection_halo.setPath(path)
- self.set_selection_halo_pen(self.DEFAULT_SELECTION_PEN_WIDTH)
+ self.set_icon_selection_pen_w(self.DEFAULT_ICON_SELECTION_PEN_W)
- def set_selection_halo_pen(self, pen_width):
+ def set_icon_selection_pen_w(self, pen_width):
"""Sets the selected items dash line width."""
selection_pen = QPen(Qt.PenStyle.DashLine)
selection_pen.setWidthF(pen_width)
@@ -197,12 +199,8 @@ def _setup(self):
)
self.rank_icon.setPos(self.rect().topLeft())
+ @property
def name(self):
- """Returns name of the item that is represented by this icon.
-
- Returns:
- str: icon's name
- """
return self._name
def update_name_item(self, new_name):
@@ -228,7 +226,8 @@ def set_name_attributes(self):
def _reposition_name_item(self):
"""Sets name item position (centered on top of the master icon)."""
- main_rect = self.sceneBoundingRect()
+
+ main_rect = self.boundingRect()
name_rect = self.name_item.sceneBoundingRect()
self.name_item.setPos(main_rect.center().x() - name_rect.width() / 2, main_rect.y() - name_rect.height() - 4)
@@ -354,7 +353,7 @@ def contextMenuEvent(self, event):
event.accept()
self.scene().clearSelection()
self.setSelected(True)
- item = self._toolbox.project.get_item(self.name())
+ item = self._toolbox.project.get_item(self.name)
self._toolbox.show_project_or_item_context_menu(event.screenPos(), item)
def itemChange(self, change, value):
@@ -376,6 +375,7 @@ def itemChange(self, change, value):
self._reposition_name_item()
self.update_links_geometry()
self._handle_collisions()
+ self.update_group_rectangle()
elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None:
self.prepareGeometryChange()
self.setGraphicsEffect(None)
@@ -389,6 +389,13 @@ def itemChange(self, change, value):
self._reposition_name_item()
return super().itemChange(change, value)
+ def update_group_rectangle(self):
+ """Updates group icon if this icon is in a group."""
+ if not self.my_groups:
+ return
+ for group in self.my_groups:
+ group.update_group_rect()
+
def set_pos_without_bumping(self, pos):
"""Sets position without bumping other items. Needed for undoing move operations.
@@ -413,7 +420,7 @@ def make_room_for_item(self, other):
"""Makes room for another item.
Args:
- item (ProjectItemIcon)
+ other (ProjectItemIcon): Other item
"""
if self not in other.bumped_rects:
other.bumped_rects[self] = self.sceneBoundingRect()
@@ -480,7 +487,7 @@ def __init__(self, toolbox, parent, position="left"):
elif position == "right":
self._rect.moveCenter(QPointF(parent_rect.right() - extent / 2, parent_rect.center().y()))
self.setAcceptHoverEvents(True)
- self.setCursor(Qt.PointingHandCursor)
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
def rect(self):
return self._rect
@@ -503,7 +510,7 @@ def incoming_links(self):
def parent_name(self):
"""Returns project item name owning this connector button."""
- return self._parent.name()
+ return self._parent.name
def project_item(self):
"""Returns the project item this connector button is attached to.
@@ -511,7 +518,7 @@ def project_item(self):
Returns:
ProjectItem: project item
"""
- return self._toolbox.project.get_item(self._parent.name())
+ return self._toolbox.project.get_item(self._parent.name)
def mousePressEvent(self, event):
"""Connector button mouse press event.
@@ -595,17 +602,17 @@ def __init__(self, parent):
self._text_item.setFont(font)
parent_rect = parent.rect()
self.setRect(0, 0, 0.5 * parent_rect.width(), 0.5 * parent_rect.height())
- self.setPen(Qt.NoPen)
+ self.setPen(Qt.PenStyle.NoPen)
# pylint: disable=undefined-variable
self.normal_brush = qApp.palette().window()
self.selected_brush = qApp.palette().highlight()
self.setBrush(self.normal_brush)
self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=False)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)
self.hide()
def item_name(self):
- return self._parent.name()
+ return self._parent.name
def _repaint(self, text, color):
self._text_item.prepareGeometryChange()
diff --git a/spinetoolbox/resources_icons_rc.py b/spinetoolbox/resources_icons_rc.py
index 4ffc2f461..8f3db47be 100644
--- a/spinetoolbox/resources_icons_rc.py
+++ b/spinetoolbox/resources_icons_rc.py
@@ -29816,6 +29816,51 @@
dth:0.70175511;f\
ill:#0000ff\x22 />\x0d\
\x0a\x0d\x0a\
+\x00\x00\x02\xa7\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 viewBox=\x22\
+0 0 512 512\x22>\
+\
\x00\x00\x0a\x8e\
<\
?xml version=\x221.\
@@ -32382,6 +32427,10 @@
\x03\xf0\xc9\xa7\
\x00c\
\x00u\x00b\x00e\x00s\x00_\x00p\x00e\x00n\x00.\x00s\x00v\x00g\
+\x00\x10\
+\x01\x90C\xc7\
+\x00o\
+\x00b\x00j\x00e\x00c\x00t\x00-\x00g\x00r\x00o\x00u\x00p\x00.\x00s\x00v\x00g\
\x00\x0e\
\x03a\x89\xe7\
\x00s\
@@ -32497,7 +32546,7 @@
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x10\x00\x02\x00\x00\x00\x03\x00\x00\x00a\
+\x00\x00\x00\x10\x00\x02\x00\x00\x00\x03\x00\x00\x00b\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1b\x00\x00\x00\x09\
\x00\x00\x00\x00\x00\x00\x00\x00\
@@ -32516,7 +32565,7 @@
\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x06A\x9c\
\x00\x00\x01\x8f\xec\xcf\x12\xea\
\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x06\x978\
-\x00\x00\x01\x8f\xee\x17E2\
+\x00\x00\x01\x91y6\xd1)\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x062\x92\
\x00\x00\x01\x8f\xec\xcf\x13\x09\
\x00\x00\x04\x94\x00\x00\x00\x00\x00\x01\x00\x06\xaa\xa4\
@@ -32551,9 +32600,9 @@
\x00\x00\x01\x8f\xec\xcf\x13\x09\
\x00\x00\x04H\x00\x00\x00\x00\x00\x01\x00\x06\xa1\xe7\
\x00\x00\x01\x8f\xec\xcf\x13\x09\
-\x00\x00\x03\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00X\
+\x00\x00\x03\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00Y\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x03~\x00\x02\x00\x00\x004\x00\x00\x00$\
+\x00\x00\x03~\x00\x02\x00\x00\x005\x00\x00\x00$\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02\x0a\x00\x00\x00\x00\x00\x01\x00\x065G\
\x00\x00\x01\x8f\xec\xcf\x13\x09\
@@ -32569,43 +32618,45 @@
\x00\x00\x01\x8f\xec\xcf\x12\xea\
\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x07\x0d\xf6\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0c,\x00\x00\x00\x00\x00\x01\x00\x07\xc5\x10\
+\x00\x00\x0cR\x00\x00\x00\x00\x00\x01\x00\x07\xc7\xbb\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06T\x00\x00\x00\x00\x00\x01\x00\x06\xee\xb6\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09T\x00\x00\x00\x00\x00\x01\x00\x07M\xd2\
+\x00\x00\x09z\x00\x00\x00\x00\x00\x01\x00\x07P}\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09\xda\x00\x00\x00\x00\x00\x01\x00\x07d\xa4\
+\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x07=|\
+\x00\x00\x01f\xd4x\xbb0\
+\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x07gO\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x076\x00\x00\x00\x00\x00\x01\x00\x07\x0f\x05\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x07H\x0e\
+\x00\x00\x08\xfe\x00\x00\x00\x00\x00\x01\x00\x07J\xb9\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09\xb6\x00\x00\x00\x00\x00\x01\x00\x07b\xe2\
+\x00\x00\x09\xdc\x00\x00\x00\x00\x00\x01\x00\x07e\x8d\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0b\x0e\x00\x00\x00\x00\x00\x01\x00\x07\xa5\xe8\
+\x00\x00\x0b4\x00\x00\x00\x00\x00\x01\x00\x07\xa8\x93\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07\xe0\x00\x00\x00\x00\x00\x01\x00\x07#\xd4\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07V\x00\x00\x00\x00\x00\x01\x00\x07\x13\x92\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09t\x00\x00\x00\x00\x00\x01\x00\x07_\xb0\
+\x00\x00\x09\x9a\x00\x00\x00\x00\x00\x01\x00\x07b[\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x08\xb8\x00\x00\x00\x00\x00\x01\x00\x07.\xfe\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x08\xfa\x00\x00\x00\x00\x00\x01\x00\x07J\x1b\
+\x00\x00\x09 \x00\x00\x00\x00\x00\x01\x00\x07L\xc6\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x082\x00\x00\x00\x00\x00\x01\x00\x07)\xcf\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09.\x00\x00\x00\x00\x00\x01\x00\x07Kw\
+\x00\x00\x09T\x00\x00\x00\x00\x00\x01\x00\x07N\x22\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06z\x00\x00\x00\x00\x00\x01\x00\x06\xf0\xbf\
\x00\x00\x01\x8f\xec\xcf\x12\xea\
-\x00\x00\x0a@\x00\x00\x00\x00\x00\x01\x00\x07\x83\xec\
+\x00\x00\x0af\x00\x00\x00\x00\x00\x01\x00\x07\x86\x97\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07\xf8\x00\x00\x00\x00\x00\x01\x00\x07%\xc7\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0b~\x00\x00\x00\x00\x00\x01\x00\x07\xac-\
+\x00\x00\x0b\xa4\x00\x00\x00\x00\x00\x01\x00\x07\xae\xd8\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06>\x00\x00\x00\x00\x00\x01\x00\x06\xecG\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
@@ -32615,61 +32666,61 @@
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x07\x1f\xb7\
\x00\x00\x01\x8f\xec\xcf\x12\xea\
-\x00\x00\x0b\xe4\x00\x00\x00\x00\x00\x01\x00\x07\xb39\
+\x00\x00\x0c\x0a\x00\x00\x00\x00\x00\x01\x00\x07\xb5\xe4\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0bN\x00\x00\x00\x00\x00\x01\x00\x07\xab(\
+\x00\x00\x0bt\x00\x00\x00\x00\x00\x01\x00\x07\xad\xd3\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x06\xe9\x22\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x08\x1a\x00\x00\x00\x00\x00\x01\x00\x07&\xf4\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0a\x12\x00\x00\x00\x00\x00\x01\x00\x07\x7f6\
+\x00\x00\x0a8\x00\x00\x00\x00\x00\x01\x00\x07\x81\xe1\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0c\x0c\x00\x00\x00\x00\x00\x01\x00\x07\xb5\x94\
+\x00\x00\x0c2\x00\x00\x00\x00\x00\x01\x00\x07\xb8?\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07z\x00\x01\x00\x00\x00\x01\x00\x07\x1eE\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0a\x94\x00\x00\x00\x00\x00\x01\x00\x07\x93\x7f\
+\x00\x00\x0a\xba\x00\x00\x00\x00\x00\x01\x00\x07\x96*\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09\x9e\x00\x00\x00\x00\x00\x01\x00\x07a\xc3\
+\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x07dn\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x09\xfc\x00\x00\x00\x00\x00\x01\x00\x07s\x12\
+\x00\x00\x0a\x22\x00\x00\x00\x00\x00\x01\x00\x07u\xbd\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x06\xf7\xfc\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0b2\x00\x00\x00\x00\x00\x01\x00\x07\xa7\xae\
+\x00\x00\x0bX\x00\x00\x00\x00\x00\x01\x00\x07\xaaY\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0a\xc0\x00\x00\x00\x00\x00\x01\x00\x07\x96A\
+\x00\x00\x0a\xe6\x00\x00\x00\x00\x00\x01\x00\x07\x98\xec\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0ad\x00\x00\x00\x00\x00\x01\x00\x07\x91\xe9\
+\x00\x00\x0a\x8a\x00\x00\x00\x00\x00\x01\x00\x07\x94\x94\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0a(\x00\x00\x00\x00\x00\x01\x00\x07\x80\xe9\
+\x00\x00\x0aN\x00\x00\x00\x00\x00\x01\x00\x07\x83\x94\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x07\xca\x00\x00\x00\x00\x00\x01\x00\x07!\x85\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06\xf8\x00\x00\x00\x00\x00\x01\x00\x06\xfc\x96\
-\x00\x00\x01f\xd4x\xbb0\
-\x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x07\x98\x8b\
+\x00\x00\x01\x91y6\xd1*\
+\x00\x00\x0a\xfe\x00\x00\x00\x00\x00\x01\x00\x07\x9b6\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x06\xf6R\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0a\xf0\x00\x00\x00\x00\x00\x01\x00\x07\x99\xf1\
+\x00\x00\x0b\x16\x00\x00\x00\x00\x00\x01\x00\x07\x9c\x9c\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0b\xc8\x00\x00\x00\x00\x00\x01\x00\x07\xaf\xef\
+\x00\x00\x0b\xee\x00\x00\x00\x00\x00\x01\x00\x07\xb2\x9a\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x08f\x00\x01\x00\x00\x00\x01\x00\x07+$\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x07+\xfe\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x0b\xa2\x00\x00\x00\x00\x00\x01\x00\x07\xae\x10\
+\x00\x00\x0b\xc8\x00\x00\x00\x00\x00\x01\x00\x07\xb0\xbb\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x06\xaa\x00\x00\x00\x00\x00\x01\x00\x06\xf2\x8b\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x07\x11a\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x05|\x00\x00\x00\x00\x00\x01\x00\x07=|\
+\x00\x00\x05|\x00\x00\x00\x00\x00\x01\x00\x07@'\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
-\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x07tf\
+\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x07w\x11\
\x00\x00\x01\x8f\xec\xcf\x12\xfa\
\x00\x00\x05\xd4\x00\x00\x00\x00\x00\x01\x00\x06\xe5\x11\
\x00\x00\x01\x8f\xec\xcf\x13\x09\
diff --git a/spinetoolbox/spine_engine_worker.py b/spinetoolbox/spine_engine_worker.py
index 850c4617b..85250da0d 100644
--- a/spinetoolbox/spine_engine_worker.py
+++ b/spinetoolbox/spine_engine_worker.py
@@ -37,15 +37,17 @@ def _handle_node_execution_started(item, direction):
icon = item.get_icon()
if direction == ExecutionDirection.FORWARD:
icon.execution_icon.mark_execution_started()
+ item.update_progress_bar()
if hasattr(icon, "animation_signaller"):
icon.animation_signaller.animation_started.emit()
-@Slot(object, object, object, object)
+@Slot(object, object, object)
def _handle_node_execution_finished(item, direction, item_state):
icon = item.get_icon()
if direction == ExecutionDirection.FORWARD:
icon.execution_icon.mark_execution_finished(item_state)
+ item.update_progress_bar()
if hasattr(icon, "animation_signaller"):
icon.animation_signaller.animation_stopped.emit()
@@ -60,7 +62,7 @@ def _handle_process_message_arrived(item, filter_id, msg_type, msg_text):
item.add_process_message(filter_id, msg_type, msg_text)
-@Slot(dict, object)
+@Slot(dict, object, object)
def _handle_prompt_arrived(prompt, engine_mngr, logger=None):
prompter_id = prompt["prompter_id"]
title, text, option_to_answer, notes, preferred = prompt["data"]
@@ -155,11 +157,11 @@ def set_engine_data(self, engine_data):
"""
self._engine_data = engine_data
- @Slot(object, str, str)
+ @Slot(object, str, str, str)
def _handle_event_message_arrived_silent(self, item, filter_id, msg_type, msg_text):
self.event_messages.setdefault(msg_type, []).append(msg_text)
- @Slot(object, str, str)
+ @Slot(object, str, str, str)
def _handle_process_message_arrived_silent(self, item, filter_id, msg_type, msg_text):
self.process_messages.setdefault(msg_type, []).append(msg_text)
diff --git a/spinetoolbox/ui/datetime_editor.py b/spinetoolbox/ui/datetime_editor.py
index 8724c1000..bfd19e85b 100644
--- a/spinetoolbox/ui/datetime_editor.py
+++ b/spinetoolbox/ui/datetime_editor.py
@@ -14,7 +14,7 @@
################################################################################
## Form generated from reading UI file 'datetime_editor.ui'
##
-## Created by: Qt User Interface Compiler version 6.6.3
+## Created by: Qt User Interface Compiler version 6.7.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
diff --git a/spinetoolbox/ui/mainwindow.py b/spinetoolbox/ui/mainwindow.py
index 09638b18b..042dcb199 100644
--- a/spinetoolbox/ui/mainwindow.py
+++ b/spinetoolbox/ui/mainwindow.py
@@ -41,7 +41,7 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
- MainWindow.resize(823, 615)
+ MainWindow.resize(820, 615)
MainWindow.setDockNestingEnabled(True)
self.actionQuit = QAction(MainWindow)
self.actionQuit.setObjectName(u"actionQuit")
@@ -202,7 +202,7 @@ def setupUi(self, MainWindow):
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
- self.menubar.setGeometry(QRect(0, 0, 823, 33))
+ self.menubar.setGeometry(QRect(0, 0, 820, 33))
self.menubar.setNativeMenuBar(False)
self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile")
@@ -418,7 +418,7 @@ def setupUi(self, MainWindow):
# setupUi
def retranslateUi(self, MainWindow):
- MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Spine Toolbox [dev mode]", None))
+ MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Spine Toolbox [design mode]", None))
self.actionQuit.setText(QCoreApplication.translate("MainWindow", u"Quit", None))
#if QT_CONFIG(tooltip)
self.actionQuit.setToolTip(QCoreApplication.translate("MainWindow", u"
Quit
", None))
diff --git a/spinetoolbox/ui/mainwindow.ui b/spinetoolbox/ui/mainwindow.ui
index bc25d3886..f42529dd3 100644
--- a/spinetoolbox/ui/mainwindow.ui
+++ b/spinetoolbox/ui/mainwindow.ui
@@ -19,12 +19,12 @@
0
0
- 823
+ 820
615
- Spine Toolbox [dev mode]
+ Spine Toolbox [design mode]
true
@@ -35,7 +35,7 @@
0
0
- 823
+ 820
33
@@ -897,16 +897,16 @@
-
- CustomQTextBrowser
- QTextBrowser
- spinetoolbox/widgets/custom_qtextbrowser.h
-
DesignQGraphicsView
QGraphicsView
spinetoolbox/widgets/custom_qgraphicsviews.h
+
+ CustomQTextBrowser
+ QTextBrowser
+ spinetoolbox/widgets/custom_qtextbrowser.h
+
graphicsView
diff --git a/spinetoolbox/ui/mainwindowlite.py b/spinetoolbox/ui/mainwindowlite.py
index 82a422fe1..aa83cbf9c 100644
--- a/spinetoolbox/ui/mainwindowlite.py
+++ b/spinetoolbox/ui/mainwindowlite.py
@@ -27,10 +27,10 @@
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
-from PySide6.QtWidgets import (QApplication, QColumnView, QComboBox, QGraphicsView,
- QGroupBox, QHBoxLayout, QMainWindow, QMenu,
- QMenuBar, QScrollArea, QSizePolicy, QSplitter,
- QStatusBar, QToolButton, QVBoxLayout, QWidget)
+from PySide6.QtWidgets import (QApplication, QColumnView, QGraphicsView, QGroupBox,
+ QHBoxLayout, QMainWindow, QMenu, QMenuBar,
+ QScrollArea, QSizePolicy, QSplitter, QStatusBar,
+ QVBoxLayout, QWidget)
from spinetoolbox.widgets.custom_qgraphicsviews import DesignQGraphicsView
from spinetoolbox import resources_icons_rc
@@ -40,11 +40,11 @@ def setupUi(self, MainWindowLite):
if not MainWindowLite.objectName():
MainWindowLite.setObjectName(u"MainWindowLite")
MainWindowLite.resize(800, 600)
- self.actionSwitch_to_expert_mode = QAction(MainWindowLite)
- self.actionSwitch_to_expert_mode.setObjectName(u"actionSwitch_to_expert_mode")
+ self.actionSwitch_to_design_mode = QAction(MainWindowLite)
+ self.actionSwitch_to_design_mode.setObjectName(u"actionSwitch_to_design_mode")
icon = QIcon()
icon.addFile(u":/icons/menu_icons/retweet.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionSwitch_to_expert_mode.setIcon(icon)
+ self.actionSwitch_to_design_mode.setIcon(icon)
self.actionExecute_group = QAction(MainWindowLite)
self.actionExecute_group.setObjectName(u"actionExecute_group")
icon1 = QIcon()
@@ -80,61 +80,18 @@ def setupUi(self, MainWindowLite):
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 780, 293))
self.horizontalLayout = QHBoxLayout(self.scrollAreaWidgetContents)
self.horizontalLayout.setObjectName(u"horizontalLayout")
- self.groupBox_2 = QGroupBox(self.scrollAreaWidgetContents)
- self.groupBox_2.setObjectName(u"groupBox_2")
- self.verticalLayout_4 = QVBoxLayout(self.groupBox_2)
- self.verticalLayout_4.setObjectName(u"verticalLayout_4")
- self.toolButton_execute_group = QToolButton(self.groupBox_2)
- self.toolButton_execute_group.setObjectName(u"toolButton_execute_group")
- self.toolButton_execute_group.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
-
- self.verticalLayout_4.addWidget(self.toolButton_execute_group)
-
- self.comboBox_groups = QComboBox(self.groupBox_2)
- self.comboBox_groups.setObjectName(u"comboBox_groups")
-
- self.verticalLayout_4.addWidget(self.comboBox_groups)
-
- self.toolButton_stop = QToolButton(self.groupBox_2)
- self.toolButton_stop.setObjectName(u"toolButton_stop")
- self.toolButton_stop.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
-
- self.verticalLayout_4.addWidget(self.toolButton_stop)
-
-
- self.horizontalLayout.addWidget(self.groupBox_2)
-
self.groupBox = QGroupBox(self.scrollAreaWidgetContents)
self.groupBox.setObjectName(u"groupBox")
- self.verticalLayout = QVBoxLayout(self.groupBox)
- self.verticalLayout.setObjectName(u"verticalLayout")
- self.toolButton_show_event_log = QToolButton(self.groupBox)
- self.toolButton_show_event_log.setObjectName(u"toolButton_show_event_log")
- self.toolButton_show_event_log.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
-
- self.verticalLayout.addWidget(self.toolButton_show_event_log)
-
- self.toolButton_to_expert_mode = QToolButton(self.groupBox)
- self.toolButton_to_expert_mode.setObjectName(u"toolButton_to_expert_mode")
- self.toolButton_to_expert_mode.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
-
- self.verticalLayout.addWidget(self.toolButton_to_expert_mode)
-
-
- self.horizontalLayout.addWidget(self.groupBox)
-
- self.groupBox_3 = QGroupBox(self.scrollAreaWidgetContents)
- self.groupBox_3.setObjectName(u"groupBox_3")
- self.verticalLayout_5 = QVBoxLayout(self.groupBox_3)
+ self.verticalLayout_5 = QVBoxLayout(self.groupBox)
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
- self.columnView = QColumnView(self.groupBox_3)
+ self.columnView = QColumnView(self.groupBox)
self.columnView.setObjectName(u"columnView")
self.columnView.setMinimumSize(QSize(50, 30))
self.verticalLayout_5.addWidget(self.columnView)
- self.horizontalLayout.addWidget(self.groupBox_3)
+ self.horizontalLayout.addWidget(self.groupBox)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.splitter.addWidget(self.scrollArea)
@@ -173,16 +130,17 @@ def setupUi(self, MainWindowLite):
def retranslateUi(self, MainWindowLite):
MainWindowLite.setWindowTitle(QCoreApplication.translate("MainWindowLite", u"Spine Toolbox [user mode]", None))
- self.actionSwitch_to_expert_mode.setText(QCoreApplication.translate("MainWindowLite", u"Switch to expert mode", None))
+ self.actionSwitch_to_design_mode.setText(QCoreApplication.translate("MainWindowLite", u"Switch to design mode", None))
+#if QT_CONFIG(tooltip)
+ self.actionSwitch_to_design_mode.setToolTip(QCoreApplication.translate("MainWindowLite", u"Switch to design mode", None))
+#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
- self.actionSwitch_to_expert_mode.setShortcut(QCoreApplication.translate("MainWindowLite", u"\u00a7", None))
+ self.actionSwitch_to_design_mode.setShortcut(QCoreApplication.translate("MainWindowLite", u"\u00a7", None))
#endif // QT_CONFIG(shortcut)
self.actionExecute_group.setText(QCoreApplication.translate("MainWindowLite", u"Execute group", None))
self.actionStop.setText(QCoreApplication.translate("MainWindowLite", u"Stop", None))
self.actionShow_event_log_console.setText(QCoreApplication.translate("MainWindowLite", u"Show event log and console", None))
- self.groupBox_2.setTitle(QCoreApplication.translate("MainWindowLite", u"Execution", None))
- self.groupBox.setTitle(QCoreApplication.translate("MainWindowLite", u"Actions", None))
- self.groupBox_3.setTitle(QCoreApplication.translate("MainWindowLite", u"Scenarios", None))
+ self.groupBox.setTitle(QCoreApplication.translate("MainWindowLite", u"Scenarios", None))
self.menuFile.setTitle(QCoreApplication.translate("MainWindowLite", u"File", None))
self.menuHelp.setTitle(QCoreApplication.translate("MainWindowLite", u"Help", None))
# retranslateUi
diff --git a/spinetoolbox/ui/mainwindowlite.ui b/spinetoolbox/ui/mainwindowlite.ui
index 63bdd4fae..37e5ab004 100644
--- a/spinetoolbox/ui/mainwindowlite.ui
+++ b/spinetoolbox/ui/mainwindowlite.ui
@@ -53,57 +53,8 @@
- -
-
-
- Execution
-
-
-
-
-
-
- Qt::ToolButtonStyle::ToolButtonTextBesideIcon
-
-
-
- -
-
-
- -
-
-
- Qt::ToolButtonStyle::ToolButtonTextBesideIcon
-
-
-
-
-
-
-
-
- Actions
-
-
-
-
-
-
- Qt::ToolButtonStyle::ToolButtonTextBesideIcon
-
-
-
- -
-
-
- Qt::ToolButtonStyle::ToolButtonTextBesideIcon
-
-
-
-
-
-
- -
-
Scenarios
@@ -171,13 +122,16 @@
-
+
:/icons/menu_icons/retweet.svg:/icons/menu_icons/retweet.svg
- Switch to expert mode
+ Switch to design mode
+
+
+ Switch to design mode
ยง
diff --git a/spinetoolbox/ui/resources/menu_icons/object-group.svg b/spinetoolbox/ui/resources/menu_icons/object-group.svg
new file mode 100644
index 000000000..b07fcecf7
--- /dev/null
+++ b/spinetoolbox/ui/resources/menu_icons/object-group.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/spinetoolbox/ui/resources/resources_icons.qrc b/spinetoolbox/ui/resources/resources_icons.qrc
index 1018d2596..334181546 100644
--- a/spinetoolbox/ui/resources/resources_icons.qrc
+++ b/spinetoolbox/ui/resources/resources_icons.qrc
@@ -15,6 +15,7 @@
menu_icons/retweet.svg
menu_icons/bolt-lightning.svg
+ menu_icons/object-group.svg
file-upload.svg
share.svg
menu_icons/server.svg
diff --git a/spinetoolbox/ui/select_database_items.py b/spinetoolbox/ui/select_database_items.py
index 46d90d1c9..14d8f41b5 100644
--- a/spinetoolbox/ui/select_database_items.py
+++ b/spinetoolbox/ui/select_database_items.py
@@ -14,7 +14,7 @@
################################################################################
## Form generated from reading UI file 'select_database_items.ui'
##
-## Created by: Qt User Interface Compiler version 6.6.3
+## Created by: Qt User Interface Compiler version 6.7.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py
index ade61591e..35a0121ef 100644
--- a/spinetoolbox/ui_main.py
+++ b/spinetoolbox/ui_main.py
@@ -77,10 +77,12 @@
ReplaceSpecificationCommand,
SaveSpecificationAsCommand,
SpineToolboxCommand,
+ RenameGroupCommand,
)
from .project_item.logging_connection import LoggingConnection, LoggingJump
from .project_item_icon import ProjectItemIcon
from .project_settings import ProjectSettings
+from .group import Group
from .spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor
from .spine_db_manager import SpineDBManager
from .spine_engine_manager import make_engine_manager
@@ -99,7 +101,7 @@
class ToolboxUI(QMainWindow):
- """Class for application main GUI functions."""
+ """Class for the application main GUI functions in Design Mode."""
# Signals to comply with the spinetoolbox.logger_interface.LoggerInterface interface.
msg = Signal(str)
@@ -200,7 +202,6 @@ def __init__(self, toolboxuibase):
self._plugin_manager.load_installed_plugins()
self.refresh_toolbars()
self.restore_dock_widgets()
- # self.restore_ui()
self.set_work_directory()
self._disable_project_actions()
self.connect_signals()
@@ -221,6 +222,14 @@ def project(self):
def undo_stack(self):
return self.toolboxuibase.undo_stack
+ @property
+ def active_ui_window(self):
+ return self.toolboxuibase.active_ui_window
+
+ @property
+ def active_ui_mode(self):
+ return self.toolboxuibase.active_ui_mode
+
def eventFilter(self, obj, ev):
# Save/restore splitter states when hiding/showing execution lists
if obj == self.ui.listView_console_executions:
@@ -318,11 +327,15 @@ def connect_signals(self):
def switch_to_user_mode(self):
"""Switches the main window into user mode."""
+ self.ui.graphicsView.scene().clearSelection()
self.disconnect_project_signals()
self.ui.graphicsView.scene().clear_icons_and_links()
self.toolboxuibase.ui.stackedWidget.setCurrentWidget(self.toolboxui_lite)
self.toolboxuibase.reload_icons_and_links()
+ self.toolboxuibase.active_ui_mode = "toolboxuilite"
self.toolboxui_lite.connect_project_signals()
+ self.toolboxui_lite.populate_groups_model()
+ self.toolboxui_lite.groups_combobox.setCurrentIndex(0)
self.toolboxui_lite.ui.graphicsView.reset_zoom()
def _update_execute_enabled(self):
@@ -486,7 +499,7 @@ def create_project(self, proj_dir):
self.ui.actionSave.setDisabled(True) # Disable in a clean project
self.toolboxuibase.connect_project_signals()
self.toolboxuibase.update_window_title()
- self.toolboxuibase.current_page.ui.graphicsView.reset_zoom()
+ self.toolboxuibase.active_ui_window.ui.graphicsView.reset_zoom()
# Update recentProjects
self.update_recent_projects()
# Update recentProjectStorages
@@ -557,7 +570,6 @@ def restore_project(self, project_dir, ask_confirmation=True, clear_event_log=Tr
logger=self,
)
self.specification_model.connect_to_project(self._project)
-
self.toolboxuibase.connect_project_signals()
self.toolboxuibase.update_window_title()
# Populate project model with project items
@@ -571,7 +583,7 @@ def restore_project(self, project_dir, ask_confirmation=True, clear_event_log=Tr
self.ui.actionSave.setDisabled(True) # Save is disabled in a clean project
self._plugin_manager.reload_plugins_with_local_data()
# Reset zoom on Design View
- self.toolboxuibase.current_page.ui.graphicsView.reset_zoom()
+ self.toolboxuibase.active_ui_window.ui.graphicsView.reset_zoom()
self.update_recent_projects()
self.msg.emit(f"Project {self._project.name} is now open")
return True
@@ -1630,19 +1642,41 @@ def show_project_or_item_context_menu(self, pos, item):
Args:
pos (QPoint): Mouse position
- item (ProjectItem, optional): Project item or None
+ item (ProjectItem, Group, optional): Project item, Group or None
"""
+ menu = QMenu(self)
+ menu.setToolTipsVisible(True)
+ menu.aboutToShow.connect(self.refresh_edit_action_states)
+ menu.aboutToHide.connect(self.enable_edit_actions)
if not item: # Clicked on a blank area in Design view
- menu = QMenu(self)
menu.addAction(self.ui.actionPaste)
menu.addAction(self.ui.actionPasteAndDuplicateFiles)
menu.addSeparator()
menu.addAction(self.ui.actionOpen_project_directory)
else: # Clicked on an item, show the context menu for that item
- menu = self.project_item_context_menu(item.actions())
- menu.setToolTipsVisible(True)
- menu.aboutToShow.connect(self.refresh_edit_action_states)
- menu.aboutToHide.connect(self.enable_edit_actions)
+ if item.actions():
+ for action in item.actions():
+ menu.addAction(action)
+ menu.addSeparator()
+ if isinstance(item, Group):
+ menu.addAction(self.ui.actionOpen_project_directory)
+ else:
+ menu.addAction(self.ui.actionCopy)
+ menu.addAction(self.ui.actionPaste)
+ menu.addAction(self.ui.actionPasteAndDuplicateFiles)
+ menu.addAction(self.ui.actionDuplicate)
+ menu.addAction(self.ui.actionDuplicateAndDuplicateFiles)
+ menu.addAction(self.ui.actionOpen_item_directory)
+ menu.addSeparator()
+ menu.addAction(self.ui.actionRemove)
+ menu.addSeparator()
+ menu.addAction(self.ui.actionRename_item)
+ if len(item.get_icon().my_groups) > 0:
+ menu.addSeparator()
+ for group in item.get_icon().my_groups:
+ text = f"Leave {group.name}"
+ slot = lambda checked=False, item_name=item.name, group_name=group.name: self.ui.graphicsView.push_remove_item_from_group_command(checked, item_name, group_name)
+ menu.addAction(text, slot)
menu.exec(pos)
menu.deleteLater()
@@ -1680,10 +1714,10 @@ def refresh_edit_action_states(self):
can_copy = any(isinstance(x, ProjectItemIcon) for x in selected_items)
has_items = self.project.n_items > 0
selected_project_items = [x for x in selected_items if isinstance(x, ProjectItemIcon)]
- _methods = [getattr(self.project.get_item(x.name()), "copy_local_data") for x in selected_project_items]
+ _methods = [getattr(self.project.get_item(x.name), "copy_local_data") for x in selected_project_items]
can_duplicate_files = any(m.__qualname__.partition(".")[0] != "ProjectItem" for m in _methods)
# Renaming an item should always be allowed except when it's a Data Store that is open in an editor
- for item in (self.project.get_item(x.name()) for x in selected_project_items):
+ for item in (self.project.get_item(x.name) for x in selected_project_items):
if item.item_type() == "Data Store" and item.has_listeners():
self.ui.actionRename_item.setEnabled(False)
self.ui.actionRename_item.setToolTip(
@@ -1799,7 +1833,7 @@ def _serialize_selected_items(self):
for item_icon in selected_project_items:
if not isinstance(item_icon, ProjectItemIcon):
continue
- name = item_icon.name()
+ name = item_icon.name
project_item = self.project.get_item(name)
item_dict = dict(project_item.item_dict())
item_dict["original_data_dir"] = project_item.data_dir
@@ -1945,6 +1979,8 @@ def connect_project_signals(self):
self.project.jump_added.connect(self.ui.graphicsView.do_add_jump)
self.project.jump_about_to_be_removed.connect(self.ui.graphicsView.do_remove_jump)
self.project.jump_updated.connect(self.ui.graphicsView.do_update_jump)
+ self.project.group_added.connect(self.ui.graphicsView.add_group_on_scene)
+ self.project.group_disbanded.connect(self.ui.graphicsView.remove_group_from_scene)
self.project.specification_added.connect(self.repair_specification)
self.project.specification_saved.connect(self._log_specification_saved)
@@ -1962,6 +1998,8 @@ def disconnect_project_signals(self):
self.project.jump_added.disconnect()
self.project.jump_about_to_be_removed.disconnect()
self.project.jump_updated.disconnect()
+ self.project.group_added.disconnect()
+ self.project.group_disbanded.disconnect()
self.project.specification_added.disconnect()
self.project.specification_saved.disconnect()
@@ -2056,7 +2094,7 @@ def _remove_selected_items(self, _):
has_connections = False
for item in selected_items:
if isinstance(item, ProjectItemIcon):
- project_item_names.add(item.name())
+ project_item_names.add(item.name)
elif isinstance(item, (JumpLink, Link)):
has_connections = True
if not project_item_names and not has_connections:
@@ -2092,46 +2130,31 @@ def _remove_selected_items(self, _):
def _rename_project_item(self, _):
"""Renames active project item."""
item = self.active_project_item
+ new_name = self._show_simple_input_dialog("Rename Item", "New name:", item.name)
+ if not new_name:
+ return
+ self.undo_stack.push(RenameProjectItemCommand(self._project, item.name, new_name))
+
+ @Slot(bool)
+ def rename_group(self, _, group_name):
+ """Renames Group."""
+ new_name = self._show_simple_input_dialog("Rename Group", "New name:", group_name)
+ if not new_name:
+ return
+ self.undo_stack.push(RenameGroupCommand(self._project, group_name, new_name))
+
+ def _show_simple_input_dialog(self, title, label_txt, prefilled_text):
+ """Shows a QInputDialog and returns typed text."""
answer = QInputDialog.getText(
self,
- "Rename Item",
- "New name:",
- text=item.name,
+ title,
+ label_txt,
+ text=prefilled_text,
flags=Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint
)
if not answer[1]:
- return
- new_name = answer[0]
- self.undo_stack.push(RenameProjectItemCommand(self._project, item.name, new_name))
-
- def project_item_context_menu(self, additional_actions):
- """Creates a context menu for project items.
-
- Args:
- additional_actions (list of QAction): Actions to be prepended to the menu
-
- Returns:
- QMenu: Project item context menu
- """
- menu = QMenu(self)
- menu.setToolTipsVisible(True)
- if additional_actions:
- for action in additional_actions:
- menu.addAction(action)
- menu.addSeparator()
- menu.addAction(self.ui.actionCopy)
- menu.addAction(self.ui.actionPaste)
- menu.addAction(self.ui.actionPasteAndDuplicateFiles)
- menu.addAction(self.ui.actionDuplicate)
- menu.addAction(self.ui.actionDuplicateAndDuplicateFiles)
- menu.addAction(self.ui.actionOpen_item_directory)
- menu.addSeparator()
- menu.addAction(self.ui.actionRemove)
- menu.addSeparator()
- menu.addAction(self.ui.actionRename_item)
- menu.aboutToShow.connect(self.refresh_edit_action_states)
- menu.aboutToHide.connect(self.enable_edit_actions)
- return menu
+ return None
+ return answer[0]
@Slot(str, QIcon, bool)
def start_detached_jupyter_console(self, kernel_name, icon, conda):
diff --git a/spinetoolbox/ui_main_base.py b/spinetoolbox/ui_main_base.py
index 9a40a2a58..af2443466 100644
--- a/spinetoolbox/ui_main_base.py
+++ b/spinetoolbox/ui_main_base.py
@@ -13,7 +13,7 @@
"""Contains a class for the base main window of Spine Toolbox."""
import sys
import locale
-from PySide6.QtCore import QSettings, Qt, Slot
+from PySide6.QtCore import QSettings, Qt, Slot, Signal
from PySide6.QtWidgets import QMainWindow, QApplication, QStyleFactory, QMessageBox, QCheckBox
from PySide6.QtGui import QIcon, QUndoStack, QGuiApplication, QAction, QKeySequence
from .helpers import set_taskbar_icon, ensure_window_is_on_screen
@@ -48,6 +48,7 @@ def __init__(self):
self.ui.stackedWidget.setCurrentWidget(self.toolboxui)
self.restore_ui()
self.connect_signals()
+ self._active_ui_mode = "toolboxui"
@property
def toolboxui(self):
@@ -70,9 +71,17 @@ def undo_stack(self):
return self._undo_stack
@property
- def current_page(self):
+ def active_ui_window(self):
return self.ui.stackedWidget.currentWidget()
+ @property
+ def active_ui_mode(self):
+ return self._active_ui_mode
+
+ @active_ui_mode.setter
+ def active_ui_mode(self, tb):
+ self._active_ui_mode = tb
+
def connect_signals(self):
"""Connects signals to slots."""
self.undo_stack.cleanChanged.connect(self.update_window_modified)
@@ -149,23 +158,25 @@ def reload_icons_and_links(self):
if not self.project:
return
for item_name in self.project.all_item_names:
- self.current_page.ui.graphicsView.add_icon(item_name)
+ self.active_ui_window.ui.graphicsView.add_icon(item_name)
for connection in self.project.connections:
- self.current_page.ui.graphicsView.do_add_link(connection)
+ self.active_ui_window.ui.graphicsView.do_add_link(connection)
connection.link.update_icons()
for jump in self.project.jumps:
- self.current_page.ui.graphicsView.do_add_jump(jump)
+ self.active_ui_window.ui.graphicsView.do_add_jump(jump)
jump.jump_link.update_icons()
+ for group in self.project.groups.values():
+ self.active_ui_window.ui.graphicsView.add_group_on_scene(group)
def connect_project_signals(self):
"""Connects project signals based on current UI mode."""
- self.current_page.connect_project_signals()
+ self.active_ui_window.connect_project_signals()
def clear_ui(self):
"""Clean UI to make room for a new or opened project."""
self.toolboxui.activate_no_selection_tab() # Clear properties widget
self.toolboxui._restore_original_console()
- self.current_page.ui.graphicsView.scene().clear_icons_and_links() # Clear all items from scene
+ self.active_ui_window.ui.graphicsView.scene().clear_icons_and_links() # Clear all items from scene
self.toolboxui._shutdown_engine_kernels()
self.toolboxui._close_consoles()
@@ -301,6 +312,6 @@ def closeEvent(self, event):
def nr_of_items(self):
"""For debugging."""
- n_items = len(self.current_page.ui.graphicsView.scene().items())
+ n_items = len(self.active_ui_window.ui.graphicsView.scene().items())
print(f"Items on scene:{n_items}")
diff --git a/spinetoolbox/ui_main_lite.py b/spinetoolbox/ui_main_lite.py
index 6008bd90a..70abc4e79 100644
--- a/spinetoolbox/ui_main_lite.py
+++ b/spinetoolbox/ui_main_lite.py
@@ -11,10 +11,11 @@
######################################################################################################################
"""Contains a class for the user mode main window of Spine Toolbox."""
-from PySide6.QtCore import Qt, Slot
-from PySide6.QtWidgets import QMainWindow, QToolBar
-from .project_item_icon import ProjectItemIcon
-from .link import JumpOrLink
+from PySide6.QtCore import Qt, Slot, QRect
+from PySide6.QtWidgets import QMainWindow, QToolBar, QMenu, QComboBox, QProgressBar
+from PySide6.QtGui import QStandardItemModel, QStandardItem, QPainterPath, QTransform
+# from .project_item_icon import ProjectItemIcon
+# from .link import JumpOrLink
class ToolboxUILite(QMainWindow):
@@ -29,14 +30,13 @@ def __init__(self, toolboxuibase):
self.ui = Ui_MainWindowLite()
self.ui.setupUi(self)
self.make_menubar()
+ self.groups_model = QStandardItemModel()
+ self.groups_combobox = QComboBox(self)
+ self.groups_combobox.setModel(self.groups_model)
+ self.progress_bar = QProgressBar(self)
+ self.toolbar = self.make_toolbar()
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar)
self.ui.graphicsView.set_ui(self)
- # self.ui.toolButton_execute_project.setDefaultAction(self.ui.actionExecute_project)
- self.ui.toolButton_execute_group.setDefaultAction(self.ui.actionExecute_group)
- self.ui.toolButton_stop.setDefaultAction(self.ui.actionStop)
- self.ui.toolButton_to_expert_mode.setDefaultAction(self.ui.actionSwitch_to_expert_mode)
- self.ui.toolButton_show_event_log.setDefaultAction(self.ui.actionShow_event_log_console)
- self.ui.comboBox_groups.addItems(["All", "Group 1", "Group 2", "Group 3"])
- # self.toolbar = self.make_toolbar()
self.connect_signals()
@property
@@ -55,6 +55,34 @@ def project(self):
def undo_stack(self):
return self.toolboxuibase.undo_stack
+ @property
+ def active_ui_mode(self):
+ return self.toolboxuibase.active_ui_mode
+
+ @property
+ def msg(self):
+ return self.toolboxui.msg
+
+ @property
+ def msg_success(self):
+ return self.toolboxui.msg_success
+
+ @property
+ def msg_error(self):
+ return self.toolboxui.msg_error
+
+ @property
+ def msg_warning(self):
+ return self.toolboxui.msg_warning
+
+ @property
+ def msg_proc(self):
+ return self.toolboxui.msg_proc
+
+ @property
+ def msg_proc_error(self):
+ return self.toolboxui.msg_proc_error
+
def make_menubar(self):
"""Populates File and Help menus."""
self.ui.menuFile.addAction(self.toolboxui.ui.actionOpen)
@@ -62,7 +90,7 @@ def make_menubar(self):
self.ui.menuFile.addAction(self.toolboxui.ui.actionSave)
self.ui.menuFile.addAction(self.toolboxui.ui.actionSave_As)
self.ui.menuFile.addSeparator()
- self.ui.menuFile.addAction(self.ui.actionSwitch_to_expert_mode)
+ self.ui.menuFile.addAction(self.ui.actionSwitch_to_design_mode)
self.ui.menuFile.addAction(self.toolboxui.ui.actionSettings)
self.ui.menuFile.addSeparator()
self.ui.menuFile.addAction(self.toolboxui.ui.actionQuit)
@@ -75,19 +103,37 @@ def make_menubar(self):
def make_toolbar(self):
"""Makes and returns a Toolbar for user mode UI."""
tb = QToolBar(self)
- tb.addAction(self.toolboxui.ui.actionExecute_project)
+ tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
+ tb.addAction(self.ui.actionExecute_group)
+ tb.addWidget(self.groups_combobox)
+ tb.addAction(self.ui.actionStop)
+ self.progress_bar.setMinimum(0)
+ self.progress_bar.setMaximum(100)
+ tb.addWidget(self.progress_bar)
+ tb.addSeparator()
+ tb.addAction(self.ui.actionShow_event_log_console)
+ tb.addAction(self.ui.actionSwitch_to_design_mode)
return tb
def connect_signals(self):
"""Connects signals to slots."""
- self.ui.actionSwitch_to_expert_mode.triggered.connect(self.switch_to_expert_mode)
- # self.ui.actionExecute_project.triggered.connect(self.toolboxui._execute_project)
+ self.msg.connect(self.add_message)
+ self.msg_success.connect(self.add_success_message)
+ self.msg_error.connect(self.add_error_message)
+ self.msg_warning.connect(self.add_warning_message)
+ self.msg_proc.connect(self.add_process_message)
+ self.msg_proc_error.connect(self.add_process_error_message)
self.ui.actionExecute_group.triggered.connect(self.execute_group)
self.ui.actionStop.triggered.connect(self.toolboxui._stop_execution)
+ self.ui.actionShow_event_log_console.triggered.connect(self.show_event_log_and_console)
+ self.ui.actionSwitch_to_design_mode.triggered.connect(self.switch_to_design_mode)
+ self.groups_combobox.currentTextChanged.connect(self._select_group)
def connect_project_signals(self):
if not self.project:
return
+ self.project.project_execution_about_to_start.connect(lambda: self.progress_bar.reset())
+ self.project.project_execution_finished.connect(self._set_progress_bar_finished)
self.project.item_added.connect(self.toolboxui.set_icon_and_properties_ui)
self.project.item_added.connect(self.ui.graphicsView.add_icon)
self.project.connection_established.connect(self.ui.graphicsView.do_add_link)
@@ -95,43 +141,160 @@ def connect_project_signals(self):
self.project.connection_about_to_be_removed.connect(self.ui.graphicsView.do_remove_link)
self.project.jump_added.connect(self.ui.graphicsView.do_add_jump)
self.project.jump_about_to_be_removed.connect(self.ui.graphicsView.do_remove_jump)
+ self.project.group_added.connect(self.ui.graphicsView.add_group_on_scene)
+ self.project.group_disbanded.connect(self.ui.graphicsView.remove_group_from_scene)
def disconnect_project_signals(self):
"""Disconnects signals emitted by project."""
if not self.project:
return
+ self.project.project_execution_about_to_start.disconnect()
+ self.project.project_execution_finished.disconnect()
self.project.item_added.disconnect()
self.project.connection_established.disconnect()
self.project.connection_updated.disconnect()
self.project.connection_about_to_be_removed.disconnect()
self.project.jump_added.disconnect()
self.project.jump_about_to_be_removed.disconnect()
+ self.project.group_added.disconnect()
+ self.project.group_disbanded.disconnect()
- def switch_to_expert_mode(self):
- """Switches the main window into expert mode."""
+ def switch_to_design_mode(self):
+ """Switches the main window into design mode."""
+ self.ui.graphicsView.scene().clearSelection()
self.disconnect_project_signals()
self.ui.graphicsView.scene().clear_icons_and_links()
self.toolboxuibase.ui.stackedWidget.setCurrentWidget(self.toolboxui)
self.toolboxuibase.reload_icons_and_links()
+ self.toolboxuibase.active_ui_mode = "toolboxui"
self.toolboxui.connect_project_signals()
self.toolboxui.ui.graphicsView.reset_zoom()
+ def populate_groups_model(self):
+ """Populates group model."""
+ items = [self.groups_model.item(i).text() for i in range(self.groups_model.rowCount())]
+ if "Select a group..." not in items:
+ i1 = QStandardItem("Select a group...")
+ self.groups_model.appendRow(i1)
+ if "All" not in items:
+ i2 = QStandardItem("All")
+ self.groups_model.appendRow(i2)
+ for group_name, group in self.project.groups.items():
+ if group_name not in items:
+ item = QStandardItem(group_name)
+ item.setData(group, Qt.ItemDataRole.UserRole)
+ self.groups_model.appendRow(item)
+
+ @Slot(str)
+ def _select_group(self, group_name):
+ """Selects a group with the given name."""
+ self.ui.graphicsView.scene().clearSelection()
+ if group_name == "Select a group...":
+ return
+ if group_name == "All":
+ path = QPainterPath()
+ path.addRect(self.ui.graphicsView.scene().sceneRect())
+ self.ui.graphicsView.scene().setSelectionArea(path, QTransform())
+ return
+ group = self.project.groups[group_name]
+ path = QPainterPath()
+ path.addRect(group.rect())
+ print(f"selection path:{path}")
+ self.ui.graphicsView.scene().setSelectionArea(path, QTransform())
+
def execute_group(self):
- if self.ui.comboBox_groups.currentText() == "All":
- for i in self.ui.graphicsView.scene().items():
- if isinstance(i, ProjectItemIcon) or isinstance(i, JumpOrLink):
- i.setSelected(True)
+ """Executes a group."""
+ if self.groups_combobox.currentIndex() == 0:
+ return
+ print(f"Executing {self.groups_combobox.currentText()}")
+ if self.groups_combobox.currentText() == "All":
self.toolboxui._execute_project()
return
- print(f"Executing {self.ui.comboBox_groups.currentText()}")
+ self.toolboxui._execute_selection()
+
+ @Slot()
+ def _set_progress_bar_finished(self):
+ self.progress_bar.setValue(self.progress_bar.maximum())
- def open_project(self):
- """Slot for opening projects in user mode."""
+ def show_event_log_and_console(self):
+ print("Not implemented")
def refresh_active_elements(self, active_project_item, active_link_item, selected_item_names):
"""Does something when scene selection has changed."""
- return True
+ self.toolboxui._selected_item_names = selected_item_names
def override_console_and_execution_list(self):
"""Does nothing."""
return True
+
+ def show_project_or_item_context_menu(self, global_pos, item):
+ """Shows the Context menu for project or item in user mode."""
+ print(f"Not implemented yet. item:{item}")
+
+ def show_link_context_menu(self, pos, link):
+ """Shows the Context menu for connection links in user mode.
+
+ Args:
+ pos (QPoint): Mouse position
+ link (Link(QGraphicsPathItem)): The link in question
+ """
+ menu = QMenu(self)
+ menu.addAction(self.toolboxui.ui.actionTake_link)
+ action = menu.exec(pos)
+ if action is self.toolboxui.ui.actionTake_link:
+ self.ui.graphicsView.take_link(link)
+ menu.deleteLater()
+
+ @Slot(str)
+ def add_message(self, msg):
+ """Appends a regular message to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ return
+
+ @Slot(str)
+ def add_success_message(self, msg):
+ """Appends a message with green text to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ return
+
+ @Slot(str)
+ def add_error_message(self, msg):
+ """Appends a message with red color to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ print(f"[ERROR]:{msg}")
+
+ @Slot(str)
+ def add_warning_message(self, msg):
+ """Appends a message with yellow (golden) color to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ print(f"[WARNING]:{msg}")
+
+ @Slot(str)
+ def add_process_message(self, msg):
+ """Writes message from stdout to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ return
+
+ @Slot(str)
+ def add_process_error_message(self, msg):
+ """Writes message from stderr to the Event Log.
+
+ Args:
+ msg (str): String written to QTextBrowser
+ """
+ return
diff --git a/spinetoolbox/widgets/add_project_item_widget.py b/spinetoolbox/widgets/add_project_item_widget.py
index e91006c00..a671c67d9 100644
--- a/spinetoolbox/widgets/add_project_item_widget.py
+++ b/spinetoolbox/widgets/add_project_item_widget.py
@@ -109,7 +109,7 @@ def handle_ok_clicked(self):
self.call_add_item()
self._toolbox.ui.graphicsView.scene().clearSelection()
for icon in self._toolbox.ui.graphicsView.scene().project_item_icons():
- if icon.name() == self.name:
+ if icon.name == self.name:
icon.setSelected(True)
self.close()
diff --git a/spinetoolbox/widgets/custom_qgraphicsscene.py b/spinetoolbox/widgets/custom_qgraphicsscene.py
index 5e56a97a3..4a6a7aff0 100644
--- a/spinetoolbox/widgets/custom_qgraphicsscene.py
+++ b/spinetoolbox/widgets/custom_qgraphicsscene.py
@@ -18,6 +18,7 @@
from ..helpers import LinkType
from ..link import ConnectionLinkDrawer, JumpLink, JumpLinkDrawer, Link
from ..project_item_icon import ProjectItemIcon
+from ..group import Group
from ..ui.resources.cat import Cat
from .project_item_drag import ProjectItemDragMixin
@@ -73,7 +74,7 @@ def __init__(self, parent, toolbox):
def clear_icons_and_links(self):
for item in self.items():
- if isinstance(item, (Link, JumpLink, ProjectItemIcon)):
+ if isinstance(item, (Link, JumpLink, ProjectItemIcon, Group)):
self.removeItem(item)
def mouseMoveEvent(self, event):
@@ -81,7 +82,8 @@ def mouseMoveEvent(self, event):
if self.link_drawer is not None:
self.link_drawer.tip = event.scenePos()
self.link_drawer.update_geometry()
- event.setButtons(Qt.NoButton) # this is so super().mouseMoveEvent sends hover events to connector buttons
+ # this enables super().mouseMoveEvent to send hover events to connector buttons
+ event.setButtons(Qt.MouseButton.NoButton)
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
@@ -163,12 +165,12 @@ def handle_selection_changed(self):
links.append(item)
# Set active project item and active link in toolbox
active_project_item = (
- self._toolbox.project.get_item(project_item_icons[0].name()) if len(project_item_icons) == 1 else None
+ self._toolbox.project.get_item(project_item_icons[0].name) if len(project_item_icons) == 1 else None
)
active_link_item = links[0].item if len(links) == 1 else None
- selected_item_names = {icon.name() for icon in project_item_icons}
+ selected_item_names = {icon.name for icon in project_item_icons}
selected_link_icons = [conn.parent for link in links for conn in (link.src_connector, link.dst_connector)]
- selected_item_names |= set(icon.name() for icon in selected_link_icons)
+ selected_item_names |= set(icon.name for icon in selected_link_icons)
self._toolbox.refresh_active_elements(active_project_item, active_link_item, selected_item_names)
self._toolbox.override_console_and_execution_list()
@@ -184,7 +186,7 @@ def set_bg_choice(self, bg_choice):
"""Set background choice when this is changed in Settings.
Args:
- bg (str): "grid", "tree", or "solid"
+ bg_choice (str): "grid", "tree", or "solid"
"""
self.bg_choice = bg_choice
diff --git a/spinetoolbox/widgets/custom_qgraphicsviews.py b/spinetoolbox/widgets/custom_qgraphicsviews.py
index edc0030d0..20631c981 100644
--- a/spinetoolbox/widgets/custom_qgraphicsviews.py
+++ b/spinetoolbox/widgets/custom_qgraphicsviews.py
@@ -17,8 +17,17 @@
from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem, QGraphicsView
from ..helpers import LinkType
from ..link import JumpLink, Link
-from ..project_commands import AddConnectionCommand, AddJumpCommand, RemoveConnectionsCommand, RemoveJumpsCommand
+from ..project_commands import (
+ AddConnectionCommand,
+ AddJumpCommand,
+ MakeGroupCommand,
+ RemoveConnectionsCommand,
+ RemoveJumpsCommand,
+ DisbandGroupCommand,
+ RemoveItemFromGroupCommand,
+)
from ..project_item_icon import ProjectItemIcon
+from ..group import Group
from ..ui_main_lite import ToolboxUILite
from .custom_qgraphicsscene import DesignGraphicsScene
@@ -66,11 +75,11 @@ def keyPressEvent(self, event):
Args:
event (QKeyEvent): key press event
"""
- if event.key() == Qt.Key_Plus:
+ if event.key() == Qt.Key.Key_Plus:
self.zoom_in()
- elif event.key() == Qt.Key_Minus:
+ elif event.key() == Qt.Key.Key_Minus:
self.zoom_out()
- elif event.key() == Qt.Key_Comma:
+ elif event.key() == Qt.Key.Key_Comma:
self.reset_zoom()
else:
super().keyPressEvent(event)
@@ -84,7 +93,7 @@ def mousePressEvent(self, event):
if not item or not item.acceptedMouseButtons() & event.buttons():
button = event.button()
if button == Qt.MouseButton.LeftButton:
- self.viewport().setCursor(Qt.CrossCursor)
+ self.viewport().setCursor(Qt.CursorShape.CrossCursor)
elif button == Qt.MouseButton.MiddleButton:
self.reset_zoom()
elif button == Qt.MouseButton.RightButton:
@@ -113,10 +122,10 @@ def mouseReleaseEvent(self, event):
if self._drag_duration_passed(event):
context_menu_disabled = True
self.disable_context_menu()
- elif event.button() == Qt.MouseButton.RightButton:
- self.contextMenuEvent(
- QContextMenuEvent(QContextMenuEvent.Reason.Mouse, event.pos(), event.globalPos(), event.modifiers())
- )
+ # elif event.button() == Qt.MouseButton.RightButton: # TODO: This creates a second context menu on Design View
+ # self.contextMenuEvent(
+ # QContextMenuEvent(QContextMenuEvent.Reason.Mouse, event.pos(), event.globalPos(), event.modifiers())
+ # )
event = _fake_left_button_event(event)
super().mouseReleaseEvent(event)
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
@@ -128,7 +137,7 @@ def mouseReleaseEvent(self, event):
if item:
self.viewport().setCursor(item.cursor())
else:
- self.viewport().setCursor(Qt.ArrowCursor)
+ self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
def _scroll_scene_by(self, dx, dy):
if dx == dy == 0:
@@ -223,7 +232,7 @@ def setScene(self, scene):
"""
super().setScene(scene)
scene.item_move_finished.connect(self._handle_item_move_finished)
- self.viewport().setCursor(Qt.ArrowCursor)
+ self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
@Slot(QGraphicsItem)
def _handle_item_move_finished(self, item):
@@ -248,13 +257,13 @@ def _compute_max_zoom(self):
raise NotImplementedError()
def _handle_zoom_time_line_advanced(self, pos):
- """Performs zoom whenever the smooth zoom time line advances."""
+ """Performs zoom whenever the smooth zoom timeline advances."""
factor = 1.0 + self._scheduled_transformations / 100.0
self.gentle_zoom(factor, pos)
@Slot()
def _handle_transformation_time_line_finished(self):
- """Cleans up after the smooth transformation time line finishes."""
+ """Cleans up after the smooth transformation timeline finishes."""
if self._scheduled_transformations > 0:
self._scheduled_transformations -= 1
else:
@@ -266,7 +275,7 @@ def _handle_transformation_time_line_finished(self):
@Slot()
def _handle_resize_time_line_finished(self):
- """Cleans up after resizing time line finishes."""
+ """Cleans up after resizing timeline finishes."""
if self.sender():
self.sender().deleteLater()
self.time_line = None
@@ -328,7 +337,7 @@ def _ensure_item_visible(self, item):
viewport_scene_rect = self._get_viewport_scene_rect()
if not viewport_scene_rect.contains(item_scene_rect.topLeft()):
scene_rect = viewport_scene_rect.united(item_scene_rect)
- self.fitInView(scene_rect, Qt.KeepAspectRatio)
+ self.fitInView(scene_rect, Qt.AspectRatioMode.KeepAspectRatio)
self._set_preferred_scene_rect()
@Slot()
@@ -393,6 +402,36 @@ def _compute_max_zoom(self):
viewport_extent = min(self.viewport().width(), self.viewport().height())
return viewport_extent / item_view_rect.width()
+ def mouseReleaseEvent(self, event):
+ """Makes an execution group if rubber band contains items and links."""
+ if self.dragMode() == QGraphicsView.DragMode.RubberBandDrag:
+ if self.rubberBandRect():
+ selected_items = list()
+ for item in self.scene().selectedItems():
+ if isinstance(item, (ProjectItemIcon, Link, JumpLink)):
+ selected_items.append(item)
+ if self.can_make_group(selected_items):
+ self.push_make_group_command(selected_items)
+ super().mouseReleaseEvent(event)
+
+ def can_make_group(self, items):
+ """Checks whether a group can be made from given items.
+
+ Args:
+ items (list): List of ProjectItemIcons, Links, or JumpLinks
+
+ Returns:
+ bool: True when a new group can be made, False otherwise
+ """
+ item_icons = [it for it in items if isinstance(it, ProjectItemIcon)]
+ if len(item_icons) < 2: # Groups must have at least two ProjectItems
+ return False
+ for group in self._toolbox.project.groups.values(): # Don't make duplicate groups
+ if set(item_icons) == set(group.project_items):
+ self._toolbox.msg_warning.emit(f"{group.name} already has the same items")
+ return False
+ return True
+
@Slot(str)
def add_icon(self, item_name):
"""Adds project item's icon to the scene.
@@ -403,9 +442,9 @@ def add_icon(self, item_name):
project_item = self._toolbox.project.get_item(item_name)
icon = project_item.get_icon()
if isinstance(self._toolbox, ToolboxUILite):
- icon.set_selection_halo_pen(5)
+ icon.set_icon_selection_pen_w(icon.USER_MODE_ICON_SELECTION_PEN_W)
else:
- icon.set_selection_halo_pen(1)
+ icon.set_icon_selection_pen_w(icon.DEFAULT_ICON_SELECTION_PEN_W)
if not icon.graphicsEffect():
# Restore effects when an icon is removed and added to another scene
icon.set_graphics_effects()
@@ -423,9 +462,56 @@ def remove_icon(self, item_name):
scene.removeItem(icon)
self._set_preferred_scene_rect()
- def add_link(self, src_connector, dst_connector):
+ def push_make_group_command(self, items):
+ """Pushes an MakeGroupCommand to toolbox undo stack.
+
+ Args:
+ items (list): List of selected icons to group
+ """
+ item_names = [item.name for item in items if isinstance(item, (ProjectItemIcon, Link, JumpLink))]
+ self._toolbox.undo_stack.push(MakeGroupCommand(self._toolbox.project, item_names))
+
+ @Slot(object)
+ def add_group_on_scene(self, group):
+ """Adds a Group on scene.
+
+ Args:
+ group (Group): Group to add
+ """
+ self.scene().addItem(group)
+
+ @Slot(bool, str, str)
+ def push_remove_item_from_group_command(self, _, item_name, group_name):
+ """Pushes a RemoveItemFromGroupCommand to toolbox undo stack.
+
+ Args:
+ _ (bool): Boolean sent by triggered signal
+ item_name (str): Item name to remove from group
+ group_name (str): Group to edit
+ """
+ self._toolbox.undo_stack.push(RemoveItemFromGroupCommand(self._toolbox.project, item_name, group_name))
+
+ @Slot(bool, str)
+ def push_disband_group_command(self, _, group_name):
+ """Pushes a DisbandGroupCommand to toolbox undo stack.
+
+ Args:
+ _ (bool): Boolean sent by triggered signal
+ group_name (str): Group to disband
"""
- Pushes an AddLinkCommand to the toolbox undo stack.
+ self._toolbox.undo_stack.push(DisbandGroupCommand(self._toolbox.project, group_name))
+
+ @Slot(object)
+ def remove_group_from_scene(self, group):
+ """Removes given group from scene.
+
+ Args:
+ group (Group): Group to remove
+ """
+ self.scene().removeItem(group)
+
+ def add_link(self, src_connector, dst_connector):
+ """Pushes an AddLinkCommand to the toolbox undo stack.
Args:
src_connector (ConnectorButton): source connector button
@@ -456,6 +542,10 @@ def do_add_link(self, connection):
connection.link = link = Link(self._toolbox, source_connector, destination_connector, connection)
source_connector.links.append(link)
destination_connector.links.append(link)
+ if isinstance(self._toolbox, ToolboxUILite):
+ link.set_link_selection_pen_w(link.USER_MODE_LINK_SELECTION_PEN_W)
+ else:
+ link.set_link_selection_pen_w(link.DEFAULT_LINK_SELECTION_PEN_W)
self.scene().addItem(link)
@Slot(object)
@@ -548,6 +638,10 @@ def do_add_jump(self, jump):
jump.jump_link = jump_link = JumpLink(self._toolbox, source_connector, destination_connector, jump)
source_connector.links.append(jump_link)
destination_connector.links.append(jump_link)
+ if isinstance(self._toolbox, ToolboxUILite):
+ jump_link.set_link_selection_pen_w(jump_link.USER_MODE_LINK_SELECTION_PEN_W)
+ else:
+ jump_link.set_link_selection_pen_w(jump_link.DEFAULT_LINK_SELECTION_PEN_W)
self.scene().addItem(jump_link)
@Slot(object)
@@ -575,18 +669,19 @@ def do_remove_jump(self, jump):
break
def contextMenuEvent(self, event):
- """Shows context menu for the blank view
+ """Shows context menu on Design View.
Args:
event (QContextMenuEvent): Event
"""
if not self._toolbox.project:
return
- QGraphicsView.contextMenuEvent(self, event) # Pass the event first to see if any item accepts it
- if not event.isAccepted():
- event.accept()
- global_pos = self.viewport().mapToGlobal(event.pos())
- self._toolbox.show_project_or_item_context_menu(global_pos, None)
+ super().contextMenuEvent(event) # Pass the event first to see if any item accepts it
+ if event.isAccepted():
+ return
+ event.accept()
+ global_pos = self.viewport().mapToGlobal(event.pos())
+ self._toolbox.show_project_or_item_context_menu(global_pos, None)
def _fake_left_button_event(mouse_event):
diff --git a/spinetoolbox/widgets/set_description_dialog.py b/spinetoolbox/widgets/set_description_dialog.py
index bd31f3bc9..203684673 100644
--- a/spinetoolbox/widgets/set_description_dialog.py
+++ b/spinetoolbox/widgets/set_description_dialog.py
@@ -25,7 +25,7 @@ def __init__(self, toolbox, project):
toolbox (ToolboxUI): QMainWindow instance
project (SpineToolboxProject)
"""
- super().__init__(parent=toolbox, f=Qt.Popup)
+ super().__init__(parent=toolbox, f=Qt.WindowType.Popup)
self._project = project
self._toolbox = toolbox
layout = QFormLayout(self)
diff --git a/tests/test_SpineToolboxProject.py b/tests/test_SpineToolboxProject.py
index 8463ed757..f4b4e9131 100644
--- a/tests/test_SpineToolboxProject.py
+++ b/tests/test_SpineToolboxProject.py
@@ -432,7 +432,7 @@ def test_rename_item(self):
dags = list(project._dag_iterator())
self.assertEqual(len(dags), 1)
self.assertEqual(node_successors(dags[0]), {"destination": [], "renamed source": ["destination"]})
- self.assertEqual(source_item.get_icon().name(), "renamed source")
+ self.assertEqual(source_item.get_icon().name, "renamed source")
self.assertEqual(os.path.split(source_item.data_dir)[1], shorten("renamed source"))
def test_connections_for_item_no_connections(self):
diff --git a/tests/test_project_item_icon.py b/tests/test_project_item_icon.py
index 0a851550a..d9230e54d 100644
--- a/tests/test_project_item_icon.py
+++ b/tests/test_project_item_icon.py
@@ -44,7 +44,7 @@ def tearDown(self):
def test_init(self):
icon = ProjectItemIcon(self._toolbox, ":/icons/home.svg", QColor(Qt.GlobalColor.gray))
- self.assertEqual(icon.name(), "")
+ self.assertEqual(icon.name, "")
self.assertEqual(icon.x(), 0)
self.assertEqual(icon.y(), 0)
self.assertEqual(icon.incoming_links(), [])
@@ -53,7 +53,7 @@ def test_init(self):
def test_finalize(self):
icon = ProjectItemIcon(self._toolbox, ":/icons/home.svg", QColor(Qt.GlobalColor.gray))
icon.finalize("new name", -43, 314)
- self.assertEqual(icon.name(), "new name")
+ self.assertEqual(icon.name, "new name")
self.assertEqual(icon.x(), -43)
self.assertEqual(icon.y(), 314)