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)