diff --git a/.github/workflows/make_bundle.yml b/.github/workflows/make_bundle.yml index aca47c640..fb6efef85 100644 --- a/.github/workflows/make_bundle.yml +++ b/.github/workflows/make_bundle.yml @@ -44,7 +44,7 @@ jobs: run: | mkdir embedded-python cd embedded-python - curl -o python.zip https://www.python.org/ftp/python/3.12.3/python-3.12.3-embed-amd64.zip + curl -o python.zip https://www.python.org/ftp/python/3.12.7/python-3.12.7-embed-amd64.zip tar xf python.zip del python.zip - name: Edit embedded Python search paths @@ -65,7 +65,7 @@ jobs: run: | cd embedded-python/Scripts ./pip.exe --version - ./pip.exe install ipykernel jill + ./pip.exe install ipykernel jill pandas spinedb-api - name: List packages in embedded Python run: | cd embedded-python/Scripts @@ -79,7 +79,7 @@ jobs: run: | python -c "from importlib.metadata import version; print('version=' + version('spinetoolbox'))" >> $GITHUB_OUTPUT - name: Upload archive - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Spine-Toolbox-win-${{ steps.toolbox-version.outputs.version }} path: "./dist/Spine Toolbox" diff --git a/requirements.txt b/requirements.txt index 0ff41d492..e72de293e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -e git+https://github.com/spine-tools/Spine-Database-API.git#egg=spinedb_api -e git+https://github.com/spine-tools/spine-engine.git#egg=spine_engine --e git+https://github.com/spine-tools/spine-items.git#egg=spine_items +-e git+https://github.com/spine-tools/spine-items.git@toolbox_issue_2794#egg=spine_items -e . diff --git a/spinetoolbox.py b/spinetoolbox.py index eaa17cfe5..f92f75264 100644 --- a/spinetoolbox.py +++ b/spinetoolbox.py @@ -10,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Starts Spine Toolbox. -""" - +"""Starts Spine Toolbox.""" if __name__ == "__main__": import sys diff --git a/spinetoolbox/group.py b/spinetoolbox/group.py new file mode 100644 index 000000000..bd011d4c4 --- /dev/null +++ b/spinetoolbox/group.py @@ -0,0 +1,322 @@ +###################################################################################################################### +# 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, Slot, QMarginsF, QRectF, QPointF +from PySide6.QtGui import QBrush, QPen, QAction, QPainterPath, QTransform +from PySide6.QtWidgets import ( + QGraphicsItem, + QGraphicsRectItem, + QGraphicsTextItem, + QGraphicsDropShadowEffect, + QStyle +) +from .project_item_icon import ProjectItemIcon +from .project_commands import RenameGroupCommand +from widgets.notification import Notification + + +class Group(QGraphicsRectItem): + + FONT_SIZE_PIXELS = 12 # pixel size to prevent font scaling by system + + def __init__(self, toolbox, name, item_names): + super().__init__() + self._toolbox = toolbox + self._scene = None + 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(self.call_disband_group) + rename_group_action = QAction("Rename group...") + rename_group_action.triggered.connect(self.rename_group) + 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.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsScenePositionChanges, enabled=True) + self.setAcceptHoverEvents(True) + self.setZValue(-10) + self.name_item = QGraphicsTextItem(self._name, parent=self) + self.set_name_attributes() + self.setRect(self.current_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() + self.previous_pos = QPointF() + self._moved_on_scene = False + self._bumping = True + # self.setOpacity(0.5) + self.mouse_press_pos = None + + @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() + + @Slot(bool) + def call_disband_group(self, _=False): + self._toolbox.toolboxuibase.active_ui_window.ui.graphicsView.push_disband_group_command(self.name) + + @Slot(bool) + def rename_group(self, _=False): + """Renames Group.""" + new_name = self._toolbox.show_simple_input_dialog("Rename Group", "New name:", self.name) + if not new_name or new_name == self.name: + return + if new_name in self._toolbox.project.groups.keys(): + notif = Notification(self._toolbox.toolboxuibase.active_ui_window, f"Group {new_name} already exists") + notif.show() + return + self._toolbox.toolboxuibase.undo_stack.push(RenameGroupCommand(self._toolbox.project, self.name, new_name)) + + 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, current_pos=None): + """Updates group rectangle, and it's position when group member(s) is/are moved.""" + self.prepareGeometryChange() + r = self.current_rect() + self.setRect(r) + if current_pos is not None: + diff_x = current_pos.x() - self.mouse_press_pos.x() + diff_y = current_pos.y() - self.mouse_press_pos.y() + self.setPos(QPointF(diff_x, diff_y)) + self._reposition_name_item() + + def current_rect(self): + """Calculates the size of the rectangle for this group.""" + united_rect = QRectF() + for item in self.items: + if isinstance(item, ProjectItemIcon): + # Combine item icon box and name item + icon_rect = item.name_item.sceneBoundingRect().united(item.sceneBoundingRect()) + # Combine spec item rect if available + if item.spec_item is not None: + icon_rect = icon_rect.united(item.spec_item.sceneBoundingRect()) + united_rect = united_rect.united(icon_rect) + else: + united_rect = united_rect.united(item.sceneBoundingRect()) + return united_rect + + def itemChange(self, change, value): + """ + Reacts to item removal and position changes. + + In particular, destroys the drop shadow effect when the item is removed from a scene + and keeps track of item's movements on the scene. + + Args: + change (GraphicsItemChange): a flag signalling the type of the change + value: a value related to the change + + Returns: + Whatever super() does with the value parameter + """ + if change == QGraphicsItem.GraphicsItemChange.ItemScenePositionHasChanged: + self._moved_on_scene = True + elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None: + self.prepareGeometryChange() + self.setGraphicsEffect(None) + elif change == QGraphicsItem.GraphicsItemChange.ItemSceneHasChanged: + scene = value + if scene is None: + self._scene.removeItem(self.name_item) + else: + self._scene = scene + self._scene.addItem(self.name_item) + self._reposition_name_item() + return super().itemChange(change, value) + + 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() + self.scene().clearSelection() + path = QPainterPath() + path.addRect(self.sceneBoundingRect()) + self._toolbox.toolboxuibase.active_ui_window.ui.graphicsView.scene().setSelectionArea(path, QTransform()) + icon_group = set(self.project_items) + for icon in icon_group: + icon.this_icons_group_is_moving = True + icon.previous_pos = icon.scenePos() + self.scene().icon_group = icon_group + + def mouseReleaseEvent(self, event): + """Accepts the event to prevent graphics view's mouseReleaseEvent from clearing the selections.""" + if (self.scenePos() - self.previous_pos).manhattanLength() > qApp.startDragDistance(): + # self._toolbox.undo_stack.push(MoveGroupCommand(self, self._toolbox.project)) + self.notify_item_move() + event.accept() + # icon_group = set(self.project_items) + # for icon in icon_group: + # icon.this_icons_group_is_moving = False + super().mouseReleaseEvent(event) + + def set_pos_without_bumping(self, pos): + self._bumping = False + self.setPos(pos) + self._bumping = True + + def notify_item_move(self): + if self._moved_on_scene: + self._moved_on_scene = False + scene = self.scene() + scene.item_move_finished.emit(self) + + 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 dash line 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/helpers.py b/spinetoolbox/helpers.py index 469d83aaa..2d57b36bd 100644 --- a/spinetoolbox/helpers.py +++ b/spinetoolbox/helpers.py @@ -117,7 +117,25 @@ def format_log_message(msg_type, message, show_datetime=True): Returns: str: formatted message """ - color = {"msg": "white", "msg_success": "#00ff00", "msg_error": "#ff3333", "msg_warning": "yellow"}[msg_type] + color = {"msg": "white", "msg_success": "#00cc00", "msg_error": "#ff3333", "msg_warning": "#e6e600"}[msg_type] + open_tag = f"" + date_str = get_datetime(show=show_datetime) + return open_tag + date_str + message + "" + + +def format_log_message_lite(msg_type, message, show_datetime=True): + """Adds color tags and optional time stamp to message. + Text colors are selected for a QTextBrowser with a white background. + + Args: + msg_type (str): message's type; accepts only 'msg', 'msg_success', 'msg_warning', or 'msg_error' + message (str): message to format + show_datetime (bool): True to add time stamp, False to omit it + + Returns: + str: formatted message + """ + color = {"msg": "black", "msg_success": "#009900", "msg_error": "#ff3333", "msg_warning": "yellow"}[msg_type] open_tag = f"" date_str = get_datetime(show=show_datetime) return open_tag + date_str + message + "" diff --git a/spinetoolbox/link.py b/spinetoolbox/link.py index c29ee61bd..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.""" @@ -107,7 +113,7 @@ def update_geometry(self, curved_links=None): """Updates geometry.""" self.prepareGeometryChange() if curved_links is None: - qsettings = self._toolbox.qsettings() + qsettings = self._toolbox.qsettings curved_links = qsettings.value("appSettings/curvedLinks", defaultValue="false") == "true" self._guide_path = self._make_guide_path(curved_links) self._do_update_geometry() @@ -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) @@ -436,7 +444,7 @@ def _make_execution_animation(self): def run_execution_animation(self): """Runs execution animation.""" - qsettings = self._toolbox.qsettings() + qsettings = self._toolbox.qsettings duration = int(qsettings.value("appSettings/dataFlowAnimationDuration", defaultValue="100")) self._anim.setDuration(duration) self._anim.start() @@ -494,7 +502,7 @@ def update_icons(self): active = self._connection.purge_before_writing self._icons.append(_TextIcon(self, self._icon_extent, self._PURGE, active=active)) if self._connection.may_have_write_index(): - sibling_conns = self._toolbox.project().incoming_connections(self.connection.destination) + sibling_conns = self._toolbox.project.incoming_connections(self.connection.destination) active = any(l.write_index > 1 for l in sibling_conns) self._icons.append(_TextIcon(self, self._icon_extent, str(self._connection.write_index), active=active)) notifications = self._connection.notifications() @@ -562,7 +570,7 @@ def issues(self): Returns: list of str: issues regarding the jump """ - return self._toolbox.project().jump_issues(self.jump) + return self._toolbox.project.jump_issues(self.jump) def update_icons(self): while self._icons: diff --git a/spinetoolbox/main.py b/spinetoolbox/main.py index 97ac4acd6..f5105d922 100644 --- a/spinetoolbox/main.py +++ b/spinetoolbox/main.py @@ -28,7 +28,7 @@ from PySide6.QtWidgets import QApplication from .headless import Status, headless_main from .helpers import pyside6_version_check -from .ui_main import ToolboxUI +from .ui_main_base import ToolboxUIBase from .version import __version__ # Importing resources_icons_rc initializes resources and Font Awesome gets added to the application @@ -59,9 +59,9 @@ def main(): status = QFontDatabase.addApplicationFont(":/fonts/fontawesome5-solid-webfont.ttf") if status < 0: logging.warning("Could not load fonts from resources file. Some icons may not render properly.") - window = ToolboxUI() + window = ToolboxUIBase() window.show() - QTimer.singleShot(0, lambda: window.init_tasks(args.project)) + QTimer.singleShot(0, lambda: window.toolboxui.init_tasks(args.project)) # Enter main event loop and wait until exit() is called return_code = app.exec() return return_code diff --git a/spinetoolbox/plugin_manager.py b/spinetoolbox/plugin_manager.py index f0e41bb04..16be28688 100644 --- a/spinetoolbox/plugin_manager.py +++ b/spinetoolbox/plugin_manager.py @@ -100,17 +100,17 @@ def plugin_specs(self): def load_installed_plugins(self): """Loads installed plugins and adds their specifications to toolbars.""" - project = self._toolbox.project() + project = self._toolbox.project local_data = load_specification_local_data(project.config_dir) if project else {} - for plugin_dir in plugins_dirs(self._toolbox.qsettings()): + for plugin_dir in plugins_dirs(self._toolbox.qsettings): self.load_individual_plugin(plugin_dir, local_data) def reload_plugins_with_local_data(self): """Reloads plugins that have project specific local data.""" - project = self._toolbox.project() + project = self._toolbox.project local_data = load_specification_local_data(project.config_dir) if project else {} specification_factories = self._toolbox.item_specification_factories() - app_settings = self._toolbox.qsettings() + app_settings = self._toolbox.qsettings for plugin_name, specifications in self._plugin_specs.items(): for specification in specifications: if not specification.may_have_local_data(): @@ -135,7 +135,7 @@ def load_individual_plugin(self, plugin_dir, specification_local_data): plugin_dict, specification_local_data, self._toolbox.item_specification_factories(), - self._toolbox.qsettings(), + self._toolbox.qsettings, self._toolbox, ) if plugin_specs is None: @@ -143,9 +143,9 @@ def load_individual_plugin(self, plugin_dir, specification_local_data): name = plugin_dict["name"] self._installed_plugins[name] = plugin_dict disabled_plugins = set() - if self._toolbox.project() is not None: + if self._toolbox.project is not None: for spec in itertools.chain(*plugin_specs.values()): - spec_id = self._toolbox.project().add_specification(spec, save_to_disk=False) + spec_id = self._toolbox.project.add_specification(spec, save_to_disk=False) if spec_id is None: disabled_plugins.add(spec.name) self._plugin_specs.update(plugin_specs) @@ -205,7 +205,7 @@ def _install_plugin(self, plugin_name): worker.start(_download_plugin, plugin, plugin_local_dir) def _load_installed_plugin(self, plugin_local_dir): - project = self._toolbox.project() + project = self._toolbox.project local_data = load_specification_local_data(project.config_dir) if project is not None else {} self.load_individual_plugin(plugin_local_dir, local_data) self._toolbox.refresh_toolbars() @@ -242,10 +242,10 @@ def _remove_plugin(self, plugin_name): plugin_dict = self._installed_plugins.pop(plugin_name) plugin_dir = plugin_dict["plugin_dir"] self._plugin_specs.pop(plugin_name, None) - if self._toolbox.project() is not None: - for spec in list(self._toolbox.project().specifications()): + if self._toolbox.project is not None: + for spec in list(self._toolbox.project.specifications()): if spec.plugin == plugin_name: - self._toolbox.project().remove_specification(spec.name) + self._toolbox.project.remove_specification(spec.name) # Remove plugin dir shutil.rmtree(plugin_dir) self._plugin_toolbars.pop(plugin_name).deleteLater() diff --git a/spinetoolbox/project.py b/spinetoolbox/project.py index 352a1d79c..667ef75b2 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,7 @@ def __init__(self, toolbox, p_dir, plugin_specs, app_settings, settings, logger) self._settings = settings self._engine_workers = [] self._execution_in_progress = False + self._groups = dict() # Dictionary, which maps group names to group instances 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 @@ -161,6 +168,67 @@ def n_items(self): def settings(self): return self._settings + @property + def connections(self): + return self._connections + + @property + def jumps(self): + return self._jumps + + @property + 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 the last item in + the group was removed, destroys the whole group.""" + self.groups[group_name].remove_item(item_name) + if not self.groups[group_name].project_items: + 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. @@ -248,6 +316,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) @@ -358,8 +427,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) @@ -369,10 +439,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 @@ -656,7 +733,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 @@ -702,10 +779,6 @@ def validate_project_item_name(self, name): return ItemNameStatus.SHORT_NAME_EXISTS return ItemNameStatus.OK - @property - def connections(self): - return self._connections - def find_connection(self, source_name, destination_name): """Searches for a connection between given items. @@ -1458,10 +1531,6 @@ def _update_ranks(self, dag): item = self._project_items[item_name] item.set_rank(ranks[item_name]) - @property - def app_settings(self): - return self._app_settings - @busy_effect def prepare_remote_execution(self): """Pings the server and sends the project as a zip-file to server. diff --git a/spinetoolbox/project_commands.py b/spinetoolbox/project_commands.py index e32c5bc9f..f204c265a 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") @@ -92,6 +92,45 @@ def _move_to(self, positions): self._representative.notify_item_move() +class MoveGroupCommand(SpineToolboxCommand): + """Command to move Group in the Design view.""" + + def __init__(self, icon, project): + """ + Args: + icon (Group): the icon + project (SpineToolboxProject): project + """ + super().__init__() + self._icon = icon + self._project = project + icon_group = icon.scene().icon_group + self._representative = next(iter(icon_group), None) + if self._representative is None: + self.setObsolete(True) + self._previous_group_position = self._icon.previous_pos + self._current_group_position = self._icon.scenePos() + self._previous_item_positions = {x.name: x.previous_pos for x in icon_group} + self._current_item_positions = {x.name: x.scenePos() for x in icon_group} + if len(icon_group) == 1: + self.setText(f"move {self._icon.name}") + else: + self.setText("move multiple items") + + def redo(self): + self._move_to(self._current_item_positions, self._current_group_position) + + def undo(self): + self._move_to(self._previous_item_positions, self._previous_group_position) + + def _move_to(self, item_positions, group_position): + for item_name, position in item_positions.items(): + icon = self._project.get_item(item_name).get_icon() + icon.set_pos_without_bumping(position) + self._icon.set_pos_without_bumping(group_position) + self._icon.notify_item_move() + + class SetProjectDescriptionCommand(SpineToolboxCommand): """Command to set the project description.""" @@ -244,6 +283,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) == 1: + 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/logging_connection.py b/spinetoolbox/project_item/logging_connection.py index 730b644cf..d91278532 100644 --- a/spinetoolbox/project_item/logging_connection.py +++ b/spinetoolbox/project_item/logging_connection.py @@ -143,10 +143,10 @@ class LoggingConnection(LogMixin, HeadlessConnection): def __init__(self, *args, toolbox, **kwargs): super().__init__(*args, **kwargs) self._toolbox = toolbox - self.resource_filter_model = ResourceFilterModel(self, toolbox.project(), toolbox.undo_stack, toolbox) + self.resource_filter_model = ResourceFilterModel(self, toolbox.project, toolbox.undo_stack, toolbox) self.link = None - self._source_item_type = self._toolbox.project().get_item(self.source).item_type() - self._destination_item_type = self._toolbox.project().get_item(self.destination).item_type() + self._source_item_type = self._toolbox.project.get_item(self.source).item_type() + self._destination_item_type = self._toolbox.project.get_item(self.destination).item_type() self._db_maps = {} self._fetch_parents = {} @@ -370,7 +370,7 @@ def set_connection_options(self, options): if options == self.options: return self.options = options - project = self._toolbox.project() + project = self._toolbox.project sibling_conns = project.incoming_connections(self.destination) for conn in sibling_conns: conn.link.update_icons() diff --git a/spinetoolbox/project_item/project_item.py b/spinetoolbox/project_item/project_item.py index 0d1c87f11..a64ddd9cc 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,11 @@ def set_rank(self, rank): else: self.get_icon().rank_icon.set_rank("X") + def update_progress_bar(self): + 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() @@ -387,7 +393,7 @@ def rename(self, new_name, rename_data_dir_message): Returns: bool: True if item was renamed successfully, False otherwise """ - new_data_dir = os.path.join(self._toolbox.project().items_dir, shorten(new_name)) + new_data_dir = os.path.join(self._toolbox.project.items_dir, shorten(new_name)) if not rename_dir(self.data_dir, new_data_dir, self._toolbox, rename_data_dir_message): return False self.set_name(new_name) diff --git a/spinetoolbox/project_item/specification_editor_window.py b/spinetoolbox/project_item/specification_editor_window.py index 19fbd474b..4e496b849 100644 --- a/spinetoolbox/project_item/specification_editor_window.py +++ b/spinetoolbox/project_item/specification_editor_window.py @@ -112,7 +112,7 @@ def __init__(self, toolbox, specification=None, item=None): self._original_spec_name = None if specification is None else specification.name self.specification = specification self.item = item - self._app_settings = toolbox.qsettings() + self._app_settings = toolbox.qsettings # Setup UI from Qt Designer file self._ui = self._make_ui() self._ui.setupUi(self) @@ -218,7 +218,7 @@ def _save(self, exiting=None): Returns: bool: True if operation was successful, False otherwise """ - if not self._toolbox.project(): + if not self._toolbox.project: self.show_error("Please open or create a project first") return False name = self._spec_toolbar.name() @@ -231,21 +231,21 @@ def _save(self, exiting=None): if spec is None: return self.prompt_exit_without_saving() if exiting else False if not self._original_spec_name: - if self._toolbox.project().is_specification_name_reserved(name): + if self._toolbox.project.is_specification_name_reserved(name): self.show_error("Specification name already in use. Please enter a new name.") return False self._toolbox.add_specification(spec) - if not self._toolbox.project().is_specification_name_reserved(name): + if not self._toolbox.project.is_specification_name_reserved(name): return False if self.item is not None: self.item.set_specification(spec) else: - if name != self._original_spec_name and self._toolbox.project().is_specification_name_reserved(name): + if name != self._original_spec_name and self._toolbox.project.is_specification_name_reserved(name): self.show_error("Specification name already in use. Please enter a new name.") return False spec.definition_file_path = self.specification.definition_file_path self._toolbox.replace_specification(self._original_spec_name, spec) - if not self._toolbox.project().is_specification_name_reserved(name): + if not self._toolbox.project.is_specification_name_reserved(name): return False self._original_spec_name = name self._undo_stack.setClean() @@ -277,7 +277,7 @@ def _duplicate_kwargs(self): return {} def _duplicate(self): - if not self._toolbox.project(): + if not self._toolbox.project: self.show_error("Please open or create a project first") return new_spec = self._make_new_specification("") @@ -287,7 +287,7 @@ def tear_down(self): if self.focusWidget(): self.focusWidget().clearFocus() if not self._undo_stack.isClean() and not prompt_to_save_changes( - self, self._toolbox.qsettings(), self._save, True + self, self._toolbox.qsettings, self._save, True ): return False self._change_notifier.tear_down() diff --git a/spinetoolbox/project_item_icon.py b/spinetoolbox/project_item_icon.py index f14280a49..68112dbac 100644 --- a/spinetoolbox/project_item_icon.py +++ b/spinetoolbox/project_item_icon.py @@ -36,6 +36,8 @@ class ProjectItemIcon(QGraphicsPathItem): ITEM_EXTENT = 64 FONT_SIZE_PIXELS = 12 # pixel size to prevent font scaling by system + DEFAULT_ICON_SELECTION_PEN_W = 1 + USER_MODE_ICON_SELECTION_PEN_W = 5 def __init__(self, toolbox, icon_file, icon_color): """ @@ -52,8 +54,10 @@ def __init__(self, toolbox, icon_file, icon_color): self.icon_file = icon_file self._icon_color = icon_color self._moved_on_scene = False + self.this_icons_group_is_moving = 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) @@ -67,7 +71,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 @@ -79,11 +83,14 @@ def __init__(self, toolbox, icon_file, icon_color): "right": ConnectorButton(toolbox, self, position="right"), } self._setup() + self.set_graphics_effects() + self._update_path() + + def set_graphics_effects(self): shadow_effect = QGraphicsDropShadowEffect() shadow_effect.setOffset(1) shadow_effect.setEnabled(False) self.setGraphicsEffect(shadow_effect) - self._update_path() def add_specification_icon(self, spec_icon_path): """Adds an SVG icon to bottom left corner of the item icon based on Tool Specification type. @@ -114,7 +121,7 @@ def rect(self): return self._rect def _update_path(self): - rounded = self._toolbox.qsettings().value("appSettings/roundedItems", defaultValue="false") == "true" + rounded = self._toolbox.qsettings.value("appSettings/roundedItems", defaultValue="false") == "true" self._do_update_path(rounded) def update_path(self, rounded): @@ -129,12 +136,15 @@ def _do_update_path(self, rounded): for conn in self.connectors.values(): conn.update_path(radius) # Selection halo - pen_width = 1 margin = 1 path = QPainterPath() path.addRoundedRect(self._rect.adjusted(-margin, -margin, margin, margin), radius + margin, radius + margin) self._selection_halo.setPath(path) - selection_pen = QPen(Qt.DashLine) + self.set_icon_selection_pen_w(self.DEFAULT_ICON_SELECTION_PEN_W) + + 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) self._selection_halo.setPen(selection_pen) @@ -157,7 +167,7 @@ def _setup(self): gradient.setColorAt(0, background_color.lighter(105)) gradient.setColorAt(1, background_color.darker(105)) brush = QBrush(gradient) - pen = QPen(QBrush(background_color.darker()), 1, Qt.SolidLine) + pen = QPen(QBrush(background_color.darker()), 1, Qt.PenStyle.SolidLine) self.setPen(pen) for conn in self.connectors.values(): conn.setPen(pen) @@ -177,12 +187,12 @@ def _setup(self): self.svg_item.setScale((rect_w - margin) / dim_max) self.svg_item.setPos(self.rect().center() - self.svg_item.sceneBoundingRect().center()) self.svg_item.setGraphicsEffect(self.colorizer) - self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True) - self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True) - self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True) - self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, enabled=True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsScenePositionChanges, enabled=True) self.setAcceptHoverEvents(True) - self.setCursor(Qt.PointingHandCursor) + self.setCursor(Qt.CursorShape.PointingHandCursor) # Set exclamation, execution_log, and rank icons position self.exclamation_icon.setPos(self.rect().topRight() - self.exclamation_icon.sceneBoundingRect().topRight()) self.execution_icon.setPos( @@ -190,12 +200,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): @@ -221,7 +227,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) @@ -302,11 +309,19 @@ def hoverLeaveEvent(self, event): def mousePressEvent(self, event): """Updates scene's icon group.""" - super().mousePressEvent(event) + for group in self.my_groups: + group.mouse_press_pos = event.pos() icon_group = set(x for x in self.scene().selectedItems() if isinstance(x, ProjectItemIcon)) | {self} for icon in icon_group: icon.previous_pos = icon.scenePos() self.scene().icon_group = icon_group + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """Updates group rectangles on scene, if this item is in group(s).""" + for group in self.my_groups: + group.update_group_rect(event.pos()) + super().mouseMoveEvent(event) def update_links_geometry(self): """Updates geometry of connected links to reflect this item's most recent position.""" @@ -317,18 +332,20 @@ def update_links_geometry(self): dirty_links = set(link for icon in icon_group for conn in icon.connectors.values() for link in conn.links) if not dirty_links: return - qsettings = self._toolbox.qsettings() + qsettings = self._toolbox.qsettings curved_links = qsettings.value("appSettings/curvedLinks", defaultValue="false") == "true" for link in dirty_links: link.update_geometry(curved_links) def mouseReleaseEvent(self, event): """Clears pre-bump rects, and pushes a move icon command if necessary.""" + for group in self.my_groups: + group.mouse_press_pos = None for icon in self.scene().icon_group: icon.bumped_rects.clear() # pylint: disable=undefined-variable if (self.scenePos() - self.previous_pos).manhattanLength() > qApp.startDragDistance(): - self._toolbox.undo_stack.push(MoveIconCommand(self, self._toolbox.project())) + self._toolbox.undo_stack.push(MoveIconCommand(self, self._toolbox.project)) event.ignore() super().mouseReleaseEvent(event) @@ -347,7 +364,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): @@ -364,7 +381,7 @@ def itemChange(self, change, value): Returns: Whatever super() does with the value parameter """ - if change == QGraphicsItem.ItemScenePositionHasChanged: + if change == QGraphicsItem.GraphicsItemChange.ItemScenePositionHasChanged: self._moved_on_scene = True self._reposition_name_item() self.update_links_geometry() @@ -394,7 +411,7 @@ def set_pos_without_bumping(self, pos): def _handle_collisions(self): """Handles collisions with other items.""" - prevent_overlapping = self._toolbox.qsettings().value("appSettings/preventOverlapping", defaultValue="false") + prevent_overlapping = self._toolbox.qsettings.value("appSettings/preventOverlapping", defaultValue="false") if not self.scene() or not self._bumping or prevent_overlapping != "true": return restablished = self._restablish_bumped_items() @@ -406,7 +423,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() @@ -473,7 +490,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 @@ -496,7 +513,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. @@ -504,7 +521,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. @@ -588,17 +605,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 c2238df72..631f1d435 100644 --- a/spinetoolbox/resources_icons_rc.py +++ b/spinetoolbox/resources_icons_rc.py @@ -27067,6 +27067,38 @@ 48zm-16 352H64V6\ 4h448v288z\x22/>\ +\x00\x00\x01\xd3\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 384 512\x22>\ \x00\x00\x02k\ <\ svg xmlns=\x22http:\ @@ -28710,6 +28742,64 @@ 88 80 80 80 80-3\ 5.888 80-80z\x22/><\ /svg>\ +\x00\x00\x03\x7f\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 640 512\x22>\ \x00\x00\x0d\xd9\ <\ ?xml version=\x221.\ @@ -28934,6 +29024,25 @@ 5391 z \x22\x0d\x0a i\ d=\x22path2-0\x22 />\x0d\x0a\ \x0d\x0a\ +\x00\x00\x01\x0b\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 384 512\x22>\ \x00\x00\x02X\ <\ svg xmlns=\x22http:\ @@ -28974,25 +29083,6 @@ 7.2 16 16 16s16-\ 7.2 16-16V208z\x22/\ >\ -\x00\x00\x01\x0b\ -<\ -svg xmlns=\x22http:\ -//www.w3.org/200\ -0/svg\x22 viewBox=\x22\ -0 0 384 512\x22>\ \x00\x00\x02-\ <\ svg xmlns=\x22http:\ @@ -29726,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.\ @@ -31243,6 +31378,39 @@ 6.207 9.997-36.2\ 04-.001z\x22/>\ +\x00\x00\x01\xe1\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 640 512\x22>\ +\ \x00\x00\x0b\xf3\ <\ ?xml version=\x221.\ @@ -32131,6 +32299,10 @@ \x0b\xb6\xce\x87\ \x00d\ \x00e\x00s\x00k\x00t\x00o\x00p\x00.\x00s\x00v\x00g\ +\x00\x0f\ +\x00\x7f`\xa7\ +\x00f\ +\x00i\x00l\x00e\x00-\x00u\x00p\x00l\x00o\x00a\x00d\x00.\x00s\x00v\x00g\ \x00\x08\ \x068W'\ \x00h\ @@ -32226,6 +32398,10 @@ \x0aZZ\x07\ \x00c\ \x00o\x00g\x00.\x00s\x00v\x00g\ +\x00\x0b\ +\x0b6\xcdg\ +\x00r\ +\x00e\x00t\x00w\x00e\x00e\x00t\x00.\x00s\x00v\x00g\ \x00\x0e\ \x04\xdfl\x87\ \x00c\ @@ -32284,6 +32460,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\ @@ -32353,6 +32533,10 @@ \x00c\ \x00h\x00e\x00c\x00k\x00.\x00s\x00v\x00g\ \x00\x0c\ +\x0b&r\xc7\ +\x00t\ +\x00e\x00r\x00m\x00i\x00n\x00a\x00l\x00.\x00s\x00v\x00g\ +\x00\x0c\ \x0c\x16L\x07\ \x00c\ \x00u\x00b\x00e\x00_\x00p\x00e\x00n\x00.\x00s\x00v\x00g\ @@ -32399,9 +32583,9 @@ 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\x00_\ +\x00\x00\x00\x10\x00\x02\x00\x00\x00\x03\x00\x00\x00c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x09\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1b\x00\x00\x00\x09\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00 \x00\x02\x00\x00\x00\x03\x00\x00\x00\x06\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -32417,23 +32601,25 @@ \x00\x00\x01\x8f\xec\xcf\x12\xea\ \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\x93IP\xaeM\ \x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x062\x92\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04p\x00\x00\x00\x00\x00\x01\x00\x06\xa8\xcd\ +\x00\x00\x04\x94\x00\x00\x00\x00\x00\x01\x00\x06\xaa\xa4\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x04\xb8\x00\x00\x00\x00\x00\x01\x00\x06\xbd\xb7\ +\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x06\xbf\x8e\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x06\x88\x1a\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x06\xaa\xa1\ +\x00\x00\x04\xbc\x00\x00\x00\x00\x00\x01\x00\x06\xacx\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x99\xa7\ +\x00\x00\x040\x00\x00\x00\x00\x00\x01\x00\x06\x9b~\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06E\xb2\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ \x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x06B\xab\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x06\x978\ +\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x06\x99\x0f\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ \x00\x00\x03^\x00\x00\x00\x00\x00\x01\x00\x06\x8a3\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ @@ -32449,15 +32635,15 @@ \x00\x00\x01\x8f\xec\xcf\x13\x09\ \x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x06/\x9c\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04$\x00\x00\x00\x00\x00\x01\x00\x06\xa0\x10\ +\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\x00V\ +\x00\x00\x03\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00Z\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x03~\x00\x02\x00\x00\x003\x00\x00\x00#\ +\x00\x00\x03~\x00\x02\x00\x00\x006\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\ -\x00\x00\x04T\x00\x00\x00\x00\x00\x01\x00\x06\xa3)\ +\x00\x00\x04x\x00\x00\x00\x00\x00\x01\x00\x06\xa5\x00\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ \x00\x00\x03\xda\x00\x00\x00\x00\x00\x01\x00\x06\x96\x00\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ @@ -32467,125 +32653,131 @@ \x00\x00\x01\x8f\xec\xcf\x13\x09\ \x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x06`G\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x07\x0a\xf8\ +\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x07\x0d\xf6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\xec\x00\x00\x00\x00\x00\x01\x00\x07\xbf\xb6\ +\x00\x00\x0cp\x00\x00\x00\x00\x00\x01\x00\x07\xc9\xa0\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x060\x00\x00\x00\x00\x00\x01\x00\x06\xec\xdf\ +\x00\x00\x06T\x00\x00\x00\x00\x00\x01\x00\x06\xee\xb6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\x14\x00\x00\x00\x00\x00\x01\x00\x07Hx\ +\x00\x00\x09z\x00\x00\x00\x00\x00\x01\x00\x07P}\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\x9a\x00\x00\x00\x00\x00\x01\x00\x07_J\ +\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x07=|\ +\x00\x00\x01\x93IP\xaeN\ +\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x07gO\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xf6\x00\x00\x00\x00\x00\x01\x00\x07\x08\x9c\ +\x00\x00\x076\x00\x00\x00\x00\x00\x01\x00\x07\x0f\x05\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\x98\x00\x00\x00\x00\x00\x01\x00\x07B\xb4\ +\x00\x00\x08\xfe\x00\x00\x00\x00\x00\x01\x00\x07J\xb9\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09v\x00\x00\x00\x00\x00\x01\x00\x07]\x88\ +\x00\x00\x09\xdc\x00\x00\x00\x00\x00\x01\x00\x07e\x8d\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xce\x00\x00\x00\x00\x00\x01\x00\x07\xa0\x8e\ +\x00\x00\x0bR\x00\x00\x00\x00\x00\x01\x00\x07\xaax\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xa0\x00\x00\x00\x00\x00\x01\x00\x07\x1ez\ +\x00\x00\x07\xe0\x00\x00\x00\x00\x00\x01\x00\x07#\xd4\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\x16\x00\x00\x00\x00\x00\x01\x00\x07\x0e8\ +\x00\x00\x07V\x00\x00\x00\x00\x00\x01\x00\x07\x13\x92\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x094\x00\x00\x00\x00\x00\x01\x00\x07ZV\ +\x00\x00\x09\x9a\x00\x00\x00\x00\x00\x01\x00\x07b[\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08x\x00\x00\x00\x00\x00\x01\x00\x07)\xa4\ +\x00\x00\x08\xb8\x00\x00\x00\x00\x00\x01\x00\x07.\xfe\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\xba\x00\x00\x00\x00\x00\x01\x00\x07D\xc1\ +\x00\x00\x09 \x00\x00\x00\x00\x00\x01\x00\x07L\xc6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xf2\x00\x00\x00\x00\x00\x01\x00\x07$u\ +\x00\x00\x082\x00\x00\x00\x00\x00\x01\x00\x07)\xcf\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\xee\x00\x00\x00\x00\x00\x01\x00\x07F\x1d\ +\x00\x00\x09T\x00\x00\x00\x00\x00\x01\x00\x07N\x22\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06V\x00\x00\x00\x00\x00\x01\x00\x06\xee\xe8\ +\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\x00\x01\x00\x07~\x92\ +\x00\x00\x0af\x00\x00\x00\x00\x00\x01\x00\x07\x86\x97\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xb8\x00\x00\x00\x00\x00\x01\x00\x07 m\ +\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\xa6\xd3\ +\x00\x00\x0b\xc2\x00\x00\x00\x00\x00\x01\x00\x07\xb0\xbd\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\x1a\x00\x00\x00\x00\x00\x01\x00\x06\xeap\ +\x00\x00\x06>\x00\x00\x00\x00\x00\x01\x00\x06\xecG\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x06\xfa\xbf\ +\x00\x00\x07\x14\x00\x00\x00\x00\x00\x01\x00\x07\x00\x19\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08T\x00\x00\x00\x00\x00\x01\x00\x07(B\ +\x00\x00\x08\x94\x00\x00\x00\x00\x00\x01\x00\x07-\x9c\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07X\x00\x00\x00\x00\x00\x01\x00\x07\x1a]\ +\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\xa4\x00\x00\x00\x00\x00\x01\x00\x07\xad\xdf\ +\x00\x00\x0c(\x00\x00\x00\x00\x00\x01\x00\x07\xb7\xc9\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\x0e\x00\x00\x00\x00\x00\x01\x00\x07\xa5\xce\ +\x00\x00\x0b\x92\x00\x00\x00\x00\x00\x01\x00\x07\xaf\xb8\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xfe\x00\x00\x00\x00\x00\x01\x00\x06\xe7K\ +\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x06\xe9\x22\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xda\x00\x00\x00\x00\x00\x01\x00\x07!\x9a\ +\x00\x00\x08\x1a\x00\x00\x00\x00\x00\x01\x00\x07&\xf4\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xd2\x00\x00\x00\x00\x00\x01\x00\x07y\xdc\ +\x00\x00\x0a8\x00\x00\x00\x00\x00\x01\x00\x07\x81\xe1\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x07\xb0:\ +\x00\x00\x0cP\x00\x00\x00\x00\x00\x01\x00\x07\xba$\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07:\x00\x01\x00\x00\x00\x01\x00\x07\x18\xeb\ +\x00\x00\x07z\x00\x01\x00\x00\x00\x01\x00\x07\x1eE\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0aT\x00\x00\x00\x00\x00\x01\x00\x07\x8e%\ +\x00\x00\x0a\xba\x00\x00\x00\x00\x00\x01\x00\x07\x96*\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09^\x00\x00\x00\x00\x00\x01\x00\x07\x5ci\ +\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x07dn\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x07m\xb8\ +\x00\x00\x0a\x22\x00\x00\x00\x00\x00\x01\x00\x07u\xbd\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xc0\x00\x00\x00\x00\x00\x01\x00\x06\xf6%\ +\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x06\xf7\xfc\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xf2\x00\x00\x00\x00\x00\x01\x00\x07\xa2T\ +\x00\x00\x0bv\x00\x00\x00\x00\x00\x01\x00\x07\xac>\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x07\x90\xe7\ +\x00\x00\x0a\xe6\x00\x00\x00\x00\x00\x01\x00\x07\x98\xec\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a$\x00\x00\x00\x00\x00\x01\x00\x07\x8c\x8f\ +\x00\x00\x0a\x8a\x00\x00\x00\x00\x00\x01\x00\x07\x94\x94\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x07{\x8f\ +\x00\x00\x0aN\x00\x00\x00\x00\x00\x01\x00\x07\x83\x94\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\x8a\x00\x00\x00\x00\x00\x01\x00\x07\x1c+\ +\x00\x00\x07\xca\x00\x00\x00\x00\x00\x01\x00\x07!\x85\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\x98\x00\x00\x00\x00\x00\x01\x00\x07\x931\ +\x00\x00\x0b\x16\x00\x00\x00\x00\x00\x01\x00\x07\x9c\x9c\ +\x00\x00\x01\x93IP\xaeP\ +\x00\x00\x06\xf8\x00\x00\x00\x00\x00\x01\x00\x06\xfc\x96\ +\x00\x00\x01\x93IP\xaeO\ +\x00\x00\x0a\xfe\x00\x00\x00\x00\x00\x01\x00\x07\x9b6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xaa\x00\x00\x00\x00\x00\x01\x00\x06\xf4{\ +\x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x06\xf6R\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xb0\x00\x00\x00\x00\x00\x01\x00\x07\x94\x97\ +\x00\x00\x0b4\x00\x00\x00\x00\x00\x01\x00\x07\x9e\x81\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\x88\x00\x00\x00\x00\x00\x01\x00\x07\xaa\x95\ +\x00\x00\x0c\x0c\x00\x00\x00\x00\x00\x01\x00\x07\xb4\x7f\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08&\x00\x01\x00\x00\x00\x01\x00\x07%\xca\ +\x00\x00\x08f\x00\x01\x00\x00\x00\x01\x00\x07+$\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08@\x00\x00\x00\x00\x00\x01\x00\x07&\xa4\ +\x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x07+\xfe\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0bb\x00\x00\x00\x00\x00\x01\x00\x07\xa8\xb6\ +\x00\x00\x0b\xe6\x00\x00\x00\x00\x00\x01\x00\x07\xb2\xa0\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\x86\x00\x00\x00\x00\x00\x01\x00\x06\xf0\xb4\ +\x00\x00\x06\xaa\x00\x00\x00\x00\x00\x01\x00\x06\xf2\x8b\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x04\xf0\x00\x00\x00\x00\x00\x01\x00\x07\x0c\x07\ +\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x07\x11a\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05X\x00\x00\x00\x00\x00\x01\x00\x078\x22\ +\x00\x00\x05|\x00\x00\x00\x00\x00\x01\x00\x07@'\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\x84\x00\x00\x00\x00\x00\x01\x00\x07o\x0c\ +\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x07w\x11\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xb0\x00\x00\x00\x00\x00\x01\x00\x06\xe3:\ +\x00\x00\x05\xd4\x00\x00\x00\x00\x00\x01\x00\x06\xe5\x11\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x06\xca\xc1\ +\x00\x00\x052\x00\x00\x00\x00\x00\x01\x00\x06\xcc\x98\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06\xc6\xaf\ +\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06\xc8\x86\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\xce\x00\x00\x00\x00\x00\x01\x00\x06\xc4\x9c\ +\x00\x00\x04\xf2\x00\x00\x00\x00\x00\x01\x00\x06\xc6s\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xe2\x00\x00\x00\x00\x00\x01\x00\x06\xe5\x0c\ +\x00\x00\x06\x06\x00\x00\x00\x00\x00\x01\x00\x06\xe6\xe3\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05(\x00\x00\x00\x00\x00\x01\x00\x06\xcdK\ +\x00\x00\x05L\x00\x00\x00\x00\x00\x01\x00\x06\xcf\x22\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\xf0\x00\x00\x00\x00\x00\x01\x00\x06\xc8\x90\ +\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x06\xcag\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x05X\x00\x00\x00\x00\x00\x01\x00\x06\xcf\xfe\ +\x00\x00\x05|\x00\x00\x00\x00\x00\x01\x00\x06\xd1\xd5\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\x84\x00\x00\x00\x00\x00\x01\x00\x06\xd9\xb8\ +\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x06\xdb\x8f\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x01&\x00\x01\x00\x00\x00\x01\x00\x03\x04\x82\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ diff --git a/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py b/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py index 66d781512..b1b061fac 100644 --- a/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py +++ b/spinetoolbox/spine_db_editor/widgets/custom_qgraphicsviews.py @@ -854,7 +854,7 @@ def set_cross_hairs_items(self, entity_class, cross_hairs_items): item.apply_zoom(self.zoom_factor) cursor_pos = self.mapFromGlobal(QCursor.pos()) self._update_cross_hairs_pos(cursor_pos) - self.viewport().setCursor(Qt.BlankCursor) + self.viewport().setCursor(Qt.CursorShape.BlankCursor) def clear_cross_hairs_items(self): self.entity_class = None @@ -875,7 +875,7 @@ def mousePressEvent(self, event): if not self.cross_hairs_items: super().mousePressEvent(event) return - if event.buttons() & Qt.RightButton or not self._hovered_ent_item: + if event.buttons() & Qt.MouseButton.RightButton or not self._hovered_ent_item: self.clear_cross_hairs_items() return if self._cross_hairs_has_valid_target(): @@ -937,7 +937,7 @@ def mouseReleaseEvent(self, event): def keyPressEvent(self, event): """Aborts relationship creation if user presses ESC.""" super().keyPressEvent(event) - if event.key() == Qt.Key_Escape and self.cross_hairs_items: + if event.key() == Qt.Key.Key_Escape and self.cross_hairs_items: self._spine_db_editor.msg.emit("Relationship creation aborted.") self.clear_cross_hairs_items() diff --git a/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py b/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py index d4027c267..d092a0787 100644 --- a/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py +++ b/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py @@ -97,7 +97,7 @@ def show_plus_button_context_menu(self, global_pos): toolbox = self.db_mngr.parent() if toolbox is None: return - data_stores = toolbox.project().get_items_by_type("Data Store") + data_stores = toolbox.project.get_items_by_type("Data Store") ds_urls = {ds.name: ds.sql_alchemy_url() for ds in data_stores} is_url_validated = {ds.name: ds.is_url_validated() for ds in data_stores} if not ds_urls: diff --git a/spinetoolbox/spine_engine_worker.py b/spinetoolbox/spine_engine_worker.py index bf7c66f9f..1e1fdd536 100644 --- a/spinetoolbox/spine_engine_worker.py +++ b/spinetoolbox/spine_engine_worker.py @@ -37,6 +37,7 @@ 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() @@ -46,6 +47,7 @@ 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/mainwindow.py b/spinetoolbox/ui/mainwindow.py index 79c337f0b..6715360d0 100644 --- a/spinetoolbox/ui/mainwindow.py +++ b/spinetoolbox/ui/mainwindow.py @@ -192,6 +192,17 @@ def setupUi(self, MainWindow): icon18 = QIcon() icon18.addFile(u":/icons/menu_icons/github-mark.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionGitHub.setIcon(icon18) + self.actionSwitch_to_user_mode = QAction(MainWindow) + self.actionSwitch_to_user_mode.setObjectName(u"actionSwitch_to_user_mode") + icon19 = QIcon() + icon19.addFile(u":/icons/menu_icons/retweet.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSwitch_to_user_mode.setIcon(icon19) + self.actionGroup_items = QAction(MainWindow) + self.actionGroup_items.setObjectName(u"actionGroup_items") + icon20 = QIcon() + icon20.addFile(u":/icons/menu_icons/object-group.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionGroup_items.setIcon(icon20) + self.actionGroup_items.setMenuRole(QAction.MenuRole.NoRole) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") MainWindow.setCentralWidget(self.centralwidget) @@ -256,9 +267,9 @@ def setupUi(self, MainWindow): self.toolButton_executions = QToolButton(self.dockWidgetContents) self.toolButton_executions.setObjectName(u"toolButton_executions") - icon19 = QIcon() - icon19.addFile(u":/icons/check-circle.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.toolButton_executions.setIcon(icon19) + icon21 = QIcon() + icon21.addFile(u":/icons/check-circle.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.toolButton_executions.setIcon(icon21) self.toolButton_executions.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.toolButton_executions.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) @@ -376,6 +387,7 @@ def setupUi(self, MainWindow): self.menuFile.addSeparator() self.menuFile.addAction(self.actionNew_DB_editor) self.menuFile.addSeparator() + self.menuFile.addAction(self.actionSwitch_to_user_mode) self.menuFile.addAction(self.actionSettings) self.menuFile.addSeparator() self.menuFile.addAction(self.actionQuit) @@ -413,7 +425,7 @@ def setupUi(self, MainWindow): # setupUi def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Spine Toolbox", 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)) @@ -609,6 +621,14 @@ def retranslateUi(self, MainWindow): self.actionGitHub.setText(QCoreApplication.translate("MainWindow", u"GitHub", None)) #if QT_CONFIG(tooltip) self.actionGitHub.setToolTip(QCoreApplication.translate("MainWindow", u"

Open Spine-Toolbox repository in GitHub

", None)) +#endif // QT_CONFIG(tooltip) + self.actionSwitch_to_user_mode.setText(QCoreApplication.translate("MainWindow", u"Switch to user mode", None)) +#if QT_CONFIG(shortcut) + self.actionSwitch_to_user_mode.setShortcut(QCoreApplication.translate("MainWindow", u"\u00a7", None)) +#endif // QT_CONFIG(shortcut) + self.actionGroup_items.setText(QCoreApplication.translate("MainWindow", u"Group items", None)) +#if QT_CONFIG(tooltip) + self.actionGroup_items.setToolTip(QCoreApplication.translate("MainWindow", u"Group selected items", None)) #endif // QT_CONFIG(tooltip) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None)) diff --git a/spinetoolbox/ui/mainwindow.ui b/spinetoolbox/ui/mainwindow.ui index 6d14f4849..493f660bf 100644 --- a/spinetoolbox/ui/mainwindow.ui +++ b/spinetoolbox/ui/mainwindow.ui @@ -24,7 +24,7 @@ - Spine Toolbox + Spine Toolbox [design mode] true @@ -59,6 +59,7 @@ + @@ -885,6 +886,33 @@ <html><head/><body><p>Open Spine-Toolbox repository in GitHub</p></body></html> + + + + :/icons/menu_icons/retweet.svg:/icons/menu_icons/retweet.svg + + + Switch to user mode + + + § + + + + + + :/icons/menu_icons/object-group.svg:/icons/menu_icons/object-group.svg + + + Group items + + + Group selected items + + + QAction::MenuRole::NoRole + + diff --git a/spinetoolbox/ui/mainwindowbase.py b/spinetoolbox/ui/mainwindowbase.py new file mode 100644 index 000000000..d19cef479 --- /dev/null +++ b/spinetoolbox/ui/mainwindowbase.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +###################################################################################################################### +# 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 . +###################################################################################################################### + +################################################################################ +## Form generated from reading UI file 'mainwindowbase.ui' +## +## Created by: Qt User Interface Compiler version 6.7.3 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, + QCursor, QFont, QFontDatabase, QGradient, + QIcon, QImage, QKeySequence, QLinearGradient, + QPainter, QPalette, QPixmap, QRadialGradient, + QTransform) +from PySide6.QtWidgets import (QApplication, QMainWindow, QSizePolicy, QStackedWidget, + QVBoxLayout, QWidget) + +class Ui_MainWindowBase(object): + def setupUi(self, MainWindowBase): + if not MainWindowBase.objectName(): + MainWindowBase.setObjectName(u"MainWindowBase") + MainWindowBase.resize(800, 600) + self.actionSwitch_UI_mode = QAction(MainWindowBase) + self.actionSwitch_UI_mode.setObjectName(u"actionSwitch_UI_mode") + self.centralwidget = QWidget(MainWindowBase) + self.centralwidget.setObjectName(u"centralwidget") + self.verticalLayout = QVBoxLayout(self.centralwidget) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.stackedWidget = QStackedWidget(self.centralwidget) + self.stackedWidget.setObjectName(u"stackedWidget") + self.page = QWidget() + self.page.setObjectName(u"page") + self.stackedWidget.addWidget(self.page) + self.page_2 = QWidget() + self.page_2.setObjectName(u"page_2") + self.stackedWidget.addWidget(self.page_2) + + self.verticalLayout.addWidget(self.stackedWidget) + + MainWindowBase.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindowBase) + + QMetaObject.connectSlotsByName(MainWindowBase) + # setupUi + + def retranslateUi(self, MainWindowBase): + MainWindowBase.setWindowTitle(QCoreApplication.translate("MainWindowBase", u"Spine Toolbox", None)) + self.actionSwitch_UI_mode.setText(QCoreApplication.translate("MainWindowBase", u"Switch UI mode", None)) + # retranslateUi + diff --git a/spinetoolbox/ui/mainwindowbase.ui b/spinetoolbox/ui/mainwindowbase.ui new file mode 100644 index 000000000..11fb6ff1c --- /dev/null +++ b/spinetoolbox/ui/mainwindowbase.ui @@ -0,0 +1,62 @@ + + + + MainWindowBase + + + + 0 + 0 + 800 + 600 + + + + Spine Toolbox + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Switch UI mode + + + + + + diff --git a/spinetoolbox/ui/mainwindowlite.py b/spinetoolbox/ui/mainwindowlite.py new file mode 100644 index 000000000..0d217f762 --- /dev/null +++ b/spinetoolbox/ui/mainwindowlite.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +###################################################################################################################### +# 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 . +###################################################################################################################### + +################################################################################ +## Form generated from reading UI file 'mainwindowlite.ui' +## +## Created by: Qt User Interface Compiler version 6.7.3 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, + QCursor, QFont, QFontDatabase, QGradient, + QIcon, QImage, QKeySequence, QLinearGradient, + QPainter, QPalette, QPixmap, QRadialGradient, + QTransform) +from PySide6.QtWidgets import (QApplication, QColumnView, QDockWidget, QGraphicsView, + QGroupBox, QHBoxLayout, QMainWindow, QMenu, + QMenuBar, QScrollArea, QSizePolicy, QSplitter, + QStatusBar, QVBoxLayout, QWidget) + +from spinetoolbox.widgets.custom_qgraphicsviews import DesignQGraphicsView +from spinetoolbox.widgets.custom_qtextbrowser import CustomQTextBrowserLite +from spinetoolbox import resources_icons_rc + +class Ui_MainWindowLite(object): + def setupUi(self, MainWindowLite): + if not MainWindowLite.objectName(): + MainWindowLite.setObjectName(u"MainWindowLite") + MainWindowLite.resize(761, 600) + 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_design_mode.setIcon(icon) + self.actionExecute_group = QAction(MainWindowLite) + self.actionExecute_group.setObjectName(u"actionExecute_group") + icon1 = QIcon() + icon1.addFile(u":/icons/menu_icons/play-circle-regular.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionExecute_group.setIcon(icon1) + self.actionExecute_group.setMenuRole(QAction.MenuRole.NoRole) + self.actionStop = QAction(MainWindowLite) + self.actionStop.setObjectName(u"actionStop") + icon2 = QIcon() + icon2.addFile(u":/icons/menu_icons/stop-circle-regular.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionStop.setIcon(icon2) + self.actionStop.setMenuRole(QAction.MenuRole.NoRole) + self.actionShow_event_log = QAction(MainWindowLite) + self.actionShow_event_log.setObjectName(u"actionShow_event_log") + icon3 = QIcon() + icon3.addFile(u":/icons/menu_icons/edit.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionShow_event_log.setIcon(icon3) + self.actionShow_event_log.setMenuRole(QAction.MenuRole.NoRole) + self.actionShow_console = QAction(MainWindowLite) + self.actionShow_console.setObjectName(u"actionShow_console") + icon4 = QIcon() + icon4.addFile(u":/icons/menu_icons/terminal.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionShow_console.setIcon(icon4) + self.actionShow_console.setMenuRole(QAction.MenuRole.NoRole) + self.centralwidget = QWidget(MainWindowLite) + self.centralwidget.setObjectName(u"centralwidget") + self.verticalLayout_2 = QVBoxLayout(self.centralwidget) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.splitter = QSplitter(self.centralwidget) + self.splitter.setObjectName(u"splitter") + self.splitter.setOrientation(Qt.Orientation.Vertical) + self.splitter.setChildrenCollapsible(False) + self.scrollArea = QScrollArea(self.splitter) + self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setAutoFillBackground(False) + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 741, 169)) + self.horizontalLayout = QHBoxLayout(self.scrollAreaWidgetContents) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.groupBox = QGroupBox(self.scrollAreaWidgetContents) + self.groupBox.setObjectName(u"groupBox") + self.verticalLayout_5 = QVBoxLayout(self.groupBox) + self.verticalLayout_5.setObjectName(u"verticalLayout_5") + 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) + + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.splitter.addWidget(self.scrollArea) + self.graphicsView = DesignQGraphicsView(self.splitter) + self.graphicsView.setObjectName(u"graphicsView") + self.graphicsView.setRenderHints(QPainter.RenderHint.Antialiasing|QPainter.RenderHint.TextAntialiasing) + self.graphicsView.setDragMode(QGraphicsView.DragMode.RubberBandDrag) + self.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) + self.graphicsView.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.graphicsView.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate) + self.graphicsView.setRubberBandSelectionMode(Qt.ItemSelectionMode.ContainsItemBoundingRect) + self.splitter.addWidget(self.graphicsView) + + self.verticalLayout_2.addWidget(self.splitter) + + MainWindowLite.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindowLite) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 761, 33)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setObjectName(u"menuFile") + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setObjectName(u"menuHelp") + MainWindowLite.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindowLite) + self.statusbar.setObjectName(u"statusbar") + MainWindowLite.setStatusBar(self.statusbar) + self.dockWidget_event_log = QDockWidget(MainWindowLite) + self.dockWidget_event_log.setObjectName(u"dockWidget_event_log") + self.dockWidget_event_log.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) + self.dockWidget_event_log.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) + self.dockWidgetContents = QWidget() + self.dockWidgetContents.setObjectName(u"dockWidgetContents") + self.verticalLayout_3 = QVBoxLayout(self.dockWidgetContents) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) + self.textBrowser = CustomQTextBrowserLite(self.dockWidgetContents) + self.textBrowser.setObjectName(u"textBrowser") + + self.verticalLayout_3.addWidget(self.textBrowser) + + self.dockWidget_event_log.setWidget(self.dockWidgetContents) + MainWindowLite.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.dockWidget_event_log) + self.dockWidget_console = QDockWidget(MainWindowLite) + self.dockWidget_console.setObjectName(u"dockWidget_console") + self.dockWidget_console.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) + self.dockWidget_console.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) + self.dockWidgetContents_2 = QWidget() + self.dockWidgetContents_2.setObjectName(u"dockWidgetContents_2") + self.dockWidget_console.setWidget(self.dockWidgetContents_2) + MainWindowLite.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.dockWidget_console) + + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) + + self.retranslateUi(MainWindowLite) + + QMetaObject.connectSlotsByName(MainWindowLite) + # setupUi + + def retranslateUi(self, MainWindowLite): + MainWindowLite.setWindowTitle(QCoreApplication.translate("MainWindowLite", u"Spine Toolbox [user 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_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.setText(QCoreApplication.translate("MainWindowLite", u"Show event log", None)) +#if QT_CONFIG(tooltip) + self.actionShow_event_log.setToolTip(QCoreApplication.translate("MainWindowLite", u"Show event log", None)) +#endif // QT_CONFIG(tooltip) + self.actionShow_console.setText(QCoreApplication.translate("MainWindowLite", u"Show console", None)) +#if QT_CONFIG(tooltip) + self.actionShow_console.setToolTip(QCoreApplication.translate("MainWindowLite", u"Show console", None)) +#endif // QT_CONFIG(tooltip) + 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)) + self.dockWidget_event_log.setWindowTitle(QCoreApplication.translate("MainWindowLite", u"Event Log", None)) + self.dockWidget_console.setWindowTitle(QCoreApplication.translate("MainWindowLite", u"Console", None)) + # retranslateUi + diff --git a/spinetoolbox/ui/mainwindowlite.ui b/spinetoolbox/ui/mainwindowlite.ui new file mode 100644 index 000000000..80320ab6e --- /dev/null +++ b/spinetoolbox/ui/mainwindowlite.ui @@ -0,0 +1,259 @@ + + + + MainWindowLite + + + + 0 + 0 + 761 + 600 + + + + Spine Toolbox [user mode] + + + + + + + Qt::Orientation::Vertical + + + false + + + + false + + + true + + + + + 0 + 0 + 741 + 169 + + + + + + + Scenarios + + + + + + + 50 + 30 + + + + + + + + + + + + + QPainter::RenderHint::Antialiasing|QPainter::RenderHint::TextAntialiasing + + + QGraphicsView::DragMode::RubberBandDrag + + + QGraphicsView::ViewportAnchor::AnchorViewCenter + + + QGraphicsView::ViewportAnchor::AnchorUnderMouse + + + QGraphicsView::ViewportUpdateMode::FullViewportUpdate + + + Qt::ItemSelectionMode::ContainsItemBoundingRect + + + + + + + + + + 0 + 0 + 761 + 33 + + + + + File + + + + + Help + + + + + + + + + QDockWidget::DockWidgetFeature::NoDockWidgetFeatures + + + Qt::DockWidgetArea::BottomDockWidgetArea + + + Event Log + + + 8 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + QDockWidget::DockWidgetFeature::NoDockWidgetFeatures + + + Qt::DockWidgetArea::BottomDockWidgetArea + + + Console + + + 8 + + + + + + + :/icons/menu_icons/retweet.svg:/icons/menu_icons/retweet.svg + + + Switch to design mode + + + Switch to design mode + + + § + + + + + + :/icons/menu_icons/play-circle-regular.svg:/icons/menu_icons/play-circle-regular.svg + + + Execute group + + + QAction::MenuRole::NoRole + + + + + + :/icons/menu_icons/stop-circle-regular.svg:/icons/menu_icons/stop-circle-regular.svg + + + Stop + + + QAction::MenuRole::NoRole + + + + + + :/icons/menu_icons/edit.svg:/icons/menu_icons/edit.svg + + + Show event log + + + Show event log + + + QAction::MenuRole::NoRole + + + + + + :/icons/menu_icons/terminal.svg:/icons/menu_icons/terminal.svg + + + Show console + + + Show console + + + QAction::MenuRole::NoRole + + + + + + DesignQGraphicsView + QGraphicsView +
spinetoolbox/widgets/custom_qgraphicsviews.h
+
+ + CustomQTextBrowserLite + QTextBrowser +
spinetoolbox/widgets/custom_qtextbrowser.h
+
+
+ + + + +
diff --git a/spinetoolbox/ui/resources/file-upload.svg b/spinetoolbox/ui/resources/file-upload.svg new file mode 100644 index 000000000..49b0a88dd --- /dev/null +++ b/spinetoolbox/ui/resources/file-upload.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/menu_icons/retweet.svg b/spinetoolbox/ui/resources/menu_icons/retweet.svg new file mode 100644 index 000000000..6c8111bc6 --- /dev/null +++ b/spinetoolbox/ui/resources/menu_icons/retweet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spinetoolbox/ui/resources/menu_icons/terminal.svg b/spinetoolbox/ui/resources/menu_icons/terminal.svg new file mode 100644 index 000000000..85b2b5e85 --- /dev/null +++ b/spinetoolbox/ui/resources/menu_icons/terminal.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 6e52c62b3..101692d7a 100644 --- a/spinetoolbox/ui/resources/resources_icons.qrc +++ b/spinetoolbox/ui/resources/resources_icons.qrc @@ -13,7 +13,11 @@ fontawesome5-searchterms.json + menu_icons/terminal.svg + menu_icons/retweet.svg menu_icons/bolt-lightning.svg + menu_icons/object-group.svg + file-upload.svg share.svg menu_icons/server.svg menu_icons/broom.svg diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py index 93124c9c6..eef191264 100644 --- a/spinetoolbox/ui_main.py +++ b/spinetoolbox/ui_main.py @@ -12,28 +12,24 @@ """Contains a class for the main window of Spine Toolbox.""" import json -import locale import logging import os -import pathlib import sys +import pathlib from zipfile import ZipFile import numpy as np -from PySide6.QtCore import QByteArray, QEvent, QMimeData, QModelIndex, QPoint, QSettings, Qt, QUrl, Signal, Slot +from PySide6.QtCore import QByteArray, QEvent, QMimeData, QModelIndex, QPoint, Qt, QUrl, Signal, Slot from PySide6.QtGui import ( QAction, QColor, QCursor, QDesktopServices, - QGuiApplication, QIcon, QKeySequence, - QUndoStack, QWindow, ) from PySide6.QtWidgets import ( QApplication, - QCheckBox, QDockWidget, QErrorMessage, QFileDialog, @@ -43,10 +39,7 @@ QMainWindow, QMenu, QMessageBox, - QScrollArea, - QStyleFactory, QToolButton, - QVBoxLayout, QWidget, ) from spine_engine.load_project_items import load_item_specification_factories @@ -58,14 +51,12 @@ busy_effect, color_from_index, create_dir, - ensure_window_is_on_screen, format_log_message, load_specification_from_file, load_specification_local_data, open_url, recursive_overwrite, same_path, - set_taskbar_icon, solve_connection_file, supported_img_formats, unique_name, @@ -87,10 +78,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 @@ -109,7 +102,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) @@ -125,25 +118,20 @@ class ToolboxUI(QMainWindow): kernel_shutdown = Signal(object, str) persistent_console_requested = Signal(object, str, tuple, str) - def __init__(self): + def __init__(self, toolboxuibase): + """Initializes application and main window.""" from .ui.mainwindow import Ui_MainWindow # pylint: disable=import-outside-toplevel super().__init__(flags=Qt.WindowType.Window) - self.set_app_style() - self.set_error_mode() - self._qsettings = QSettings("SpineProject", "Spine Toolbox", self) - self._update_qsettings() - locale.setlocale(locale.LC_NUMERIC, 'C') + self.toolboxuibase = toolboxuibase self.ui = Ui_MainWindow() - self.ui.setupUi(self) + self.ui.setupUi(self) # Set up gui widgets from Qt Designer files + self.takeCentralWidget().deleteLater() add_keyboard_shortcuts_to_action_tool_tips(self.ui) self.label_item_name = QLabel() self._button_item_dir = QToolButton() self._properties_title = QWidget() self._setup_properties_title() - self.takeCentralWidget().deleteLater() - self.setWindowIcon(QIcon(":/symbols/app.ico")) - set_taskbar_icon() self.ui.graphicsView.set_ui(self) self.key_press_filter = ChildCyclingKeyPressFilter(self) self.ui.tabWidget_item_properties.installEventFilter(self.key_press_filter) @@ -152,14 +140,12 @@ def __init__(self): # Set style sheets self.setStyleSheet(MAINWINDOW_SS) # Class variables - self.undo_stack = QUndoStack(self) self._item_properties_uis = {} self.item_factories = {} # maps item types to `ProjectItemFactory` objects self._item_specification_factories = {} # maps item types to `ProjectItemSpecificationFactory` objects self._project = None self.specification_model = None self.filtered_spec_factory_models = {} - self.show_datetime = self.update_datetime() self.active_project_item = None self.active_link_item = None self._selected_item_names = set() @@ -167,7 +153,7 @@ def __init__(self): self._anchor_callbacks = {} self.ui.textBrowser_eventlog.set_toolbox(self) # DB manager - self.db_mngr = SpineDBManager(self._qsettings, self) + self.db_mngr = SpineDBManager(self.qsettings, self) # Widget and form references self.settings_form = None self.add_project_item_form = None @@ -217,18 +203,41 @@ def __init__(self): 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() + @property + def toolboxui_lite(self): + return self.toolboxuibase.toolboxui_lite + + @property + def qsettings(self): + return self.toolboxuibase.qsettings + + @property + def project(self): + return self._project + + @property + 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: if ev.type() == QEvent.Type.Hide: - self._qsettings.setValue("mainWindow/consoleSplitterPosition", self.ui.splitter_console.saveState()) + self.qsettings.setValue("mainWindow/consoleSplitterPosition", self.ui.splitter_console.saveState()) elif ev.type() == QEvent.Type.Show: - splitter_state = self._qsettings.value("mainWindow/consoleSplitterPosition", defaultValue="false") + splitter_state = self.qsettings.value("mainWindow/consoleSplitterPosition", defaultValue="false") if splitter_state != "false": self.ui.splitter_console.restoreState(splitter_state) return super().eventFilter(obj, ev) @@ -238,8 +247,8 @@ def _setup_properties_title(self): self.label_item_name.setMinimumHeight(28) self._button_item_dir.setIcon(QIcon(":icons/folder-open-regular.svg")) layout = QHBoxLayout(self._properties_title) - layout.addWidget(self.label_item_name) layout.addWidget(self._button_item_dir) + layout.addWidget(self.label_item_name) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) @@ -281,6 +290,7 @@ def connect_signals(self): self.ui.actionRetrieve_project.triggered.connect(self.retrieve_project) self.ui.menuEdit.aboutToShow.connect(self.refresh_edit_action_states) self.ui.menuEdit.aboutToHide.connect(self.enable_edit_actions) + self.ui.actionSwitch_to_user_mode.triggered.connect(self.switch_to_user_mode) # noinspection PyArgumentList self.ui.actionAbout_Qt.triggered.connect(lambda: QApplication.aboutQt()) # pylint: disable=unnecessary-lambda self.ui.actionRestore_Dock_Widgets.triggered.connect(self.restore_dock_widgets) @@ -297,11 +307,10 @@ def connect_signals(self): self.ui.actionOpen_item_directory.triggered.connect(self._open_project_item_directory) self.ui.actionRename_item.triggered.connect(self._rename_project_item) self.ui.actionRemove.triggered.connect(self._remove_selected_items) + self.ui.actionGroup_items.triggered.connect(self.ui.graphicsView.group_items) # Debug actions self.show_properties_tabbar.triggered.connect(self.toggle_properties_tabbar_visibility) self.show_supported_img_formats.triggered.connect(supported_img_formats) - # Undo stack - self.undo_stack.cleanChanged.connect(self.update_window_modified) # Views self.ui.listView_console_executions.selectionModel().currentChanged.connect(self._select_console_execution) self.ui.listView_console_executions.model().layoutChanged.connect(self._refresh_console_execution_list) @@ -318,45 +327,18 @@ def connect_signals(self): self._setup_persistent_console, Qt.ConnectionType.BlockingQueuedConnection ) - @staticmethod - def set_app_style(): - """Sets app style on Windows to 'windowsvista' or to a default if not available.""" - if sys.platform == "win32": - if "windowsvista" not in QStyleFactory.keys(): - return - QApplication.setStyle("windowsvista") - - @staticmethod - def set_error_mode(): - """Sets Windows error mode to show all error dialog boxes from subprocesses. - - See https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode - for documentation. - """ - if sys.platform == "win32": - import ctypes # pylint: disable=import-outside-toplevel - - ctypes.windll.kernel32.SetErrorMode(0) - - def _update_qsettings(self): - """Updates obsolete settings.""" - old_new = { - "appSettings/useEmbeddedJulia": "appSettings/useJuliaKernel", - "appSettings/useEmbeddedPython": "appSettings/usePythonKernel", - } - for old, new in old_new.items(): - if not self._qsettings.contains(new) and self._qsettings.contains(old): - self._qsettings.setValue(new, self._qsettings.value(old)) - self._qsettings.remove(old) - if self._qsettings.contains("appSettings/saveAtExit"): - try: - old_value = int(self._qsettings.value("appSettings/saveAtExit")) - except ValueError: - # Old value is already of correct form - pass - else: - new_value = {0: "prompt", 1: "prompt", 2: "automatic"}[old_value] - self._qsettings.setValue("appSettings/saveAtExit", new_value) + 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): enabled_by_project = self._project.settings.enable_execute_all if self._project is not None else False @@ -371,12 +353,6 @@ def _update_execute_selected_enabled(self): has_selection = bool(self._selected_item_names) self.ui.actionExecute_selection.setEnabled(has_selection and not self.execution_in_progress) - @Slot(bool) - def update_window_modified(self, clean): - """Updates window modified status and save actions depending on the state of the undo stack.""" - self.setWindowModified(not clean) - self.ui.actionSave.setDisabled(clean) - def parse_project_item_modules(self): """Collects data from project item factories.""" self.item_factories = load_project_items("spine_items") @@ -391,30 +367,18 @@ def set_work_directory(self, new_work_dir=None): """ verbose = new_work_dir is not None if not new_work_dir: - new_work_dir = self._qsettings.value("appSettings/workDir", defaultValue=DEFAULT_WORK_DIR) + new_work_dir = self.qsettings.value("appSettings/workDir", defaultValue=DEFAULT_WORK_DIR) if not new_work_dir: # It is possible "appSettings/workDir" is an empty string??? new_work_dir = DEFAULT_WORK_DIR try: create_dir(new_work_dir) - self._qsettings.setValue("appSettings/workDir", new_work_dir) + self.qsettings.setValue("appSettings/workDir", new_work_dir) if verbose: self.msg.emit(f"Work directory is now {new_work_dir}") except OSError: self.msg_error.emit(f"[OSError] Creating work directory {new_work_dir} failed. Check permissions.") - def project(self): - """Returns current project or None if no project open. - - Returns: - SpineToolboxProject: current project or None - """ - return self._project - - def qsettings(self): - """Returns application preferences object.""" - return self._qsettings - def item_specification_factories(self): """Returns project item specification factories. @@ -423,13 +387,6 @@ def item_specification_factories(self): """ return self._item_specification_factories - def update_window_title(self): - """Updates main window title.""" - if not self._project: - self.setWindowTitle("Spine Toolbox") - return - self.setWindowTitle(f"{self._project.name} [{self._project.project_dir}][*] - Spine Toolbox") - @Slot(str) def init_tasks(self, project_dir_from_args): """Performs tasks right after the main window is shown. @@ -471,13 +428,12 @@ def init_project(self, project_dir): project_dir (str): project directory """ if not project_dir: - open_previous_project = int(self._qsettings.value("appSettings/openPreviousProject", defaultValue="0")) + open_previous_project = int(self.qsettings.value("appSettings/openPreviousProject", defaultValue="0")) # 2: Qt.CheckState.Checked, ie. open_previous_project==True - if ( - open_previous_project != Qt.CheckState.Checked.value - ): + if open_previous_project != Qt.CheckState.Checked.value: return - project_dir = self._qsettings.value("appSettings/previousProject", defaultValue="") + # Get previous project (directory) + project_dir = self.qsettings.value("appSettings/previousProject", defaultValue="") if not project_dir: return if os.path.isfile(project_dir) and project_dir.endswith(".proj"): @@ -496,7 +452,7 @@ def new_project(self): Pops up a question box if selected directory is not empty or if it already contains a Spine Toolbox project. Initial project name is the directory name. """ - recents = self.qsettings().value("appSettings/recentProjectStorages", defaultValue=None) + recents = self.qsettings.value("appSettings/recentProjectStorages", defaultValue=None) home_dir = os.path.abspath(os.path.join(str(pathlib.Path.home()))) if not recents: initial_path = home_dir @@ -504,7 +460,7 @@ def new_project(self): recents_lst = str(recents).split("\n") if not os.path.isdir(recents_lst[0]): # Remove obsolete entry from recentProjectStorages - OpenProjectDialog.remove_directory_from_recents(recents_lst[0], self.qsettings()) + OpenProjectDialog.remove_directory_from_recents(recents_lst[0], self.qsettings) initial_path = home_dir else: initial_path = recents_lst[0] @@ -535,20 +491,20 @@ def create_project(self, proj_dir): self, proj_dir, self._plugin_manager.plugin_specs, - app_settings=self._qsettings, + app_settings=self.qsettings, settings=ProjectSettings(), logger=self, ) self.specification_model.connect_to_project(self._project) self._enable_project_actions() self.ui.actionSave.setDisabled(True) # Disable in a clean project - self._connect_project_signals() - self.update_window_title() - self.ui.graphicsView.reset_zoom() + self.toolboxuibase.connect_project_signals() + self.toolboxuibase.update_window_title() + self.toolboxuibase.active_ui_window.ui.graphicsView.reset_zoom() # Update recentProjects self.update_recent_projects() # Update recentProjectStorages - OpenProjectDialog.update_recents(os.path.abspath(os.path.join(proj_dir, os.path.pardir)), self.qsettings()) + OpenProjectDialog.update_recents(os.path.abspath(os.path.join(proj_dir, os.path.pardir)), self.qsettings) self.save_project() self._plugin_manager.reload_plugins_with_local_data() self.msg.emit(f"New project {self._project.name} is now open") @@ -567,14 +523,14 @@ def open_project(self, load_dir=None, clear_event_log=True): bool: True when opening the project succeeded, False otherwise """ if not load_dir: - custom_open_dialog = self.qsettings().value("appSettings/customOpenProjectDialog", defaultValue="true") + custom_open_dialog = self.qsettings.value("appSettings/customOpenProjectDialog", defaultValue="true") if custom_open_dialog == "true": dialog = OpenProjectDialog(self) if not dialog.exec(): return False load_dir = dialog.selection() else: - recents = self.qsettings().value("appSettings/recentProjectStorages", defaultValue=None) + recents = self.qsettings.value("appSettings/recentProjectStorages", defaultValue=None) if not recents: start_dir = os.path.abspath(os.path.join(str(pathlib.Path.home()))) else: @@ -610,14 +566,13 @@ def restore_project(self, project_dir, ask_confirmation=True, clear_event_log=Tr self, project_dir, self._plugin_manager.plugin_specs, - app_settings=self._qsettings, + app_settings=self.qsettings, settings=ProjectSettings(), logger=self, ) self.specification_model.connect_to_project(self._project) - self.ui.actionSave.setDisabled(True) # Save is disabled in a clean project - self._connect_project_signals() - self.update_window_title() + self.toolboxuibase.connect_project_signals() + self.toolboxuibase.update_window_title() # Populate project model with project items if clear_event_log: self.ui.textBrowser_eventlog.clear() @@ -626,9 +581,10 @@ def restore_project(self, project_dir, ask_confirmation=True, clear_event_log=Tr self.remove_path_from_recent_projects(self._project.project_dir) return False self._enable_project_actions() + 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.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 @@ -676,11 +632,11 @@ def refresh_toolbars(self): for k, toolbar in enumerate(all_toolbars): color = color_from_index(k, len(all_toolbars), base_hue=217.0, saturation=0.6) toolbar.set_color(color) - if self.toolBarArea(toolbar) == Qt.NoToolBarArea: - self.addToolBar(Qt.TopToolBarArea, toolbar) + if self.toolBarArea(toolbar) == Qt.ToolBarArea.NoToolBarArea: + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar) self.execute_toolbar.set_color(QColor("silver")) - if self.toolBarArea(self.execute_toolbar) == Qt.NoToolBarArea: - self.addToolBar(Qt.TopToolBarArea, self.execute_toolbar) + if self.toolBarArea(self.execute_toolbar) == Qt.ToolBarArea.NoToolBarArea: + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.execute_toolbar) @Slot() def show_recent_projects_menu(self): @@ -696,7 +652,7 @@ def fetch_kernels(self): return QApplication.setOverrideCursor(Qt.CursorShape.BusyCursor) self.kernels_menu.clear() - conda_path = self.qsettings().value("appSettings/condaPath", defaultValue="") + conda_path = self.qsettings.value("appSettings/condaPath", defaultValue="") self.kernel_fetcher = KernelFetcher(conda_path) self.kernel_fetcher.kernel_found.connect(self.kernels_menu.add_kernel) self.kernel_fetcher.finished.connect(self.restore_override_cursor) @@ -771,19 +727,19 @@ def close_project(self, ask_confirmation=True, clear_event_log=True): if not self._project: return True if ask_confirmation and not self.undo_stack.isClean(): - save_at_exit = self._qsettings.value("appSettings/saveAtExit", defaultValue="prompt") - if save_at_exit == "prompt" and not self._confirm_project_close(): + save_at_exit = self.qsettings.value("appSettings/saveAtExit", defaultValue="prompt") + if save_at_exit == "prompt" and not self.toolboxuibase._confirm_project_close(): return False if save_at_exit == "automatic" and not self.save_project(): return False if not self.undo_critical_commands(): return False - self.clear_ui() + self.toolboxuibase.clear_ui() self._project.tear_down() self._project = None self._disable_project_actions() self.undo_stack.clear() - self.update_window_title() + self.toolboxuibase.update_window_title() if clear_event_log: self.ui.textBrowser_eventlog.clear() return True @@ -828,41 +784,6 @@ def supports_specifications(self, item_type): """ return item_type in self._item_specification_factories - def restore_ui(self): - """Restore UI state from previous session.""" - window_size = self._qsettings.value("mainWindow/windowSize", defaultValue="false") - window_pos = self._qsettings.value("mainWindow/windowPosition", defaultValue="false") - window_state = self._qsettings.value("mainWindow/windowState", defaultValue="false") - window_maximized = self._qsettings.value("mainWindow/windowMaximized", defaultValue="false") # returns str - n_screens = self._qsettings.value("mainWindow/n_screens", defaultValue=1) # number of screens on last exit - # noinspection PyArgumentList - n_screens_now = len(QGuiApplication.screens()) # Number of screens now - original_size = self.size() - # Note: cannot use booleans since Windows saves them as strings to registry - if window_size != "false": - self.resize(window_size) # Expects QSize - else: - self.resize(1024, 800) - if window_pos != "false": - self.move(window_pos) # Expects QPoint - if window_state != "false": - self.restoreState(window_state, version=1) # Toolbar and dockWidget positions. Expects QByteArray - if n_screens_now < int(n_screens): - # There are less screens available now than on previous application startup - # Move main window to position 0,0 to make sure that it is not lost on another screen that does not exist - self.move(0, 0) - ensure_window_is_on_screen(self, original_size) - if window_maximized == "true": - self.setWindowState(Qt.WindowMaximized) - - def clear_ui(self): - """Clean UI to make room for a new or opened project.""" - self.activate_no_selection_tab() # Clear properties widget - self._restore_original_console() - self.ui.graphicsView.scene().clear_icons_and_links() # Clear all items from scene - self._shutdown_engine_kernels() - self._close_consoles() - def undo_critical_commands(self): """Undoes critical commands in the undo stack. @@ -1009,7 +930,8 @@ def activate_item_tab(self): if self.ui.tabWidget_item_properties.tabText(i) == self.active_project_item.item_type(): self.ui.tabWidget_item_properties.setCurrentIndex(i) break - self.ui.tabWidget_item_properties.currentWidget().layout().insertWidget(0, self._properties_title) + # Insert properties title widget inside scrollArea + self.ui.tabWidget_item_properties.currentWidget().ui.scrollArea.widget().layout().insertWidget(0, self._properties_title) # Set QDockWidget title to selected item's type self.ui.dockWidget_item.setWindowTitle(self.active_project_item.item_type() + " Properties") color = self._item_properties_uis[self.active_project_item.item_type()].fg_color @@ -1025,7 +947,8 @@ def activate_link_tab(self): if self.ui.tabWidget_item_properties.tabText(i) == tab_text: self.ui.tabWidget_item_properties.setCurrentIndex(i) break - self.ui.tabWidget_item_properties.currentWidget().layout().insertWidget(0, self._properties_title) + # Insert properties title widget inside scrollArea + self.ui.tabWidget_item_properties.currentWidget().ui.scrollArea.widget().layout().insertWidget(0, self._properties_title) self.ui.dockWidget_item.setWindowTitle(tab_text) color = self.link_properties_widgets[type(self.active_link_item)].fg_color ss = f"QWidget{{background: {color.name()};}}" @@ -1067,7 +990,7 @@ def import_specification(self): # Load specification local_data = load_specification_local_data(self._project.config_dir) specification = load_specification_from_file( - def_file, local_data, self._item_specification_factories, self._qsettings, self + def_file, local_data, self._item_specification_factories, self.qsettings, self ) if not specification: self.msg_error.emit("Failed to load specification.") @@ -1132,7 +1055,7 @@ def remove_all_items(self): if self._project is None or not self._project.has_items(): self.msg.emit("No project items to remove.") return - delete_data = int(self._qsettings.value("appSettings/deleteData", defaultValue="0")) != 0 + delete_data = int(self.qsettings.value("appSettings/deleteData", defaultValue="0")) != 0 msg = "Remove all items from project? " if not delete_data: msg += "Item data directory will still be available in the project directory after this operation." @@ -1231,7 +1154,7 @@ def show_specification_context_menu(self, ind, global_pos): ind (QModelIndex): In the ProjectItemSpecificationModel global_pos (QPoint): Mouse position """ - if not self.project(): + if not self.project: return spec = self.specification_model.specification(ind.row()) if not self.supports_specification(spec.item_type): @@ -1385,8 +1308,8 @@ def add_menu_actions(self): self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_console.toggleViewAction()) undo_action = self.undo_stack.createUndoAction(self) redo_action = self.undo_stack.createRedoAction(self) - undo_action.setShortcuts(QKeySequence.Undo) - redo_action.setShortcuts(QKeySequence.Redo) + undo_action.setShortcuts(QKeySequence.StandardKey.Undo) + redo_action.setShortcuts(QKeySequence.StandardKey.Redo) undo_action.setIcon(QIcon(":/icons/menu_icons/undo.svg")) redo_action.setIcon(QIcon(":/icons/menu_icons/redo.svg")) before = self.ui.menuEdit.actions()[0] @@ -1401,12 +1324,6 @@ def toggle_properties_tabbar_visibility(self): else: self.ui.tabWidget_item_properties.tabBar().show() - def update_datetime(self): - """Returns a boolean, which determines whether - date and time is prepended to every Event Log message.""" - d = int(self._qsettings.value("appSettings/dateTime", defaultValue="2")) - return d != 0 - @Slot(str) def add_message(self, msg): """Appends a regular message to the Event Log. @@ -1414,7 +1331,7 @@ def add_message(self, msg): Args: msg (str): String written to QTextBrowser """ - message = format_log_message("msg", msg, self.show_datetime) + message = format_log_message("msg", msg, self.toolboxuibase.show_datetime) self.ui.textBrowser_eventlog.append(message) @Slot(str) @@ -1424,7 +1341,7 @@ def add_success_message(self, msg): Args: msg (str): String written to QTextBrowser """ - message = format_log_message("msg_success", msg, self.show_datetime) + message = format_log_message("msg_success", msg, self.toolboxuibase.show_datetime) self.ui.textBrowser_eventlog.append(message) @Slot(str) @@ -1434,7 +1351,7 @@ def add_error_message(self, msg): Args: msg (str): String written to QTextBrowser """ - message = format_log_message("msg_error", msg, self.show_datetime) + message = format_log_message("msg_error", msg, self.toolboxuibase.show_datetime) self.ui.textBrowser_eventlog.append(message) @Slot(str) @@ -1444,7 +1361,7 @@ def add_warning_message(self, msg): Args: msg (str): String written to QTextBrowser """ - message = format_log_message("msg_warning", msg, self.show_datetime) + message = format_log_message("msg_warning", msg, self.toolboxuibase.show_datetime) self.ui.textBrowser_eventlog.append(message) @Slot(str) @@ -1643,7 +1560,9 @@ def retrieve_project(self): """Retrieves project from server.""" msg = "Retrieve project by Job Id" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList - answer = QInputDialog.getText(self, msg, "Job Id?:", flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) + answer = QInputDialog.getText( + self, msg, "Job Id?:", flags=Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint + ) job_id = answer[0] if not job_id: # Cancel button clicked return @@ -1702,14 +1621,14 @@ def retrieve_project(self): def engine_server_settings(self): """Returns the user given Spine Engine Server settings in a tuple.""" - host = self._qsettings.value("engineSettings/remoteHost", defaultValue="") # Host name - port = self._qsettings.value("engineSettings/remotePort", defaultValue="49152") # Host port - sec_model = self._qsettings.value("engineSettings/remoteSecurityModel", defaultValue="") # ZQM security model + host = self.qsettings.value("engineSettings/remoteHost", defaultValue="") # Host name + port = self.qsettings.value("engineSettings/remotePort", defaultValue="49152") # Host port + sec_model = self.qsettings.value("engineSettings/remoteSecurityModel", defaultValue="") # ZQM security model security = ClientSecurityModel.NONE if not sec_model else ClientSecurityModel.STONEHOUSE sec_folder = ( "" if security == ClientSecurityModel.NONE - else self._qsettings.value("engineSettings/remoteSecurityFolder", defaultValue="") + else self.qsettings.value("engineSettings/remoteSecurityFolder", defaultValue="") ) return host, port, sec_model, sec_folder @@ -1718,19 +1637,44 @@ 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 """ + if item is not None: + print(f"item my_groups:{item.get_icon().my_groups}") + 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.actionGroup_items) 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() @@ -1741,6 +1685,7 @@ def show_link_context_menu(self, pos, link): pos (QPoint): Mouse position link (Link(QGraphicsPathItem)): The link in question """ + print(f"link my_groups:{link.my_groups}") menu = QMenu(self) menu.addAction(self.ui.actionRemove) self.ui.actionRemove.setEnabled(True) @@ -1758,7 +1703,7 @@ def refresh_edit_action_states(self): remove and rename actions in File-Edit menu, project tree view context menu, and in Design View context menus just before the menus are shown to user.""" - if not self.project(): + if not self.project: self.disable_edit_actions() return clipboard = QApplication.clipboard() @@ -1766,12 +1711,13 @@ def refresh_edit_action_states(self): can_paste = not byte_data.isNull() selected_items = self.ui.graphicsView.scene().selectedItems() can_copy = any(isinstance(x, ProjectItemIcon) for x in selected_items) - has_items = self.project().n_items > 0 + 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) + can_make_group = True if len(selected_project_items) > 0 else False # 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( @@ -1787,6 +1733,7 @@ def refresh_edit_action_states(self): self.ui.actionDuplicateAndDuplicateFiles.setEnabled(can_duplicate_files) self.ui.actionRemove.setEnabled(bool(selected_items)) self.ui.actionRemove_all.setEnabled(has_items) + self.ui.actionGroup_items.setEnabled(can_make_group) def disable_edit_actions(self): """Disables edit actions.""" @@ -1810,104 +1757,13 @@ def enable_edit_actions(self): self.ui.actionDuplicateAndDuplicateFiles.setEnabled(True) self.ui.actionRemove.setEnabled(True) - def _tasks_before_exit(self): - """Returns a list of tasks to perform before exiting the application. - - Possible tasks are: - - - `"prompt exit"`: prompt user if quitting is really desired - - `"prompt save"`: prompt user if project should be saved before quitting - - `"save"`: save project before quitting - - Returns: - list: Zero or more tasks in a list - """ - save_at_exit = ( - self._qsettings.value("appSettings/saveAtExit", defaultValue="prompt") - if self._project is not None and not self.undo_stack.isClean() - else None - ) - if save_at_exit == "prompt": - return ["prompt save"] - show_confirm_exit = int(self._qsettings.value("appSettings/showExitPrompt", defaultValue="2")) - tasks = [] - if show_confirm_exit == 2: - tasks.append("prompt exit") - if save_at_exit == "automatic": - tasks.append("save") - return tasks - - def _perform_pre_exit_tasks(self): - """Prompts user to confirm quitting and saves the project if necessary. - - Returns: - bool: True if exit should proceed, False if the process was cancelled - """ - tasks = self._tasks_before_exit() - for task in tasks: - if task == "prompt exit": - if not self._confirm_exit(): - return False - elif task == "prompt save": - if not self._confirm_project_close(): - return False - elif task == "save": - self.save_project() - return True - - def _confirm_exit(self): - """Confirms exiting from user. - - Returns: - bool: True if exit should proceed, False if user cancelled - """ - msg = QMessageBox(parent=self) - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm exit") - msg.setText("Are you sure you want to exit Spine Toolbox?") - msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) - msg.button(QMessageBox.StandardButton.Ok).setText("Exit") - chkbox = QCheckBox() - chkbox.setText("Do not ask me again") - msg.setCheckBox(chkbox) - answer = msg.exec() # Show message box - if answer == QMessageBox.StandardButton.Ok: - # Update conf file according to checkbox status - if not chkbox.isChecked(): - show_prompt = "2" # 2 as in True - else: - show_prompt = "0" # 0 as in False - self._qsettings.setValue("appSettings/showExitPrompt", show_prompt) - return True - return False - - def _confirm_project_close(self): - """Confirms exit from user and saves the project if requested. - - Returns: - bool: True if exiting should proceed, False if user cancelled - """ - msg = QMessageBox(parent=self) - msg.setIcon(QMessageBox.Icon.Question) - msg.setWindowTitle("Confirm project close") - msg.setText("Current project has unsaved changes.") - msg.setStandardButtons( - QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel - ) - answer = msg.exec() - if answer == QMessageBox.StandardButton.Cancel: - return False - if answer == QMessageBox.StandardButton.Save: - self.save_project() - return True - def remove_path_from_recent_projects(self, p): """Removes entry that contains given path from the recent project files list in QSettings. Args: p (str): Full path to a project directory """ - recents = self._qsettings.value("appSettings/recentProjects", defaultValue=None) + recents = self.qsettings.value("appSettings/recentProjects", defaultValue=None) if not recents: return recents = str(recents) @@ -1919,8 +1775,8 @@ def remove_path_from_recent_projects(self, p): break updated_recents = "\n".join(recents_list) # Save updated recent paths - self._qsettings.setValue("appSettings/recentProjects", updated_recents) - self._qsettings.sync() # Commit change immediately + self.qsettings.setValue("appSettings/recentProjects", updated_recents) + self.qsettings.sync() # Commit change immediately def clear_recent_projects(self): """Clears recent projects list in File->Open recent menu.""" @@ -1936,14 +1792,14 @@ def clear_recent_projects(self): answer = message_box.exec() if answer == QMessageBox.StandardButton.No: return - self._qsettings.remove("appSettings/recentProjects") - self._qsettings.remove("appSettings/recentProjectStorages") - self._qsettings.sync() + self.qsettings.remove("appSettings/recentProjects") + self.qsettings.remove("appSettings/recentProjectStorages") + self.qsettings.sync() def update_recent_projects(self): """Adds a new entry to QSettings variable that remembers twenty most recent project paths.""" - recents = self._qsettings.value("appSettings/recentProjects", defaultValue=None) - entry = self.project().name + "<>" + self.project().project_dir + recents = self.qsettings.value("appSettings/recentProjects", defaultValue=None) + entry = self.project.name + "<>" + self.project.project_dir if not recents: updated_recents = entry else: @@ -1962,45 +1818,8 @@ def update_recent_projects(self): recents_list.insert(0, recents_list.pop(index)) updated_recents = "\n".join(recents_list) # Save updated recent paths - self._qsettings.setValue("appSettings/recentProjects", updated_recents) - self._qsettings.sync() # Commit change immediately - - def closeEvent(self, event): - """Method for handling application exit. - - Args: - event (QCloseEvent): PySide6 event - """ - # Show confirm exit message box - exit_confirmed = self._perform_pre_exit_tasks() - if not exit_confirmed: - event.ignore() - return - if not self.undo_critical_commands(): - event.ignore() - return - # Save settings - if self._project is None: - self._qsettings.setValue("appSettings/previousProject", "") - else: - self._qsettings.setValue("appSettings/previousProject", self._project.project_dir) - self.update_recent_projects() - self._qsettings.setValue("appSettings/toolbarIconOrdering", self.items_toolbar.icon_ordering()) - self._qsettings.setValue("mainWindow/windowSize", self.size()) - self._qsettings.setValue("mainWindow/windowPosition", self.pos()) - self._qsettings.setValue("mainWindow/windowState", self.saveState(version=1)) - self._qsettings.setValue("mainWindow/windowMaximized", self.windowState() == Qt.WindowMaximized) - # Save number of screens - # noinspection PyArgumentList - self._qsettings.setValue("mainWindow/n_screens", len(QGuiApplication.screens())) - self._shutdown_engine_kernels() - self._close_consoles() - if self._project is not None: - self._project.tear_down() - for item_type in self.item_factories: - for editor in self.get_all_multi_tab_spec_editors(item_type): - editor.close() - event.accept() + self.qsettings.setValue("appSettings/recentProjects", updated_recents) + self.qsettings.sync() # Commit change immediately def _serialize_selected_items(self): """Serializes selected project items into a dictionary. @@ -2015,8 +1834,8 @@ def _serialize_selected_items(self): for item_icon in selected_project_items: if not isinstance(item_icon, ProjectItemIcon): continue - name = item_icon.name() - project_item = self.project().get_item(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 item_dict["original_db_url"] = item_dict.get("url") @@ -2075,8 +1894,8 @@ def _deserialize_items(self, items_dict, duplicate_files=False): final_items_dict = {} for name, item_dict in items_dict.items(): item_dict["duplicate_files"] = duplicate_files - if name in self.project().all_item_names: - new_name = unique_name(name, self.project().all_item_names) + if name in self.project.all_item_names: + new_name = unique_name(name, self.project.all_item_names) final_items_dict[new_name] = item_dict else: final_items_dict[name] = item_dict @@ -2130,7 +1949,7 @@ def _add_item_edit_actions(self): self.ui.actionRemove, ] for action in actions: - action.setShortcutContext(Qt.WidgetShortcut) + action.setShortcutContext(Qt.ShortcutContext.WidgetShortcut) self.ui.graphicsView.addAction(action) @Slot(str, str) @@ -2143,24 +1962,47 @@ def _show_error_box(self, title, message): """Shows an error message with the given title and message.""" box = QErrorMessage(self) box.setWindowTitle(title) - box.setWindowModality(Qt.ApplicationModal) + box.setWindowModality(Qt.WindowModality.ApplicationModal) box.showMessage(message) - def _connect_project_signals(self): + def connect_project_signals(self): """Connects signals emitted by project.""" - self._project.project_execution_about_to_start.connect(self._set_execution_in_progress) - self._project.project_execution_finished.connect(self._unset_execution_in_progress) - self._project.item_added.connect(self.set_icon_and_properties_ui) - self._project.item_added.connect(self.ui.graphicsView.add_icon) - self._project.item_about_to_be_removed.connect(self.ui.graphicsView.remove_icon) - self._project.connection_established.connect(self.ui.graphicsView.do_add_link) - self._project.connection_updated.connect(self.ui.graphicsView.do_update_link) - 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.jump_updated.connect(self.ui.graphicsView.do_update_jump) - self._project.specification_added.connect(self.repair_specification) - self._project.specification_saved.connect(self._log_specification_saved) + if not self.project: + return + self.project.project_execution_about_to_start.connect(self._set_execution_in_progress) + self.project.project_execution_finished.connect(self._unset_execution_in_progress) + self.project.item_added.connect(self.set_icon_and_properties_ui) + self.project.item_added.connect(self.ui.graphicsView.add_icon) + self.project.item_about_to_be_removed.connect(self.ui.graphicsView.remove_icon) + self.project.connection_established.connect(self.ui.graphicsView.do_add_link) + self.project.connection_updated.connect(self.ui.graphicsView.do_update_link) + 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.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) + + 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.item_about_to_be_removed.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.jump_updated.disconnect() + self.project.group_added.disconnect() + self.project.group_disbanded.disconnect() + self.project.specification_added.disconnect() + self.project.specification_saved.disconnect() @Slot(bool) def _execute_project(self, _=False): @@ -2253,12 +2095,12 @@ 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: return - delete_data = int(self._qsettings.value("appSettings/deleteData", defaultValue="0")) != 0 + delete_data = int(self.qsettings.value("appSettings/deleteData", defaultValue="0")) != 0 if project_item_names: msg = f"Remove item(s) {', '.join(project_item_names)} from project? " if not delete_data: @@ -2289,42 +2131,23 @@ def _remove_selected_items(self, _): def _rename_project_item(self, _): """Renames active project item.""" item = self.active_project_item - answer = QInputDialog.getText( - self, "Rename Item", "New name:", text=item.name, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint - ) - if not answer[1]: + new_name = self.show_simple_input_dialog("Rename Item", "New name:", item.name) + if not new_name: 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 + def show_simple_input_dialog(self, title, label_txt, prefilled_text): + """Shows a QInputDialog and returns typed text.""" + answer = QInputDialog.getText( + self, + title, + label_txt, + text=prefilled_text, + flags=Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint + ) + if not answer[1]: + return None + return answer[0] @Slot(str, QIcon, bool) def start_detached_jupyter_console(self, kernel_name, icon, conda): @@ -2469,13 +2292,13 @@ def _cleanup_jupyter_console(self, conn_file): c = self._jupyter_consoles.pop(conn_file, None) if not c: return - exec_remotely = self.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" + exec_remotely = self.qsettings.value("engineSettings/remoteExecutionEnabled", "false") == "true" engine_mngr = make_engine_manager(exec_remotely) engine_mngr.shutdown_kernel(conn_file) def _shutdown_engine_kernels(self): """Shuts down all persistent and Jupyter kernels managed by Spine Engine.""" - exec_remotely = self.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" + exec_remotely = self.qsettings.value("engineSettings/remoteExecutionEnabled", "false") == "true" engine_mngr = make_engine_manager(exec_remotely) for key in self._persistent_consoles: engine_mngr.kill_persistent(key) @@ -2523,3 +2346,14 @@ def add_log_message(self, item_name, filter_id, message): message (str): formatted message """ self.ui.textBrowser_eventlog.add_log_message(item_name, filter_id, message) + + def closeEvent(self, event): + """Method for handling application exit. + + Args: + event (QCloseEvent): PySide6 event + """ + if not self.toolboxuibase.close(): + event.ignore() + return + event.accept() diff --git a/spinetoolbox/ui_main_base.py b/spinetoolbox/ui_main_base.py new file mode 100644 index 000000000..1a38a6206 --- /dev/null +++ b/spinetoolbox/ui_main_base.py @@ -0,0 +1,333 @@ +###################################################################################################################### +# 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 . +###################################################################################################################### + +"""Contains a class for the base main window of Spine Toolbox.""" +import sys +import locale +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 +from .ui_main import ToolboxUI +from .ui_main_lite import ToolboxUILite +from .link import JumpOrLink + + +class ToolboxUIBase(QMainWindow): + """Class for the actual app main window.""" + + def __init__(self): + """Initializes top main window.""" + from .ui.mainwindowbase import Ui_MainWindowBase # pylint: disable=import-outside-toplevel + + super().__init__(flags=Qt.WindowType.Window) + locale.setlocale(locale.LC_NUMERIC, 'C') + self.ui = Ui_MainWindowBase() + self.ui.setupUi(self) + self.show_nr_of_items = QAction(self) + self.show_nr_of_items.setShortcut(QKeySequence(Qt.Modifier.CTRL.value | Qt.Key.Key_7.value)) + self.addAction(self.show_nr_of_items) + self.setWindowIcon(QIcon(":/symbols/app.ico")) + set_taskbar_icon() + self._set_app_style() + self._set_error_mode() + self._qsettings = QSettings("SpineProject", "Spine Toolbox", self) + self._undo_stack = QUndoStack(self) + self._toolboxui = ToolboxUI(self) + self._toolboxui_lite = ToolboxUILite(self) + self.ui.stackedWidget.addWidget(self.toolboxui) + self.ui.stackedWidget.addWidget(self.toolboxui_lite) + self.ui.stackedWidget.setCurrentWidget(self.toolboxui) + self.show_datetime = self.update_datetime() + self.restore_ui() + self.connect_signals() + self._active_ui_mode = "toolboxui" + + @property + def toolboxui(self): + return self._toolboxui + + @property + def toolboxui_lite(self): + return self._toolboxui_lite + + @property + def qsettings(self): + return self._qsettings + + @property + def project(self): + return self.toolboxui.project + + @property + def undo_stack(self): + return self._undo_stack + + @property + 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) + self.show_nr_of_items.triggered.connect(self.nr_of_items) + + @staticmethod + def _set_app_style(): + """Sets app style on Windows to 'windowsvista' or to a default if not available.""" + if sys.platform == "win32": + if "windowsvista" not in QStyleFactory.keys(): + return + QApplication.setStyle("windowsvista") + + @staticmethod + def _set_error_mode(): + """Sets Windows error mode to show all error dialog boxes from subprocesses. + + See https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode + for documentation. + """ + if sys.platform == "win32": + import ctypes # pylint: disable=import-outside-toplevel + + ctypes.windll.kernel32.SetErrorMode(0) + + def update_window_title(self): + """Updates main window title.""" + if not self.project: + self.setWindowTitle("Spine Toolbox") + return + self.setWindowTitle(f"{self.project.name} [{self.project.project_dir}][*] - Spine Toolbox") + + @Slot(bool) + def update_window_modified(self, clean): + """Updates window modified status and save actions depending on the state of the undo stack.""" + self.setWindowModified(not clean) + self.toolboxui.ui.actionSave.setDisabled(clean) + + def restore_ui(self): + """Restore UI state from previous session.""" + window_size = self.qsettings.value("mainWindow/windowSize", defaultValue="false") + window_pos = self.qsettings.value("mainWindow/windowPosition", defaultValue="false") + window_state = self.qsettings.value("mainWindow/windowState", defaultValue="false") + window_state_lite = self.qsettings.value("mainWindowLite/windowState", defaultValue="false") + hz_splitter = self.qsettings.value("mainWindowLite/horizontalSplitter", defaultValue="false") + window_maximized = self.qsettings.value("mainWindow/windowMaximized", defaultValue="false") # returns str + n_screens = self.qsettings.value("mainWindow/n_screens", defaultValue=1) # number of screens on last exit + # noinspection PyArgumentList + n_screens_now = len(QGuiApplication.screens()) # Number of screens now + original_size = self.size() + # Note: cannot use booleans since Windows saves them as strings to registry + if window_size != "false": + self.resize(window_size) # Expects QSize + else: + self.resize(1024, 800) + if window_pos != "false": + self.move(window_pos) # Expects QPoint + if window_state != "false": + self.toolboxui.restoreState(window_state, version=1) # Toolbar and dockWidget positions [QByteArray] + if window_state_lite != "false": + self.toolboxui_lite.restoreState(window_state_lite, version=1) + if hz_splitter != "false": + self.toolboxui_lite.ui.splitter.restoreState(hz_splitter) # Splitter position + if n_screens_now < int(n_screens): + # There are less screens available now than on previous application startup + # Move main window to position 0,0 to make sure that it is not lost on another screen that does not exist + self.move(0, 0) + ensure_window_is_on_screen(self, original_size) + if window_maximized == "true": + self.setWindowState(Qt.WindowState.WindowMaximized) + + def reload_icons_and_links(self): + """Reloads item icons and links on Design View when UI mode is changed.""" + if not self.project: + return + for item_name in self.project.all_item_names: + self.active_ui_window.ui.graphicsView.add_icon(item_name) + for connection in self.project.connections: + self.active_ui_window.ui.graphicsView.do_add_link(connection) + connection.link.update_icons() + for jump in self.project.jumps: + 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) + # TODO: + # Remove all links from Groups. items contains wrong link icon references + # Then find the new links from the scene and add them back to the group and to the links my_groups + # ex_items = [for item in] + # for item in group.items: + # if isinstance(item, JumpOrLink): + # print(item.name) + # item.my_groups.add(group) + + def connect_project_signals(self): + """Connects project signals based on current UI mode.""" + 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.active_ui_window.ui.graphicsView.scene().clear_icons_and_links() # Clear all items from scene + self.toolboxui._shutdown_engine_kernels() + self.toolboxui._close_consoles() + + def update_datetime(self): + """Returns a boolean, which determines whether + date and time is prepended to every Event Log message.""" + d = int(self.qsettings.value("appSettings/dateTime", defaultValue="2")) + return d != 0 + + def _tasks_before_exit(self): + """Returns a list of tasks to perform before exiting the application. + + Possible tasks are: + + - `"prompt exit"`: prompt user if quitting is really desired + - `"prompt save"`: prompt user if project should be saved before quitting + - `"save"`: save project before quitting + + Returns: + list: Zero or more tasks in a list + """ + save_at_exit = ( + self.qsettings.value("appSettings/saveAtExit", defaultValue="prompt") + if self.project is not None and not self.undo_stack.isClean() + else None + ) + if save_at_exit == "prompt": + return ["prompt save"] + show_confirm_exit = int(self.qsettings.value("appSettings/showExitPrompt", defaultValue="2")) + tasks = [] + if show_confirm_exit == 2: + tasks.append("prompt exit") + if save_at_exit == "automatic": + tasks.append("save") + return tasks + + def _perform_pre_exit_tasks(self): + """Prompts user to confirm quitting and saves the project if necessary. + + Returns: + bool: True if exit should proceed, False if the process was cancelled + """ + tasks = self._tasks_before_exit() + for task in tasks: + if task == "prompt exit": + if not self._confirm_exit(): + return False + elif task == "prompt save": + if not self._confirm_project_close(): + return False + elif task == "save": + self.toolboxui.save_project() + return True + + def _confirm_exit(self): + """Confirms exiting from user. + + Returns: + bool: True if exit should proceed, False if user cancelled + """ + msg = QMessageBox(parent=self) + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm exit") + msg.setText("Are you sure you want to exit Spine Toolbox?") + msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) + msg.button(QMessageBox.StandardButton.Ok).setText("Exit") + chkbox = QCheckBox() + chkbox.setText("Do not ask me again") + msg.setCheckBox(chkbox) + answer = msg.exec() # Show message box + if answer == QMessageBox.StandardButton.Ok: + # Update conf file according to checkbox status + if not chkbox.isChecked(): + show_prompt = "2" # 2 as in True + else: + show_prompt = "0" # 0 as in False + self.qsettings.setValue("appSettings/showExitPrompt", show_prompt) + return True + return False + + def _confirm_project_close(self): + """Confirms exit from user and saves the project if requested. + + Returns: + bool: True if exiting should proceed, False if user cancelled + """ + msg = QMessageBox(parent=self) + msg.setIcon(QMessageBox.Icon.Question) + msg.setWindowTitle("Confirm project close") + msg.setText("Current project has unsaved changes.") + msg.setStandardButtons( + QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel + ) + answer = msg.exec() + if answer == QMessageBox.StandardButton.Cancel: + return False + if answer == QMessageBox.StandardButton.Save: + self.toolboxui.save_project() + return True + + def closeEvent(self, event): + """Method for handling application exit event. + + Args: + event (QCloseEvent): PySide6 event + """ + # Show confirm exit message box + exit_confirmed = self._perform_pre_exit_tasks() + if not exit_confirmed: + event.ignore() + return + if not self.toolboxui.undo_critical_commands(): + event.ignore() + return + # Save settings + if self.project is None: + self.qsettings.setValue("appSettings/previousProject", "") + else: + self.qsettings.setValue("appSettings/previousProject", self.project.project_dir) + self.toolboxui.update_recent_projects() + self.qsettings.setValue("appSettings/toolbarIconOrdering", self.toolboxui.items_toolbar.icon_ordering()) + self.qsettings.setValue("mainWindow/windowSize", self.size()) + self.qsettings.setValue("mainWindow/windowPosition", self.pos()) + self.qsettings.setValue("mainWindow/windowState", self.toolboxui.saveState(version=1)) + self.qsettings.setValue("mainWindow/windowMaximized", self.windowState() == Qt.WindowState.WindowMaximized) + # ToolboxUI Lite settings + self.qsettings.setValue("mainWindowLite/windowState", self.toolboxui_lite.saveState(version=1)) + self.qsettings.setValue("mainWindowLite/horizontalSplitter", self.toolboxui_lite.ui.splitter.saveState()) + # Save number of screens + self.qsettings.setValue("mainWindow/n_screens", len(QGuiApplication.screens())) + self.toolboxui._shutdown_engine_kernels() + self.toolboxui._close_consoles() + if self.project is not None: + self.project.tear_down() + for item_type in self.toolboxui.item_factories: + for editor in self.toolboxui.get_all_multi_tab_spec_editors(item_type): + editor.close() + event.accept() + + def nr_of_items(self): + """For debugging.""" + 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 new file mode 100644 index 000000000..c3754b4ec --- /dev/null +++ b/spinetoolbox/ui_main_lite.py @@ -0,0 +1,346 @@ +###################################################################################################################### +# 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 . +###################################################################################################################### + +"""Contains a class for the user mode main window of Spine Toolbox.""" +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 .helpers import format_log_message_lite + + +class ToolboxUILite(QMainWindow): + """Class for the user mode main window functions.""" + + def __init__(self, toolboxuibase): + """Initializes application and main window.""" + from .ui.mainwindowlite import Ui_MainWindowLite # pylint: disable=import-outside-toplevel + + super().__init__(parent=toolboxuibase, flags=Qt.WindowType.Window) + self.toolboxuibase = 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.dockWidget_event_log.setVisible(False) + self.ui.dockWidget_console.setVisible(False) + self.connect_signals() + + @property + def toolboxui(self): + return self.toolboxuibase.toolboxui + + @property + def qsettings(self): + return self.toolboxuibase.qsettings + + @property + def project(self): + return self.toolboxui.project + + @property + 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) + self.ui.menuFile.addAction(self.toolboxui.ui.actionOpen_recent) + 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_design_mode) + self.ui.menuFile.addAction(self.toolboxui.ui.actionSettings) + self.ui.menuFile.addSeparator() + self.ui.menuFile.addAction(self.toolboxui.ui.actionQuit) + self.ui.menuHelp.addAction(self.toolboxui.ui.actionUser_Guide) + self.ui.menuHelp.addAction(self.toolboxui.ui.actionGitHub) + self.ui.menuHelp.addSeparator() + self.ui.menuHelp.addAction(self.toolboxui.ui.actionAbout_Qt) + self.ui.menuHelp.addAction(self.toolboxui.ui.actionAbout) + + def make_toolbar(self): + """Makes and returns a Toolbar for user mode UI.""" + tb = QToolBar(self) + 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) + tb.addAction(self.ui.actionShow_console) + tb.addAction(self.ui.actionSwitch_to_design_mode) + return tb + + def connect_signals(self): + """Connects signals to slots.""" + 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.triggered.connect(self.show_event_log) + self.ui.actionShow_console.triggered.connect(self.show_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) + self.project.connection_updated.connect(self.ui.graphicsView.do_update_link) + 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_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()) + self.ui.graphicsView.scene().setSelectionArea(path, QTransform()) + + def execute_group(self): + """Executes a group.""" + if self.groups_combobox.currentIndex() == 0: + return + if self.groups_combobox.currentText() == "All": + self.toolboxui._execute_project() + return + self.toolboxui._execute_selection() + + @Slot() + def _set_progress_bar_finished(self): + self.progress_bar.setValue(self.progress_bar.maximum()) + + @Slot(bool) + def show_event_log(self, _=False): + """Shows or hides the Event Log. If Console is already visible, + splits the bottom dock widget area 50-50.""" + if self.ui.dockWidget_event_log.isVisible(): + self.ui.dockWidget_event_log.setVisible(False) + return + self.ui.dockWidget_event_log.setVisible(True) + if self.ui.dockWidget_console.isVisible(): + # If Console is visible, split bottom dock widget area 50-50 + # Hide console first and show it again, so that Event Log is always on the left side, console on the right + self.ui.dockWidget_console.setVisible(False) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ui.dockWidget_event_log) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ui.dockWidget_console) + self.ui.dockWidget_console.setVisible(True) + docks = (self.ui.dockWidget_event_log, self.ui.dockWidget_console) + width = sum(d.size().width() for d in docks) + self.resizeDocks(docks, [0.5 * width, 0.5 * width], Qt.Orientation.Horizontal) + else: + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ui.dockWidget_event_log) + + @Slot(bool) + def show_console(self, _=False): + """Shows or hides the Console. If Event Log is already visible, + splits the bottom dock widget area 50-50.""" + if self.ui.dockWidget_console.isVisible(): + self.ui.dockWidget_console.setVisible(False) + return + self.ui.dockWidget_console.setVisible(True) + if self.ui.dockWidget_event_log.isVisible(): + # If Event Log is visible, split bottom dock widget area 50-50 + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ui.dockWidget_console) + docks = (self.ui.dockWidget_event_log, self.ui.dockWidget_console) + width = sum(d.size().width() for d in docks) + self.resizeDocks(docks, [0.5 * width, 0.5 * width], Qt.Orientation.Horizontal) + else: + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ui.dockWidget_console) + + def refresh_active_elements(self, active_project_item, active_link_item, selected_item_names): + """Does something when scene selection has changed.""" + 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("Not implemented yet") + if not item: + return + print(f"item my_groups:{item.get_icon().my_groups}") + + 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 + """ + print(f"link my_groups:{link.my_groups}") + 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 + """ + message = format_log_message_lite("msg", msg, self.toolboxuibase.show_datetime) + self.ui.textBrowser.append(message) + + @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 + """ + message = format_log_message_lite("msg_success", msg, self.toolboxuibase.show_datetime) + self.ui.textBrowser.append(message) + + @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 + """ + message = format_log_message_lite("msg_error", msg, self.toolboxuibase.show_datetime) + self.ui.textBrowser.append(message) + + @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 + """ + message = format_log_message_lite("msg_warning", msg, self.toolboxuibase.show_datetime) + self.ui.textBrowser.append(message) + + @Slot(str) + def add_process_message(self, msg): + """Writes message from stdout to the Event Log. + + Args: + msg (str): String written to QTextBrowser + """ + message = format_log_message_lite("msg", msg) + self.ui.textBrowser.append(message) + + @Slot(str) + def add_process_error_message(self, msg): + """Writes message from stderr to the Event Log. + + Args: + msg (str): String written to QTextBrowser + """ + message = format_log_message_lite("msg_error", msg) + self.ui.textBrowser.append(message) diff --git a/spinetoolbox/widgets/add_project_item_widget.py b/spinetoolbox/widgets/add_project_item_widget.py index 77f13f9e6..a671c67d9 100644 --- a/spinetoolbox/widgets/add_project_item_widget.py +++ b/spinetoolbox/widgets/add_project_item_widget.py @@ -57,7 +57,7 @@ def __init__(self, toolbox, x, y, class_, spec=""): else: prefix = class_.item_type() self.ui.comboBox_specification.hide() - existing_item_names = toolbox.project().all_item_names + existing_item_names = toolbox.project.all_item_names self.name = unique_name(prefix, existing_item_names) if prefix in existing_item_names else prefix self.description = "" self.connect_signals() @@ -94,7 +94,7 @@ def handle_ok_clicked(self): if not self.name: self.statusbar.showMessage("Name missing", 3000) return - name_status = self._toolbox.project().validate_project_item_name(self.name) + name_status = self._toolbox.project.validate_project_item_name(self.name) if name_status == ItemNameStatus.INVALID: self.statusbar.showMessage("Name not valid for a folder name", 3000) return @@ -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_combobox.py b/spinetoolbox/widgets/custom_combobox.py index 04c116c99..555c82e5b 100644 --- a/spinetoolbox/widgets/custom_combobox.py +++ b/spinetoolbox/widgets/custom_combobox.py @@ -62,7 +62,7 @@ def keyPressEvent(self, e): # Remove path from qsettings # This is done because pressing enter adds an entry to combobox drop-down list automatically # and we don't want to clog it with irrelevant paths - parent.remove_directory_from_recents(os.path.abspath(parent.selection()), parent._toolbox.qsettings()) + parent.remove_directory_from_recents(os.path.abspath(parent.selection()), parent._toolbox.qsettings) # Remove path from combobox as well cb_index = self.findText(os.path.abspath(parent.selection())) if cb_index == -1: diff --git a/spinetoolbox/widgets/custom_menus.py b/spinetoolbox/widgets/custom_menus.py index ab6836c15..46fec2b48 100644 --- a/spinetoolbox/widgets/custom_menus.py +++ b/spinetoolbox/widgets/custom_menus.py @@ -110,11 +110,11 @@ def __init__(self, parent): def has_recents(self): """Returns True if recent projects available, False otherwise.""" - return bool(self._parent.qsettings().value("appSettings/recentProjects", defaultValue=None)) + return bool(self._parent.qsettings.value("appSettings/recentProjects", defaultValue=None)) def add_recent_projects(self): """Reads the previous project names and paths from QSettings. Adds them to the QMenu as QActions.""" - recents = self._parent.qsettings().value("appSettings/recentProjects", defaultValue=None) + recents = self._parent.qsettings.value("appSettings/recentProjects", defaultValue=None) if recents: recents = str(recents) recents_list = recents.split("\n") @@ -152,8 +152,8 @@ def call_open_project(self, checked, p): ) return # Check if the same project is already open - if self._parent.project(): - if p == self._parent.project().project_dir: + if self._parent.project: + if p == self._parent.project.project_dir: self._parent.msg.emit("Project already open") return if not self._parent.open_project(p): diff --git a/spinetoolbox/widgets/custom_qgraphicsscene.py b/spinetoolbox/widgets/custom_qgraphicsscene.py index 9e4c91063..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 @@ -58,7 +59,7 @@ def __init__(self, parent, toolbox): self.item_shadow = None self._last_selected_items = set() # Set background attributes - settings = toolbox.qsettings() + settings = toolbox.qsettings self.bg_choice = settings.value("appSettings/bgChoice", defaultValue="solid") bg_color = settings.value("appSettings/bgColor", defaultValue="false") self.bg_color = QColor("#f5f5f5") if bg_color == "false" else bg_color @@ -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): @@ -90,7 +92,7 @@ def mousePressEvent(self, event): if event.button() == Qt.MouseButton.RightButton: return if ( - self._toolbox.qsettings().value("appSettings/dragToDrawLinks", defaultValue="false") == "false" + self._toolbox.qsettings.value("appSettings/dragToDrawLinks", defaultValue="false") == "false" and event.button() == Qt.MouseButton.LeftButton and self._finish_link() ): @@ -106,7 +108,7 @@ def mouseReleaseEvent(self, event): event.accept() return if ( - self._toolbox.qsettings().value("appSettings/dragToDrawLinks", defaultValue="false") == "true" + self._toolbox.qsettings.value("appSettings/dragToDrawLinks", defaultValue="false") == "true" and event.button() == Qt.MouseButton.LeftButton and self._finish_link() ): @@ -137,7 +139,7 @@ def emit_connection_failed(self): def keyPressEvent(self, event): """Puts link drawer to sleep if user presses ESC.""" super().keyPressEvent(event) - if self.link_drawer is not None and event.key() == Qt.Key_Escape: + if self.link_drawer is not None and event.key() == Qt.Key.Key_Escape: self.link_drawer.sleep() def connect_signals(self): @@ -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 @@ -211,7 +213,7 @@ def dropEvent(self, event): source = event.source() if not isinstance(source, ProjectItemDragMixin): return - if not self._toolbox.project(): + if not self._toolbox.project: self._toolbox.msg.emit("Please open or create a project first") event.ignore() return diff --git a/spinetoolbox/widgets/custom_qgraphicsviews.py b/spinetoolbox/widgets/custom_qgraphicsviews.py index 7c8f297b3..b54cb4796 100644 --- a/spinetoolbox/widgets/custom_qgraphicsviews.py +++ b/spinetoolbox/widgets/custom_qgraphicsviews.py @@ -17,8 +17,18 @@ 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 @@ -65,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) @@ -83,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: @@ -112,7 +122,7 @@ def mouseReleaseEvent(self, event): if self._drag_duration_passed(event): context_menu_disabled = True self.disable_context_menu() - elif event.button() == Qt.MouseButton.RightButton: + 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()) ) @@ -127,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: @@ -222,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): @@ -247,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: @@ -265,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 @@ -327,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() @@ -364,7 +374,7 @@ def __init__(self, parent): @property def _qsettings(self): - return self._toolbox.qsettings() + return self._toolbox.qsettings def set_ui(self, toolbox): """Set a new scene into the Design View when app is started.""" @@ -399,8 +409,15 @@ def add_icon(self, item_name): Args: item_name (str): project item's name """ - project_item = self._toolbox.project().get_item(item_name) + project_item = self._toolbox.project.get_item(item_name) icon = project_item.get_icon() + if isinstance(self._toolbox, ToolboxUILite): + icon.set_icon_selection_pen_w(icon.USER_MODE_ICON_SELECTION_PEN_W) + else: + 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() self.scene().addItem(icon) @Slot(str) @@ -410,14 +427,72 @@ def remove_icon(self, item_name): Args: item_name (str): name of the icon to remove """ - icon = self._toolbox.project().get_item(item_name).get_icon() + icon = self._toolbox.project.get_item(item_name).get_icon() scene = self.scene() scene.removeItem(icon) self._set_preferred_scene_rect() - def add_link(self, src_connector, dst_connector): + @Slot(bool) + def group_items(self, _): + selected = self.scene().selectedItems() + all_item_icons = [item for item in selected if isinstance(item, (ProjectItemIcon, Link, JumpLink))] + all_item_icon_names = [item.name for item in all_item_icons] + project_item_icons = [icon for icon in all_item_icons if isinstance(icon, ProjectItemIcon)] + for group in self._toolbox.project.groups.values(): # Don't make duplicate groups + if set(project_item_icons) == set(group.project_items): + self._toolbox.msg_warning.emit(f"{group.name} already has the same item(s)") + return + self.push_make_group_command(all_item_icon_names) + + def push_make_group_command(self, item_names): + """Pushes a MakeGroupCommand to toolbox undo stack. + + Args: + item_names (list): List of item names to group + """ + 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 + """ + if not group.graphicsEffect(): + group.set_graphics_effects() + 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)) + + def push_disband_group_command(self, group_name): + """Pushes a DisbandGroupCommand to toolbox undo stack. + + Args: + group_name (str): Group to disband + """ + 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 """ - Pushes an AddLinkCommand to the toolbox undo stack. + 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 @@ -425,7 +500,7 @@ def add_link(self, src_connector, dst_connector): """ self._toolbox.undo_stack.push( AddConnectionCommand( - self._toolbox.project(), + self._toolbox.project, src_connector.parent_name(), src_connector.position, dst_connector.parent_name(), @@ -440,7 +515,7 @@ def do_add_link(self, connection): Args: connection (Connection): the connection to add """ - project = self._toolbox.project() + project = self._toolbox.project source_connector = project.get_item(connection.source).get_icon().conn_button(connection.source_position) destination_connector = ( project.get_item(connection.destination).get_icon().conn_button(connection.destination_position) @@ -448,6 +523,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) @@ -470,9 +549,9 @@ def remove_links(self, links): jumps = [l.jump for l in links if isinstance(l, JumpLink)] self._toolbox.undo_stack.beginMacro("remove links") if connections: - self._toolbox.undo_stack.push(RemoveConnectionsCommand(self._toolbox.project(), connections)) + self._toolbox.undo_stack.push(RemoveConnectionsCommand(self._toolbox.project, connections)) if jumps: - self._toolbox.undo_stack.push(RemoveJumpsCommand(self._toolbox.project(), jumps)) + self._toolbox.undo_stack.push(RemoveJumpsCommand(self._toolbox.project, jumps)) self._toolbox.undo_stack.endMacro() @Slot(object) @@ -519,7 +598,7 @@ def add_jump(self, src_connector, dst_connector): """ self._toolbox.undo_stack.push( AddJumpCommand( - self._toolbox.project(), + self._toolbox.project, src_connector.parent_name(), src_connector.position, dst_connector.parent_name(), @@ -534,12 +613,16 @@ def do_add_jump(self, jump): Args: jump (Jump): jump to add """ - project = self._toolbox.project() + project = self._toolbox.project source_connector = project.get_item(jump.source).get_icon().conn_button(jump.source_position) destination_connector = project.get_item(jump.destination).get_icon().conn_button(jump.destination_position) 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) @@ -567,18 +650,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(): + if not self._toolbox.project: + return + super().contextMenuEvent(event) # Pass the event first to see if any item accepts it + if event.isAccepted(): 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) + 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/custom_qtextbrowser.py b/spinetoolbox/widgets/custom_qtextbrowser.py index a1a82b6b1..a9fa22730 100644 --- a/spinetoolbox/widgets/custom_qtextbrowser.py +++ b/spinetoolbox/widgets/custom_qtextbrowser.py @@ -10,7 +10,7 @@ # this program. If not, see . ###################################################################################################################### -"""Class for a custom QTextBrowser for showing the logs and tool output.""" +"""Classes for custom QTextBrowser's for showing the logs and tool output.""" from PySide6.QtCore import Slot from PySide6.QtGui import QAction, QBrush, QFontDatabase, QPalette, QTextBlockFormat, QTextCursor, QTextFrameFormat from PySide6.QtWidgets import QMenu, QTextBrowser @@ -18,7 +18,7 @@ from ..helpers import scrolling_to_bottom -class CustomQTextBrowser(QTextBrowser): +class CustomQTextBrowserBase(QTextBrowser): """Custom QTextBrowser class.""" _ALL_RUNS = "All executions" @@ -31,7 +31,6 @@ def __init__(self, parent): super().__init__(parent=parent) self._toolbox = None self.document().setMaximumBlockCount(2000) - self.setStyleSheet(TEXTBROWSER_SS) self.setOpenExternalLinks(True) self.setOpenLinks(False) # Don't try open file:/// links in the browser widget, we'll open them externally self._executions_menu = QMenu(self) @@ -226,6 +225,17 @@ def set_item_log_selected(self, selected): frame.setFrameFormat(frame_format) +class CustomQTextBrowserLite(CustomQTextBrowserBase): + def __init__(self, parent): + super().__init__(parent=parent) + + +class CustomQTextBrowser(CustomQTextBrowserBase): + def __init__(self, parent): + super().__init__(parent=parent) + self.setStyleSheet(TEXTBROWSER_SS) + + class MonoSpaceFontTextBrowser(QTextBrowser): def __init__(self, parent): """ diff --git a/spinetoolbox/widgets/jump_properties_widget.py b/spinetoolbox/widgets/jump_properties_widget.py index f077c398b..ddaf6db29 100644 --- a/spinetoolbox/widgets/jump_properties_widget.py +++ b/spinetoolbox/widgets/jump_properties_widget.py @@ -33,49 +33,49 @@ def __init__(self, toolbox, base_color=None): self._cmd_line_args_model = JumpCommandLineArgsModel(self) self._input_file_model = FileListModel(header_label="Available resources", draggable=True) self._jump = None - self._ui = Ui_Form() - self._ui.setupUi(self) - self._ui.treeView_cmd_line_args.setModel(self._cmd_line_args_model) - self._ui.treeView_input_files.setModel(self._input_file_model) - self._ui.condition_script_edit.set_lexer_name("python") - self._ui.comboBox_tool_spec.setModel(self._toolbox.filtered_spec_factory_models["Tool"]) - self._ui.radioButton_tool_spec.clicked.connect(self._change_condition) - self._ui.radioButton_py_script.clicked.connect(self._change_condition) - self._ui.comboBox_tool_spec.activated.connect(self._change_condition) - self._ui.toolButton_edit_tool_spec.clicked.connect(self._show_tool_spec_form) - self._ui.condition_script_edit.textChanged.connect(self._set_save_script_button_enabled) - self._ui.pushButton_save_script.clicked.connect(self._change_condition) - self._ui.toolButton_remove_arg.clicked.connect(self._remove_arg) - self._ui.toolButton_add_arg.clicked.connect(self._add_args) + self.ui = Ui_Form() + self.ui.setupUi(self) + self.ui.treeView_cmd_line_args.setModel(self._cmd_line_args_model) + self.ui.treeView_input_files.setModel(self._input_file_model) + self.ui.condition_script_edit.set_lexer_name("python") + self.ui.comboBox_tool_spec.setModel(self._toolbox.filtered_spec_factory_models["Tool"]) + self.ui.radioButton_tool_spec.clicked.connect(self._change_condition) + self.ui.radioButton_py_script.clicked.connect(self._change_condition) + self.ui.comboBox_tool_spec.activated.connect(self._change_condition) + self.ui.toolButton_edit_tool_spec.clicked.connect(self._show_tool_spec_form) + self.ui.condition_script_edit.textChanged.connect(self._set_save_script_button_enabled) + self.ui.pushButton_save_script.clicked.connect(self._change_condition) + self.ui.toolButton_remove_arg.clicked.connect(self._remove_arg) + self.ui.toolButton_add_arg.clicked.connect(self._add_args) self._cmd_line_args_model.args_updated.connect(self._push_update_cmd_line_args_command) - self._ui.treeView_cmd_line_args.selectionModel().selectionChanged.connect( + self.ui.treeView_cmd_line_args.selectionModel().selectionChanged.connect( self._update_remove_args_button_enabled ) - self._ui.treeView_input_files.selectionModel().selectionChanged.connect(self._update_add_args_button_enabled) + self.ui.treeView_input_files.selectionModel().selectionChanged.connect(self._update_add_args_button_enabled) def _load_condition_into_ui(self, condition): self._track_changes = False - self._ui.pushButton_save_script.setEnabled(False) - self._ui.condition_script_edit.set_enabled_with_greyed(condition["type"] == "python-script") - self._ui.comboBox_tool_spec.setEnabled(condition["type"] == "tool-specification") - self._ui.toolButton_edit_tool_spec.setEnabled(condition["type"] == "tool-specification") + self.ui.pushButton_save_script.setEnabled(False) + self.ui.condition_script_edit.set_enabled_with_greyed(condition["type"] == "python-script") + self.ui.comboBox_tool_spec.setEnabled(condition["type"] == "tool-specification") + self.ui.toolButton_edit_tool_spec.setEnabled(condition["type"] == "tool-specification") if condition["type"] == "python-script": - self._ui.radioButton_py_script.setChecked(True) - self._ui.condition_script_edit.setPlainText(condition["script"]) + self.ui.radioButton_py_script.setChecked(True) + self.ui.condition_script_edit.setPlainText(condition["script"]) elif condition["type"] == "tool-specification": - self._ui.radioButton_tool_spec.setChecked(True) - self._ui.comboBox_tool_spec.setCurrentText(condition["specification"]) + self.ui.radioButton_tool_spec.setChecked(True) + self.ui.comboBox_tool_spec.setCurrentText(condition["specification"]) self._track_changes = True def _make_condition_from_ui(self): condition = { - "script": self._ui.condition_script_edit.toPlainText(), - "specification": self._ui.comboBox_tool_spec.currentText(), + "script": self.ui.condition_script_edit.toPlainText(), + "specification": self.ui.comboBox_tool_spec.currentText(), } - if self._ui.radioButton_py_script.isChecked(): + if self.ui.radioButton_py_script.isChecked(): condition["type"] = "python-script" return condition - if self._ui.radioButton_tool_spec.isChecked(): + if self.ui.radioButton_tool_spec.isChecked(): condition["type"] = "tool-specification" return condition return {} @@ -86,19 +86,19 @@ def _change_condition(self): return condition = self._make_condition_from_ui() if self._jump.condition != condition: - self._toolbox.undo_stack.push(SetJumpConditionCommand(self._toolbox.project(), self._jump, self, condition)) + self._toolbox.undo_stack.push(SetJumpConditionCommand(self._toolbox.project, self._jump, self, condition)) @Slot(bool) def _show_tool_spec_form(self, _checked=False): name = self._jump.condition["specification"] - specification = self._toolbox.project().get_specification(name) + specification = self._toolbox.project.get_specification(name) self._toolbox.show_specification_form("Tool", specification) @Slot() def _set_save_script_button_enabled(self): condition = self._jump.condition - self._ui.pushButton_save_script.setEnabled( - condition["type"] == "python-script" and condition["script"] != self._ui.condition_script_edit.toPlainText() + self.ui.pushButton_save_script.setEnabled( + condition["type"] == "python-script" and condition["script"] != self.ui.condition_script_edit.toPlainText() ) @Slot(QItemSelection, QItemSelection) @@ -106,16 +106,16 @@ def _update_add_args_button_enabled(self, _selected, _deselected): self._do_update_add_args_button_enabled() def _do_update_add_args_button_enabled(self): - enabled = self._ui.treeView_input_files.selectionModel().hasSelection() - self._ui.toolButton_add_arg.setEnabled(enabled) + enabled = self.ui.treeView_input_files.selectionModel().hasSelection() + self.ui.toolButton_add_arg.setEnabled(enabled) @Slot(QItemSelection, QItemSelection) def _update_remove_args_button_enabled(self, _selected, _deselected): self._do_update_remove_args_button_enabled() def _do_update_remove_args_button_enabled(self): - enabled = self._ui.treeView_cmd_line_args.selectionModel().hasSelection() - self._ui.toolButton_remove_arg.setEnabled(enabled) + enabled = self.ui.treeView_cmd_line_args.selectionModel().hasSelection() + self.ui.toolButton_remove_arg.setEnabled(enabled) def _populate_cmd_line_args_model(self): self._cmd_line_args_model.reset_model(self._jump.cmd_line_args) @@ -124,18 +124,18 @@ def _populate_cmd_line_args_model(self): def _push_update_cmd_line_args_command(self, cmd_line_args): if self._jump.cmd_line_args != cmd_line_args: self._toolbox.undo_stack.push( - UpdateJumpCmdLineArgsCommand(self._toolbox.project(), self._jump, self, cmd_line_args) + UpdateJumpCmdLineArgsCommand(self._toolbox.project, self._jump, self, cmd_line_args) ) @Slot(bool) def _remove_arg(self, _=False): - removed_rows = [index.row() for index in self._ui.treeView_cmd_line_args.selectedIndexes()] + removed_rows = [index.row() for index in self.ui.treeView_cmd_line_args.selectedIndexes()] cmd_line_args = [arg for row, arg in enumerate(self._jump.cmd_line_args) if row not in removed_rows] self._push_update_cmd_line_args_command(cmd_line_args) @Slot(bool) def _add_args(self, _=False): - new_args = [LabelArg(index.data()) for index in self._ui.treeView_input_files.selectedIndexes()] + new_args = [LabelArg(index.data()) for index in self.ui.treeView_input_files.selectedIndexes()] self._push_update_cmd_line_args_command(self._jump.cmd_line_args + new_args) def set_link(self, jump): diff --git a/spinetoolbox/widgets/jupyter_console_widget.py b/spinetoolbox/widgets/jupyter_console_widget.py index 54d2aa823..27322f7ab 100644 --- a/spinetoolbox/widgets/jupyter_console_widget.py +++ b/spinetoolbox/widgets/jupyter_console_widget.py @@ -57,7 +57,7 @@ def __init__(self, toolbox, kernel_name, owner=None): self.kernel_client = None self._connection_file = None self._execution_manager = None - exec_remotely = self._toolbox.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" + exec_remotely = self._toolbox.qsettings.value("engineSettings/remoteExecutionEnabled", "false") == "true" self._engine_manager = make_engine_manager(exec_remotely) self._q = multiprocessing.Queue() self._logger = QueueLogger(self._q, "DetachedPythonConsole", None, {}) @@ -81,7 +81,7 @@ def request_start_kernel(self, conda=False): environment = "" if conda: environment = "conda" - conda_exe = self._toolbox.qsettings().value("appSettings/condaPath", defaultValue="") + conda_exe = self._toolbox.qsettings.value("appSettings/condaPath", defaultValue="") conda_exe = resolve_conda_executable(conda_exe) self._execution_manager = KernelExecutionManager( self._logger, diff --git a/spinetoolbox/widgets/link_properties_widget.py b/spinetoolbox/widgets/link_properties_widget.py index 2731471ca..5fc666c81 100644 --- a/spinetoolbox/widgets/link_properties_widget.py +++ b/spinetoolbox/widgets/link_properties_widget.py @@ -101,7 +101,7 @@ def _handle_auto_check_filters_state_changed(self, checked): if checked == self._connection.is_filter_online_by_default: return self._toolbox.undo_stack.push( - SetConnectionDefaultFilterOnlineStatus(self._toolbox.project(), self._connection, checked) + SetConnectionDefaultFilterOnlineStatus(self._toolbox.project, self._connection, checked) ) def set_auto_check_filters_state(self, checked): @@ -134,7 +134,7 @@ def _update_filter_validation_options(self, checked): if self._connection.require_filter_online(filter_type) != action.isChecked(): options = {"require_" + filter_type: checked} self._toolbox.undo_stack.push( - SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options) + SetConnectionOptionsCommand(self._toolbox.project, self._connection, options) ) return @@ -143,7 +143,7 @@ def _handle_write_index_value_changed(self, value): if self._connection.write_index == value: return options = {"write_index": value} - self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options)) + self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project, self._connection, options)) @Slot(int) def _handle_use_datapackage_state_changed(self, _state): @@ -151,7 +151,7 @@ def _handle_use_datapackage_state_changed(self, _state): if self._connection.use_datapackage == checked: return options = {"use_datapackage": checked} - self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options)) + self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project, self._connection, options)) @Slot(int) def _handle_use_memory_db_state_changed(self, _state): @@ -159,7 +159,7 @@ def _handle_use_memory_db_state_changed(self, _state): if self._connection.use_memory_db == checked: return options = {"use_memory_db": checked} - self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options)) + self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project, self._connection, options)) @Slot(int) def _handle_purge_before_writing_state_changed(self, _state): @@ -167,7 +167,7 @@ def _handle_purge_before_writing_state_changed(self, _state): if self._connection.purge_before_writing == checked: return options = {"purge_before_writing": checked} - self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options)) + self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project, self._connection, options)) @Slot(bool) def _open_purge_settings_dialog(self, _=False): @@ -188,7 +188,7 @@ def _handle_purge_settings_changed(self): if self._connection.purge_settings == purge_settings: return options = {"purge_settings": purge_settings} - self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project(), self._connection, options)) + self._toolbox.undo_stack.push(SetConnectionOptionsCommand(self._toolbox.project, self._connection, options)) @Slot() def _clean_up_purge_settings_dialog(self): @@ -213,10 +213,10 @@ def _select_mutually_exclusive_filter(self, label): self._toolbox.undo_stack.beginMacro(f"enable {label}s on connection {self._connection.link.name}") for disabled_type in disabled_filter_types: self._toolbox.undo_stack.push( - SetConnectionFilterTypeEnabled(self._toolbox.project(), self._connection, disabled_type, False) + SetConnectionFilterTypeEnabled(self._toolbox.project, self._connection, disabled_type, False) ) self._toolbox.undo_stack.push( - SetConnectionFilterTypeEnabled(self._toolbox.project(), self._connection, enabled_filter_type, True) + SetConnectionFilterTypeEnabled(self._toolbox.project, self._connection, enabled_filter_type, True) ) self._toolbox.undo_stack.endMacro() diff --git a/spinetoolbox/widgets/multi_tab_spec_editor.py b/spinetoolbox/widgets/multi_tab_spec_editor.py index 0a862f08f..fb929dcb4 100644 --- a/spinetoolbox/widgets/multi_tab_spec_editor.py +++ b/spinetoolbox/widgets/multi_tab_spec_editor.py @@ -19,7 +19,7 @@ class MultiTabSpecEditor(MultiTabWindow): def __init__(self, toolbox, item_type): - super().__init__(toolbox.qsettings(), f"{item_type}SpecEditor") + super().__init__(toolbox.qsettings, f"{item_type}SpecEditor") self.setStyleSheet(toolbox.styleSheet()) self._toolbox = toolbox self.item_type = item_type diff --git a/spinetoolbox/widgets/open_project_dialog.py b/spinetoolbox/widgets/open_project_dialog.py index 1f8134ba0..57bf4fb7a 100644 --- a/spinetoolbox/widgets/open_project_dialog.py +++ b/spinetoolbox/widgets/open_project_dialog.py @@ -33,7 +33,7 @@ def __init__(self, toolbox): from ..ui import open_project_dialog # pylint: disable=import-outside-toplevel super().__init__(parent=toolbox, f=Qt.Dialog) # Setting the parent inherits the stylesheet - self._qsettings = toolbox.qsettings() + self._qsettings = toolbox.qsettings # Set up the user interface from Designer file self.ui = open_project_dialog.Ui_Dialog() self.ui.setupUi(self) diff --git a/spinetoolbox/widgets/persistent_console_widget.py b/spinetoolbox/widgets/persistent_console_widget.py index 3c5b35827..6d2f034b8 100644 --- a/spinetoolbox/widgets/persistent_console_widget.py +++ b/spinetoolbox/widgets/persistent_console_widget.py @@ -564,7 +564,7 @@ def create_engine_manager(self): """Returns a new local or remote spine engine manager or an existing remote spine engine manager. Returns None if connecting to Spine Engine Server fails.""" - exec_remotely = self._toolbox.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" + exec_remotely = self._toolbox.qsettings.value("engineSettings/remoteExecutionEnabled", "false") == "true" if exec_remotely: if self.engine_mngr: return self.engine_mngr diff --git a/spinetoolbox/widgets/properties_widget.py b/spinetoolbox/widgets/properties_widget.py index 04009f440..3629ad7af 100644 --- a/spinetoolbox/widgets/properties_widget.py +++ b/spinetoolbox/widgets/properties_widget.py @@ -67,7 +67,7 @@ def eventFilter(self, obj, ev): def paintEvent(self, ev): """Paints background""" - settings = self._toolbox.qsettings() + settings = self._toolbox.qsettings if settings.value("appSettings/colorPropertiesWidgets", defaultValue="false") == "false": super().paintEvent(ev) return 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/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py index e6a944b56..c8fa70cc9 100644 --- a/spinetoolbox/widgets/settings_widget.py +++ b/spinetoolbox/widgets/settings_widget.py @@ -299,7 +299,7 @@ def __init__(self, toolbox): Args: toolbox (ToolboxUI): Parent widget. """ - super().__init__(toolbox.qsettings()) + super().__init__(toolbox.qsettings) self.ui.stackedWidget.setCurrentIndex(0) self.ui.listWidget.setFocus() self.ui.listWidget.setCurrentRow(0) @@ -1051,7 +1051,7 @@ def start_fetching_julia_kernels(self): return self._julia_kernel_model.clear() self.ui.comboBox_julia_kernel.addItem("Select Julia kernel...") - conda_path = self._toolbox.qsettings().value("appSettings/condaPath", defaultValue="") + conda_path = self._toolbox.qsettings.value("appSettings/condaPath", defaultValue="") self.julia_kernel_fetcher = KernelFetcher(conda_path, fetch_mode=4) self.julia_kernel_fetcher.kernel_found.connect(self.add_julia_kernel) self.julia_kernel_fetcher.finished.connect(self.restore_saved_julia_kernel) @@ -1102,7 +1102,7 @@ def start_fetching_python_kernels(self, conda_path_updated=False): self._python_kernel_model.clear() self.ui.comboBox_python_kernel.addItem("Select Python kernel...") if not conda_path_updated: - conda_path = self._toolbox.qsettings().value("appSettings/condaPath", defaultValue="") + conda_path = self._toolbox.qsettings.value("appSettings/condaPath", defaultValue="") else: conda_path = self.ui.lineEdit_conda_path.text().strip() self.python_kernel_fetcher = KernelFetcher(conda_path, fetch_mode=2) diff --git a/spinetoolbox/widgets/toolbars.py b/spinetoolbox/widgets/toolbars.py index f49a82071..90fab68d9 100644 --- a/spinetoolbox/widgets/toolbars.py +++ b/spinetoolbox/widgets/toolbars.py @@ -170,7 +170,7 @@ def _make_tool_button(self, icon, text, slot=None, tip=None): return button def _icon_from_factory(self, factory): - colored = self._toolbox.qsettings().value("appSettings/colorToolbarIcons", defaultValue="false") == "true" + colored = self._toolbox.qsettings.value("appSettings/colorToolbarIcons", defaultValue="false") == "true" icon_file_name = factory.icon() icon_color = factory.icon_color().darker(120) return ColoredIcon(icon_file_name, icon_color, self.iconSize(), colored=colored) @@ -329,7 +329,7 @@ def setup(self): self.add_project_item_buttons() def add_project_item_buttons(self): - icon_ordering = self._toolbox.qsettings().value("appSettings/toolbarIconOrdering", defaultValue="") + icon_ordering = self._toolbox.qsettings.value("appSettings/toolbarIconOrdering", defaultValue="") ordered_item_types = icon_ordering.split(self._SEPARATOR) for item_type in ordered_item_types: factory = self._toolbox.item_factories.get(item_type) diff --git a/tests/mock_helpers.py b/tests/mock_helpers.py index 78370e99a..25d27783a 100644 --- a/tests/mock_helpers.py +++ b/tests/mock_helpers.py @@ -43,7 +43,9 @@ def create_toolboxui(): ): mock_qsettings_value.side_effect = qsettings_value_side_effect mock_set_app_style.return_value = True - toolbox = ToolboxUI() + toolbox = ToolboxUI(None) + mock_qsettings_value.assert_called() + mock_set_app_style.assert_called() return toolbox @@ -71,8 +73,10 @@ def create_toolboxui_with_project(project_dir): ): mock_qsettings_value.side_effect = qsettings_value_side_effect mock_set_app_style.return_value = True - toolbox = ToolboxUI() + toolbox = ToolboxUI(None) toolbox.create_project(project_dir) + mock_qsettings_value.assert_called() + mock_set_app_style.assert_called() return toolbox @@ -80,7 +84,7 @@ def clean_up_toolbox(toolbox): """Cleans up toolbox and project.""" with mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value: mock_qsettings_value.side_effect = qsettings_value_side_effect - if toolbox.project(): + if toolbox.project: toolbox.close_project(ask_confirmation=False) QApplication.processEvents() # Makes sure Design view animations finish properly. mock_qsettings_value.assert_called() # The call in _shutdown_engine_kernels() diff --git a/tests/project_item/test_ProjectItem.py b/tests/project_item/test_ProjectItem.py index 0387201b8..d07e13280 100644 --- a/tests/project_item/test_ProjectItem.py +++ b/tests/project_item/test_ProjectItem.py @@ -23,7 +23,7 @@ def setUp(self): """Set up.""" self._temp_dir = TemporaryDirectory() self.toolbox = create_toolboxui_with_project(self._temp_dir.name) - self.project = self.toolbox.project() + self.project = self.toolbox.project def tearDown(self): """Clean up.""" diff --git a/tests/project_item/test_logging_connection.py b/tests/project_item/test_logging_connection.py index 69c67a83e..fe4f0d8b6 100644 --- a/tests/project_item/test_logging_connection.py +++ b/tests/project_item/test_logging_connection.py @@ -73,7 +73,7 @@ def tearDown(self): self._temp_dir.cleanup() def test_removing_connection_that_has_None_db_map_does_not_raise(self): - project = self._toolbox.project() + project = self._toolbox.project self._toolbox.item_factories[_DataStore.item_type()] = _DataStoreFactory self._toolbox._item_properties_uis[_DataStore.item_type()] = _DataStoreFactory.make_properties_widget(self) store_1 = _DataStore("Store 1", project) @@ -96,7 +96,7 @@ def setUp(self): self._toolbox = create_toolboxui_with_project(self._temp_dir.name) self._toolbox.item_factories[_DataStore.item_type()] = _DataStoreFactory self._toolbox._item_properties_uis[_DataStore.item_type()] = _DataStoreFactory.make_properties_widget(self) - project = self._toolbox.project() + project = self._toolbox.project store_1 = _DataStore("Store 1", project) project.add_item(store_1) store_2 = _DataStore("Store 2", project) diff --git a/tests/project_item/test_specification_editor_window.py b/tests/project_item/test_specification_editor_window.py index 3fb9a76f2..e04f28323 100644 --- a/tests/project_item/test_specification_editor_window.py +++ b/tests/project_item/test_specification_editor_window.py @@ -139,9 +139,9 @@ def test_make_new_specification_for_item(self): mock_icon_color.return_value = QColor("white") self._toolbox.item_factories = {"Mock": ProjectItemFactory()} self._toolbox._item_properties_uis = {"Mock": MagicMock()} - project_item = _MockProjectItem("item name", "item description", 0.0, 0.0, self._toolbox.project()) + project_item = _MockProjectItem("item name", "item description", 0.0, 0.0, self._toolbox.project) project_item._toolbox = self._toolbox - self._toolbox.project().add_item(project_item) + self._toolbox.project.add_item(project_item) window = SpecificationEditorWindowBase(self._toolbox, item=project_item) self.assertIs(window.item, project_item) name_edit = window._spec_toolbar._line_edit_name @@ -177,9 +177,9 @@ def test_rename_specification_for_item(self): self._toolbox.item_factories = {"Mock": ProjectItemFactory()} self._toolbox._item_properties_uis = {"Mock": MagicMock()} specification = ProjectItemSpecification("spec name", "spec description", "Mock") - project_item = _MockProjectItem("item name", "item description", 0.0, 0.0, self._toolbox.project()) + project_item = _MockProjectItem("item name", "item description", 0.0, 0.0, self._toolbox.project) project_item._toolbox = self._toolbox - self._toolbox.project().add_item(project_item) + self._toolbox.project.add_item(project_item) project_item.set_specification(specification) window = SpecificationEditorWindowBase(self._toolbox, item=project_item) mock_make_specification.side_effect = lambda name: ProjectItemSpecification( diff --git a/tests/server/test_EngineClient.py b/tests/server/test_EngineClient.py index 09446a3c8..7e62cd5da 100644 --- a/tests/server/test_EngineClient.py +++ b/tests/server/test_EngineClient.py @@ -36,7 +36,7 @@ class TestEngineClient(TestCaseWithQApplication): def setUp(self): self._temp_dir = TemporaryDirectory() self.toolbox = create_toolboxui_with_project(self._temp_dir.name) - self.project = self.toolbox.project() + self.project = self.toolbox.project self.service = EngineServer("tcp", 5601, ServerSecurityModel.NONE, "") self.context = zmq.Context() diff --git a/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py b/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py index 2a13bf2ed..de58051b1 100644 --- a/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py +++ b/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py @@ -42,7 +42,7 @@ def test_multi_spine_db_editor(self): multieditor.make_context_menu(0) multieditor.show_plus_button_context_menu(QPoint(0, 0)) # Add fake data stores to project - self._toolbox.project()._project_items = {"a": FakeDataStore("a")} + self._toolbox.project._project_items = {"a": FakeDataStore("a")} multieditor.show_plus_button_context_menu(QPoint(0, 0)) multieditor._take_tab(0) diff --git a/tests/test_SpineToolboxProject.py b/tests/test_SpineToolboxProject.py index df26f9115..abd75049e 100644 --- a/tests/test_SpineToolboxProject.py +++ b/tests/test_SpineToolboxProject.py @@ -75,9 +75,9 @@ def node_is_isolated(project, node): def test_add_data_store(self): name = "DS" - add_ds(self.toolbox.project(), self.toolbox.item_factories, name) + add_ds(self.toolbox.project, self.toolbox.item_factories, name) # Check that an item with the created name is in project - item = self.toolbox.project().get_item(name) + item = self.toolbox.project.get_item(name) self.assertEqual(item.name, name) # Check that the created item is a Data Store self.assertEqual(item.item_type(), "Data Store") @@ -88,35 +88,35 @@ def check_dag_handler(self, name): """Checks that project dag handler contains only one graph, which has one node and its name matches the given argument.""" - dags = list(self.toolbox.project()._dag_iterator()) + dags = list(self.toolbox.project._dag_iterator()) self.assertTrue(len(dags) == 1) - g = self.toolbox.project().dag_with_node(name) + g = self.toolbox.project.dag_with_node(name) self.assertTrue(len(g.nodes()) == 1) for node_name in g.nodes(): self.assertTrue(node_name == name) def test_add_data_connection(self): name = "DC" - add_dc(self.toolbox.project(), self.toolbox.item_factories, name) + add_dc(self.toolbox.project, self.toolbox.item_factories, name) # Check that an item with the created name is in project - item = self.toolbox.project().get_item(name) + item = self.toolbox.project.get_item(name) self.assertEqual(item.name, name) # Check that the created item is a Data Connection self.assertEqual(item.item_type(), "Data Connection") # Check that dag handler has this and only this node self.check_dag_handler(name) # test get_items_by_type() - data_connections = self.toolbox.project().get_items_by_type("Data Connection") + data_connections = self.toolbox.project.get_items_by_type("Data Connection") self.assertEqual(1, len(data_connections)) self.assertIsInstance(data_connections[0], ProjectItem) - tools = self.toolbox.project().get_items_by_type("Tool") + tools = self.toolbox.project.get_items_by_type("Tool") self.assertEqual(0, len(tools)) def test_add_tool(self): name = "Tool" - add_tool(self.toolbox.project(), self.toolbox.item_factories, name) + add_tool(self.toolbox.project, self.toolbox.item_factories, name) # Check that an item with the created name is in project - item = self.toolbox.project().get_item(name) + item = self.toolbox.project.get_item(name) self.assertEqual(item.name, name) # Check that the created item is a Tool self.assertEqual(item.item_type(), "Tool") @@ -125,9 +125,9 @@ def test_add_tool(self): def test_add_view(self): name = "View" - add_view(self.toolbox.project(), self.toolbox.item_factories, name) + add_view(self.toolbox.project, self.toolbox.item_factories, name) # Check that an item with the created name is in project - item = self.toolbox.project().get_item(name) + item = self.toolbox.project.get_item(name) self.assertEqual(item.name, name) # Check that the created item is a View self.assertEqual(item.item_type(), "View") @@ -135,7 +135,7 @@ def test_add_view(self): self.check_dag_handler(name) def test_add_all_available_items(self): - p = self.toolbox.project() + p = self.toolbox.project ds_name = "DS" dc_name = "DC" dt_name = "DT" @@ -173,71 +173,71 @@ def test_add_all_available_items(self): self.assertTrue(p.has_items()) self.assertEqual(8, len(p.get_items())) # DAG handler should now have eight graphs, each with one item - dags = list(self.toolbox.project()._dag_iterator()) + dags = list(self.toolbox.project._dag_iterator()) self.assertEqual(8, len(dags)) # Check that all created items are in graphs - ds_graph = self.toolbox.project().dag_with_node(ds_name) + ds_graph = self.toolbox.project.dag_with_node(ds_name) self.assertIsNotNone(ds_graph) - dc_graph = self.toolbox.project().dag_with_node(dc_name) + dc_graph = self.toolbox.project.dag_with_node(dc_name) self.assertIsNotNone(dc_graph) - dt_graph = self.toolbox.project().dag_with_node(dt_name) + dt_graph = self.toolbox.project.dag_with_node(dt_name) self.assertIsNotNone(dt_graph) - tool_graph = self.toolbox.project().dag_with_node(tool_name) + tool_graph = self.toolbox.project.dag_with_node(tool_name) self.assertIsNotNone(tool_graph) - view_graph = self.toolbox.project().dag_with_node(view_name) + view_graph = self.toolbox.project.dag_with_node(view_name) self.assertIsNotNone(view_graph) - importer_graph = self.toolbox.project().dag_with_node(imp_name) + importer_graph = self.toolbox.project.dag_with_node(imp_name) self.assertIsNotNone(importer_graph) - exporter_graph = self.toolbox.project().dag_with_node(exporter_name) + exporter_graph = self.toolbox.project.dag_with_node(exporter_name) self.assertIsNotNone(exporter_graph) - merger_graph = self.toolbox.project().dag_with_node(merger_name) + merger_graph = self.toolbox.project.dag_with_node(merger_name) self.assertIsNotNone(merger_graph) def test_remove_item_by_name(self): view_name = "View" - add_view(self.toolbox.project(), self.toolbox.item_factories, view_name) - view = self.toolbox.project().get_item(view_name) + add_view(self.toolbox.project, self.toolbox.item_factories, view_name) + view = self.toolbox.project.get_item(view_name) self.assertEqual(view_name, view.name) - self.toolbox.project().remove_item_by_name(view_name) - self.assertEqual(self.toolbox.project().n_items, 0) + self.toolbox.project.remove_item_by_name(view_name) + self.assertEqual(self.toolbox.project.n_items, 0) def test_remove_item_by_name_removes_outgoing_connections(self): - project = self.toolbox.project() + project = self.toolbox.project view1_name = "View 1" add_view(project, self.toolbox.item_factories, view1_name) view2_name = "View 2" add_view(project, self.toolbox.item_factories, view2_name) project.add_connection(LoggingConnection(view1_name, "top", view2_name, "bottom", toolbox=self.toolbox)) - view = self.toolbox.project().get_item(view1_name) + view = self.toolbox.project.get_item(view1_name) self.assertEqual(view1_name, view.name) - view = self.toolbox.project().get_item(view2_name) + view = self.toolbox.project.get_item(view2_name) self.assertEqual(view2_name, view.name) - self.assertEqual(self.toolbox.project().n_items, 2) + self.assertEqual(self.toolbox.project.n_items, 2) self.assertEqual(len(project.connections), 1) project.remove_item_by_name(view1_name) - self.assertEqual(self.toolbox.project().n_items, 1) + self.assertEqual(self.toolbox.project.n_items, 1) self.assertEqual(len(project.connections), 0) - view = self.toolbox.project().get_item(view2_name) + view = self.toolbox.project.get_item(view2_name) self.assertEqual(view2_name, view.name) self.assertTrue(self.node_is_isolated(project, view2_name)) def test_remove_item_by_name_removes_incoming_connections(self): - project = self.toolbox.project() + project = self.toolbox.project view1_name = "View 1" add_view(project, self.toolbox.item_factories, view1_name) view2_name = "View 2" add_view(project, self.toolbox.item_factories, view2_name) project.add_connection(LoggingConnection(view1_name, "top", view2_name, "bottom", toolbox=self.toolbox)) - view = self.toolbox.project().get_item(view1_name) + view = self.toolbox.project.get_item(view1_name) self.assertEqual(view1_name, view.name) - view = self.toolbox.project().get_item(view2_name) + view = self.toolbox.project.get_item(view2_name) self.assertEqual(view2_name, view.name) - self.assertEqual(self.toolbox.project().n_items, 2) + self.assertEqual(self.toolbox.project.n_items, 2) self.assertEqual(len(project.connections), 1) project.remove_item_by_name(view2_name) - self.assertEqual(self.toolbox.project().n_items, 1) + self.assertEqual(self.toolbox.project.n_items, 1) self.assertEqual(len(project.connections), 0) - view = self.toolbox.project().get_item(view1_name) + view = self.toolbox.project.get_item(view1_name) self.assertEqual(view1_name, view.name) self.assertTrue(self.node_is_isolated(project, view1_name)) @@ -248,7 +248,7 @@ def _execute_project(self, names=None): names (list): List of selected item names to execute, or None to execute the whole project. """ waiter = SignalWaiter() - self.toolbox.project().project_execution_finished.connect(waiter.trigger) + self.toolbox.project.project_execution_finished.connect(waiter.trigger) with ( mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value, mock.patch("spinetoolbox.project.make_settings_dict_for_engine") as mock_settings_dict, @@ -259,16 +259,16 @@ def _execute_project(self, names=None): # This mocks the call to make_settings_dict_for_engine in SpineToolboxProject._execute_dags() mock_settings_dict.return_value = {} if not names: - self.toolbox.project().execute_project() + self.toolbox.project.execute_project() else: - self.toolbox.project().execute_selected(names) + self.toolbox.project.execute_selected(names) mock_qsettings_value.assert_called() mock_settings_dict.assert_called() waiter.wait() - self.toolbox.project().project_execution_finished.disconnect(waiter.trigger) + self.toolbox.project.project_execution_finished.disconnect(waiter.trigger) def test_execute_project_with_single_item(self): - view = add_view(self.toolbox.project(), self.toolbox.item_factories, "View") + view = add_view(self.toolbox.project, self.toolbox.item_factories, "View") view_executable = self._make_mock_executable(view) with mock.patch("spine_engine.spine_engine.SpineEngine.make_item") as mock_make_item: mock_make_item.return_value = view_executable @@ -276,9 +276,9 @@ def test_execute_project_with_single_item(self): self.assertTrue(view_executable.execute_called) def test_execute_project_with_two_dags(self): - item1 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") + item1 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") item1_executable = self._make_mock_executable(item1) - item2 = add_view(self.toolbox.project(), self.toolbox.item_factories, "View") + item2 = add_view(self.toolbox.project, self.toolbox.item_factories, "View") item2_executable = self._make_mock_executable(item2) with mock.patch("spine_engine.spine_engine.SpineEngine.make_item") as mock_make_item: mock_make_item.side_effect = lambda name, *args, **kwargs: { @@ -290,9 +290,9 @@ def test_execute_project_with_two_dags(self): self.assertTrue(item2_executable.execute_called) def test_execute_selected_dag(self): - item1 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") + item1 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") item1_executable = self._make_mock_executable(item1) - item2 = add_view(self.toolbox.project(), self.toolbox.item_factories, "View") + item2 = add_view(self.toolbox.project, self.toolbox.item_factories, "View") item2_executable = self._make_mock_executable(item2) with mock.patch("spine_engine.spine_engine.SpineEngine.make_item") as mock_make_item: mock_make_item.side_effect = lambda name, *args, **kwargs: { @@ -304,16 +304,16 @@ def test_execute_selected_dag(self): self.assertTrue(item2_executable.execute_called) def test_execute_selected_item_within_single_dag(self): - data_store = add_ds(self.toolbox.project(), self.toolbox.item_factories, "DS") + data_store = add_ds(self.toolbox.project, self.toolbox.item_factories, "DS") data_store_executable = self._make_mock_executable(data_store) - data_connection = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") + data_connection = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") data_connection_executable = self._make_mock_executable(data_connection) - view = add_view(self.toolbox.project(), self.toolbox.item_factories, "View") + view = add_view(self.toolbox.project, self.toolbox.item_factories, "View") view_executable = self._make_mock_executable(view) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(data_store.name, "right", data_connection.name, "left", toolbox=self.toolbox) ) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(data_connection.name, "bottom", view.name, "top", toolbox=self.toolbox) ) with mock.patch("spine_engine.spine_engine.SpineEngine.make_item") as mock_make_item: @@ -328,26 +328,26 @@ def test_execute_selected_item_within_single_dag(self): self.assertFalse(view_executable.execute_called) def test_execute_selected_items_within_single_dag(self): - dc1 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC1") + dc1 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC1") dc1_executable = self._make_mock_executable(dc1) - dc2 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC2") + dc2 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC2") dc2_executable = self._make_mock_executable(dc2) - dc3 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC3") + dc3 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC3") dc3_executable = self._make_mock_executable(dc3) - dc4 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC4") + dc4 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC4") dc4_executable = self._make_mock_executable(dc4) - dc5 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC5") + dc5 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC5") dc5_executable = self._make_mock_executable(dc5) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(dc1.name, "right", dc2.name, "left", toolbox=self.toolbox) ) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(dc2.name, "bottom", dc3.name, "top", toolbox=self.toolbox) ) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(dc1.name, "right", dc4.name, "left", toolbox=self.toolbox) ) - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(dc4.name, "right", dc5.name, "left", toolbox=self.toolbox) ) # DAG contains 5 items and 4 connections. dc1->dc2->dc3 and dc1->dc4->dc5. @@ -366,57 +366,57 @@ def test_execute_selected_items_within_single_dag(self): self.assertTrue(dc5_executable.execute_called) def test_making_a_yellow_feedback_loop_makes_a_jump_instead(self): - item1 = add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") - item2 = add_view(self.toolbox.project(), self.toolbox.item_factories, "View") - self.toolbox.project().add_connection( + item1 = add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") + item2 = add_view(self.toolbox.project, self.toolbox.item_factories, "View") + self.toolbox.project.add_connection( LoggingConnection(item1.name, "right", item2.name, "left", toolbox=self.toolbox) ) # There should be one connection and no jumps in the project - self.assertEqual(1, len(self.toolbox.project().connections)) - self.assertEqual("DC", self.toolbox.project().connections[0].source) - self.assertEqual("View", self.toolbox.project().connections[0].destination) - self.assertEqual(0, len(self.toolbox.project()._jumps)) + self.assertEqual(1, len(self.toolbox.project.connections)) + self.assertEqual("DC", self.toolbox.project.connections[0].source) + self.assertEqual("View", self.toolbox.project.connections[0].destination) + self.assertEqual(0, len(self.toolbox.project._jumps)) with mock.patch("PySide6.QtWidgets.QMessageBox.exec") as mock_msgbox_exec: mock_msgbox_exec.return_value = QMessageBox.StandardButton.Cancel - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(item2.name, "bottom", item1.name, "top", toolbox=self.toolbox) ) # Operation was cancelled - self.assertEqual(1, len(self.toolbox.project().connections)) - self.assertEqual(0, len(self.toolbox.project()._jumps)) + self.assertEqual(1, len(self.toolbox.project.connections)) + self.assertEqual(0, len(self.toolbox.project._jumps)) # Do again but click Ok (Add Loop) mock_msgbox_exec.assert_called() self.assertEqual(1, mock_msgbox_exec.call_count) mock_msgbox_exec.return_value = QMessageBox.StandardButton.Ok - self.toolbox.project().add_connection( + self.toolbox.project.add_connection( LoggingConnection(item2.name, "bottom", item1.name, "top", toolbox=self.toolbox) ) self.assertEqual(2, mock_msgbox_exec.call_count) # There should be one connection and one jump in the project - self.assertEqual(1, len(self.toolbox.project().connections)) - self.assertIsInstance(self.toolbox.project().connections[0], LoggingConnection) - self.assertEqual("DC", self.toolbox.project().connections[0].source) - self.assertEqual("View", self.toolbox.project().connections[0].destination) - self.assertEqual(1, len(self.toolbox.project()._jumps)) - self.assertIsInstance(self.toolbox.project()._jumps[0], LoggingJump) - self.assertEqual("View", self.toolbox.project()._jumps[0].source) - self.assertEqual("DC", self.toolbox.project()._jumps[0].destination) + self.assertEqual(1, len(self.toolbox.project.connections)) + self.assertIsInstance(self.toolbox.project.connections[0], LoggingConnection) + self.assertEqual("DC", self.toolbox.project.connections[0].source) + self.assertEqual("View", self.toolbox.project.connections[0].destination) + self.assertEqual(1, len(self.toolbox.project._jumps)) + self.assertIsInstance(self.toolbox.project._jumps[0], LoggingJump) + self.assertEqual("View", self.toolbox.project._jumps[0].source) + self.assertEqual("DC", self.toolbox.project._jumps[0].destination) def test_rename_project(self): new_name = "New Project Name" new_short_name = "new_project_name" with mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"): - self.toolbox.project().set_name(new_name) - self.assertEqual(self.toolbox.project().name, new_name) - self.assertEqual(self.toolbox.project().short_name, new_short_name) + self.toolbox.project.set_name(new_name) + self.assertEqual(self.toolbox.project.name, new_name) + self.assertEqual(self.toolbox.project.short_name, new_short_name) def test_set_project_description(self): desc = "Project Description" - self.toolbox.project().set_description(desc) - self.assertEqual(self.toolbox.project().description, desc) + self.toolbox.project.set_description(desc) + self.assertEqual(self.toolbox.project.description, desc) def test_rename_item(self): - project = self.toolbox.project() + project = self.toolbox.project source_name = "source" destination_name = "destination" add_view(project, self.toolbox.item_factories, source_name) @@ -433,17 +433,17 @@ 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): - project = self.toolbox.project() + project = self.toolbox.project dc_name = "DC" add_dc(project, self.toolbox.item_factories, dc_name) self.assertEqual(project.connections_for_item(dc_name), []) def test_connections_for_item(self): - project = self.toolbox.project() + project = self.toolbox.project dc1_name = "My first DC" add_dc(project, self.toolbox.item_factories, dc1_name) dc2_name = "My second DC" @@ -469,7 +469,7 @@ def test_connections_for_item(self): ) def test_add_connection_updates_dag_handler(self): - project = self.toolbox.project() + project = self.toolbox.project dc_name = "DC" add_dc(project, self.toolbox.item_factories, dc_name) importer_name = "Importer" @@ -480,7 +480,7 @@ def test_add_connection_updates_dag_handler(self): self.assertEqual(node_successors(dag), {dc_name: [importer_name], importer_name: []}) def test_add_connection_updates_resources(self): - project = self.toolbox.project() + project = self.toolbox.project dc_name = "DC" add_dc(project, self.toolbox.item_factories, dc_name) tool_name = "Tool" @@ -495,7 +495,7 @@ def test_add_connection_updates_resources(self): self.assertEqual(tool._input_file_model.rowCount(), 1) def test_modifying_connected_item_updates_resources(self): - project = self.toolbox.project() + project = self.toolbox.project dc_name = "DC" add_dc(project, self.toolbox.item_factories, dc_name) tool_name = "Tool" @@ -515,7 +515,7 @@ def test_removing_connection_does_not_break_available_resources(self): # Tests issue #1310. # Make two DC's connected to a tool and provide a resource from both to Tool. # Remove one connection, and test that the other one still provides the resource to Tool - project = self.toolbox.project() + project = self.toolbox.project add_dc(project, self.toolbox.item_factories, "dc1") add_dc(project, self.toolbox.item_factories, "dc2") add_tool(project, self.toolbox.item_factories, "t") @@ -536,7 +536,7 @@ def test_removing_connection_does_not_break_available_resources(self): self.assertEqual(t._input_file_model.rowCount(), 1) # There should be 1 resource left def test_update_connection(self): - project = self.toolbox.project() + project = self.toolbox.project dc1_name = "DC 1" add_dc(project, self.toolbox.item_factories, dc1_name) dc2_name = "DC 2" @@ -556,7 +556,7 @@ def test_update_connection(self): self.assertEqual(node_successors(dag), {dc1_name: [dc2_name], dc2_name: []}) def test_save_when_storing_item_local_data(self): - project = self.toolbox.project() + project = self.toolbox.project item = _MockItemWithLocalData(project) with ( mock.patch.object(self.toolbox, "project_item_properties_ui"), @@ -585,7 +585,7 @@ def test_save_when_storing_item_local_data(self): self.assertEqual(local_data_dict, {"items": {"test item": {"a": {"b": 1, "d": 3}}}}) def test_load_when_storing_item_local_data(self): - project = self.toolbox.project() + project = self.toolbox.project item = _MockItemWithLocalData(project) with ( mock.patch.object(self.toolbox, "project_item_properties_ui"), @@ -601,11 +601,11 @@ def test_load_when_storing_item_local_data(self): mock.patch.object(self.toolbox, "project_item_icon"), ): self.assertTrue(self.toolbox.restore_project(self._temp_dir.name, ask_confirmation=False)) - item = self.toolbox.project().get_item("test item") + item = self.toolbox.project.get_item("test item") self.assertEqual(item.kwargs, {"type": "Tester", "a": {"b": 1, "c": 2, "d": 3}}) def test_add_and_save_specification(self): - project = self.toolbox.project() + project = self.toolbox.project self.toolbox.item_factories = {"Tester": ProjectItemFactory()} specification = _MockSpecification("a specification", "Specification for testing.", "Tester") with ( @@ -630,7 +630,7 @@ def test_add_and_save_specification(self): self.assertFalse(local_data_dir.exists()) def test_add_and_save_specification_with_local_data(self): - project = self.toolbox.project() + project = self.toolbox.project self.toolbox.item_factories = {"Tester": _MockItemFactoryForLocalDataTests} specification = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" @@ -664,7 +664,7 @@ def test_add_and_save_specification_with_local_data(self): self.assertEqual(local_data, {"Tester": {"a specification": {"data": "my precious data"}}}) def test_renaming_specification_with_local_data_updates_local_data_file(self): - project = self.toolbox.project() + project = self.toolbox.project self.toolbox.item_factories = {"Tester": _MockItemFactoryForLocalDataTests} original_specification = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" @@ -700,7 +700,7 @@ def test_renaming_specification_with_local_data_updates_local_data_file(self): self.assertEqual(local_data, {"Tester": {"another specification": {"data": "my precious data"}}}) def test_replace_specification_with_local_data_by_one_without_removes_local_data_from_the_file(self): - project = self.toolbox.project() + project = self.toolbox.project self.toolbox.item_factories = {"Tester": _MockItemFactoryForLocalDataTests} specification_with_local_data = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" @@ -732,8 +732,8 @@ def test_replace_specification_with_local_data_by_one_without_removes_local_data def _make_mock_executable(self, item): item_name = item.name - item = self.toolbox.project().get_item(item_name) - item_executable = _MockExecutableItem(item_name, self.toolbox.project().project_dir, self.toolbox) + item = self.toolbox.project.get_item(item_name) + item_executable = _MockExecutableItem(item_name, self.toolbox.project.project_dir, self.toolbox) animation = QVariantAnimation() animation.setDuration(0) item.make_execution_leave_animation = mock.MagicMock(return_value=animation) diff --git a/tests/test_ToolboxUI.py b/tests/test_ToolboxUI.py index b7a965640..5c4910a03 100644 --- a/tests/test_ToolboxUI.py +++ b/tests/test_ToolboxUI.py @@ -69,7 +69,7 @@ def test_init_specification_model(self): """Check that specification model has no items after init and that signals are connected just once. """ - self.assertIsNone(self.toolbox.project()) # Make sure that there is no project open + self.assertIsNone(self.toolbox.project) # Make sure that there is no project open self.toolbox.init_specification_model() self.assertEqual(self.toolbox.specification_model.rowCount(), 0) @@ -79,7 +79,7 @@ def test_create_project(self): """ with TemporaryDirectory() as project_dir: create_project(self.toolbox, project_dir) - self.assertIsInstance(self.toolbox.project(), SpineToolboxProject) # Check that a project is open + self.assertIsInstance(self.toolbox.project, SpineToolboxProject) # Check that a project is open def test_open_project(self): """Test that opening a project directory works. @@ -90,7 +90,7 @@ def test_open_project(self): """ project_dir = os.path.abspath(os.path.join(str(Path(__file__).parent), "test_resources", "Project Directory")) self.assertTrue(os.path.exists(project_dir)) - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) with ( mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch("spinetoolbox.project.create_dir"), @@ -98,23 +98,23 @@ def test_open_project(self): mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.open_project(project_dir) - self.assertIsInstance(self.toolbox.project(), SpineToolboxProject) + self.assertIsInstance(self.toolbox.project, SpineToolboxProject) # Check that project contains four items - self.assertEqual(self.toolbox.project().n_items, 4) + self.assertEqual(self.toolbox.project.n_items, 4) # Check that design view has three links links = [item for item in self.toolbox.ui.graphicsView.scene().items() if isinstance(item, Link)] self.assertEqual(len(links), 3) # Check project items have the right links - item_a = self.toolbox.project().get_item("a") + item_a = self.toolbox.project.get_item("a") icon_a = item_a.get_icon() links_a = [link for conn in icon_a.connectors.values() for link in conn.links] - item_b = self.toolbox.project().get_item("b") + item_b = self.toolbox.project.get_item("b") icon_b = item_b.get_icon() links_b = [link for conn in icon_b.connectors.values() for link in conn.links] - item_c = self.toolbox.project().get_item("c") + item_c = self.toolbox.project.get_item("c") icon_c = item_c.get_icon() links_c = [link for conn in icon_c.connectors.values() for link in conn.links] - item_d = self.toolbox.project().get_item("d") + item_d = self.toolbox.project.get_item("d") icon_d = item_d.get_icon() links_d = [link for conn in icon_d.connectors.values() for link in conn.links] self.assertEqual(len(links_a), 1) @@ -133,7 +133,7 @@ def test_open_project(self): self.assertEqual(len(links_d), 1) self.assertEqual(links_c[0], links_d[0]) # Check that DAG graph is correct - dags = list(self.toolbox.project()._dag_iterator()) + dags = list(self.toolbox.project._dag_iterator()) self.assertTrue(len(dags) == 1) # Only one graph g = dags[0] self.assertTrue(len(g.nodes()) == 4) # graph has four nodes @@ -149,7 +149,7 @@ def test_open_project(self): def test_init_project(self): project_dir = os.path.abspath(os.path.join(str(Path(__file__).parent), "test_resources", "Project Directory")) self.assertTrue(os.path.exists(project_dir)) - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) with ( mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch("spinetoolbox.project.create_dir"), @@ -157,8 +157,8 @@ def test_init_project(self): mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.init_project(project_dir) - self.assertIsNotNone(self.toolbox.project()) - self.assertEqual(self.toolbox.project().name, "Project Directory") + self.assertIsNotNone(self.toolbox.project) + self.assertEqual(self.toolbox.project.name, "Project Directory") def test_new_project(self): self._temp_dir = TemporaryDirectory() @@ -169,8 +169,8 @@ def test_new_project(self): ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() - self.assertIsNotNone(self.toolbox.project()) - self.assertEqual(self.toolbox.project().name, os.path.basename(self._temp_dir.name)) + self.assertIsNotNone(self.toolbox.project) + self.assertEqual(self.toolbox.project.name, os.path.basename(self._temp_dir.name)) def test_save_project(self): self._temp_dir = TemporaryDirectory() @@ -197,8 +197,8 @@ def test_save_project(self): mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.open_project(self._temp_dir.name) - self.assertIsNotNone(self.toolbox.project()) - self.assertEqual(self.toolbox.project().get_item("DC").name, "DC") + self.assertIsNotNone(self.toolbox.project) + self.assertEqual(self.toolbox.project.get_item("DC").name, "DC") def test_prevent_project_closing_with_unsaved_changes(self): self._temp_dir = TemporaryDirectory() @@ -212,7 +212,7 @@ def test_prevent_project_closing_with_unsaved_changes(self): add_dc_trough_undo_stack(self.toolbox, "DC1") self.toolbox.save_project() self.assertTrue(self.toolbox.undo_stack.isClean()) - self.assertEqual(self.toolbox.project().get_item("DC1").name, "DC1") + self.assertEqual(self.toolbox.project.get_item("DC1").name, "DC1") add_dc_trough_undo_stack(self.toolbox, "DC2") self.assertFalse(self.toolbox.undo_stack.isClean()) with mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value: @@ -236,23 +236,23 @@ def test_prevent_project_closing_with_unsaved_changes(self): warning_msg.assert_called_with( f"Cancelled opening project {self._temp_dir.name}. Current project has unsaved changes." ) - self.assertIsNotNone(self.toolbox.project()) - self.assertEqual(self.toolbox.project().get_item("DC1").name, "DC1") - self.assertEqual(self.toolbox.project().get_item("DC2").name, "DC2") + self.assertIsNotNone(self.toolbox.project) + self.assertEqual(self.toolbox.project.get_item("DC1").name, "DC1") + self.assertEqual(self.toolbox.project.get_item("DC2").name, "DC2") def test_close_project(self): - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) self.assertTrue(self.toolbox.close_project()) - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) with TemporaryDirectory() as project_dir: create_project(self.toolbox, project_dir) - self.assertIsInstance(self.toolbox.project(), SpineToolboxProject) + self.assertIsInstance(self.toolbox.project, SpineToolboxProject) with mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value: # Make sure that the test uses LocalSpineEngineManager mock_qsettings_value.side_effect = qsettings_value_side_effect self.assertTrue(self.toolbox.close_project()) mock_qsettings_value.assert_called() - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) def test_show_project_or_item_context_menu(self): self._temp_dir = TemporaryDirectory() @@ -266,14 +266,14 @@ def test_show_project_or_item_context_menu(self): mock_set_value.assert_called() mock_sync.assert_called() mock_dir_getter.assert_called() - add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") + add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") # mocking "PySide6.QtWidgets.QMenu.exec directly doesn't work because QMenu.exec is overloaded! with mock.patch("spinetoolbox.ui_main.QMenu") as mock_qmenu: mock_qmenu.side_effect = MockQMenu self.toolbox.show_project_or_item_context_menu(QPoint(0, 0), None) with mock.patch("spinetoolbox.ui_main.QMenu") as mock_qmenu: mock_qmenu.side_effect = MockQMenu - dc = self.toolbox.project().get_item("DC") + dc = self.toolbox.project.get_item("DC") self.toolbox.show_project_or_item_context_menu(QPoint(0, 0), dc) def test_refresh_edit_action_states(self): @@ -298,8 +298,8 @@ def test_refresh_edit_action_states(self): mock_set_value.assert_called() mock_sync.assert_called() mock_dir_getter.assert_called() - add_dc(self.toolbox.project(), self.toolbox.item_factories, "DC") - dc = self.toolbox.project().get_item("DC") + add_dc(self.toolbox.project, self.toolbox.item_factories, "DC") + dc = self.toolbox.project.get_item("DC") icon = dc.get_icon() icon.setSelected(True) with mock.patch("spinetoolbox.ui_main.QApplication.clipboard") as mock_clipboard: @@ -329,17 +329,17 @@ def test_selection_in_design_view_1(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) - n_items = self.toolbox.project().n_items + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 1) # Check that the project contains one item gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) + dc1_item = self.toolbox.project.get_item(dc1) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) # Center point in graphics view viewport coords. # Simulate mouse click on Data Connection in Design View QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) self.assertEqual(1, len(gv.scene().selectedItems())) # Active project item should be DC1 - self.assertEqual(self.toolbox.project().get_item(dc1), self.toolbox.active_project_item) + self.assertEqual(self.toolbox.project.get_item(dc1), self.toolbox.active_project_item) def test_selection_in_design_view_2(self): """Test item selection in Design View. @@ -349,13 +349,13 @@ def test_selection_in_design_view_2(self): create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" dc2 = "DC2" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc2, x=100, y=100) - n_items = self.toolbox.project().n_items + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc2, x=100, y=100) + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 2) # Check the number of project items gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) - dc2_item = self.toolbox.project().get_item(dc2) + dc1_item = self.toolbox.project.get_item(dc1) + dc2_item = self.toolbox.project.get_item(dc2) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) dc2_center_point = self.find_click_point_of_pi(dc2_item, gv) # Mouse click on dc1 @@ -364,7 +364,7 @@ def test_selection_in_design_view_2(self): QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc2_center_point) self.assertEqual(1, len(gv.scene().selectedItems())) # Active project item should be DC2 - self.assertEqual(self.toolbox.project().get_item(dc2), self.toolbox.active_project_item) + self.assertEqual(self.toolbox.project.get_item(dc2), self.toolbox.active_project_item) def test_selection_in_design_view_3(self): """Test item selection in Design View. @@ -373,9 +373,9 @@ def test_selection_in_design_view_3(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) + dc1_item = self.toolbox.project.get_item(dc1) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) # Mouse click on dc1 QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) @@ -393,13 +393,13 @@ def test_selection_in_design_view_4(self): create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" dc2 = "DC2" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc2, x=100, y=100) - n_items = self.toolbox.project().n_items + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc2, x=100, y=100) + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 2) # Check the number of project items gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) - dc2_item = self.toolbox.project().get_item(dc2) + dc1_item = self.toolbox.project.get_item(dc1) + dc2_item = self.toolbox.project.get_item(dc2) # Add link between dc1 and dc2 gv.add_link(dc1_item.get_icon().conn_button("bottom"), dc2_item.get_icon().conn_button("bottom")) # Find link @@ -427,13 +427,13 @@ def test_selection_in_design_view_5(self): create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" dc2 = "DC2" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc2, x=100, y=100) - n_items = self.toolbox.project().n_items + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc2, x=100, y=100) + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 2) # Check the number of project items gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) - dc2_item = self.toolbox.project().get_item(dc2) + dc1_item = self.toolbox.project.get_item(dc1) + dc2_item = self.toolbox.project.get_item(dc2) # Add link between dc1 and dc2 gv.add_link(dc1_item.get_icon().conn_button("bottom"), dc2_item.get_icon().conn_button("bottom")) # Find link @@ -465,13 +465,13 @@ def test_selection_in_design_view_6(self): create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" dc2 = "DC2" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1, x=0, y=0) - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc2, x=100, y=100) - n_items = self.toolbox.project().n_items + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1, x=0, y=0) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc2, x=100, y=100) + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 2) # Check the number of project items gv = self.toolbox.ui.graphicsView - dc1_item = self.toolbox.project().get_item(dc1) - dc2_item = self.toolbox.project().get_item(dc2) + dc1_item = self.toolbox.project.get_item(dc1) + dc2_item = self.toolbox.project.get_item(dc2) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) dc2_center_point = self.find_click_point_of_pi(dc2_item, gv) # Mouse click on dc1 @@ -524,12 +524,12 @@ def test_remove_item(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) dc1 = "DC1" - add_dc(self.toolbox.project(), self.toolbox.item_factories, dc1) + add_dc(self.toolbox.project, self.toolbox.item_factories, dc1) # Check the size of project item model - n_items = self.toolbox.project().n_items + n_items = self.toolbox.project.n_items self.assertEqual(n_items, 1) # Check DAG handler - dags = list(self.toolbox.project()._dag_iterator()) + dags = list(self.toolbox.project._dag_iterator()) self.assertEqual(1, len(dags)) # Number of DAGs (DiGraph objects) in project self.assertEqual(1, len(dags[0].nodes())) # Number of nodes in the DiGraph # Check number of items in Design View @@ -540,8 +540,8 @@ def test_remove_item(self): with mock.patch.object(spinetoolbox.ui_main.QMessageBox, "exec") as mock_message_box_exec: mock_message_box_exec.return_value = QMessageBox.StandardButton.Ok self.toolbox.ui.actionRemove.trigger() - self.assertEqual(self.toolbox.project().n_items, 0) # Check the number of project items - dags = list(self.toolbox.project()._dag_iterator()) + self.assertEqual(self.toolbox.project.n_items, 0) # Check the number of project items + dags = list(self.toolbox.project._dag_iterator()) self.assertEqual(0, len(dags)) # Number of DAGs (DiGraph) objects in project item_icons = self.toolbox.ui.graphicsView.scene().project_item_icons() self.assertEqual(len(item_icons), 0) @@ -556,7 +556,7 @@ def test_add_and_remove_specification(self): specifications when this test starts and ends.""" project_dir = os.path.abspath(os.path.join(str(Path(__file__).parent), "test_resources", "Project Directory")) self.assertTrue(os.path.exists(project_dir)) - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) with ( mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), @@ -593,7 +593,7 @@ def test_tasks_before_exit_without_open_project(self): """_tasks_before_exit is called with every possible combination of the two QSettings values that it uses. This test is done without a project so MUT only calls QSettings.value() once. This can probably be simplified but at least it does not edit user's Settings, while doing the test.""" - self.assertIsNone(self.toolbox.project()) + self.assertIsNone(self.toolbox.project) with mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value: mock_qsettings_value.side_effect = self._tasks_before_exit_scenario_1 tasks = self.toolbox._tasks_before_exit() @@ -650,7 +650,7 @@ def test_tasks_before_exit_with_open_dirty_project(self): def test_copy_project_item_to_clipboard(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) - add_dc(self.toolbox.project(), self.toolbox.item_factories, "data_connection") + add_dc(self.toolbox.project, self.toolbox.item_factories, "data_connection") items_on_design_view = self.toolbox.ui.graphicsView.scene().project_item_icons() self.assertEqual(len(items_on_design_view), 1) items_on_design_view[0].setSelected(True) @@ -667,36 +667,36 @@ def test_copy_project_item_to_clipboard(self): def test_paste_project_item_from_clipboard(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) - add_dc(self.toolbox.project(), self.toolbox.item_factories, "data_connection") - self.assertEqual(self.toolbox.project().n_items, 1) + add_dc(self.toolbox.project, self.toolbox.item_factories, "data_connection") + self.assertEqual(self.toolbox.project.n_items, 1) items_on_design_view = self.toolbox.ui.graphicsView.scene().project_item_icons() self.assertEqual(len(items_on_design_view), 1) items_on_design_view[0].setSelected(True) self.toolbox.ui.actionCopy.triggered.emit() self.toolbox.ui.actionPaste.triggered.emit() - self.assertEqual(self.toolbox.project().n_items, 2) - new_item = self.toolbox.project().get_item("data_connection (1)") + self.assertEqual(self.toolbox.project.n_items, 2) + new_item = self.toolbox.project.get_item("data_connection (1)") self.assertIsInstance(new_item, ProjectItem) def test_duplicate_project_item(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) - add_dc(self.toolbox.project(), self.toolbox.item_factories, "data_connection") - self.assertEqual(self.toolbox.project().n_items, 1) + add_dc(self.toolbox.project, self.toolbox.item_factories, "data_connection") + self.assertEqual(self.toolbox.project.n_items, 1) items_on_design_view = self.toolbox.ui.graphicsView.scene().project_item_icons() self.assertEqual(len(items_on_design_view), 1) items_on_design_view[0].setSelected(True) with mock.patch("spinetoolbox.project_item.project_item.create_dir"): self.toolbox.ui.actionDuplicate.triggered.emit() - self.assertEqual(self.toolbox.project().n_items, 2) - new_item = self.toolbox.project().get_item("data_connection (1)") + self.assertEqual(self.toolbox.project.n_items, 2) + new_item = self.toolbox.project.get_item("data_connection (1)") self.assertIsInstance(new_item, ProjectItem) def test_persistent_console_requested(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) - add_tool(self.toolbox.project(), self.toolbox.item_factories, "tool") - item = self.toolbox.project().get_item("tool") + add_tool(self.toolbox.project, self.toolbox.item_factories, "tool") + item = self.toolbox.project.get_item("tool") filter_id = "" key = ("too", "") language = "julia" @@ -711,8 +711,8 @@ def test_persistent_console_requested(self): def test_filtered_persistent_consoles_requested(self): self._temp_dir = TemporaryDirectory() create_project(self.toolbox, self._temp_dir.name) - add_tool(self.toolbox.project(), self.toolbox.item_factories, "tool") - item = self.toolbox.project().get_item("tool") + add_tool(self.toolbox.project, self.toolbox.item_factories, "tool") + item = self.toolbox.project.get_item("tool") language = "julia" self.toolbox.refresh_active_elements(item, None, {"tool"}) self.toolbox._setup_persistent_console(item, "filter1", ("tool", "filter1"), language) @@ -844,7 +844,7 @@ class TestToolboxUIWithTestSettings(TestCaseWithQApplication): def test_legacy_settings_keys_get_renamed(self): settings_dict = {"appSettings/useEmbeddedJulia": "julia value", "appSettings/useEmbeddedPython": "python value"} with toolbox_with_settings(settings_dict) as toolbox: - settings = toolbox.qsettings() + settings = toolbox.qsettings self.assertTrue(settings.contains("appSettings/useJuliaKernel")) self.assertTrue(settings.contains("appSettings/usePythonKernel")) self.assertEqual(settings.value("appSettings/useJuliaKernel"), "julia value") @@ -853,19 +853,19 @@ def test_legacy_settings_keys_get_renamed(self): def test_legacy_saveAtExit_value_0_is_updated_to_prompt(self): settings_dict = {"appSettings/saveAtExit": "0"} with toolbox_with_settings(settings_dict) as toolbox: - settings = toolbox.qsettings() + settings = toolbox.qsettings self.assertEqual(settings.value("appSettings/saveAtExit"), "prompt") def test_legacy_saveAtExit_value_1_is_updated_to_prompt(self): settings_dict = {"appSettings/saveAtExit": "1"} with toolbox_with_settings(settings_dict) as toolbox: - settings = toolbox.qsettings() + settings = toolbox.qsettings self.assertEqual(settings.value("appSettings/saveAtExit"), "prompt") def test_legacy_saveAtExit_value_2_is_updated_to_automatic(self): settings_dict = {"appSettings/saveAtExit": "2"} with toolbox_with_settings(settings_dict) as toolbox: - settings = toolbox.qsettings() + settings = toolbox.qsettings self.assertEqual(settings.value("appSettings/saveAtExit"), "automatic") diff --git a/tests/test_project_item_icon.py b/tests/test_project_item_icon.py index ea5c40e07..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) @@ -69,7 +69,7 @@ def test_conn_button(self): def test_outgoing_and_incoming_links(self): source_icon = ProjectItemIcon(self._toolbox, ":/icons/home.svg", QColor(Qt.GlobalColor.gray)) target_icon = ProjectItemIcon(self._toolbox, ":/icons/home.svg", QColor(Qt.GlobalColor.gray)) - self._toolbox.project().get_item = MagicMock() + self._toolbox.project.get_item = MagicMock() connection = LoggingConnection("source item", "bottom", "destination item", "bottom", toolbox=self._toolbox) link = Link(self._toolbox, source_icon.conn_button("bottom"), target_icon.conn_button("bottom"), connection) link.src_connector.links.append(link) @@ -78,7 +78,7 @@ def test_outgoing_and_incoming_links(self): self.assertEqual(target_icon.incoming_links(), [link]) def test_drag_icon(self): - item = add_view(self._toolbox.project(), self._toolbox.item_factories, "View") + item = add_view(self._toolbox.project, self._toolbox.item_factories, "View") icon = item.get_icon() self.assertEqual(icon.x(), 0.0) self.assertEqual(icon.y(), 0.0) @@ -93,7 +93,7 @@ def test_drag_icon(self): self.assertIsInstance(move_command, MoveIconCommand) def test_context_menu_event(self): - item = add_view(self._toolbox.project(), self._toolbox.item_factories, "View") + item = add_view(self._toolbox.project, self._toolbox.item_factories, "View") icon = item.get_icon() with patch("spinetoolbox.ui_main.ToolboxUI.show_project_or_item_context_menu") as mock_show_menu: mock_show_menu.return_value = True @@ -150,7 +150,7 @@ def setUp(self): source_item_icon.update_name_item("source icon") destination_item_icon = ProjectItemIcon(self._toolbox, ":/icons/home.svg", QColor(Qt.GlobalColor.gray)) destination_item_icon.update_name_item("destination icon") - project = self._toolbox.project() + project = self._toolbox.project project.get_item = MagicMock() connection = LoggingConnection("source icon", "right", "destination icon", "left", toolbox=self._toolbox) connection.link = self._link = Link( diff --git a/tests/widgets/test_jump_properties_widget.py b/tests/widgets/test_jump_properties_widget.py index 42e170e5d..9137ab631 100644 --- a/tests/widgets/test_jump_properties_widget.py +++ b/tests/widgets/test_jump_properties_widget.py @@ -61,7 +61,7 @@ def test_edit_condition(self): self.assertEqual(properties_widget._ui.condition_script_edit.toPlainText(), "exit(5)") def _set_link(self, properties_widget): - project = self._toolbox.project() + project = self._toolbox.project item1 = DataConnection("dc 1", "", 0.0, 0.0, self._toolbox, project) item2 = DataConnection("dc 2", "", 50.0, 0.0, self._toolbox, project) project.add_item(item1)