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