diff --git a/.github/workflows/execution_tests.yml b/.github/workflows/execution_tests.yml new file mode 100644 index 000000000..2d5367995 --- /dev/null +++ b/.github/workflows/execution_tests.yml @@ -0,0 +1,59 @@ +name: Execution tests + +on: + workflow_call: + inputs: + host-os: + required: true + type: string + python-version: + required: true + type: string + repository: + required: false + type: string + default: ${{ github.repository }} + post-installation-command: + required: false + type: string + default: "" + secrets: + CODECOV_TOKEN: + required: true + +jobs: + execution-tests: + name: Run execution tests + runs-on: ${{ inputs.host-os }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ inputs.repository }} + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: pip + cache-dependency-path: | + pyproject.toml + requirements.txt + dev-requirements.txt + - name: Install additional packages for Linux + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install -y libegl1 + - name: Install dependencies + env: + PYTHONUTF8: 1 + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -r dev-requirements.txt + - name: Run post-install command + if: ${{ inputs.post-installation-command }} + run: + ${{ inputs.post-installation-command }} + - name: Run tests + run: + python -m unittest discover --pattern execution_test.py --verbose diff --git a/.github/workflows/test_runner.yml b/.github/workflows/test_runner.yml index 0c8ae02d0..3e6f7287b 100644 --- a/.github/workflows/test_runner.yml +++ b/.github/workflows/test_runner.yml @@ -12,93 +12,27 @@ on: - "execution_tests/**" jobs: - unit-tests: + call-unit-tests: name: Unit tests - runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [windows-latest, ubuntu-22.04] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Version from Git tags - run: git describe --tags - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: | - pyproject.toml - requirements.txt - dev-requirements.txt - - name: Display Python version - run: - python -c "import sys; print(sys.version)" - - name: Install additional packages for Linux - if: runner.os == 'Linux' - run: | - sudo apt-get update -y - sudo apt-get install -y libegl1 - - name: Install dependencies - env: - PYTHONUTF8: 1 - run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - python -m pip install -r dev-requirements.txt - - name: List packages - run: - python -m pip list - - name: Run tests - run: | - if [ "$RUNNER_OS" != "Windows" ]; then - export QT_QPA_PLATFORM=offscreen - fi - coverage run -m unittest discover --verbose - shell: bash - - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - execution-tests: + uses: ./.github/workflows/unit_tests.yml + with: + host-os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + secrets: inherit + call-execution-tests: name: Execution tests - runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [windows-latest, ubuntu-22.04] -# needs: unit-tests - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: | - pyproject.toml - requirements.txt - dev-requirements.txt - - name: Install additional packages for Linux - if: runner.os == 'Linux' - run: | - sudo apt-get update -y - sudo apt-get install -y libegl1 - - name: Install dependencies - env: - PYTHONUTF8: 1 - run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - python -m pip install -r dev-requirements.txt - - name: List packages - run: - python -m pip list - - name: Run tests - run: - python -m unittest discover --pattern execution_test.py --verbose + uses: ./.github/workflows/execution_tests.yml + with: + host-os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + secrets: inherit diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 000000000..6e291dec8 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,75 @@ +name: Unit tests + +on: + workflow_call: + inputs: + host-os: + required: true + type: string + python-version: + required: true + type: string + repository: + required: false + type: string + default: ${{ github.repository }} + coverage: + required: false + type: boolean + default: true + post-installation-command: + required: false + type: string + default: "" + secrets: + CODECOV_TOKEN: + required: true + +jobs: + unit-tests: + name: Run unit tests + runs-on: ${{ inputs.host-os }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ inputs.repository }} + fetch-depth: 0 + - name: Version from Git tags + run: git describe --tags + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: pip + cache-dependency-path: | + pyproject.toml + requirements.txt + dev-requirements.txt + - name: Install additional packages for Linux + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install -y libegl1 + - name: Install dependencies + env: + PYTHONUTF8: 1 + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -r dev-requirements.txt + - name: Run post-install command + if: ${{ inputs.post-installation-command }} + run: + ${{ inputs.post-installation-command }} + - name: Run tests + run: | + if [ "$RUNNER_OS" != "Windows" ]; then + export QT_QPA_PLATFORM=offscreen + fi + ${{ inputs.coverage && 'coverage run' || 'python' }} -m unittest discover --verbose + shell: bash + - name: Upload coverage report to Codecov + if: ${{ inputs.coverage }} + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2ab8048..72f81f097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All **notable** changes to this project are documented here. -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/) ## [Unreleased] @@ -13,8 +13,56 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ### Removed +- The Filter button has been removed from Spine Database Editor's toolbar. + The functionality was broken anyhow and has been superseded by filtering from the Scenario tree. + ### Fixed +### Security + +## [0.9.1] + +### Changed + +- **Add/Update SpineOpt** wizard in **File->Settings->Tools** now installs SpineOpt v0.9.0 +from Julia's General Registry. Previously SpineOpt was installed from a custom +SpineJuliaRegistry registry. + +### Removed + +- Removed support for MSSQL dialect. It did not work anyway. + +## [0.9.0] + +Dropped support for Python 3.8. +This version of Spine Toolbox requires Python version from 3.9 to 3.12. + +### Changed + +- **Add/Update SpineOpt** wizard in **File->Settings->Tools** now requires SpineOpt v0.8.3 or higher. + +## [0.8.5] + +### Changed + +- Execution button shortcuts have been changed because F5 is traditionally reserved for refreshing. + The shortcuts are now: + + - **Shift+F9** to execute project + - **F9** to execute selection + - **F10** to stop execution + +### Fixed + +- Fixed a bug where scenario filters could cause Tracebacks in tools that were using ``spinedb_api``. + +## [0.8.4] + +### Changed + +- ``gamsapi`` replaced the ancient ``gdxcc`` package. + You may need to have a relatively recent GAMS installed to utilize .gdx import/export functionalities. + ## [0.8.3] ### Added diff --git a/README.md b/README.md index 399e99524..66e0ccaad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Spine Toolbox Link to the documentation: [https://spine-toolbox.readthedocs.io/en/latest/?badge=latest](https://spine-toolbox.readthedocs.io/en/latest/?badge=latest) -[![Python](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11|%203.12-blue.svg)](https://www.python.org/downloads/release/python-379/) +[![Python](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) [![Documentation Status](https://readthedocs.org/projects/spine-toolbox/badge/?version=latest)](https://spine-toolbox.readthedocs.io/en/latest/?badge=latest) [![Test suite](https://github.com/spine-tools/Spine-Toolbox/actions/workflows/test_runner.yml/badge.svg)](https://github.com/spine-tools/Spine-Toolbox/actions/workflows/test_runner.yml) [![codecov](https://codecov.io/gh/spine-tools/Spine-Toolbox/branch/master/graph/badge.svg)](https://codecov.io/gh/spine-tools/Spine-Toolbox) @@ -51,6 +51,7 @@ These steps apply to both [Python/pipx](#installation-with-python-and-pipx) opti 1. If you don't have Python installed, please install it e.g. from [Python.org](https://www.python.org/downloads/). +Please note that we support Python versions from 3.9 to 3.12. (As of 21st Oct. 2024, Python 3.13 has issues with some of our dependencies) 2. Test that python is now in your PATH. Open a new terminal (e.g. Command Prompt) window and type @@ -145,10 +146,10 @@ anywhere on your system.

Optional: Instead of venv, one can also use a [miniconda](https://docs.conda.io/projects/conda/en/stable/glossary.html#miniconda-glossary) environment. You can [download miniconda from here](https://docs.conda.io/en/latest/miniconda.html). **Note: Anaconda -environments are not supported.** Create a new Python 3.11 miniconda environment without linking packages from the +environments are not supported.** Create a new Python 3.12 (check the supported Python versions above) miniconda environment without linking packages from the base environment using - conda create -n spinetoolbox python=3.11 + conda create -n spinetoolbox python=3.12 4. Activate the venv environment on Windows (provided that you are in `Spine-Toolbox` directory) using @@ -244,7 +245,7 @@ run it, and follow the instructions to install Spine Toolbox. ### About requirements -Python 3.8.1 or later is required. Python 3.8.0 is not supported due to problems in DLL loading on Windows. +Python 3.9 or later is required (check above for supported Python versions). See the files `pyproject.toml` and `requirements.txt` for packages required to run Spine Toolbox. (Additional packages needed for development are listed in `dev-requirements.txt`.) @@ -277,7 +278,7 @@ also [Problems in starting the application](#problems-in-starting-the-applicatio #### Installation fails -Please make sure you are using Python 3.8.1 or later to install the requirements. +Please make sure you are using Python 3.9 or later to install the requirements (check above for supported Python versions). #### 'No Python' error when installing with pipx @@ -317,8 +318,6 @@ The required `qtconsole` package from the ***conda-forge*** channel also installs `qt` and `PyQt` packages. Since this is a `PySide6` application, those are not needed and there is a chance of conflicts between the packages. -**Note**: Python 3.8.0 is not supported. Use Python 3.8.1 or later. - ## Recorded Webinars showing the use of Spine Tools ### Spine Toolbox: Data, workflow and scenario management for modelling diff --git a/pyproject.toml b/pyproject.toml index d5c116fad..f194bb2b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,13 +11,13 @@ classifiers = [ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", ] -requires-python = ">=3.8.1" +requires-python = ">=3.9, <3.13" dependencies = [ - "PySide6 >= 6.5.0, != 6.5.3, != 6.6.3, != 6.7.0", + "PySide6 >= 6.5.0, != 6.5.3, != 6.6.3, != 6.7.0, < 6.8", "jupyter_client >=6.0", "qtconsole >=5.1", - "spinedb_api>=0.31.3", - "spine_engine>=0.24.2", + "spinedb_api>=0.32.1", + "spine_engine>=0.25.0", "numpy >=1.20.2", "matplotlib >= 3.5", "scipy >=1.7.1", @@ -26,7 +26,7 @@ dependencies = [ "Pygments >=2.8", "jill >=0.9.2", "pyzmq >=21.0", - "spine_items>=0.22.3", + "spine_items>=0.23.1", ] [project.urls] 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/config.py b/spinetoolbox/config.py index e3882ee35..a31424bf6 100644 --- a/spinetoolbox/config.py +++ b/spinetoolbox/config.py @@ -18,7 +18,7 @@ LATEST_PROJECT_VERSION = 13 # For the Add/Update SpineOpt wizard -REQUIRED_SPINE_OPT_VERSION = "0.6.9" +REQUIRED_SPINE_OPT_VERSION = "0.9.0" # Invalid characters for directory names # NOTE: "." is actually valid in a directory name but this is diff --git a/spinetoolbox/database_display_names.py b/spinetoolbox/database_display_names.py new file mode 100644 index 000000000..4b6ed677d --- /dev/null +++ b/spinetoolbox/database_display_names.py @@ -0,0 +1,120 @@ +###################################################################################################################### +# 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 . +###################################################################################################################### + +"""This module contains functionality to manage database display names.""" +import hashlib +import pathlib +from PySide6.QtCore import QObject, Signal, Slot +from sqlalchemy.engine.url import URL, make_url + + +class NameRegistry(QObject): + display_name_changed = Signal(str, str) + """Emitted when the display name of a database changes.""" + + def __init__(self, parent=None): + """ + Args: + parent (QObject, optional): parent object + """ + super().__init__(parent) + self._names_by_url: dict[str, set[str]] = {} + + @Slot(str, str) + def register(self, db_url, name): + """Registers a new name for given database URL. + + Args: + db_url (URL or str): database URL + name (str): name to register + """ + url = str(db_url) + if url in self._names_by_url and name in self._names_by_url[url]: + return + self._names_by_url.setdefault(url, set()).add(name) + self.display_name_changed.emit(url, self.display_name(db_url)) + + @Slot(str, str) + def unregister(self, db_url, name): + """Removes a name from the registry. + + Args: + db_url (URL or str): database URL + name (str): name to remove + """ + url = str(db_url) + names = self._names_by_url[url] + old_name = self.display_name(url) if len(names) in (1, 2) else None + names.remove(name) + if old_name is not None: + new_name = self.display_name(url) + self.display_name_changed.emit(url, new_name) + + def display_name(self, db_url): + """Makes display name for a database. + + Args: + db_url (URL or str): database URL + + Returns: + str: display name + """ + try: + registered_names = self._names_by_url[str(db_url)] + except KeyError: + return suggest_display_name(db_url) + else: + if len(registered_names) == 1: + return next(iter(registered_names)) + return suggest_display_name(db_url) + + def display_name_iter(self, db_maps): + """Yields database mapping display names. + + Args: + db_maps (Iterable of DatabaseMapping): database mappings + + Yields: + str: display name + """ + yield from (self.display_name(db_map.sa_url) for db_map in db_maps) + + def map_display_names_to_db_maps(self, db_maps): + """Returns a dictionary that maps display names to database mappings. + + Args: + db_maps (Iterable of DatabaseMapping): database mappings + + Returns: + dict: database mappings keyed by display names + """ + return {self.display_name(db_map.sa_url): db_map for db_map in db_maps} + + +def suggest_display_name(db_url): + """Returns a short name for the database mapping. + + Args: + db_url (URL or str): database URL + + Returns: + str: suggested name for the database for display purposes. + """ + if not isinstance(db_url, URL): + db_url = make_url(db_url) + if not db_url.drivername.startswith("sqlite"): + return db_url.database + if db_url.database is not None: + return pathlib.Path(db_url.database).stem + hashing = hashlib.sha1() + hashing.update(bytes(str(id(db_url)), "utf-8")) + return hashing.hexdigest() diff --git a/spinetoolbox/helpers.py b/spinetoolbox/helpers.py index 892083285..2d57b36bd 100644 --- a/spinetoolbox/helpers.py +++ b/spinetoolbox/helpers.py @@ -29,11 +29,13 @@ import tempfile import time from typing import Sequence # pylint: disable=unused-import +from xml.etree import ElementTree import matplotlib from PySide6.QtCore import QEvent, QFile, QIODevice, QObject, QPoint, QRect, QSize, Qt, QUrl, Slot from PySide6.QtCore import __version__ as qt_version from PySide6.QtCore import __version_info__ as qt_version_info from PySide6.QtGui import ( + QAction, QBrush, QColor, QCursor, @@ -1078,24 +1080,26 @@ def file_is_valid(parent, file_path, msgbox_title, extra_check=None): return True -def dir_is_valid(parent, dir_path, msgbox_title): +def dir_is_valid(parent, dir_path, msgbox_title, msg=None): """Checks that given path is a directory. Needed in - SettingsWdiget and KernelEditor because the QLineEdits + SettingsWidget and KernelEditor because the QLineEdits are editable. Returns True when dir_path is an empty string so that - we can use default values (e.g. from line edit place holder text) + we can use default values (e.g. from line edit placeholder text) Args: parent (QWidget): Parent widget for the message box dir_path (str): Directory path to check msgbox_title (str): Message box title + msg (str): Warning message Returns: bool: True if given path is an empty string or if path is an existing directory, False otherwise """ + if not msg: + msg = "Please select a valid directory" if dir_path == "": return True if not os.path.isdir(dir_path): - msg = "Please select a valid directory" # noinspection PyCallByClass, PyArgumentList QMessageBox.warning(parent, msgbox_title, msg) return False @@ -1864,3 +1868,38 @@ def order_key(name): if key_list and key_list[0].isdigit(): key_list.insert(0, "\U0010FFFF") return key_list + + +def add_keyboard_shortcut_to_tool_tip(action): + """Adds keyboard shortcut to action's tool tip. + + Args: + action (QAction): action to modify + """ + shortcut = action.shortcut() + if shortcut.isEmpty(): + return + tool_tip = action.toolTip() + if "" not in tool_tip or "

" not in tool_tip: + tool_tip = "

" + tool_tip + "

" + root = ElementTree.fromstring(tool_tip) + paragraphs = [paragraph for paragraph in root.iter("p")] + new_root = ElementTree.Element("qt") + for paragraph in paragraphs: + new_root.append(paragraph) + tool_tip_element = ElementTree.SubElement(new_root, "p") + ElementTree.SubElement(tool_tip_element, "em").text = shortcut.toString() + action.setToolTip(ElementTree.tostring(new_root, encoding="utf-8", method="html").decode()) + + +def add_keyboard_shortcuts_to_action_tool_tips(ui): + """Appends keyboard shortcuts to the tool tip texts of given UI's actions. + + Args: + ui (object): UI to modify + """ + for attribute in dir(ui): + action = getattr(ui, attribute) + if not isinstance(action, QAction): + continue + add_keyboard_shortcut_to_tool_tip(action) diff --git a/spinetoolbox/main.py b/spinetoolbox/main.py index e177879a8..f5105d922 100644 --- a/spinetoolbox/main.py +++ b/spinetoolbox/main.py @@ -47,7 +47,6 @@ def main(): ) if not pyside6_version_check(): return 1 - _add_pywin32_system32_to_path() parser = _make_argument_parser() args = parser.parse_args() if args.execute_only or args.list_items or args.execute_remotely: @@ -98,14 +97,3 @@ def _make_argument_parser(): ) parser.add_argument("--execute-remotely", help="execute remotely", action="append", metavar="SERVER CONFIG FILE") return parser - - -def _add_pywin32_system32_to_path(): - """Adds a directory to PATH on Windows that is required to make pywin32 work - on (Conda) Python 3.8. See https://github.com/spine-tools/Spine-Toolbox/issues/1230.""" - if sys.platform != "win32": - return - if sys.version_info[0:2] == (3, 8): - p = os.path.join(sys.exec_prefix, "Lib", "site-packages", "pywin32_system32") - if os.path.exists(p): - os.environ["PATH"] = p + ";" + os.environ["PATH"] diff --git a/spinetoolbox/multi_tab_windows.py b/spinetoolbox/multi_tab_windows.py new file mode 100644 index 000000000..cd5fc6716 --- /dev/null +++ b/spinetoolbox/multi_tab_windows.py @@ -0,0 +1,71 @@ +###################################################################################################################### +# 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 functionality to keep track on open MultiTabWindow instances.""" +from spinetoolbox.widgets.multi_tab_window import MultiTabWindow + + +class MultiTabWindowRegistry: + """Registry that holds multi tab windows.""" + + def __init__(self): + self._multi_tab_windows: list[MultiTabWindow] = [] + + def has_windows(self): + """Tests if there are any windows registered. + + Returns: + bool: True if editor windows exist, False otherwise + """ + return bool(self._multi_tab_windows) + + def windows(self): + """Returns a list of multi tab windows. + + Returns: + list of MultiTabWindow: windows + """ + return list(self._multi_tab_windows) + + def tabs(self): + """Returns a list of tabs across all windows. + + Returns: + list of QWidget: tab widgets + """ + return [ + window.tab_widget.widget(k) for window in self._multi_tab_windows for k in range(window.tab_widget.count()) + ] + + def register_window(self, window): + """Registers a new multi tab window. + + Args: + window (MultiTabWindow): window to register + """ + self._multi_tab_windows.append(window) + + def unregister_window(self, window): + """Removes multi tab window from the registry. + + Args: + window (MultiTabWindow): window to unregister + """ + self._multi_tab_windows.remove(window) + + def get_some_window(self): + """Returns a random multi tab window or None if none is available. + + Returns: + MultiTabWindow: editor window + """ + return self._multi_tab_windows[0] if self._multi_tab_windows else None diff --git a/spinetoolbox/plotting.py b/spinetoolbox/plotting.py index 1c5f3cd90..26af616ef 100644 --- a/spinetoolbox/plotting.py +++ b/spinetoolbox/plotting.py @@ -692,12 +692,13 @@ def plot_pivot_table_selection(model, model_indexes, plot_widget=None): return plot_data(data_list, plot_widget) -def plot_db_mngr_items(items, db_maps, plot_widget=None): +def plot_db_mngr_items(items, db_maps, db_name_registry, plot_widget=None): """Returns a plot widget with plots of database manager parameter value items. Args: items (list of dict): parameter value items db_maps (list of DatabaseMapping): database mappings corresponding to items + db_name_registry (NameRegistry): database display name registry plot_widget (PlotWidget, optional): widget to add plots to """ if not items: @@ -707,13 +708,13 @@ def plot_db_mngr_items(items, db_maps, plot_widget=None): root_node = TreeNode("database") for item, db_map in zip(items, db_maps): value = from_database(item["value"], item["type"]) + db_name = db_name_registry.display_name(db_map.sa_url) if value is None: continue try: leaf_content = _convert_to_leaf(value) except PlottingError as error: - raise PlottingError(f"Failed to plot value in {db_map.codename}: {error}") from error - db_name = db_map.codename + raise PlottingError(f"Failed to plot value in {db_name}: {error}") from error parameter_name = item["parameter_definition_name"] entity_byname = item["entity_byname"] if not isinstance(entity_byname, tuple): diff --git a/spinetoolbox/resources_icons_rc.py b/spinetoolbox/resources_icons_rc.py index d8f2ea2fb..2050a4460 100644 --- a/spinetoolbox/resources_icons_rc.py +++ b/spinetoolbox/resources_icons_rc.py @@ -11,7 +11,7 @@ # this program. If not, see . ###################################################################################################################### # Created by: object code -# Created by: The Resource Compiler for Qt version 6.7.2 +# Created by: The Resource Compiler for Qt version 6.7.3 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore @@ -32602,7 +32602,7 @@ \x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x06A\x9c\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ \x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x06\x978\ -\x00\x00\x01\x91\xeb21|\ +\x00\x00\x01\x93D[e\x88\ \x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x062\x92\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ \x00\x00\x04\x94\x00\x00\x00\x00\x00\x01\x00\x06\xaa\xa4\ @@ -32662,7 +32662,7 @@ \x00\x00\x09z\x00\x00\x00\x00\x00\x01\x00\x07P}\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x07=|\ -\x00\x00\x01\x91\xeb21}\ +\x00\x00\x01\x93D[e\x88\ \x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x07gO\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x076\x00\x00\x00\x00\x00\x01\x00\x07\x0f\x05\ @@ -32736,9 +32736,9 @@ \x00\x00\x07\xca\x00\x00\x00\x00\x00\x01\x00\x07!\x85\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x0b\x16\x00\x00\x00\x00\x00\x01\x00\x07\x9c\x9c\ -\x00\x00\x01f\xd4x\xbb0\ +\x00\x00\x01\x93D[e\x89\ \x00\x00\x06\xf8\x00\x00\x00\x00\x00\x01\x00\x06\xfc\x96\ -\x00\x00\x01\x91\xeb21~\ +\x00\x00\x01\x93D[e\x89\ \x00\x00\x0a\xfe\x00\x00\x00\x00\x00\x01\x00\x07\x9b6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ \x00\x00\x06\xce\x00\x00\x00\x00\x00\x01\x00\x06\xf6R\ diff --git a/spinetoolbox/resources_logos_rc.py b/spinetoolbox/resources_logos_rc.py index 5a4ad7cd7..6cc3ebc57 100644 --- a/spinetoolbox/resources_logos_rc.py +++ b/spinetoolbox/resources_logos_rc.py @@ -11,7 +11,7 @@ # this program. If not, see . ###################################################################################################################### # Created by: object code -# Created by: The Resource Compiler for Qt version 6.7.1 +# Created by: The Resource Compiler for Qt version 6.7.3 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore diff --git a/spinetoolbox/spine_db_commands.py b/spinetoolbox/spine_db_commands.py index a7467f851..2495e1444 100644 --- a/spinetoolbox/spine_db_commands.py +++ b/spinetoolbox/spine_db_commands.py @@ -107,7 +107,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs): self.redo_data = data self.undo_ids = None self._check = check - self.setText(f"add {item_type} items to {db_map.codename}") + self.setText(f"add {item_type} items to {db_mngr.name_registry.display_name(db_map.sa_url)}") def redo(self): super().redo() @@ -143,7 +143,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs): if self.redo_data == self.undo_data: self.setObsolete(True) self._check = check - self.setText(f"update {item_type} items in {db_map.codename}") + self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}") def redo(self): super().redo() @@ -183,7 +183,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs): self.redo_update_data = None self.undo_remove_ids = None self.undo_update_data = None - self.setText(f"update {item_type} items in {db_map.codename}") + self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}") def redo(self): super().redo() @@ -225,7 +225,7 @@ def __init__(self, db_mngr, db_map, item_type, ids, check=True, **kwargs): self.item_type = item_type self.ids = ids self._check = check - self.setText(f"remove {item_type} items from {db_map.codename}") + self.setText(f"remove {item_type} items from {self.db_mngr.name_registry.display_name(db_map.sa_url)}") def redo(self): super().redo() diff --git a/spinetoolbox/spine_db_editor/editors.py b/spinetoolbox/spine_db_editor/editors.py new file mode 100644 index 000000000..d3b2f7d09 --- /dev/null +++ b/spinetoolbox/spine_db_editor/editors.py @@ -0,0 +1,16 @@ +###################################################################################################################### +# 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 Spine Database editor's window registry.""" +from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry + +db_editor_registry = MultiTabWindowRegistry() diff --git a/spinetoolbox/spine_db_editor/graphics_items.py b/spinetoolbox/spine_db_editor/graphics_items.py index a37da029f..ffc036858 100644 --- a/spinetoolbox/spine_db_editor/graphics_items.py +++ b/spinetoolbox/spine_db_editor/graphics_items.py @@ -157,7 +157,7 @@ def display_data(self): @property def display_database(self): - return ",".join([db_map.codename for db_map in self.db_maps]) + return ", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps)) @property def db_maps(self): @@ -370,7 +370,7 @@ def default_parameter_data(self): return { "entity_class_name": self.entity_class_name, "entity_byname": DB_ITEM_SEPARATOR.join(self.byname), - "database": self.first_db_map.codename, + "database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url), } def shape(self): @@ -667,7 +667,7 @@ def _populate_connect_entities_menu(self, menu): for name, db_map_ent_clss in self._db_map_entity_class_lists.items(): for db_map, ent_cls in db_map_ent_clss: icon = self.db_mngr.entity_class_icon(db_map, ent_cls["id"]) - action_name = name + "@" + db_map.codename + action_name = name + "@" + self.db_mngr.name_registry.display_name(db_map.sa_url) enabled = set(ent_cls["dimension_id_list"]) <= entity_class_ids_in_graph.get(db_map, set()) action_name_icon_enabled.append((action_name, icon, enabled)) for action_name, icon, enabled in sorted(action_name_icon_enabled): @@ -702,7 +702,11 @@ def _start_connecting_entities(self, action): class_name, db_name = action.text().split("@") db_map_ent_cls_lst = self._db_map_entity_class_lists[class_name] db_map, ent_cls = next( - iter((db_map, ent_cls) for db_map, ent_cls in db_map_ent_cls_lst if db_map.codename == db_name) + iter( + (db_map, ent_cls) + for db_map, ent_cls in db_map_ent_cls_lst + if self.db_mngr.name_registry.display_name(db_map.sa_url) == db_name + ) ) self._spine_db_editor.start_connecting_entities(db_map, ent_cls, self) diff --git a/spinetoolbox/spine_db_editor/main.py b/spinetoolbox/spine_db_editor/main.py index 11e8819d7..7fc105f1f 100644 --- a/spinetoolbox/spine_db_editor/main.py +++ b/spinetoolbox/spine_db_editor/main.py @@ -29,9 +29,9 @@ def main(): editor = MultiSpineDBEditor(db_mngr) if args.separate_tabs: for url in args.url: - editor.add_new_tab({url: None}) + editor.add_new_tab([url]) else: - editor.add_new_tab({url: None for url in args.url}) + editor.add_new_tab(args.url) editor.show() return_code = app.exec() return return_code diff --git a/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py b/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py index c1f11c2df..c4c7535be 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py @@ -20,10 +20,6 @@ class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem): """A root item representing a db.""" - @property - def item_type(self): - return "db" - @property def fetch_item_type(self): return "alternative" @@ -38,13 +34,8 @@ def _make_child(self, id_): class AlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem): """An alternative leaf item.""" - @property - def item_type(self): - return "alternative" - - @property - def icon_code(self): - return _ALTERNATIVE_ICON + item_type = "alternative" + icon_code = _ALTERNATIVE_ICON def tool_tip(self, column): if column == 0 and self.id: diff --git a/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py b/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py index 845be17d5..e797bcc84 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py @@ -24,7 +24,7 @@ class AlternativeModel(TreeModelBase): """A model to display alternatives in a tree view.""" def _make_db_item(self, db_map): - return DBItem(self, db_map) + return DBItem(self, db_map, self.db_mngr.name_registry) def mimeData(self, indexes): """Stores selected indexes into MIME data. diff --git a/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py b/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py index e4663f4fd..c3830d73f 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py @@ -11,6 +11,7 @@ ###################################################################################################################### """Empty models for dialogs as well as parameter definitions and values.""" +from typing import ClassVar from PySide6.QtCore import Qt from ...helpers import DB_ITEM_SEPARATOR, rows_to_row_count_tuples from ...mvcmodels.empty_row_model import EmptyRowModel @@ -22,6 +23,9 @@ class EmptyModelBase(EmptyRowModel): """Base class for all empty models that go in a CompoundModelBase subclass.""" + item_type: ClassVar[str] = None + can_be_filtered = False + def __init__(self, parent): """ Args: @@ -33,10 +37,6 @@ def __init__(self, parent): self.entity_class_id = None self._db_map_entities_to_add = {} - @property - def item_type(self): - raise NotImplementedError() - @property def field_map(self): return self._parent.field_map @@ -68,7 +68,7 @@ def add_items_to_db(self, db_map_data): def _notify_about_added_entities(self): editor = self.parent().parent() - popup = AddedEntitiesPopup(editor, self._db_map_entities_to_add) + popup = AddedEntitiesPopup(editor, self.db_mngr.name_registry, self._db_map_entities_to_add) popup.show() def _clean_to_be_added_entities(self, db_map_items): @@ -91,10 +91,6 @@ def _make_unique_id(self, item): which rows have been added and thus need to be removed.""" raise NotImplementedError() - @property - def can_be_filtered(self): - return False - def accepted_rows(self): return range(self.rowCount()) @@ -109,8 +105,8 @@ def handle_items_added(self, db_map_data): Finds and removes model items that were successfully added to the db.""" added_ids = set() for db_map, items in db_map_data.items(): + database = self.db_mngr.name_registry.display_name(db_map.sa_url) for item in items: - database = db_map.codename unique_id = (database, *self._make_unique_id(item)) added_ids.add(unique_id) removed_rows = [] @@ -167,8 +163,13 @@ def _make_db_map_data(self, rows): db_map_data = {} for item in items: database = item.pop("database") - db_map = next(iter(x for x in self.db_mngr.db_maps if x.codename == database), None) - if not db_map: + try: + db_map = next( + iter( + x for x in self.db_mngr.db_maps if self.db_mngr.name_registry.display_name(x.sa_url) == database + ) + ) + except StopIteration: continue item = {k: v for k, v in item.items() if v is not None} db_map_data.setdefault(db_map, []).append(item) @@ -177,7 +178,10 @@ def _make_db_map_data(self, rows): def data(self, index, role=Qt.ItemDataRole.DisplayRole): if role == DB_MAP_ROLE: database = self.data(index, Qt.ItemDataRole.DisplayRole) - return next(iter(x for x in self.db_mngr.db_maps if x.codename == database), None) + return next( + iter(x for x in self.db_mngr.db_maps if self.db_mngr.name_registry.display_name(x.sa_url) == database), + None, + ) return super().data(index, role) @@ -244,9 +248,7 @@ def _entity_class_name_candidates_by_entity(db_map, item): class EmptyParameterDefinitionModel(SplitValueAndTypeMixin, ParameterMixin, EmptyModelBase): """An empty parameter_definition model.""" - @property - def item_type(self): - return "parameter_definition" + item_type = "parameter_definition" def _make_unique_id(self, item): return tuple(item.get(x) for x in ("entity_class_name", "name")) @@ -268,9 +270,7 @@ class EmptyParameterValueModel( ): """An empty parameter_value model.""" - @property - def item_type(self): - return "parameter_value" + item_type = "parameter_value" @staticmethod def _check_item(item): @@ -309,9 +309,7 @@ def _entity_class_name_candidates(self, db_map, item): class EmptyEntityAlternativeModel(MakeEntityOnTheFlyMixin, EntityMixin, EmptyModelBase): - @property - def item_type(self): - return "entity_alternative" + item_type = "entity_alternative" @staticmethod def _check_item(item): diff --git a/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py b/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py index 9886a48cf..6abbd9310 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py @@ -116,7 +116,10 @@ def _children_sort_key(self): def default_parameter_data(self): """Return data to put as default in a parameter table when this item is selected.""" - return {"entity_class_name": self.name, "database": self.first_db_map.codename} + return { + "entity_class_name": self.name, + "database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url), + } @property def display_data(self): @@ -260,12 +263,13 @@ def set_data(self, column, value, role): def default_parameter_data(self): """Return data to put as default in a parameter table when this item is selected.""" item = self.db_map_data(self.first_db_map) + db_name = self.db_mngr.name_registry.display_name(self.first_db_map.sa_url) if not item: - return {"database": self.first_db_map.codename} + return {"database": db_name} return { "entity_class_name": item["entity_class_name"], "entity_byname": DB_ITEM_SEPARATOR.join(item["entity_byname"]), - "database": self.first_db_map.codename, + "database": db_name, } def is_valid(self): diff --git a/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py b/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py index f439ae0e5..1329e956c 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py @@ -336,7 +336,7 @@ def _tooltip_from_data(self, row, column): elif header == "index": tool_tip = str(value[1]) elif header == "database": - tool_tip = value.codename + tool_tip = self.db_mngr.name_registry.display_name(value.sa_url) elif header == "entity": db_map, id_ = value tool_tip = self.db_mngr.get_item(db_map, "entity", id_).get("description") @@ -365,7 +365,7 @@ def _name_from_data(self, value, header): if header == "index": return str(value[1]) if header == "database": - return value.codename + return self.db_mngr.name_registry.display_name(value.sa_url) db_map, id_ = value item = self.db_mngr.get_item(db_map, "entity", id_) return item.get("name") diff --git a/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py b/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py index 241d3179c..c84e9d22f 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py @@ -127,7 +127,7 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole: if column == Column.DB_MAP: db_map = self._data[row][column] if row < len(self._data) else self._adder_row[column] - return db_map.codename if db_map is not None else "" + return self._db_mngr.name_registry.display_name(db_map.sa_url) if db_map is not None else "" return self._data[row][column] if row < len(self._data) else self._adder_row[column] if ( role == Qt.ItemDataRole.BackgroundRole @@ -208,7 +208,7 @@ def batch_set_data(self, indexes, values): columns = [] previous_values = [] data_length = len(self._data) - available_codenames = {db_map.codename: db_map for db_map in self._db_maps} + available_codenames = self._db_mngr.name_registry.map_display_names_to_db_maps(self._db_maps) reserved = self._reserved_metadata() for index, value in zip(indexes, values): if not self.flags(index) & Qt.ItemIsEditable: @@ -440,35 +440,33 @@ def _remove_data(self, db_map_data, id_column): self._data = self._data[:row] + self._data[row + count :] self.endRemoveRows() - def sort(self, column, order=Qt.AscendingOrder): + def sort(self, column, order=Qt.SortOrder.AscendingOrder): if not self._data or column < 0: return def db_map_sort_key(row): db_map = row[Column.DB_MAP] - return db_map.codename if db_map is not None else "" + return self._db_mngr.name_registry.display_name(db_map.sa_url) if db_map is not None else "" sort_key = itemgetter(column) if column != Column.DB_MAP else db_map_sort_key - self._data.sort(key=sort_key, reverse=order == Qt.DescendingOrder) + self._data.sort(key=sort_key, reverse=order == Qt.SortOrder.DescendingOrder) top_left = self.index(0, 0) bottom_right = self.index(len(self._data) - 1, Column.DB_MAP) self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.BackgroundRole]) - def _find_db_map(self, codename): - """Finds database mapping with given codename. + def _find_db_map(self, name): + """Finds database mapping with given name. Args: - codename (str): database mapping's code name + name (str): database mapping's name Returns: - DiffDatabaseMapping: database mapping or None if not found + DatabaseMapping: database mapping or None if not found """ - match = None - for db_map in self._db_maps: - if codename == db_map.codename: - match = db_map - break - return match + return next( + iter(db_map for db_map in self._db_maps if name == self._db_mngr.name_registry.display_name(db_map.sa_url)), + None, + ) def _reserved_metadata(self): """Collects metadata names and values that are already in database. diff --git a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py index 5c8a329be..e265e7017 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py @@ -117,7 +117,7 @@ def display_data(self): @property def display_database(self): """Returns the database for display.""" - return ",".join([db_map.codename for db_map in self.db_maps]) + return ", ".join(self.model.db_mngr.name_registry.display_name_iter(self._db_map_ids)) @property def display_icon(self): @@ -489,7 +489,7 @@ def data(self, column, role=Qt.ItemDataRole.DisplayRole): def default_parameter_data(self): """Returns data to set as default in a parameter table when this item is selected.""" - return {"database": self.first_db_map.codename} + return {"database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url)} def tear_down(self): super().tear_down() diff --git a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_model.py b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_model.py index 7a9650524..d2727fa9b 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_model.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_model.py @@ -19,8 +19,7 @@ class MultiDBTreeModel(MinimalTreeModel): """Base class for all tree models in Spine db editor.""" def __init__(self, db_editor, db_mngr, *db_maps): - """Init class. - + """ Args: db_editor (SpineDBEditor) db_mngr (SpineDBManager): A manager for the given db_maps diff --git a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py index 2e7af66b5..77bfa7f2b 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py @@ -30,10 +30,6 @@ class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem): """An item representing a db.""" - @property - def item_type(self): - return "db" - @property def fetch_item_type(self): return "parameter_value_list" @@ -50,9 +46,7 @@ class ListItem( ): """A list item.""" - @property - def item_type(self): - return "parameter_value_list" + item_type = "parameter_value_list" @property def fetch_item_type(self): @@ -95,9 +89,7 @@ def update_item_in_db(self, db_item): class ValueItem(GrayIfLastMixin, EditableMixin, LeafItem): - @property - def item_type(self): - return "list_value" + item_type = "list_value" def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole and not self.id: diff --git a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py index b2d704dcf..f922fa353 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py @@ -20,7 +20,7 @@ class ParameterValueListModel(TreeModelBase): """A model to display parameter_value_list data in a tree view.""" def _make_db_item(self, db_map): - return DBItem(self, db_map) + return DBItem(self, db_map, self.db_mngr.name_registry) def columnCount(self, parent=QModelIndex()): """Returns the number of columns under the given parent. Always 1.""" diff --git a/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py b/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py index 5802ceb67..554cd0c1d 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py @@ -278,7 +278,7 @@ class TopLeftDatabaseHeaderItem(TopLeftHeaderItem): def __init__(self, model): super().__init__(model) - self._suggested_codename = None + self._suggested_db_name = None @property def header_type(self): @@ -290,7 +290,7 @@ def name(self): def header_data(self, header_id, role=Qt.ItemDataRole.DisplayRole): """See base class.""" - return header_id.codename + return self._model.db_mngr.name_registry.display_name(header_id.sa_url) def update_data(self, db_map_data): """See base class.""" @@ -300,17 +300,17 @@ def add_data(self, names, db_map): """See base class.""" return False - def set_data(self, codename): - """Sets database mapping's codename. + def set_data(self, name): + """Sets database mapping's name. Args: - codename (str): database codename + name (str): database name Returns: - bool: True if codename was acceptable, False otherwise + bool: True if name was acceptable, False otherwise """ - if any(db_map.codename == codename for db_map in self.model.db_maps): - self._suggested_codename = codename + if any(self._model.db_mngr.name_registry.display_name(db_map.sa_url) == name for db_map in self._model.db_maps): + self._suggested_db_name = name return True return False @@ -320,23 +320,23 @@ def take_suggested_db_map(self): Returns: DatabaseMapping: database mapping """ - if self._suggested_codename is not None: + if self._suggested_db_name is not None: for db_map in self.model.db_maps: - if db_map.codename == self._suggested_codename: - self._suggested_codename = None + if self._model.db_mngr.name_registry.display_name(db_map.sa_url) == self._suggested_db_name: + self._suggested_db_name = None return db_map - raise RuntimeError(f"Logic error: no such database mapping `{self._suggested_codename}`") + raise RuntimeError(f"Logic error: no such database mapping `{self._suggested_db_name}`") return next(iter(self.model.db_maps)) - def suggest_db_map_codename(self): - """Suggests a database mapping codename. + def suggest_db_map_name(self): + """Suggests a database mapping name. Returns: - str: codename + str: database display name """ - if self._suggested_codename is not None: - return self._suggested_codename - return next(iter(self.model.db_maps)).codename + if self._suggested_db_name is not None: + return self._suggested_db_name + return self._model.db_mngr.name_registry.display_name(next(iter(self.model.db_maps)).sa_url) class PivotTableModelBase(QAbstractTableModel): @@ -837,14 +837,14 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): with suppress(ValueError): database_header_column = self.model.pivot_rows.index("database") if index.column() == database_header_column: - return self.top_left_headers["database"].suggest_db_map_codename() + return self.top_left_headers["database"].suggest_db_map_name() elif ( self.emptyColumnCount() > 0 and index.column() == self.headerColumnCount() + self.dataColumnCount() ): with suppress(ValueError): database_header_row = self.model.pivot_columns.index("database") if index.row() == database_header_row: - return self.top_left_headers["database"].suggest_db_map_codename() + return self.top_left_headers["database"].suggest_db_map_name() return None if role == Qt.ItemDataRole.FontRole and self.index_in_top_left(index): font = QFont() @@ -1200,7 +1200,7 @@ def all_header_names(self, index): entity_names = [self.db_mngr.get_item(db_map, "entity", id_)["name"] for id_ in entity_ids] parameter_name = self.db_mngr.get_item(db_map, "parameter_definition", parameter_id).get("name", "") alternative_name = self.db_mngr.get_item(db_map, "alternative", alternative_id).get("name", "") - return entity_names, parameter_name, alternative_name, db_map.codename + return entity_names, parameter_name, alternative_name, self.db_mngr.name_registry.display_name(db_map.sa_url) def index_name(self, index): """Returns a string that concatenates the object and parameter names corresponding to the given data index. diff --git a/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py b/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py index 4a3ec4437..fcf0cd770 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py @@ -27,10 +27,6 @@ class ScenarioDBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem): """A root item representing a db.""" - @property - def item_type(self): - return "db" - @property def fetch_item_type(self): return "scenario" @@ -45,18 +41,13 @@ def _make_child(self, id_): class ScenarioItem(GrayIfLastMixin, EditableMixin, EmptyChildMixin, FetchMoreMixin, BoldTextMixin, LeafItem): """A scenario leaf item.""" - @property - def item_type(self): - return "scenario" + item_type = "scenario" + icon_code = _SCENARIO_ICON @property def fetch_item_type(self): return "scenario_alternative" - @property - def icon_code(self): - return _SCENARIO_ICON - def tool_tip(self, column): if column == 0 and not self.id: return "

Note: Scenario names longer than 20 characters might appear shortened in generated files.

" @@ -125,9 +116,7 @@ def _make_child(self, id_): class ScenarioAlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem): """A scenario alternative leaf item.""" - @property - def item_type(self): - return "scenario_alternative" + item_type = "scenario_alternative" def tool_tip(self, column): if column == 0: diff --git a/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py b/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py index ac574a019..4ab0d3eb0 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py @@ -24,7 +24,7 @@ class ScenarioModel(TreeModelBase): """A model to display scenarios in a tree view.""" def _make_db_item(self, db_map): - return ScenarioDBItem(self, db_map) + return ScenarioDBItem(self, db_map, self.db_mngr.name_registry) def supportedDropActions(self): return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction diff --git a/spinetoolbox/spine_db_editor/mvcmodels/single_models.py b/spinetoolbox/spine_db_editor/mvcmodels/single_models.py index 885fb1871..c43e843ec 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/single_models.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/single_models.py @@ -48,6 +48,7 @@ class SingleModelBase(HalfSortedTableModel): item_type: ClassVar[str] = NotImplemented group_fields: ClassVar[Iterable[str]] = () + can_be_filtered = True def __init__(self, parent, db_map, entity_class_id, committed, lazy=False): """ @@ -67,7 +68,9 @@ def __init__(self, parent, db_map, entity_class_id, committed, lazy=False): def __lt__(self, other): if self.entity_class_name == other.entity_class_name: - return self.db_map.codename < other.db_map.codename + return self.db_mngr.name_registry.display_name( + self.db_map.sa_url + ) < self.db_mngr.name_registry.display_name(other.db_map.sa_url) keys = {} for side, model in {"left": self, "right": other}.items(): dim = len(model.dimension_id_list) @@ -113,10 +116,6 @@ def dimension_id_list(self): def fixed_fields(self): return ["entity_class_name", "database"] - @property - def can_be_filtered(self): - return True - def _mapped_field(self, field): return self.field_map.get(field, field) @@ -202,7 +201,7 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return FIXED_FIELD_COLOR if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole, Qt.ItemDataRole.ToolTipRole): if field == "database": - return self.db_map.codename + return self.db_mngr.name_registry.display_name(self.db_map.sa_url) id_ = self._main_data[index.row()] item = self.db_mngr.get_item(self.db_map, self.item_type, id_) if role == Qt.ItemDataRole.ToolTipRole: diff --git a/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py b/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py index cb05c4eba..17369a576 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py @@ -11,6 +11,7 @@ ###################################################################################################################### """A tree model for parameter_value lists.""" +from typing import ClassVar from PySide6.QtCore import Qt from PySide6.QtGui import QBrush, QFont, QGuiApplication, QIcon from spinetoolbox.fetch_parent import FlexibleFetchParent @@ -21,9 +22,8 @@ class StandardTreeItem(TreeItem): """A tree item that fetches their children as they are inserted.""" - @property - def item_type(self): - return None + item_type: ClassVar[str] = None + icon_code: ClassVar[str] = None @property def db_mngr(self): @@ -33,10 +33,6 @@ def db_mngr(self): def display_data(self): return None - @property - def icon_code(self): - return None - def tool_tip(self, column): return None @@ -223,19 +219,18 @@ def handle_items_updated(self, db_map_data): class StandardDBItem(SortChildrenMixin, StandardTreeItem): """An item representing a db.""" - def __init__(self, model, db_map): - """Init class. + item_type = "db" + def __init__(self, model, db_map, db_name_registry): + """ Args: - model (MinimalTreeModel) - db_map (DatabaseMapping) + model (MinimalTreeModel): tree model + db_map (DatabaseMapping): database mapping + db_name_registry (NameRegistry): database display name registry """ super().__init__(model) self.db_map = db_map - - @property - def item_type(self): - return "db" + self._db_name_registry = db_name_registry def data(self, column, role=Qt.ItemDataRole.DisplayRole): """Shows Spine icon for fun.""" @@ -244,7 +239,7 @@ def data(self, column, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DecorationRole: return QIcon(":/symbols/Spine_symbol.png") if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return self.db_map.codename + return self._db_name_registry.display_name(self.db_map.sa_url) class LeafItem(StandardTreeItem): @@ -260,10 +255,6 @@ def __init__(self, model, identifier=None): def _make_item_data(self): return {"name": f"Type new {self.item_type} name here...", "description": ""} - @property - def item_type(self): - raise NotImplementedError() - @property def db_map(self): return self.parent_item.db_map diff --git a/spinetoolbox/spine_db_editor/mvcmodels/tree_model_base.py b/spinetoolbox/spine_db_editor/mvcmodels/tree_model_base.py index 412f6b185..bbf1d5db2 100644 --- a/spinetoolbox/spine_db_editor/mvcmodels/tree_model_base.py +++ b/spinetoolbox/spine_db_editor/mvcmodels/tree_model_base.py @@ -30,7 +30,7 @@ def __init__(self, db_editor, db_mngr, *db_maps): self.db_editor = db_editor self.db_mngr = db_mngr self.db_maps = db_maps - self.destroyed.connect(lambda _: self._tear_down_tree) + self.destroyed.connect(lambda _: self._invisible_root_item.tear_down_recursively()) def columnCount(self, parent=QModelIndex()): """Returns the number of columns under the given parent. Always 2. @@ -66,7 +66,3 @@ def db_item(item): def db_row(self, item): return self.db_item(item).child_number() - - def _tear_down_tree(self): - """Tears down tree items recursively""" - self._invisible_root_item.tear_down_recursively() diff --git a/spinetoolbox/spine_db_editor/ui/commit_viewer_affected_item_info.py b/spinetoolbox/spine_db_editor/ui/commit_viewer_affected_item_info.py index b1ffe9be9..5c5787140 100644 --- a/spinetoolbox/spine_db_editor/ui/commit_viewer_affected_item_info.py +++ b/spinetoolbox/spine_db_editor/ui/commit_viewer_affected_item_info.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'commit_viewer_affected_item_info.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/spine_db_editor/ui/db_commit_viewer.py b/spinetoolbox/spine_db_editor/ui/db_commit_viewer.py index a50a1e07b..c18d483ae 100644 --- a/spinetoolbox/spine_db_editor/ui/db_commit_viewer.py +++ b/spinetoolbox/spine_db_editor/ui/db_commit_viewer.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'db_commit_viewer.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/spine_db_editor/ui/parameter_type_editor.py b/spinetoolbox/spine_db_editor/ui/parameter_type_editor.py index bbe5ad146..09c4c9f21 100644 --- a/spinetoolbox/spine_db_editor/ui/parameter_type_editor.py +++ b/spinetoolbox/spine_db_editor/ui/parameter_type_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'parameter_type_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.6.3 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/spine_db_editor/ui/scenario_generator.py b/spinetoolbox/spine_db_editor/ui/scenario_generator.py index 44c1038b4..858da0526 100644 --- a/spinetoolbox/spine_db_editor/ui/scenario_generator.py +++ b/spinetoolbox/spine_db_editor/ui/scenario_generator.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'scenario_generator.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -92,7 +92,7 @@ def setupUi(self, Form): self.horizontalLayout.addWidget(self.alternative_list) - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout.addItem(self.horizontalSpacer) diff --git a/spinetoolbox/spine_db_editor/ui/select_databases.py b/spinetoolbox/spine_db_editor/ui/select_databases.py index e7aae00ab..0b3c26c81 100644 --- a/spinetoolbox/spine_db_editor/ui/select_databases.py +++ b/spinetoolbox/spine_db_editor/ui/select_databases.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'select_databases.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -58,7 +58,7 @@ def setupUi(self, Form): self.horizontalLayout.addWidget(self.deselect_all_button) - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout.addItem(self.horizontalSpacer) diff --git a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.py b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.py index 25bfacf30..541bcadde 100644 --- a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.py +++ b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'spine_db_editor_window.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -44,8 +44,8 @@ def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") MainWindow.resize(965, 1159) - MainWindow.setLayoutDirection(Qt.LayoutDirection.LeftToRight) - MainWindow.setDockOptions(QMainWindow.DockOption.AllowNestedDocks|QMainWindow.DockOption.AllowTabbedDocks|QMainWindow.DockOption.AnimatedDocks|QMainWindow.DockOption.GroupedDragging) + MainWindow.setLayoutDirection(Qt.LeftToRight) + MainWindow.setDockOptions(QMainWindow.AllowNestedDocks|QMainWindow.AllowTabbedDocks|QMainWindow.AnimatedDocks|QMainWindow.GroupedDragging) self.actionCommit = QAction(MainWindow) self.actionCommit.setObjectName(u"actionCommit") self.actionCommit.setEnabled(True) @@ -198,7 +198,7 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.alternative_dock_widget = QDockWidget(MainWindow) self.alternative_dock_widget.setObjectName(u"alternative_dock_widget") - self.alternative_dock_widget.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas) + self.alternative_dock_widget.setAllowedAreas(Qt.AllDockWidgetAreas) self.dockWidgetContents_15 = QWidget() self.dockWidgetContents_15.setObjectName(u"dockWidgetContents_15") self.verticalLayout_18 = QVBoxLayout(self.dockWidgetContents_15) @@ -207,10 +207,10 @@ def setupUi(self, MainWindow): self.verticalLayout_18.setContentsMargins(0, 0, 0, 0) self.alternative_tree_view = AlternativeTreeView(self.dockWidgetContents_15) self.alternative_tree_view.setObjectName(u"alternative_tree_view") - self.alternative_tree_view.setEditTriggers(QAbstractItemView.EditTrigger.AnyKeyPressed|QAbstractItemView.EditTrigger.DoubleClicked|QAbstractItemView.EditTrigger.EditKeyPressed) + self.alternative_tree_view.setEditTriggers(QAbstractItemView.AnyKeyPressed|QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) self.alternative_tree_view.setDragEnabled(True) - self.alternative_tree_view.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly) - self.alternative_tree_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.alternative_tree_view.setDragDropMode(QAbstractItemView.DragOnly) + self.alternative_tree_view.setSelectionMode(QAbstractItemView.ExtendedSelection) self.alternative_tree_view.setUniformRowHeights(False) self.verticalLayout_18.addWidget(self.alternative_tree_view) @@ -227,8 +227,8 @@ def setupUi(self, MainWindow): self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.treeView_parameter_value_list = ParameterValueListTreeView(self.dockWidgetContents) self.treeView_parameter_value_list.setObjectName(u"treeView_parameter_value_list") - self.treeView_parameter_value_list.setEditTriggers(QAbstractItemView.EditTrigger.AnyKeyPressed|QAbstractItemView.EditTrigger.DoubleClicked|QAbstractItemView.EditTrigger.EditKeyPressed) - self.treeView_parameter_value_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.treeView_parameter_value_list.setEditTriggers(QAbstractItemView.AnyKeyPressed|QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) + self.treeView_parameter_value_list.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView_parameter_value_list.header().setVisible(True) self.verticalLayout.addWidget(self.treeView_parameter_value_list) @@ -246,11 +246,11 @@ def setupUi(self, MainWindow): self.tableView_parameter_value = ParameterValueTableView(self.dockWidgetContents_2) self.tableView_parameter_value.setObjectName(u"tableView_parameter_value") self.tableView_parameter_value.setMouseTracking(True) - self.tableView_parameter_value.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) - self.tableView_parameter_value.setLayoutDirection(Qt.LayoutDirection.LeftToRight) + self.tableView_parameter_value.setContextMenuPolicy(Qt.DefaultContextMenu) + self.tableView_parameter_value.setLayoutDirection(Qt.LeftToRight) self.tableView_parameter_value.setTabKeyNavigation(False) - self.tableView_parameter_value.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) - self.tableView_parameter_value.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) + self.tableView_parameter_value.setSelectionBehavior(QAbstractItemView.SelectItems) + self.tableView_parameter_value.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.tableView_parameter_value.setSortingEnabled(False) self.tableView_parameter_value.setWordWrap(False) self.tableView_parameter_value.horizontalHeader().setHighlightSections(False) @@ -271,10 +271,10 @@ def setupUi(self, MainWindow): self.verticalLayout_10.setContentsMargins(0, 0, 0, 0) self.tableView_parameter_definition = ParameterDefinitionTableView(self.dockWidgetContents_5) self.tableView_parameter_definition.setObjectName(u"tableView_parameter_definition") - self.tableView_parameter_definition.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) + self.tableView_parameter_definition.setContextMenuPolicy(Qt.DefaultContextMenu) self.tableView_parameter_definition.setTabKeyNavigation(False) - self.tableView_parameter_definition.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) - self.tableView_parameter_definition.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) + self.tableView_parameter_definition.setSelectionBehavior(QAbstractItemView.SelectItems) + self.tableView_parameter_definition.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.tableView_parameter_definition.setSortingEnabled(False) self.tableView_parameter_definition.setWordWrap(False) self.tableView_parameter_definition.horizontalHeader().setHighlightSections(False) @@ -287,7 +287,7 @@ def setupUi(self, MainWindow): MainWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dockWidget_parameter_definition) self.dockWidget_entity_tree = QDockWidget(MainWindow) self.dockWidget_entity_tree.setObjectName(u"dockWidget_entity_tree") - self.dockWidget_entity_tree.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas) + self.dockWidget_entity_tree.setAllowedAreas(Qt.AllDockWidgetAreas) self.dockWidgetContents_6 = QWidget() self.dockWidgetContents_6.setObjectName(u"dockWidgetContents_6") self.verticalLayout_4 = QVBoxLayout(self.dockWidgetContents_6) @@ -301,9 +301,9 @@ def setupUi(self, MainWindow): sizePolicy1.setVerticalStretch(0) sizePolicy1.setHeightForWidth(self.treeView_entity.sizePolicy().hasHeightForWidth()) self.treeView_entity.setSizePolicy(sizePolicy1) - self.treeView_entity.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed) - self.treeView_entity.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.treeView_entity.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) + self.treeView_entity.setEditTriggers(QAbstractItemView.EditKeyPressed) + self.treeView_entity.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.treeView_entity.setSelectionBehavior(QAbstractItemView.SelectItems) self.treeView_entity.setIconSize(QSize(20, 20)) self.treeView_entity.setUniformRowHeights(False) @@ -327,8 +327,8 @@ def setupUi(self, MainWindow): sizePolicy2.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) self.graphicsView.setSizePolicy(sizePolicy2) self.graphicsView.setMouseTracking(True) - self.graphicsView.setFrameShape(QFrame.Shape.NoFrame) - self.graphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + self.graphicsView.setFrameShape(QFrame.NoFrame) + self.graphicsView.setDragMode(QGraphicsView.ScrollHandDrag) self.verticalLayout_7.addWidget(self.graphicsView) @@ -359,9 +359,9 @@ def setupUi(self, MainWindow): self.verticalLayout_13.setContentsMargins(0, 0, 0, 0) self.pivot_table = PivotTableView(self.dockWidgetContents_10) self.pivot_table.setObjectName(u"pivot_table") - self.pivot_table.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) + self.pivot_table.setContextMenuPolicy(Qt.DefaultContextMenu) self.pivot_table.setTabKeyNavigation(False) - self.pivot_table.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) + self.pivot_table.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.verticalLayout_13.addWidget(self.pivot_table) @@ -379,8 +379,8 @@ def setupUi(self, MainWindow): self.frozen_table.setObjectName(u"frozen_table") self.frozen_table.setAcceptDrops(True) self.frozen_table.setTabKeyNavigation(False) - self.frozen_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) - self.frozen_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.frozen_table.setSelectionMode(QAbstractItemView.NoSelection) + self.frozen_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.frozen_table.horizontalHeader().setVisible(False) self.frozen_table.verticalHeader().setVisible(False) @@ -391,8 +391,8 @@ def setupUi(self, MainWindow): self.dockWidget_exports = QDockWidget(MainWindow) self.dockWidget_exports.setObjectName(u"dockWidget_exports") self.dockWidget_exports.setMaximumSize(QSize(524287, 64)) - self.dockWidget_exports.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable) - self.dockWidget_exports.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) + self.dockWidget_exports.setFeatures(QDockWidget.DockWidgetClosable) + self.dockWidget_exports.setAllowedAreas(Qt.BottomDockWidgetArea) self.dockWidgetContents_12 = QWidget() self.dockWidgetContents_12.setObjectName(u"dockWidgetContents_12") self.horizontalLayout_3 = QHBoxLayout(self.dockWidgetContents_12) @@ -457,11 +457,11 @@ def setupUi(self, MainWindow): self.scenario_tree_view = ScenarioTreeView(self.dockWidgetContents_9) self.scenario_tree_view.setObjectName(u"scenario_tree_view") self.scenario_tree_view.setAcceptDrops(True) - self.scenario_tree_view.setEditTriggers(QAbstractItemView.EditTrigger.AnyKeyPressed|QAbstractItemView.EditTrigger.DoubleClicked|QAbstractItemView.EditTrigger.EditKeyPressed) + self.scenario_tree_view.setEditTriggers(QAbstractItemView.AnyKeyPressed|QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed) self.scenario_tree_view.setDragEnabled(True) - self.scenario_tree_view.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop) - self.scenario_tree_view.setDefaultDropAction(Qt.DropAction.MoveAction) - self.scenario_tree_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.scenario_tree_view.setDragDropMode(QAbstractItemView.DragDrop) + self.scenario_tree_view.setDefaultDropAction(Qt.MoveAction) + self.scenario_tree_view.setSelectionMode(QAbstractItemView.ExtendedSelection) self.scenario_tree_view.setUniformRowHeights(False) self.verticalLayout_12.addWidget(self.scenario_tree_view) @@ -479,7 +479,7 @@ def setupUi(self, MainWindow): self.tableView_entity_alternative = EntityAlternativeTableView(self.dockWidgetContents_3) self.tableView_entity_alternative.setObjectName(u"tableView_entity_alternative") self.tableView_entity_alternative.setTabKeyNavigation(False) - self.tableView_entity_alternative.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) + self.tableView_entity_alternative.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.tableView_entity_alternative.setWordWrap(False) self.tableView_entity_alternative.horizontalHeader().setHighlightSections(False) self.tableView_entity_alternative.verticalHeader().setVisible(False) @@ -538,22 +538,25 @@ def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) self.actionCommit.setText(QCoreApplication.translate("MainWindow", u"&Commit...", None)) #if QT_CONFIG(tooltip) - self.actionCommit.setToolTip(QCoreApplication.translate("MainWindow", u"

Commit

Ctrl+Return

", None)) + self.actionCommit.setToolTip(QCoreApplication.translate("MainWindow", u"

Commit changes to database

", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionCommit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Return", None)) #endif // QT_CONFIG(shortcut) self.actionRollback.setText(QCoreApplication.translate("MainWindow", u"Roll&back", None)) #if QT_CONFIG(tooltip) - self.actionRollback.setToolTip(QCoreApplication.translate("MainWindow", u"

Rollback

Ctrl+Backspace

", None)) + self.actionRollback.setToolTip(QCoreApplication.translate("MainWindow", u"

Rollback changes since last commit.

Ctrl+Backspace

", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionRollback.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Backspace", None)) #endif // QT_CONFIG(shortcut) self.actionClose.setText(QCoreApplication.translate("MainWindow", u"Close", None)) +#if QT_CONFIG(tooltip) + self.actionClose.setToolTip(QCoreApplication.translate("MainWindow", u"Close current tab", None)) +#endif // QT_CONFIG(tooltip) self.actionImport.setText(QCoreApplication.translate("MainWindow", u"I&mport...", None)) #if QT_CONFIG(tooltip) - self.actionImport.setToolTip(QCoreApplication.translate("MainWindow", u"

Import data from file

", None)) + self.actionImport.setToolTip(QCoreApplication.translate("MainWindow", u"

Import data or template from file

", None)) #endif // QT_CONFIG(tooltip) self.actionExport.setText(QCoreApplication.translate("MainWindow", u"&Export...", None)) #if QT_CONFIG(tooltip) @@ -561,79 +564,91 @@ def retranslateUi(self, MainWindow): #endif // QT_CONFIG(tooltip) self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"Cop&y as text", None)) #if QT_CONFIG(tooltip) - self.actionCopy.setToolTip(QCoreApplication.translate("MainWindow", u"

Copy as text

Ctrl+C

", None)) + self.actionCopy.setToolTip(QCoreApplication.translate("MainWindow", u"

Copy selection to clipboard

Ctrl+C

", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionCopy.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+C", None)) #endif // QT_CONFIG(shortcut) self.actionPaste.setText(QCoreApplication.translate("MainWindow", u"P&aste", None)) #if QT_CONFIG(tooltip) - self.actionPaste.setToolTip(QCoreApplication.translate("MainWindow", u"

Paste

Ctrl+V

", None)) + self.actionPaste.setToolTip(QCoreApplication.translate("MainWindow", u"

Paste into selection

Ctrl+V

", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None)) #endif // QT_CONFIG(shortcut) self.actionStacked_style.setText(QCoreApplication.translate("MainWindow", u"T&able", None)) #if QT_CONFIG(tooltip) - self.actionStacked_style.setToolTip(QCoreApplication.translate("MainWindow", u"Table view", None)) + self.actionStacked_style.setToolTip(QCoreApplication.translate("MainWindow", u"

Switch to Table view

", None)) #endif // QT_CONFIG(tooltip) self.actionGraph_style.setText(QCoreApplication.translate("MainWindow", u"&Graph", None)) #if QT_CONFIG(tooltip) - self.actionGraph_style.setToolTip(QCoreApplication.translate("MainWindow", u"Graph view", None)) + self.actionGraph_style.setToolTip(QCoreApplication.translate("MainWindow", u"

Switch to Graph view

", None)) #endif // QT_CONFIG(tooltip) self.actionView_history.setText(QCoreApplication.translate("MainWindow", u"&History...", None)) +#if QT_CONFIG(tooltip) + self.actionView_history.setToolTip(QCoreApplication.translate("MainWindow", u"

Open Commit viewer

", None)) +#endif // QT_CONFIG(tooltip) self.actionMass_remove_items.setText(QCoreApplication.translate("MainWindow", u"P&urge...", None)) #if QT_CONFIG(tooltip) self.actionMass_remove_items.setToolTip(QCoreApplication.translate("MainWindow", u"

Mass-remove items

", None)) #endif // QT_CONFIG(tooltip) self.actionExport_session.setText(QCoreApplication.translate("MainWindow", u"E&xport session...", None)) #if QT_CONFIG(tooltip) - self.actionExport_session.setToolTip(QCoreApplication.translate("MainWindow", u"

Export current session (changes since last commit) into file

", None)) + self.actionExport_session.setToolTip(QCoreApplication.translate("MainWindow", u"

Export changes since last commit into file

", None)) #endif // QT_CONFIG(tooltip) self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"Settings...", None)) +#if QT_CONFIG(tooltip) + self.actionSettings.setToolTip(QCoreApplication.translate("MainWindow", u"

Open editor settings

", None)) +#endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionSettings.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+,", None)) #endif // QT_CONFIG(shortcut) self.actionUser_guide.setText(QCoreApplication.translate("MainWindow", u"User guide", None)) +#if QT_CONFIG(tooltip) + self.actionUser_guide.setToolTip(QCoreApplication.translate("MainWindow", u"

Open User guide in web browser

", None)) +#endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionUser_guide.setShortcut(QCoreApplication.translate("MainWindow", u"F1", None)) #endif // QT_CONFIG(shortcut) self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"Un&do", None)) #if QT_CONFIG(tooltip) - self.actionUndo.setToolTip(QCoreApplication.translate("MainWindow", u"

Undo

", None)) + self.actionUndo.setToolTip(QCoreApplication.translate("MainWindow", u"

Undo last action

", None)) #endif // QT_CONFIG(tooltip) self.actionRedo.setText(QCoreApplication.translate("MainWindow", u"&Redo", None)) #if QT_CONFIG(tooltip) - self.actionRedo.setToolTip(QCoreApplication.translate("MainWindow", u"

Redo

", None)) + self.actionRedo.setToolTip(QCoreApplication.translate("MainWindow", u"

Redo last undone action

", None)) #endif // QT_CONFIG(tooltip) self.actionNew_db_file.setText(QCoreApplication.translate("MainWindow", u"&New...", None)) #if QT_CONFIG(tooltip) - self.actionNew_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"

New database file

", None)) + self.actionNew_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"

Create new SQLite database file

", None)) #endif // QT_CONFIG(tooltip) self.actionOpen_db_file.setText(QCoreApplication.translate("MainWindow", u"&Open...", None)) #if QT_CONFIG(tooltip) - self.actionOpen_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"

Open database file

", None)) + self.actionOpen_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"

Open SQLite database file

", None)) #endif // QT_CONFIG(tooltip) self.actionAdd_db_file.setText(QCoreApplication.translate("MainWindow", u"&Add...", None)) #if QT_CONFIG(tooltip) - self.actionAdd_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"Add database file to the current view", None)) + self.actionAdd_db_file.setToolTip(QCoreApplication.translate("MainWindow", u"

Add database to the current view

", None)) #endif // QT_CONFIG(tooltip) self.actionVacuum.setText(QCoreApplication.translate("MainWindow", u"Vacuum", None)) +#if QT_CONFIG(tooltip) + self.actionVacuum.setToolTip(QCoreApplication.translate("MainWindow", u"

Optimize SQLite database file size

", None)) +#endif // QT_CONFIG(tooltip) self.actionValue.setText(QCoreApplication.translate("MainWindow", u"&Value", None)) #if QT_CONFIG(tooltip) - self.actionValue.setToolTip(QCoreApplication.translate("MainWindow", u"Pivot view: Value", None)) + self.actionValue.setToolTip(QCoreApplication.translate("MainWindow", u"

Switch to Pivot table's Value view

", None)) #endif // QT_CONFIG(tooltip) self.actionIndex.setText(QCoreApplication.translate("MainWindow", u"&Index", None)) #if QT_CONFIG(tooltip) - self.actionIndex.setToolTip(QCoreApplication.translate("MainWindow", u"Pivot view: Index", None)) + self.actionIndex.setToolTip(QCoreApplication.translate("MainWindow", u"

Switch to Pivot table's Index view

", None)) #endif // QT_CONFIG(tooltip) self.actionElement.setText(QCoreApplication.translate("MainWindow", u"E&lement", None)) #if QT_CONFIG(tooltip) - self.actionElement.setToolTip(QCoreApplication.translate("MainWindow", u"Pivot view: Element", None)) + self.actionElement.setToolTip(QCoreApplication.translate("MainWindow", u"

Switch to Pivot table's Element view to manage entity elements

", None)) #endif // QT_CONFIG(tooltip) self.actionScenario.setText(QCoreApplication.translate("MainWindow", u"&Scenario", None)) #if QT_CONFIG(tooltip) - self.actionScenario.setToolTip(QCoreApplication.translate("MainWindow", u"Pivot view: Scenario", None)) + self.actionScenario.setToolTip(QCoreApplication.translate("MainWindow", u"

Swith to Pivot table's Scenario view to manage scenarios

", None)) #endif // QT_CONFIG(tooltip) self.actionOpen_recent.setText(QCoreApplication.translate("MainWindow", u"Open recent", None)) self.actionGitHub.setText(QCoreApplication.translate("MainWindow", u"GitHub", None)) diff --git a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui index 0852f5715..4de573026 100644 --- a/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui +++ b/spinetoolbox/spine_db_editor/ui/spine_db_editor_window.ui @@ -27,10 +27,10 @@ MainWindow - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight - QMainWindow::DockOption::AllowNestedDocks|QMainWindow::DockOption::AllowTabbedDocks|QMainWindow::DockOption::AnimatedDocks|QMainWindow::DockOption::GroupedDragging + QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging @@ -42,7 +42,7 @@ - Qt::DockWidgetArea::AllDockWidgetAreas + Qt::AllDockWidgetAreas Alternative @@ -73,16 +73,16 @@ alternative tree - QAbstractItemView::EditTrigger::AnyKeyPressed|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed true - QAbstractItemView::DragDropMode::DragOnly + QAbstractItemView::DragOnly - QAbstractItemView::SelectionMode::ExtendedSelection + QAbstractItemView::ExtendedSelection false @@ -122,10 +122,10 @@ parameter value list - QAbstractItemView::EditTrigger::AnyKeyPressed|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - QAbstractItemView::SelectionMode::ExtendedSelection + QAbstractItemView::ExtendedSelection true @@ -165,22 +165,22 @@ true - Qt::ContextMenuPolicy::DefaultContextMenu + Qt::DefaultContextMenu parameter value - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight false - QAbstractItemView::SelectionBehavior::SelectItems + QAbstractItemView::SelectItems - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel false @@ -229,7 +229,7 @@ - Qt::ContextMenuPolicy::DefaultContextMenu + Qt::DefaultContextMenu parameter definition @@ -238,10 +238,10 @@ false - QAbstractItemView::SelectionBehavior::SelectItems + QAbstractItemView::SelectItems - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel false @@ -265,7 +265,7 @@ - Qt::DockWidgetArea::AllDockWidgetAreas + Qt::AllDockWidgetAreas Entity tree @@ -302,13 +302,13 @@ entity tree - QAbstractItemView::EditTrigger::EditKeyPressed + QAbstractItemView::EditKeyPressed - QAbstractItemView::SelectionMode::ExtendedSelection + QAbstractItemView::ExtendedSelection - QAbstractItemView::SelectionBehavior::SelectItems + QAbstractItemView::SelectItems @@ -360,10 +360,10 @@ true - QFrame::Shape::NoFrame + QFrame::NoFrame - QGraphicsView::DragMode::ScrollHandDrag + QGraphicsView::ScrollHandDrag @@ -406,13 +406,13 @@ - Qt::ContextMenuPolicy::DefaultContextMenu + Qt::DefaultContextMenu false - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel @@ -452,10 +452,10 @@ false - QAbstractItemView::SelectionMode::NoSelection + QAbstractItemView::NoSelection - QAbstractItemView::SelectionBehavior::SelectRows + QAbstractItemView::SelectRows false @@ -476,10 +476,10 @@ - QDockWidget::DockWidgetFeature::DockWidgetClosable + QDockWidget::DockWidgetClosable - Qt::DockWidgetArea::BottomDockWidgetArea + Qt::BottomDockWidgetArea Exports @@ -512,7 +512,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -637,19 +637,19 @@ scenario tree - QAbstractItemView::EditTrigger::AnyKeyPressed|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed true - QAbstractItemView::DragDropMode::DragDrop + QAbstractItemView::DragDrop - Qt::DropAction::MoveAction + Qt::MoveAction - QAbstractItemView::SelectionMode::ExtendedSelection + QAbstractItemView::ExtendedSelection false @@ -689,7 +689,7 @@ false - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel false @@ -776,7 +776,7 @@ &Commit... - <html><head/><body><p>Commit</p><p>Ctrl+Return</p></body></html> + <html><head/><body><p>Commit changes to database</p></body></html> Ctrl+Return @@ -794,7 +794,7 @@ Roll&back - <html><head/><body><p>Rollback</p><p>Ctrl+Backspace</p></body></html> + <html><head/><body><p>Rollback changes since last commit.</p><p>Ctrl+Backspace</p></body></html> Ctrl+Backspace @@ -811,6 +811,9 @@ Close + + Close current tab + @@ -824,7 +827,7 @@ I&mport... - <html><head/><body><p>Import data from file</p></body></html> + <html><head/><body><p>Import data or template from file</p></body></html> @@ -854,7 +857,7 @@ Cop&y as text - <html><head/><body><p>Copy as text</p><p>Ctrl+C</p></body></html> + <html><head/><body><p>Copy selection to clipboard</p><p>Ctrl+C</p></body></html> Ctrl+C @@ -872,7 +875,7 @@ P&aste - <html><head/><body><p>Paste</p><p>Ctrl+V</p></body></html> + <html><head/><body><p>Paste into selection</p><p>Ctrl+V</p></body></html> Ctrl+V @@ -890,7 +893,7 @@ T&able - Table view + <html><head/><body><p>Switch to Table view</p></body></html> @@ -905,7 +908,7 @@ &Graph - Graph view + <html><head/><body><p>Switch to Graph view</p></body></html> @@ -919,6 +922,9 @@ &History... + + <html><head/><body><p>Open Commit viewer</p></body></html> + @@ -947,7 +953,7 @@ E&xport session... - <html><head/><body><p>Export current session (changes since last commit) into file</p></body></html> + <html><head/><body><p>Export changes since last commit into file</p></body></html> @@ -958,6 +964,9 @@ Settings... + + <html><head/><body><p>Open editor settings</p></body></html> + Ctrl+, @@ -970,6 +979,9 @@ User guide + + <html><head/><body><p>Open User guide in web browser</p></body></html> + F1 @@ -986,7 +998,7 @@ Un&do - <html><head/><body><p>Undo</p></body></html> + <html><head/><body><p>Undo last action</p></body></html> @@ -1001,7 +1013,7 @@ &Redo - <html><head/><body><p>Redo</p></body></html> + <html><head/><body><p>Redo last undone action</p></body></html> @@ -1013,7 +1025,7 @@ &New... - <html><head/><body><p>New database file</p></body></html> + <html><head/><body><p>Create new SQLite database file</p></body></html> @@ -1025,7 +1037,7 @@ &Open... - <html><head/><body><p>Open database file</p></body></html> + <html><head/><body><p>Open SQLite database file</p></body></html> @@ -1037,7 +1049,7 @@ &Add... - Add database file to the current view + <html><head/><body><p>Add database to the current view</p></body></html> @@ -1048,6 +1060,9 @@ Vacuum + + <html><head/><body><p>Optimize SQLite database file size</p></body></html> + @@ -1061,7 +1076,7 @@ &Value - Pivot view: Value + <html><head/><body><p>Switch to Pivot table's Value view</p></body></html> @@ -1076,7 +1091,7 @@ &Index - Pivot view: Index + <html><head/><body><p>Switch to Pivot table's Index view</p></body></html> @@ -1091,7 +1106,7 @@ E&lement - Pivot view: Element + <html><head/><body><p>Switch to Pivot table's Element view to manage entity elements</p></body></html> @@ -1106,7 +1121,7 @@ &Scenario - Pivot view: Scenario + <html><head/><body><p>Swith to Pivot table's Scenario view to manage scenarios</p></body></html> diff --git a/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py index a7764234b..a97618ad6 100644 --- a/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py +++ b/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py @@ -145,11 +145,11 @@ def __init__(self, parent, db_mngr, *db_maps): Args: parent (SpineDBEditor) db_mngr (SpineDBManager) - *db_maps: DiffDatabaseMapping instances + *db_maps: DatabaseMapping instances """ super().__init__(parent, db_mngr) self.db_maps = db_maps - self.keyed_db_maps = {x.codename: x for x in db_maps} + self.keyed_db_maps = db_mngr.name_registry.map_display_names_to_db_maps(db_maps) self.remove_rows_button = QToolButton(self) self.remove_rows_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.remove_rows_button.setText("Remove selected rows") @@ -170,10 +170,8 @@ def remove_selected_rows(self, checked=True): self.model.removeRows(row, 1) def all_databases(self, row): - """Returns a list of db names available for a given row. - Used by delegates. - """ - return [x.codename for x in self.db_maps] + """Returns a list of db names available for a given row.""" + return [self.db_mngr.name_registry.display_name(x.sa_url) for x in self.db_maps] class AddEntityClassesDialog(ShowIconColorEditorMixin, GetEntityClassesMixin, AddItemsDialog): @@ -211,7 +209,7 @@ def __init__(self, parent, item, db_mngr, *db_maps, force_default=False): labels = ["dimension name (1)"] if dimension_one_name is not None else [] labels += ["entity class name", "description", "display icon", "active by default", "databases"] self.model.set_horizontal_header_labels(labels) - db_names = ",".join(x.codename for x in item.db_maps) + db_names = ", ".join(db_mngr.name_registry.display_name_iter(item.db_maps)) self.default_display_icon = None self.model.set_default_row( **{ @@ -310,7 +308,7 @@ def accept(self): db_names = row_data[db_column] if db_names is None: db_names = "" - for db_name in db_names.split(","): + for db_name in db_names.split(", "): if db_name not in self.keyed_db_maps: self.parent().msg_error.emit(f"Invalid database {db_name} at row {i + 1}") return @@ -501,7 +499,7 @@ def _class_key_to_str(self, key, *db_maps): class_name = self.db_map_ent_cls_lookup[db_maps[0]][key]["name"] if len(db_maps) == len(self.db_maps): return class_name - return class_name + "@(" + ", ".join(db_map.codename for db_map in db_maps) + ")" + return class_name + "@(" + ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) + ")" def _accepts_class(self, ent_cls): if self.entity_class is None: @@ -519,7 +517,7 @@ def _do_reset_model(self): header = self.dimension_name_list + ("entity name", "alternative", "entity group", "databases") self.model.set_horizontal_header_labels(header) default_db_maps = [db_map for db_map, keys in self.db_map_ent_cls_lookup.items() if self.class_key in keys] - db_names = ",".join([db_name for db_name, db_map in self.keyed_db_maps.items() if db_map in default_db_maps]) + db_names = ", ".join([db_name for db_name, db_map in self.keyed_db_maps.items() if db_map in default_db_maps]) alt_selection_model = self.parent().ui.alternative_tree_view.selectionModel() alt_selection = alt_selection_model.selection() selected_alt_name = None @@ -553,7 +551,7 @@ def append_db_codenames(self, name, db_maps): """ if len(db_maps) == len(self.parent().db_maps): return name - return name + "@(" + ", ".join(db_map.codename for db_map in db_maps) + ")" + return name + "@(" + ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) + ")" def get_db_map_data(self): db_map_data = {} @@ -570,7 +568,7 @@ def get_db_map_data(self): db_names = row_data[db_column] if db_names is None: db_names = "" - for db_name in db_names.split(","): + for db_name in db_names.split(", "): if db_name not in self.keyed_db_maps: self.parent().msg_error.emit(f"Invalid database {db_name} at row {i + 1}") return @@ -636,7 +634,7 @@ def make_entity_alternatives(self, entities): entity_name = row_data[name_column] entity = entities[entity_name] db_names = row_data[db_column] - for db_name in db_names.split(","): + for db_name in db_names.split(", "): db_map = self.keyed_db_maps[db_name] entity_alternatives.setdefault(db_map, []).append( { @@ -664,7 +662,7 @@ def make_entity_groups(self, entities): entity = entities[entity_name] class_name = entity["entity_class_name"] db_names = row_data[db_column] - for db_name in db_names.split(","): + for db_name in db_names.split(", "): db_map = self.keyed_db_maps[db_name] db_map_data.setdefault(db_map, {}).setdefault("entities", set()).add((class_name, entity_group)) db_map_data.setdefault(db_map, {}).setdefault("entity_groups", set()).add( @@ -725,8 +723,9 @@ def __init__(self, parent, item, db_mngr, *db_maps): self.existing_items_model = MinimalTableModel(self, lazy=False) self.new_items_model = MinimalTableModel(self, lazy=False) self.model.sub_models = [self.new_items_model, self.existing_items_model] - self.db_combo_box.addItems([db_map.codename for db_map in db_maps]) - self.reset_entity_class_combo_box(db_maps[0].codename) + names = list(db_mngr.name_registry.display_name_iter(db_maps)) + self.db_combo_box.addItems(names) + self.reset_entity_class_combo_box(names[0]) self.connect_signals() def _populate_layout(self): @@ -891,7 +890,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps): self.db_mngr = db_mngr self.db_maps = db_maps self.db_map = db_maps[0] - self.db_maps_by_codename = {db_map.codename: db_map for db_map in db_maps} + self.db_maps_by_db_name = db_mngr.name_registry.map_display_names_to_db_maps(db_maps) self.db_combo_box = QComboBox(self) self.header_widget = QWidget(self) self.group_name_line_edit = QLineEdit(self) @@ -938,7 +937,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps): layout.addWidget(self.members_tree, 1, 2) layout.addWidget(self.button_box, 2, 0, 1, 3) self.setAttribute(Qt.WA_DeleteOnClose) - self.db_combo_box.addItems(list(self.db_maps_by_codename)) + self.db_combo_box.addItems(list(self.db_maps_by_db_name)) self.db_map_entity_ids = { db_map: { x["name"]: x["id"] @@ -958,7 +957,7 @@ def connect_signals(self): self.remove_button.clicked.connect(self.remove_members) def reset_list_widgets(self, database): - self.db_map = self.db_maps_by_codename[database] + self.db_map = self.db_maps_by_db_name[database] entity_ids = self.db_map_entity_ids[self.db_map] members = [] non_members = [] @@ -1015,7 +1014,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps): self.setWindowTitle("Add entity group") self.group_name_line_edit.setFocus() self.group_name_line_edit.setPlaceholderText("Type group name here") - self.reset_list_widgets(db_maps[0].codename) + self.reset_list_widgets(self.db_mngr.name_registry.display_name(db_maps[0].sa_url)) self.connect_signals() def initial_member_ids(self): @@ -1071,7 +1070,7 @@ def __init__(self, parent, entity_item, db_mngr, *db_maps): self.group_name_line_edit.setReadOnly(True) self.group_name_line_edit.setText(entity_item.name) self.entity_item = entity_item - self.reset_list_widgets(db_maps[0].codename) + self.reset_list_widgets(self.db_mngr.name_registry.display_name(db_maps[0].sa_url)) self.connect_signals() def _entity_groups(self): diff --git a/spinetoolbox/spine_db_editor/widgets/commit_viewer.py b/spinetoolbox/spine_db_editor/widgets/commit_viewer.py index 94510aecc..0ffab7fa3 100644 --- a/spinetoolbox/spine_db_editor/widgets/commit_viewer.py +++ b/spinetoolbox/spine_db_editor/widgets/commit_viewer.py @@ -251,7 +251,7 @@ def __init__(self, qsettings, db_mngr, *db_maps, parent=None): self._current_index = 0 for db_map in self._db_maps: widget = _DBCommitViewer(self._db_mngr, db_map) - tab_widget.addTab(widget, db_map.codename) + tab_widget.addTab(widget, db_mngr.name_registry.display_name(db_map.sa_url)) restore_ui(self, self._qsettings, "commitViewer") self._qsettings.beginGroup("commitViewer") current = self.centralWidget().widget(self._current_index) diff --git a/spinetoolbox/spine_db_editor/widgets/custom_delegates.py b/spinetoolbox/spine_db_editor/widgets/custom_delegates.py index 98ec8ffb7..525c25a5b 100644 --- a/spinetoolbox/spine_db_editor/widgets/custom_delegates.py +++ b/spinetoolbox/spine_db_editor/widgets/custom_delegates.py @@ -275,7 +275,10 @@ class DatabaseNameDelegate(TableDelegate): def createEditor(self, parent, option, index): """Returns editor.""" editor = SearchBarEditor(self.parent(), parent) - editor.set_data(index.data(Qt.ItemDataRole.DisplayRole), [x.codename for x in self.db_mngr.db_maps]) + editor.set_data( + index.data(Qt.ItemDataRole.DisplayRole), + list(self.db_mngr.name_registry.display_name_iter(self.db_mngr.db_maps)), + ) editor.data_committed.connect(lambda *_: self._close_editor(editor, index)) return editor @@ -758,7 +761,7 @@ def _create_alternative_editor(self, parent, index): dbs_by_alternative_name = {} database_column = self.parent().model.horizontal_header_labels().index("databases") database_index = index.model().index(index.row(), database_column) - databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(",") + databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(", ") for db_map_codename in databases: # Filter possible alternatives based on selected databases db_map = self.parent().keyed_db_maps[db_map_codename] alternatives = self.parent().db_mngr.get_items(db_map, "alternative") @@ -781,11 +784,12 @@ def _create_entity_group_editor(self, parent, index): """ database_column = self.parent().model.horizontal_header_labels().index("databases") database_index = index.model().index(index.row(), database_column) - databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(",") + databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(", ") entity_class = self.parent().class_item - dbs_by_entity_group = {} # A mapping from entity_group to db_map(s) + dbs_by_entity_group = {} for db_map in entity_class.db_maps: - if db_map.codename not in databases: # Allow groups that are in selected DBs under "databases" -column. + if parent.db_mngr.name_registry.display_name(db_map.sa_url) not in databases: + # Allow groups that are in selected DBs under "databases" column. continue class_item = self.parent().db_mngr.get_item_by_field(db_map, "entity_class", "name", entity_class.name) if not class_item: @@ -814,7 +818,7 @@ def _create_database_editor(self, parent, index): """ editor = CheckListEditor(parent) all_databases = self.parent().all_databases(index.row()) - databases = index.data(Qt.ItemDataRole.DisplayRole).split(",") + databases = index.data(Qt.ItemDataRole.DisplayRole).split(", ") editor.set_data(all_databases, databases) return editor diff --git a/spinetoolbox/spine_db_editor/widgets/custom_menus.py b/spinetoolbox/spine_db_editor/widgets/custom_menus.py index 90cd7e3d2..94fa04ecd 100644 --- a/spinetoolbox/spine_db_editor/widgets/custom_menus.py +++ b/spinetoolbox/spine_db_editor/widgets/custom_menus.py @@ -28,11 +28,12 @@ class AutoFilterMenu(FilterMenuBase): def __init__(self, parent, db_mngr, db_maps, item_type, field, show_empty=True): """ Args: - parent (SpineDBEditor) + parent (SpineDBEditor): parent widget db_mngr (SpineDBManager) db_maps (Sequence of DatabaseMapping) item_type (str) field (str): the field name + show_empty (bool) """ super().__init__(parent) self._item_type = item_type @@ -62,12 +63,12 @@ def set_filter_rejected_values(self, rejected_values): def _get_value(self, item, db_map): if self._field == "database": - return db_map.codename + return self._db_mngr.name_registry.display_name(db_map.sa_url) return item[self._field] def _get_display_value(self, item, db_map): if self._field in ("value", "default_value"): - return self._db_mngr.get_value(db_map, item, role=Qt.DisplayRole) + return self._db_mngr.get_value(db_map, item, role=Qt.ItemDataRole.DisplayRole) if self._field == "entity_byname": return DB_ITEM_SEPARATOR.join(item[self._field]) return self._get_value(item, db_map) or "(empty)" @@ -216,20 +217,21 @@ def emit_filter_changed(self, valid_values): self.filterChanged.emit(self._identifier, valid_values, self._filter.has_filter()) -class TabularViewCodenameFilterMenu(TabularViewFilterMenuBase): - """Filter menu to filter database codenames in Pivot table.""" +class TabularViewDatabaseNameFilterMenu(TabularViewFilterMenuBase): + """Filter menu to filter database names in Pivot table.""" - def __init__(self, parent, db_maps, identifier, show_empty=True): + def __init__(self, parent, db_maps, identifier, db_name_registry, show_empty=True): """ Args: parent (SpineDBEditor): parent widget db_maps (Sequence of DatabaseMapping): database mappings identifier (str): header identifier + db_name_registry (NameRegistry): database display name registry show_empty (bool): if True, an empty row will be added to the end of the item list """ super().__init__(parent, identifier) self._set_up(SimpleFilterCheckboxListModel, self, show_empty=show_empty) - self._filter.set_filter_list([db_map.codename for db_map in db_maps]) + self._filter.set_filter_list(list(db_name_registry.display_name_iter(db_maps))) def emit_filter_changed(self, valid_values): """See base class.""" diff --git a/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py b/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py index 4358c8b6e..a36415b41 100644 --- a/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py +++ b/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py @@ -150,8 +150,7 @@ def __init__(self, file_path, progress, db_editor): @Slot(bool) def open_file(self, checked=False): - codename = os.path.splitext(self.file_name)[0] - self.db_editor._open_sqlite_url(self.url, codename) + self.db_editor.add_new_tab(self.url) class ShootingLabel(QLabel): @@ -459,14 +458,14 @@ def selections(self): class AddedEntitiesPopup(QDialog): """Class for showing automatically added entities""" - def __init__(self, parent, added_entities): + def __init__(self, parent, db_name_registry, added_entities): super().__init__(parent) self.setWindowTitle("Added Entities") self._textEdit = QTextEdit(self) self._text = None self._entity_names = None self._create_entity_names(added_entities) - self._create_text() + self._create_text(db_name_registry) self._textEdit.setHtml(self._text) self._textEdit.setReadOnly(True) self._textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) @@ -484,10 +483,10 @@ def __init__(self, parent, added_entities): self.setSizeGripEnabled(True) self.resize(400, 400) - def _create_text(self): + def _create_text(self, db_name_registry): lines = [] for db_map, classes in self._entity_names.items(): - lines.append(f"{db_map.codename}:") + lines.append(f"{db_name_registry.display_name(db_map.sa_url)}:") for cls_name, ent_names in classes.items(): lines.append(f"
  • {cls_name}:
  • ") for ent_name in ent_names: diff --git a/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py index 4bd3ffde6..389ea3ce5 100644 --- a/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py +++ b/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py @@ -32,11 +32,9 @@ def __init__(self, parent, db_mngr): self.items = [] def all_databases(self, row): - """Returns a list of db names available for a given row. - Used by delegates. - """ + """Returns a list of db names available for a given row.""" item = self.items[row] - return [db_map.codename for db_map in item.db_maps] + return list(self.db_mngr.name_registry.display_name_iter(item.db_maps)) class EditEntityClassesDialog(ShowIconColorEditorMixin, EditOrRemoveItemsDialog): @@ -89,8 +87,15 @@ def accept(self): db_names = "" item = self.items[i] db_maps = [] - for database in db_names.split(","): - db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None) + for database in db_names.split(", "): + db_map = next( + ( + db_map + for db_map in item.db_maps + if self.db_mngr.name_registry.display_name(db_map.sa_url) == database + ), + None, + ) if db_map is None: self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}") return @@ -137,7 +142,7 @@ def __init__(self, parent, db_mngr, selected, class_key): self.table_view.setItemDelegate(ManageEntitiesDelegate(self)) self.connect_signals() self.db_maps = set(db_map for item in selected for db_map in item.db_maps) - self.keyed_db_maps = {x.codename: x for x in self.db_maps} + self.keyed_db_maps = self.db_mngr.name_registry.map_display_names_to_db_maps(self.db_maps) self.class_key = class_key self.model.set_horizontal_header_labels( [x + " byname" for x in self.dimension_name_list] + ["entity name", "databases"] @@ -174,9 +179,16 @@ def accept(self): if db_names is None: db_names = "" db_maps = [] - for database in db_names.split(","): - db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None) - if db_map is None: + for database in db_names.split(", "): + try: + db_map = next( + ( + db_map + for db_map in item.db_maps + if self.db_mngr.name_registry.display_name(db_map.sa_url) == database + ) + ) + except StopIteration: self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}") return db_maps.append(db_map) @@ -187,7 +199,7 @@ def accept(self): entity_classes = self.db_map_ent_cls_lookup[db_map] if (self.class_key) not in entity_classes: self.parent().msg_error.emit( - f"Invalid entity class '{self.class_name}' for db '{db_map.codename}' at row {i + 1}" + f"Invalid entity class '{self.class_name}' for db '{self.db_mngr.name_registry.display_name(db_map.sa_url)}' at row {i + 1}" ) return ent_cls = entity_classes[self.class_key] @@ -198,7 +210,7 @@ def accept(self): for dimension_id, element_name in zip(dimension_id_list, element_name_list): if (dimension_id, element_name) not in entities: self.parent().msg_error.emit( - f"Invalid entity '{element_name}' for db '{db_map.codename}' at row {i + 1}" + f"Invalid entity '{element_name}' for db '{self.db_mngr.name_registry.display_name(db_map.sa_url)}' at row {i + 1}" ) return element_id = entities[dimension_id, element_name]["id"] @@ -248,8 +260,15 @@ def accept(self): db_names = "" item = self.items[i] db_maps = [] - for database in db_names.split(","): - db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None) + for database in db_names.split(", "): + db_map = next( + ( + db_map + for db_map in item.db_maps + if self.db_mngr.name_registry.display_name(db_map.sa_url) == database + ), + None, + ) if db_map is None: self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}") return @@ -282,7 +301,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps): combobox.setCurrentText(superclass_subclass["superclass_name"]) else: combobox.setCurrentIndex(0) - self._tab_widget.addTab(combobox, db_map.codename) + self._tab_widget.addTab(combobox, self.db_mngr.name_registry.display_name(db_map.sa_url)) self.connect_signals() self.setWindowTitle(f"Select {self._subclass_name}'s superclass") diff --git a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py index 1cb17cff6..a5428f125 100644 --- a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py +++ b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py @@ -661,7 +661,7 @@ def get_entity_key(self, db_map_entity_id): entity = self.db_mngr.get_item(db_map, "entity", entity_id) key = (entity["entity_class_name"], entity["dimension_name_list"], entity["entity_byname"]) if not self.ui.graphicsView.get_property("merge_dbs"): - key += (db_map.codename,) + key += (self.db_mngr.name_registry.display_name(db_map.sa_url),) return key def _update_entity_element_inds(self, db_map_element_id_lists): diff --git a/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py index 6d3ba4722..b776dc5dc 100644 --- a/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py +++ b/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py @@ -167,7 +167,7 @@ def entity_class_name_list(self, row): """ db_column = self.model.header.index("databases") db_names = self.model._main_data[row][db_column] - db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps] + db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps] return self._entity_class_name_list_from_db_maps(*db_maps) def _entity_class_name_list_from_db_maps(self, *db_maps): @@ -234,7 +234,7 @@ def alternative_name_list(self, row): """ db_column = self.model.header.index("databases") db_names = self.model._main_data[row][db_column] - db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps] + db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps] return sorted(set(x for db_map in db_maps for x in self.db_map_alt_id_lookup[db_map])) def entity_name_list(self, row, column): @@ -243,7 +243,7 @@ def entity_name_list(self, row, column): """ db_column = self.model.header.index("databases") db_names = self.model._main_data[row][db_column] - db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps] + db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps] entity_name_lists = [] for db_map in db_maps: entity_classes = self.db_map_ent_cls_lookup[db_map] diff --git a/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py index 349332612..eeeb1b390 100644 --- a/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py +++ b/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py @@ -22,11 +22,12 @@ class _SelectDatabases(QWidget): checked_state_changed = Signal(int) - def __init__(self, db_maps, checked_states, parent): + def __init__(self, db_maps, checked_states, db_name_registry, parent): """ Args: db_maps (tuple of DatabaseMapping): database maps checked_states (dict, optional): mapping from item name to check state boolean + db_name_registry (NameRegistry): database display name registry parent (QWidget): parent widget """ super().__init__(parent) @@ -34,7 +35,9 @@ def __init__(self, db_maps, checked_states, parent): self._ui = Ui_Form() self._ui.setupUi(self) - self._check_boxes = {db_map: QCheckBox(db_map.codename, self) for db_map in db_maps} + self._check_boxes = { + db_map: QCheckBox(db_name_registry.display_name(db_map.sa_url), self) for db_map in db_maps + } add_check_boxes( self._check_boxes, checked_states, @@ -80,7 +83,9 @@ def __init__(self, parent, db_mngr, *db_maps, stored_state, ok_button_text): database_checked_states = ( stored_state["databases"] if stored_state is not None else {db_map: True for db_map in db_maps} ) - self._database_check_boxes_widget = _SelectDatabases(tuple(db_maps), database_checked_states, self) + self._database_check_boxes_widget = _SelectDatabases( + tuple(db_maps), database_checked_states, db_mngr.name_registry, self + ) self._database_check_boxes_widget.checked_state_changed.connect(self._handle_check_box_state_changed) self._ui.root_layout.insertWidget(0, self._database_check_boxes_widget) 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 6d576fc96..d092a0787 100644 --- a/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py +++ b/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py @@ -19,6 +19,7 @@ from ...helpers import CharIconEngine, open_url from ...widgets.multi_tab_window import MultiTabWindow from ...widgets.settings_widget import SpineDBEditorSettingsWidget +from ..editors import db_editor_registry from .custom_qwidgets import OpenFileButton, OpenSQLiteFileButton, ShootingLabel from .spine_db_editor import SpineDBEditor @@ -26,11 +27,11 @@ class MultiSpineDBEditor(MultiTabWindow): """Database editor's tabbed main window.""" - def __init__(self, db_mngr, db_url_codenames=None): + def __init__(self, db_mngr, db_urls=None): """ Args: db_mngr (SpineDBManager): database manager - db_url_codenames (dict, optional): mapping from database URL to its codename + db_urls (Iterable of str, optional): URLs of database to load """ super().__init__(db_mngr.qsettings, "spineDBEditor") self.db_mngr = db_mngr @@ -42,9 +43,10 @@ def __init__(self, db_mngr, db_url_codenames=None): self.setStatusBar(_CustomStatusBar(self)) self.statusBar().hide() self.tab_load_success = True - if db_url_codenames is not None: - if not self.add_new_tab(db_url_codenames, window=True): + if db_urls is not None: + if not self.add_new_tab(db_urls): self.tab_load_success = False + db_editor_registry.register_window(self) def _make_other(self): return MultiSpineDBEditor(self.db_mngr) @@ -84,10 +86,10 @@ def _disconnect_tab_signals(self, index): tab.ui.actionClose.triggered.disconnect(self.handle_close_request_from_tab) return True - def _make_new_tab(self, db_url_codenames=None, window=False): # pylint: disable=arguments-differ + def _make_new_tab(self, db_urls=None): # pylint: disable=arguments-differ """Makes a new tab, if successful return the tab, returns None otherwise""" tab = SpineDBEditor(self.db_mngr) - if not tab.load_db_urls(db_url_codenames, create=True, window=window): + if not tab.load_db_urls(db_urls if db_urls is not None else [], create=True): return return tab @@ -102,7 +104,7 @@ def show_plus_button_context_menu(self, global_pos): return menu = QMenu(self) for name, url in ds_urls.items(): - action = menu.addAction(name, lambda name=name, url=url: self.db_mngr.open_db_editor({url: name}, True)) + action = menu.addAction(name, lambda url=url: open_db_editor([url], self.db_mngr, True)) action.setEnabled(url is not None and is_url_validated[name]) menu.popup(global_pos) menu.aboutToHide.connect(menu.deleteLater) @@ -114,13 +116,11 @@ def make_context_menu(self, index): tab = self.tab_widget.widget(index) menu.addSeparator() menu.addAction(tab.toolbar.reload_action) - db_url_codenames = tab.db_url_codenames + db_urls = tab.db_urls menu.addAction( QIcon(CharIconEngine("\uf24d")), "Duplicate", - lambda _=False, index=index + 1, db_url_codenames=db_url_codenames: self.insert_new_tab( - index, db_url_codenames - ), + lambda _=False, index=index + 1, db_urls=db_urls: self.insert_new_tab(index, db_urls), ) return menu @@ -160,10 +160,6 @@ def insert_open_file_button(self, file_path, progress, is_sqlite): button = (OpenSQLiteFileButton if is_sqlite else OpenFileButton)(file_path, progress, self) self._insert_statusbar_button(button) - def _open_sqlite_url(self, url, codename): - """Opens sqlite url.""" - self.add_new_tab({url: codename}) - @Slot(bool) def show_user_guide(self, checked=False): """Opens Spine db editor documentation page in browser.""" @@ -171,6 +167,11 @@ def show_user_guide(self, checked=False): if not open_url(doc_url): self.msg_error.emit(f"Unable to open url {doc_url}") + def closeEvent(self, event): + super().closeEvent(event) + if event.isAccepted(): + db_editor_registry.unregister_window(self) + class _CustomStatusBar(QStatusBar): def __init__(self, parent=None): @@ -198,3 +199,49 @@ def __init__(self, parent=None): self.insertPermanentWidget(0, self._hide_button) self.setSizeGripEnabled(False) self._hide_button.clicked.connect(self.hide) + + +def _get_existing_spine_db_editor(db_urls): + """Returns existing editor window and tab or None for given database URLs. + + Args: + db_urls (Sequence of str): database URLs + + Returns: + tuple: editor window and tab or None if not found + """ + for multi_db_editor in db_editor_registry.windows(): + for k in range(multi_db_editor.tab_widget.count()): + db_editor = multi_db_editor.tab_widget.widget(k) + if db_editor.db_urls and all(url in db_urls for url in db_editor.db_urls): + return multi_db_editor, db_editor + return None + + +def open_db_editor(db_urls, db_mngr, reuse_existing_editor): + """Opens a SpineDBEditor with given urls. + + Optionally uses an existing MultiSpineDBEditor if any. + Also, if the same urls are open in an existing SpineDBEditor, just raises that one + instead of creating another. + + Args: + db_urls (Iterable of str): URLs of databases to open + db_mngr (SpineDBManager): database manager + reuse_existing_editor (bool): if True and the same URL is already open, just raise the existing window + """ + multi_db_editor = db_editor_registry.get_some_window() if reuse_existing_editor else None + if multi_db_editor is None: + multi_db_editor = MultiSpineDBEditor(db_mngr, db_urls) + if multi_db_editor.tab_load_success: + multi_db_editor.show() + return + existing = _get_existing_spine_db_editor(list(map(str, db_urls))) + if existing is None: + multi_db_editor.add_new_tab(db_urls) + else: + multi_db_editor, db_editor = existing + multi_db_editor.set_current_tab(db_editor) + if multi_db_editor.isMinimized(): + multi_db_editor.showNormal() + multi_db_editor.activateWindow() diff --git a/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py b/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py index 17671149e..fd647cb0c 100644 --- a/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py +++ b/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py @@ -31,6 +31,7 @@ from spinedb_api.spine_io.importers.excel_reader import get_mapped_data_from_xlsx from ...config import APPLICATION_PATH, SPINE_TOOLBOX_REPO_URL from ...helpers import ( + add_keyboard_shortcuts_to_action_tool_tips, busy_effect, call_on_focused_widget, format_string_list, @@ -74,8 +75,8 @@ def __init__(self, db_mngr): from ..ui.spine_db_editor_window import Ui_MainWindow # pylint: disable=import-outside-toplevel self.db_mngr = db_mngr - self.db_maps = [] - self.db_urls = [] + self.db_maps: list[DatabaseMapping] = [] + self.db_urls: list[str] = [] self._history = [] self.recent_dbs_menu = RecentDatabasesPopupMenu(self) self._change_notifiers = [] @@ -83,6 +84,7 @@ def __init__(self, db_mngr): # Setup UI from Qt Designer file self.ui = Ui_MainWindow() self.ui.setupUi(self) + add_keyboard_shortcuts_to_action_tool_tips(self.ui) self.takeCentralWidget().deleteLater() self.toolbar = DBEditorToolBar(self) self.addToolBar(Qt.TopToolBarArea, self.toolbar) @@ -122,37 +124,38 @@ def toolbox(self): def settings_subgroup(self): return ";".join(self.db_urls) - @property - def db_names(self): - return ", ".join([f"{db_map.codename}" for db_map in self.db_maps]) - @property def first_db_map(self): return self.db_maps[0] - @property - def db_url_codenames(self): - return {db_map.db_url: db_map.codename for db_map in self.db_maps} - @staticmethod def is_db_map_editor(): - """Always returns True as SpineDBEditors are truly database editors. + """Always returns True as SpineDBEditors are truly database editors.""" + return True - Unless, of course, the database can one day be opened in read-only mode. - In that case this method should return False. + @Slot(str, str) + def _update_title(self, url, name): + """Updates window title if database display name has changed. - Returns: - bool: Always True + Args: + url (str): database url + name (str): database display name """ - return True + if not any(str(db_map.sa_url) == url for db_map in self.db_maps): + return + self._reset_window_title() + + def _reset_window_title(self): + """Sets new window title according to open databases.""" + self.setWindowTitle(", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps))) - def load_db_urls(self, db_url_codenames, create=False, update_history=True, window=False): + def load_db_urls(self, db_urls, create=False, update_history=True): self.ui.actionImport.setEnabled(False) self.ui.actionExport.setEnabled(False) self.ui.actionMass_remove_items.setEnabled(False) self.ui.actionVacuum.setEnabled(False) self.toolbar.reload_action.setEnabled(False) - if not db_url_codenames: + if not db_urls: return True if not self.tear_down(): return False @@ -161,10 +164,8 @@ def load_db_urls(self, db_url_codenames, create=False, update_history=True, wind self.db_maps = [] self._changelog.clear() self._purge_change_notifiers() - for url, codename in db_url_codenames.items(): - db_map = self.db_mngr.get_db_map( - url, self, codename=codename, create=create, window=window, force_upgrade_prompt=True - ) + for url in db_urls: + db_map = self.db_mngr.get_db_map(url, self, create=create, force_upgrade_prompt=True) if db_map is not None: self.db_maps.append(db_map) if not self.db_maps: @@ -182,9 +183,9 @@ def load_db_urls(self, db_url_codenames, create=False, update_history=True, wind self.db_mngr.register_listener(self, *self.db_maps) self.init_models() self.init_add_undo_redo_actions() - self.setWindowTitle(f"{self.db_names}") # This sets the tab name, just in case + self._reset_window_title() if update_history: - self.add_urls_to_history(self.db_url_codenames) + self.add_urls_to_history() self.update_last_view() self.restore_ui(self.last_view, fresh=True) self.update_commit_enabled() @@ -198,19 +199,16 @@ def show_recent_db(self): self.recent_dbs_menu = RecentDatabasesPopupMenu(self) self.ui.actionOpen_recent.setMenu(self.recent_dbs_menu) - def add_urls_to_history(self, db_urls): - """Adds urls to history. - - Args: - db_urls (dict) - """ + def add_urls_to_history(self): + """Adds current urls to history.""" opened_names = set() for row in self._history: for name in row: opened_names.add(name) - for db_url, name in db_urls.items(): + for url in self.db_urls: + name = self.db_mngr.name_registry.display_name(url) if name not in opened_names: - self._history.insert(0, {name: db_url}) + self._history.insert(0, {name: url}) def init_add_undo_redo_actions(self): new_undo_action = self.db_mngr.undo_action[self.first_db_map] @@ -226,8 +224,8 @@ def open_db_file(self, _=False): self.qsettings.endGroup() if not file_path: return - url = "sqlite:///" + file_path - self.load_db_urls({url: None}) + url = "sqlite:///" + os.path.normcase(file_path) + self.load_db_urls([url]) @Slot(bool) def add_db_file(self, _=False): @@ -238,10 +236,8 @@ def add_db_file(self, _=False): self.qsettings.endGroup() if not file_path: return - url = "sqlite:///" + file_path - db_url_codenames = self.db_url_codenames - db_url_codenames[url] = None - self.load_db_urls(db_url_codenames) + url = "sqlite:///" + os.path.normcase(file_path) + self.load_db_urls(self.db_urls + [url]) @Slot(bool) def create_db_file(self, _=False): @@ -256,8 +252,8 @@ def create_db_file(self, _=False): os.remove(file_path) except OSError: pass - url = "sqlite:///" + file_path - self.load_db_urls({url: None}, create=True) + url = "sqlite:///" + os.path.normcase(file_path) + self.load_db_urls([url], create=True) def reset_docks(self): """Resets the layout of the dock widgets for this URL""" @@ -282,13 +278,12 @@ def _browse_commits(self): def connect_signals(self): """Connects signals to slots.""" - # Message signals self.msg.connect(self.add_message) self.msg_error.connect(self.err_msg.showMessage) self.db_mngr.items_added.connect(self._handle_items_added) self.db_mngr.items_updated.connect(self._handle_items_updated) self.db_mngr.items_removed.connect(self._handle_items_removed) - # Menu actions + self.db_mngr.name_registry.display_name_changed.connect(self._update_title) self.ui.actionCommit.triggered.connect(self.commit_session) self.ui.actionRollback.triggered.connect(self.rollback_session) self.ui.actionView_history.triggered.connect(self._browse_commits) @@ -308,7 +303,7 @@ def vacuum(self, _checked=False): msg = "Vacuum finished
      " for db_map in self.db_maps: freed, unit = vacuum(db_map.db_url) - msg += f"
    • {freed} {unit} freed from {db_map.codename}
    • " + msg += f"
    • {freed} {unit} freed from {self.db_mngr.name_registry.display_name(db_map.sa_url)}
    • " msg += "
    " self.msg.emit(msg) @@ -336,9 +331,7 @@ def _replace_undo_redo_actions(self, new_undo_action, new_redo_action): @Slot() def _refresh_undo_redo_actions(self): self.ui.actionUndo.setEnabled(self.undo_action.isEnabled()) - self.ui.actionUndo.setToolTip(f"

    {self.undo_action.text()}

    Ctrl+Z

    ") self.ui.actionRedo.setEnabled(self.redo_action.isEnabled()) - self.ui.actionRedo.setToolTip(f"

    {self.redo_action.text()}

    Ctrl+Y

    ") @Slot(bool) def update_commit_enabled(self, _clean=False): @@ -555,7 +548,7 @@ def duplicate_scenario(self, db_map, scen_id): Duplicates a scenario. Args: - db_map (DiffDatabaseMapping) + db_map (DatabaseMapping) scen_id (int) """ orig_name = self.db_mngr.get_item(db_map, "scenario", scen_id)["name"] @@ -597,7 +590,7 @@ def commit_session(self, checked=False): dirty_db_maps = self.db_mngr.dirty(*self.db_maps) if not dirty_db_maps: return - db_names = ", ".join([db_map.codename for db_map in dirty_db_maps]) + db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps)) commit_msg = self._get_commit_msg(db_names) if not commit_msg: return @@ -609,7 +602,7 @@ def rollback_session(self, checked=False): dirty_db_maps = self.db_mngr.dirty(*self.db_maps) if not dirty_db_maps: return - db_names = ", ".join([db_map.codename for db_map in dirty_db_maps]) + db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps)) if not self._get_rollback_confirmation(db_names): return self.db_mngr.rollback_session(*dirty_db_maps) @@ -618,7 +611,7 @@ def receive_session_committed(self, db_maps, cookie): db_maps = set(self.db_maps) & set(db_maps) if not db_maps: return - db_names = ", ".join([x.codename for x in db_maps]) + db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) if cookie is self: msg = f"All changes in {db_names} committed successfully." self.msg.emit(msg) @@ -631,7 +624,7 @@ def receive_session_rolled_back(self, db_maps): db_maps = set(self.db_maps) & set(db_maps) if not db_maps: return - db_names = ", ".join([x.codename for x in db_maps]) + db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) msg = f"All changes in {db_names} rolled back successfully." self.msg.emit(msg) @@ -681,7 +674,9 @@ def receive_error_msg(self, db_map_error_log): for db_map, error_log in db_map_error_log.items(): if isinstance(error_log, str): error_log = [error_log] - msg = "From " + db_map.codename + ":" + format_string_list(error_log) + msg = ( + "From " + self.db_mngr.name_registry.display_name(db_map.sa_url) + ": " + format_string_list(error_log) + ) msgs.append(msg) self.msg_error.emit(format_string_list(msgs)) @@ -769,7 +764,7 @@ def tear_down(self): answer = self._prompt_to_commit_changes() if answer == QMessageBox.StandardButton.Cancel: return False - db_names = ", ".join([db_map.codename for db_map in dirty_db_maps]) + db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps)) if answer == QMessageBox.StandardButton.Save: commit_dirty = True commit_msg = self._get_commit_msg(db_names) @@ -781,7 +776,7 @@ def tear_down(self): self, *self.db_maps, dirty_db_maps=dirty_db_maps, commit_dirty=commit_dirty, commit_msg=commit_msg ) if failed_db_maps: - msg = f"Failed to commit {[db_map.codename for db_map in failed_db_maps]}" + msg = f"Failed to commit {list(self.db_mngr.name_registry.display_name_iter(failed_db_maps))}" self.db_mngr.receive_error_msg({i: [msg] for i in failed_db_maps}) return False return True @@ -872,6 +867,7 @@ def closeEvent(self, event): event.ignore() return self.save_window_state() + self.db_mngr.name_registry.display_name_changed.disconnect(self._update_title) super().closeEvent(event) @staticmethod @@ -907,11 +903,11 @@ class SpineDBEditor(TabularViewMixin, GraphViewMixin, StackedViewMixin, TreeView pinned_values_updated = Signal(list) - def __init__(self, db_mngr, db_url_codenames=None): - """Initializes everything. - + def __init__(self, db_mngr, db_urls=None): + """ Args: db_mngr (SpineDBManager): The manager to use + db_urls (Iterable of str, optional): URLs of databases to load """ super().__init__(db_mngr) self._original_size = None @@ -926,8 +922,8 @@ def __init__(self, db_mngr, db_url_codenames=None): self.connect_signals() self.apply_stacked_style() self.set_db_column_visibility(False) - if db_url_codenames is not None: - self.load_db_urls(db_url_codenames) + if db_urls is not None: + self.load_db_urls(db_urls) def set_db_column_visibility(self, visible): """Set the visibility of the database -column in all the views it is present""" diff --git a/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py index 9197dd936..b1702a809 100644 --- a/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py +++ b/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py @@ -22,9 +22,7 @@ class StackedViewMixin: - """ - Provides stacked parameter tables for the Spine db editor. - """ + """Provides stacked parameter tables for the Spine db editor.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -102,7 +100,7 @@ def _set_default_parameter_data(self, index=None): """ if index is None or not index.isValid(): default_db_map = next(iter(self.db_maps)) - default_data = {"database": default_db_map.codename} + default_data = {"database": self.db_mngr.name_registry.display_name(default_db_map.sa_url)} else: item = index.model().item_from_index(index) default_db_map = item.first_db_map diff --git a/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py index 06ff27058..2435eb265 100644 --- a/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py +++ b/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py @@ -27,7 +27,7 @@ PivotTableSortFilterProxy, ScenarioAlternativePivotTableModel, ) -from .custom_menus import TabularViewCodenameFilterMenu, TabularViewDBItemFilterMenu +from .custom_menus import TabularViewDatabaseNameFilterMenu, TabularViewDBItemFilterMenu from .tabular_view_header_widget import TabularViewHeaderWidget @@ -402,7 +402,9 @@ def create_filter_menu(self, identifier): """ if identifier not in self.filter_menus: if identifier == "database": - menu = TabularViewCodenameFilterMenu(self, self.db_maps, identifier, show_empty=False) + menu = TabularViewDatabaseNameFilterMenu( + self, self.db_maps, identifier, self.db_mngr.name_registry, show_empty=False + ) else: header = self.pivot_table_model.top_left_headers[identifier] if header.header_type == "parameter": diff --git a/spinetoolbox/spine_db_editor/widgets/toolbar.py b/spinetoolbox/spine_db_editor/widgets/toolbar.py index a87e08e49..087973275 100644 --- a/spinetoolbox/spine_db_editor/widgets/toolbar.py +++ b/spinetoolbox/spine_db_editor/widgets/toolbar.py @@ -11,31 +11,10 @@ ###################################################################################################################### """Contains the DBEditorToolBar class and helpers.""" -from PySide6.QtCore import QSize, Qt, Signal, Slot +from PySide6.QtCore import QSize, Qt from PySide6.QtGui import QAction, QIcon, QKeySequence, QTextCursor -from PySide6.QtWidgets import ( - QDialog, - QDialogButtonBox, - QHBoxLayout, - QLabel, - QSizePolicy, - QTextEdit, - QToolBar, - QToolButton, - QTreeWidget, - QTreeWidgetItem, - QVBoxLayout, - QWidget, -) -from spinedb_api.filters.tools import ( - SCENARIO_FILTER_TYPE, - append_filter_config, - clear_filter_configs, - filter_config, - filter_configs, - name_from_dict, -) -from spinetoolbox.helpers import CharIconEngine +from PySide6.QtWidgets import QDialog, QSizePolicy, QTextEdit, QToolBar, QToolButton, QVBoxLayout, QWidget +from spinetoolbox.helpers import CharIconEngine, add_keyboard_shortcut_to_tool_tip, plain_to_rich class DBEditorToolBar(QToolBar): @@ -44,14 +23,18 @@ def __init__(self, db_editor): self.setObjectName("spine_db_editor_toolbar") self._db_editor = db_editor self.reload_action = QAction(QIcon(CharIconEngine("\uf021")), "Reload") + self.reload_action.setToolTip(plain_to_rich("Reload data from database keeping changes")) self.reload_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_R)) + add_keyboard_shortcut_to_tool_tip(self.reload_action) self.reload_action.setEnabled(False) self.reset_docks_action = QAction(QIcon(CharIconEngine("\uf2d2")), "Reset docks") + self.reset_docks_action.setToolTip(plain_to_rich("Reset window back to default configuration")) self.show_toolbox_action = QAction(QIcon(":/symbols/Spine_symbol.png"), "Show Toolbox") self.show_toolbox_action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Escape)) - self.show_toolbox_action.setToolTip("Show Spine Toolbox (Ctrl+ESC)") - self._filter_action = QAction(QIcon(CharIconEngine("\uf0b0")), "Filter") + self.show_toolbox_action.setToolTip(plain_to_rich("Show Spine Toolbox window")) + add_keyboard_shortcut_to_tool_tip(self.show_toolbox_action) self.show_url_action = QAction(QIcon(CharIconEngine("\uf550")), "Show URLs") + self.show_url_action.setToolTip(plain_to_rich("Show URLs of currently databases in the session")) self._add_actions() self._connect_signals() self.setMovable(False) @@ -85,7 +68,6 @@ def _add_actions(self): self.addWidget(spacer) self.addSeparator() self.create_button_for_action(self.show_url_action) - self.create_button_for_action(self._filter_action) self.addSeparator() self.create_button_for_action(self.show_toolbox_action) @@ -94,7 +76,6 @@ def _connect_signals(self): self.reload_action.triggered.connect(self._db_editor.refresh_session) self.reset_docks_action.triggered.connect(self._db_editor.reset_docks) self.show_url_action.triggered.connect(self._show_url_codename_widget) - self._filter_action.triggered.connect(self._show_filter_menu) def create_button_for_action(self, action): """Creates a button for the given action and adds it to the widget""" @@ -105,164 +86,19 @@ def create_button_for_action(self, action): self.addWidget(tool_button) def _show_url_codename_widget(self): - """Shows the url codename widget""" - dialog = _URLDialog(self._db_editor.db_url_codenames, parent=self) + """Shows the url widget""" + dialog = _URLDialog(self._db_editor.db_urls, self._db_editor.db_mngr.name_registry, self) dialog.show() - @Slot(bool) - def _show_filter_menu(self, _checked=False): - """Shows the filter menu""" - dialog = _UrlFilterDialog(self._db_editor.db_mngr, self._db_editor.db_maps, parent=self) - dialog.show() - dialog.filter_accepted.connect(self._db_editor.load_db_urls) - dialog.filter_accepted.connect(self.set_filter_action_icon_color) - - def set_filter_action_icon_color(self, codenames): - filtered = any(filter_configs(url) for url in codenames.keys()) - color = Qt.magenta if filtered else None - self._filter_action.setIcon(QIcon(CharIconEngine("\uf0b0", color=color))) - - -class _FilterWidget(QTreeWidget): - def __init__(self, db_mngr, db_map, item_type, filter_type, active_item, parent=None): - super().__init__(parent=parent) - self._filter_type = filter_type - self.setIndentation(0) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setHeaderLabel(filter_type) - items = db_mngr.get_items(db_map, item_type) - top_level_items = [QTreeWidgetItem([x["name"]]) for x in items] - self.addTopLevelItems(top_level_items) - self.resizeColumnToContents(0) - current = next(iter(item for item in top_level_items if item.text(0) == active_item), None) - if current is not None: - self.setCurrentItem(current) - - def sizeHint(self): - size = super().sizeHint() - size.setWidth(self.header().sectionSize(0) + self.frameWidth() * 2 + 2) - return size - - def filter_config(self): - selected = self.selectedItems() - if not selected: - return {} - return filter_config(self._filter_type, selected[0].text(0)) - - -class _FilterArrayWidget(QWidget): - filter_selection_changed = Signal() - - def __init__(self, db_mngr, db_map, parent=None): - super().__init__(parent=parent) - layout = QHBoxLayout(self) - self._offset = 0 - self._db_map = db_map - self._filter_widgets = [] - active_filter_configs = {cfg["type"]: cfg for cfg in filter_configs(db_map.db_url)} - for item_type, filter_type in (("scenario", SCENARIO_FILTER_TYPE),): - active_cfg = active_filter_configs.get(filter_type, {}) - active_item = name_from_dict(active_cfg) if active_cfg else None - filter_widget = _FilterWidget(db_mngr, db_map, item_type, filter_type, active_item, parent=self) - layout.addWidget(filter_widget) - self._filter_widgets.append(filter_widget) - filter_widget.itemSelectionChanged.connect(self.filter_selection_changed) - - def filtered_url_codename(self): - url = clear_filter_configs(self._db_map.db_url) - for filter_widget in self._filter_widgets: - filter_config_ = filter_widget.filter_config() - if not filter_config_: - continue - url = append_filter_config(url, filter_config_) - return url, self._db_map.codename - - def sizeHint(self): - size = super().sizeHint() - size.setWidth(size.width() - self._offset) - return size - - def moveEvent(self, ev): - if ev.pos().x() > 0: - margin = 2 - self._offset = ev.pos().x() - margin - self.move(margin, ev.pos().y()) - self.adjustSize() - return - super().moveEvent(ev) - - -class _DBListWidget(QTreeWidget): - db_filter_selection_changed = Signal() - - def __init__(self, db_mngr, db_maps, parent=None): - super().__init__(parent=parent) - self.header().hide() - self._filter_arrays = [] - for db_map in db_maps: - top_level_item = QTreeWidgetItem([db_map.codename]) - self.addTopLevelItem(top_level_item) - child = QTreeWidgetItem() - top_level_item.addChild(child) - filter_array = _FilterArrayWidget(db_mngr, db_map, parent=self) - self.setItemWidget(child, 0, filter_array) - self._filter_arrays.append(filter_array) - top_level_item.setExpanded(True) - filter_array.filter_selection_changed.connect(self.db_filter_selection_changed) - self.resizeColumnToContents(0) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - def sizeHint(self): - size = super().sizeHint() - last = self.topLevelItem(self.topLevelItemCount() - 1) - last_child = self.itemBelow(last) - rect = self.visualItemRect(last_child) - size.setWidth(rect.right() + 2 * self.frameWidth() + 2) - size.setHeight(rect.bottom() + 2 * self.frameWidth() + 2) - return size - - def filtered_url_codenames(self): - return dict(filter_array.filtered_url_codename() for filter_array in self._filter_arrays) - - -class _UrlFilterDialog(QDialog): - filter_accepted = Signal(dict) - - def __init__(self, db_mngr, db_maps, parent=None): - super().__init__(parent=parent, f=Qt.Popup) - outer_layout = QVBoxLayout(self) - button_box = QDialogButtonBox(self) - self._filter_button = button_box.addButton("Update filters", QDialogButtonBox.ButtonRole.AcceptRole) - self._db_list = _DBListWidget(db_mngr, db_maps, parent=self) - self._orig_filtered_url_codenames = self._db_list.filtered_url_codenames() - self._update_filter_enabled() - outer_layout.addWidget(QLabel("Select URL filters")) - outer_layout.addWidget(self._db_list) - outer_layout.addWidget(button_box) - button_box.accepted.connect(self.accept) - self._db_list.db_filter_selection_changed.connect(self._update_filter_enabled) - self.adjustSize() - - def sizeHint(self): - size = super().sizeHint() - return size - - def _update_filter_enabled(self): - self._filter_button.setEnabled(self._orig_filtered_url_codenames != self._db_list.filtered_url_codenames()) - - def accept(self): - super().accept() - self.filter_accepted.emit(self._db_list.filtered_url_codenames()) - class _URLDialog(QDialog): - """Class for showing URLs and codenames in the database""" + """Class for showing URLs and database names in the editor""" - def __init__(self, url_codenames, parent=None): + def __init__(self, urls, name_registry, parent=None): super().__init__(parent=parent, f=Qt.Popup) self.textEdit = QTextEdit(self) self.textEdit.setObjectName("textEdit") - text = "
    ".join([f"{codename}: {url}" for url, codename in url_codenames.items()]) + text = "
    ".join([f"{name_registry.display_name(url)}: {url}" for url in urls]) self.textEdit.setHtml(text) self.textEdit.setReadOnly(True) self.textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) diff --git a/spinetoolbox/spine_db_manager.py b/spinetoolbox/spine_db_manager.py index 4f0ae90f9..1c06c3a1a 100644 --- a/spinetoolbox/spine_db_manager.py +++ b/spinetoolbox/spine_db_manager.py @@ -15,8 +15,7 @@ import json import os from PySide6.QtCore import QObject, Qt, Signal, Slot -from PySide6.QtGui import QWindow -from PySide6.QtWidgets import QApplication, QMessageBox, QWidget +from PySide6.QtWidgets import QApplication, QMessageBox from sqlalchemy.engine.url import URL from spinedb_api import ( Array, @@ -48,6 +47,7 @@ split_value_and_type, ) from spinedb_api.spine_io.exporters.excel import export_spine_database_to_xlsx +from spinetoolbox.database_display_names import NameRegistry from .helpers import busy_effect, plain_to_tool_tip from .mvcmodels.shared import INVALID_TYPE, PARAMETER_TYPE_VALIDATION_ROLE, PARSED_ROLE, TYPE_NOT_VALIDATED, VALID_TYPE from .parameter_type_validation import ParameterTypeValidator, ValidationKey @@ -58,7 +58,6 @@ RemoveItemsCommand, UpdateItemsCommand, ) -from .spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor from .spine_db_icon_manager import SpineDBIconManager from .spine_db_worker import SpineDBWorker from .widgets.options_dialog import OptionsDialog @@ -96,6 +95,13 @@ class SpineDBManager(QObject): str: item type, such as "object_class" dict: mapping DatabaseMapping to list of updated dict-items. """ + database_clean_changed = Signal(object, bool) + """Emitted whenever database becomes clean or dirty. + + Args: + object: database mapping + bool: True if database has become clean, False if it became dirty + """ def __init__(self, settings, parent, synchronous=False): """ @@ -107,6 +113,7 @@ def __init__(self, settings, parent, synchronous=False): super().__init__(parent) self.qsettings = settings self._db_maps = {} + self.name_registry = NameRegistry(self) self._workers = {} self.listeners = {} self.undo_stack = {} @@ -116,7 +123,6 @@ def __init__(self, settings, parent, synchronous=False): self._connect_signals() self._cmd_id = 0 self._synchronous = synchronous - self.data_stores = {} self._validated_values = {"parameter_definition": {}, "parameter_value": {}} self._parameter_type_validator = ParameterTypeValidator(self) self._parameter_type_validator.validated.connect(self._parameter_value_validated) @@ -190,7 +196,7 @@ def register_fetch_parent(self, db_map, parent): worker.register_fetch_parent(parent) def can_fetch_more(self, db_map, parent): - """Whether or not we can fetch more items of given type from given db. + """Whether we can fetch more items of given type from given db. Args: db_map (DatabaseMapping) @@ -327,6 +333,7 @@ def close_session(self, url): worker.clean_up() del self._validated_values["parameter_definition"][id(db_map)] del self._validated_values["parameter_value"][id(db_map)] + self.undo_stack[db_map].cleanChanged.disconnect() del self.undo_stack[db_map] del self.undo_action[db_map] del self.redo_action[db_map] @@ -336,15 +343,13 @@ def close_all_sessions(self): for url in list(self._db_maps): self.close_session(url) - def get_db_map(self, url, logger, window=False, codename=None, create=False, force_upgrade_prompt=False): + def get_db_map(self, url, logger, create=False, force_upgrade_prompt=False): """Returns a DatabaseMapping instance from url if possible, None otherwise. If needed, asks the user to upgrade to the latest db version. Args: url (str, URL) logger (LoggerInterface) - window (bool) - codename (str, optional) create (bool) force_upgrade_prompt (bool) @@ -354,8 +359,6 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for url = str(url) db_map = self._db_maps.get(url) if db_map is not None: - if not window and codename is not None and db_map.codename != codename: - return None return db_map try: prompt_data = DatabaseMapping.get_upgrade_db_prompt_data(url, create=create) @@ -374,7 +377,7 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for return None else: kwargs = {} - kwargs.update(codename=codename, create=create) + kwargs["create"] = create try: return self._do_get_db_map(url, **kwargs) except SpineDBAPIError as err: @@ -384,13 +387,10 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for @busy_effect def _do_get_db_map(self, url, **kwargs): """Returns a memorized DatabaseMapping instance from url. - Called by `get_db_map`. Args: url (str, URL) - codename (str, NoneType) - upgrade (bool) - create (bool) + **kwargs: arguments passed to worker's get_db_map() Returns: DatabaseMapping @@ -406,6 +406,7 @@ def _do_get_db_map(self, url, **kwargs): self._validated_values["parameter_definition"][id(db_map)] = {} self._validated_values["parameter_value"][id(db_map)] = {} stack = self.undo_stack[db_map] = AgedUndoStack(self) + stack.cleanChanged.connect(lambda clean: self.database_clean_changed.emit(db_map, clean)) self.undo_action[db_map] = stack.createUndoAction(self) self.redo_action[db_map] = stack.createRedoAction(self) return db_map @@ -435,7 +436,6 @@ def register_listener(self, listener, *db_maps): listener (object) *db_maps """ - self.update_data_store_db_maps() for db_map in db_maps: self.add_db_map_listener(db_map, listener) stack = self.undo_stack[db_map] @@ -443,11 +443,6 @@ def register_listener(self, listener, *db_maps): stack.canRedoChanged.connect(listener.update_undo_redo_actions) stack.canUndoChanged.connect(listener.update_undo_redo_actions) stack.cleanChanged.connect(listener.update_commit_enabled) - stores = self.data_stores.get(db_map) - if not stores: - continue - for store in stores: - self.undo_stack[db_map].cleanChanged.connect(store.notify_about_dirtiness) except AttributeError: pass @@ -476,12 +471,6 @@ def unregister_listener(self, listener, *db_maps, dirty_db_maps=None, commit_dir # for that db map is closed. This way the dirtiness state of a data store that is # not open in a db editor can still be affected. continue - stores = self.data_stores.get(db_map) - if not stores: - continue - for store in stores: - self.undo_stack[db_map].cleanChanged.disconnect(store.notify_about_dirtiness) - store.notify_about_dirtiness(True) except AttributeError: pass if dirty_db_maps: @@ -496,11 +485,6 @@ def unregister_listener(self, listener, *db_maps, dirty_db_maps=None, commit_dir self.undo_stack[db_map].canRedoChanged.connect(listener.update_undo_redo_actions) self.undo_stack[db_map].canUndoChanged.connect(listener.update_undo_redo_actions) self.undo_stack[db_map].cleanChanged.connect(listener.update_commit_enabled) - stores = self.data_stores.get(db_map) - if not stores: - continue - for store in stores: - self.undo_stack[db_map].cleanChanged.connect(store.notify_about_dirtiness) except AttributeError: pass for db_map in db_maps: @@ -508,32 +492,6 @@ def unregister_listener(self, listener, *db_maps, dirty_db_maps=None, commit_dir self.close_session(db_map.db_url) return failed_db_maps - def add_data_store_db_map(self, db_map, store): - """Adds a Data Store instance under a db_map into memory""" - if db_map in self.data_stores: - self.data_stores[db_map].append(store) - else: - self.data_stores[db_map] = [store] - - def remove_data_store_db_map(self, db_map, store): - """Removes a Data Store instance from memory""" - if self.data_stores.get(db_map): - self.data_stores[db_map].remove(store) - - def update_data_store_db_maps(self): - """Updates the db maps of the dict that maps db_maps to Data Stores""" - new_data_stores = {} - old_stores = [] - for store in self.data_stores.values(): - old_stores.extend(store) - for store in old_stores: - db_map = store.get_db_map_for_ds() - if db_map in new_data_stores: - new_data_stores[db_map].append(store) - else: - new_data_stores[db_map] = [store] - self.data_stores = new_data_stores - def is_dirty(self, db_map): """Returns True if mapping has pending changes. @@ -1684,7 +1642,7 @@ def export_to_sqlite(self, file_path, data_for_export, caller): try: db_map.commit_session("Export data from Spine Toolbox.") except SpineDBAPIError as err: - error_msg = f"[SpineDBAPIError] Unable to export file {db_map.codename}: {err.msg}" + error_msg = f"[SpineDBAPIError] Unable to export file {file_path}: {err.msg}" caller.msg_error.emit(error_msg) else: caller.file_exported.emit(file_path, 1.0, True) @@ -1736,63 +1694,6 @@ def get_items_for_commit(self, db_map, commit_id): return {} return worker.commit_cache.get(commit_id.db_id, {}) - @staticmethod - def get_all_multi_spine_db_editors(): - """Yields all instances of MultiSpineDBEditor currently open. - - Yields: - MultiSpineDBEditor - """ - for window in qApp.topLevelWindows(): # pylint: disable=undefined-variable - if isinstance(window, QWindow): - widget = QWidget.find(window.winId()) - if isinstance(widget, MultiSpineDBEditor) and widget.accepting_new_tabs: - yield widget - - def get_all_spine_db_editors(self): - """Yields all instances of SpineDBEditor currently open. - - Yields: - SpineDBEditor - """ - for w in self.get_all_multi_spine_db_editors(): - for k in range(w.tab_widget.count()): - yield w.tab_widget.widget(k) - - def _get_existing_spine_db_editor(self, db_url_codenames): - db_url_codenames = {str(url): codename for url, codename in db_url_codenames.items()} - for multi_db_editor in self.get_all_multi_spine_db_editors(): - for k in range(multi_db_editor.tab_widget.count()): - db_editor = multi_db_editor.tab_widget.widget(k) - if db_editor.db_url_codenames == db_url_codenames: - return multi_db_editor, db_editor - return None - - def open_db_editor(self, db_url_codenames, reuse_existing_editor): - """Opens a SpineDBEditor with given urls. Uses an existing MultiSpineDBEditor if any. - Also, if the same urls are open in an existing SpineDBEditor, just raises that one - instead of creating another. - - Args: - db_url_codenames (dict): mapping url to codename - reuse_existing_editor (bool): if True and the same URL is already open, just raise the existing window - """ - multi_db_editor = next(self.get_all_multi_spine_db_editors(), None) if reuse_existing_editor else None - if multi_db_editor is None: - multi_db_editor = MultiSpineDBEditor(self, db_url_codenames) - if multi_db_editor.tab_load_success: # don't open an editor if tabs were not loaded successfully - multi_db_editor.show() - return - existing = self._get_existing_spine_db_editor(db_url_codenames) - if existing is None: - multi_db_editor.add_new_tab(db_url_codenames) - else: - multi_db_editor, db_editor = existing - multi_db_editor.set_current_tab(db_editor) - if multi_db_editor.isMinimized(): - multi_db_editor.showNormal() - multi_db_editor.activateWindow() - @Slot(ValidationKey, bool) def _parameter_value_validated(self, key, is_valid): with suppress(KeyError): diff --git a/spinetoolbox/spine_engine_worker.py b/spinetoolbox/spine_engine_worker.py index 85250da0d..1e1fdd536 100644 --- a/spinetoolbox/spine_engine_worker.py +++ b/spinetoolbox/spine_engine_worker.py @@ -52,12 +52,12 @@ def _handle_node_execution_finished(item, direction, item_state): icon.animation_signaller.animation_stopped.emit() -@Slot(object, str, str) +@Slot(object, str, str, str) def _handle_event_message_arrived(item, filter_id, msg_type, msg_text): item.add_event_message(filter_id, msg_type, msg_text) -@Slot(object, str, str) +@Slot(object, str, str, str) def _handle_process_message_arrived(item, filter_id, msg_type, msg_text): item.add_process_message(filter_id, msg_type, msg_text) @@ -370,39 +370,30 @@ def _handle_kernel_execution_msg(self, msg): self._logger.kernel_shutdown.emit(item, msg["filter_id"]) def _handle_process_msg(self, data): - self._do_handle_process_msg(**data) - - def _do_handle_process_msg(self, item_name, filter_id, msg_type, msg_text): + item_name = data["item_name"] item = self._project_items.get(item_name) or self._connections.get(item_name) - self._process_message_arrived.emit(item, filter_id, msg_type, msg_text) + self._process_message_arrived.emit(item, data["filter_id"], data["msg_type"], data["msg_text"]) def _handle_event_msg(self, data): - self._do_handle_event_msg(**data) - - def _do_handle_event_msg(self, item_name, filter_id, msg_type, msg_text): + item_name = data["item_name"] item = self._project_items.get(item_name) or self._connections.get(item_name) - self._event_message_arrived.emit(item, filter_id, msg_type, msg_text) + self._event_message_arrived.emit(item, data["filter_id"], data["msg_type"], data["msg_text"]) def _handle_node_execution_started(self, data): - self._do_handle_node_execution_started(**data) - - def _do_handle_node_execution_started(self, item_name, direction): """Starts item icon animation when executing forward.""" - item = self._project_items[item_name] + item = self._project_items[data["item_name"]] self._executing_items.add(item) - self._node_execution_started.emit(item, direction) + self._node_execution_started.emit(item, data["direction"]) def _handle_node_execution_finished(self, data): - self._do_handle_node_execution_finished(**data) - - def _do_handle_node_execution_finished(self, item_name, direction, state, item_state): - item = self._project_items[item_name] - if item_state == ItemExecutionFinishState.SUCCESS: - self.successful_executions.append((item, direction, state)) + item = self._project_items[data["item_name"]] + direction = data["direction"] + if data["item_state"] == ItemExecutionFinishState.SUCCESS: + self.successful_executions.append((item, direction, data["state"])) self._executing_items.discard(item) # NOTE: A single item may seemingly finish multiple times # when the execution is stopped by user during filtered execution. - self._node_execution_finished.emit(item, direction, item_state) + self._node_execution_finished.emit(item, direction, data["item_state"]) def _handle_server_status_msg(self, data): if data["msg_type"] == "success": diff --git a/spinetoolbox/ui/about.py b/spinetoolbox/ui/about.py index 4718db627..ee7658e75 100644 --- a/spinetoolbox/ui/about.py +++ b/spinetoolbox/ui/about.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'about.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -38,7 +38,7 @@ def setupUi(self, Form): Form.setObjectName(u"Form") Form.setWindowModality(Qt.ApplicationModal) Form.resize(400, 614) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) @@ -63,7 +63,7 @@ def setupUi(self, Form): self.horizontalLayout.setObjectName(u"horizontalLayout") self.label = QLabel(Form) self.label.setObjectName(u"label") - sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(0) sizePolicy1.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) @@ -94,18 +94,18 @@ def setupUi(self, Form): palette1.setBrush(QPalette.Disabled, QPalette.Base, brush1) palette1.setBrush(QPalette.Disabled, QPalette.Window, brush1) self.frame_9.setPalette(palette1) - self.frame_9.setCursor(QCursor(Qt.ArrowCursor)) + self.frame_9.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) self.frame_9.setAutoFillBackground(True) self.verticalLayout = QVBoxLayout(self.frame_9) self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setContentsMargins(0, -1, 0, 0) - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(self.verticalSpacer) self.label_spine_toolbox = QLabel(self.frame_9) self.label_spine_toolbox.setObjectName(u"label_spine_toolbox") - sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) sizePolicy2.setHeightForWidth(self.label_spine_toolbox.sizePolicy().hasHeightForWidth()) @@ -169,20 +169,20 @@ def setupUi(self, Form): self.verticalLayout.addWidget(self.label_python) - self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(self.verticalSpacer_3) self.horizontalLayout_8 = QHBoxLayout() self.horizontalLayout_8.setObjectName(u"horizontalLayout_8") - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout_8.addItem(self.horizontalSpacer) self.toolButton_copy_to_clipboard = QToolButton(self.frame_9) self.toolButton_copy_to_clipboard.setObjectName(u"toolButton_copy_to_clipboard") icon = QIcon() - icon.addFile(u":/icons/menu_icons/copy.svg", QSize(), QIcon.Normal, QIcon.Off) + icon.addFile(u":/icons/menu_icons/copy.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_copy_to_clipboard.setIcon(icon) self.horizontalLayout_8.addWidget(self.toolButton_copy_to_clipboard) @@ -267,7 +267,7 @@ def setupUi(self, Form): self.label_3 = QLabel(Form) self.label_3.setObjectName(u"label_3") - sizePolicy3 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) sizePolicy3.setHorizontalStretch(0) sizePolicy3.setVerticalStretch(0) sizePolicy3.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) diff --git a/spinetoolbox/ui/add_project_item.py b/spinetoolbox/ui/add_project_item.py index ffa81ac01..2a3002a08 100644 --- a/spinetoolbox/ui/add_project_item.py +++ b/spinetoolbox/ui/add_project_item.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'add_project_item.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/array_editor.py b/spinetoolbox/ui/array_editor.py index c49e8be31..087987886 100644 --- a/spinetoolbox/ui/array_editor.py +++ b/spinetoolbox/ui/array_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'array_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/datetime_editor.py b/spinetoolbox/ui/datetime_editor.py index bfd19e85b..5b903b9ef 100644 --- a/spinetoolbox/ui/datetime_editor.py +++ b/spinetoolbox/ui/datetime_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'datetime_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/duration_editor.py b/spinetoolbox/ui/duration_editor.py index 67b19f7c3..59bbb19dc 100644 --- a/spinetoolbox/ui/duration_editor.py +++ b/spinetoolbox/ui/duration_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'duration_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/import_source_selector.py b/spinetoolbox/ui/import_source_selector.py index 89876e5a7..db097f0ab 100644 --- a/spinetoolbox/ui/import_source_selector.py +++ b/spinetoolbox/ui/import_source_selector.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'import_source_selector.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/jump_properties.py b/spinetoolbox/ui/jump_properties.py index 2504476ba..718f0d6e6 100644 --- a/spinetoolbox/ui/jump_properties.py +++ b/spinetoolbox/ui/jump_properties.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'jump_properties.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/link_properties.py b/spinetoolbox/ui/link_properties.py index 103607311..55a76298a 100644 --- a/spinetoolbox/ui/link_properties.py +++ b/spinetoolbox/ui/link_properties.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'link_properties.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/mainwindow.py b/spinetoolbox/ui/mainwindow.py index c6a0f5ba1..203e40fae 100644 --- a/spinetoolbox/ui/mainwindow.py +++ b/spinetoolbox/ui/mainwindow.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'mainwindow.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -241,7 +241,7 @@ def setupUi(self, MainWindow): sizePolicy.setHeightForWidth(self.dockWidget_eventlog.sizePolicy().hasHeightForWidth()) self.dockWidget_eventlog.setSizePolicy(sizePolicy) self.dockWidget_eventlog.setMinimumSize(QSize(82, 104)) - self.dockWidget_eventlog.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable|QDockWidget.DockWidgetFeature.DockWidgetFloatable|QDockWidget.DockWidgetFeature.DockWidgetMovable) + self.dockWidget_eventlog.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetFloatable|QDockWidget.DockWidgetMovable) self.dockWidgetContents = QWidget() self.dockWidgetContents.setObjectName(u"dockWidgetContents") sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) @@ -260,7 +260,7 @@ def setupUi(self, MainWindow): self.textBrowser_eventlog.setObjectName(u"textBrowser_eventlog") sizePolicy.setHeightForWidth(self.textBrowser_eventlog.sizePolicy().hasHeightForWidth()) self.textBrowser_eventlog.setSizePolicy(sizePolicy) - self.textBrowser_eventlog.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) + self.textBrowser_eventlog.setContextMenuPolicy(Qt.DefaultContextMenu) self.textBrowser_eventlog.setOpenLinks(False) self.verticalLayout_7.addWidget(self.textBrowser_eventlog) @@ -270,8 +270,8 @@ def setupUi(self, MainWindow): 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) + self.toolButton_executions.setPopupMode(QToolButton.InstantPopup) + self.toolButton_executions.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.verticalLayout_7.addWidget(self.toolButton_executions) @@ -291,7 +291,7 @@ def setupUi(self, MainWindow): self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) self.splitter_console = QSplitter(self.dockWidgetContents_console) self.splitter_console.setObjectName(u"splitter_console") - self.splitter_console.setOrientation(Qt.Orientation.Vertical) + self.splitter_console.setOrientation(Qt.Vertical) self.splitter_console.setChildrenCollapsible(False) self.listView_console_executions = QTreeView(self.splitter_console) self.listView_console_executions.setObjectName(u"listView_console_executions") @@ -303,8 +303,8 @@ def setupUi(self, MainWindow): self.splitter_console.addWidget(self.listView_console_executions) self.label_no_console = QLabel(self.splitter_console) self.label_no_console.setObjectName(u"label_no_console") - self.label_no_console.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) - self.label_no_console.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.label_no_console.setContextMenuPolicy(Qt.DefaultContextMenu) + self.label_no_console.setAlignment(Qt.AlignCenter) self.label_no_console.setWordWrap(True) self.splitter_console.addWidget(self.label_no_console) @@ -314,6 +314,7 @@ def setupUi(self, MainWindow): MainWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.dockWidget_console) self.dockWidget_design_view = QDockWidget(MainWindow) self.dockWidget_design_view.setObjectName(u"dockWidget_design_view") + self.dockWidget_design_view.setFeatures(QDockWidget.DockWidgetMovable) self.dockWidgetContents_5 = QWidget() self.dockWidgetContents_5.setObjectName(u"dockWidgetContents_5") self.verticalLayout_2 = QVBoxLayout(self.dockWidgetContents_5) @@ -322,13 +323,13 @@ def setupUi(self, MainWindow): self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.graphicsView = DesignQGraphicsView(self.dockWidgetContents_5) self.graphicsView.setObjectName(u"graphicsView") - self.graphicsView.setFrameShape(QFrame.Shape.NoFrame) - self.graphicsView.setFrameShadow(QFrame.Shadow.Raised) - self.graphicsView.setRenderHints(QPainter.RenderHint.Antialiasing|QPainter.RenderHint.TextAntialiasing) - self.graphicsView.setDragMode(QGraphicsView.DragMode.RubberBandDrag) - self.graphicsView.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) - self.graphicsView.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate) - self.graphicsView.setRubberBandSelectionMode(Qt.ItemSelectionMode.ContainsItemBoundingRect) + self.graphicsView.setFrameShape(QFrame.NoFrame) + self.graphicsView.setFrameShadow(QFrame.Raised) + self.graphicsView.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing) + self.graphicsView.setDragMode(QGraphicsView.RubberBandDrag) + self.graphicsView.setResizeAnchor(QGraphicsView.AnchorUnderMouse) + self.graphicsView.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) + self.graphicsView.setRubberBandSelectionMode(Qt.ContainsItemBoundingRect) self.verticalLayout_2.addWidget(self.graphicsView) @@ -595,21 +596,21 @@ def retranslateUi(self, MainWindow): self.actionExecute_project.setToolTip(QCoreApplication.translate("MainWindow", u"Execute all items in project.", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) - self.actionExecute_project.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None)) + self.actionExecute_project.setShortcut(QCoreApplication.translate("MainWindow", u"Shift+F9", None)) #endif // QT_CONFIG(shortcut) self.actionExecute_selection.setText(QCoreApplication.translate("MainWindow", u"Selection", None)) #if QT_CONFIG(tooltip) self.actionExecute_selection.setToolTip(QCoreApplication.translate("MainWindow", u"Execute selected items.", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) - self.actionExecute_selection.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None)) + self.actionExecute_selection.setShortcut(QCoreApplication.translate("MainWindow", u"F9", None)) #endif // QT_CONFIG(shortcut) self.actionStop_execution.setText(QCoreApplication.translate("MainWindow", u"Stop", None)) #if QT_CONFIG(tooltip) self.actionStop_execution.setToolTip(QCoreApplication.translate("MainWindow", u"Stop execution.", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) - self.actionStop_execution.setShortcut(QCoreApplication.translate("MainWindow", u"F7", None)) + self.actionStop_execution.setShortcut(QCoreApplication.translate("MainWindow", u"F10", None)) #endif // QT_CONFIG(shortcut) self.actionTake_link.setText(QCoreApplication.translate("MainWindow", u"Take link", None)) #if QT_CONFIG(tooltip) diff --git a/spinetoolbox/ui/mainwindow.ui b/spinetoolbox/ui/mainwindow.ui index 8cf03c7ac..6ca3e2775 100644 --- a/spinetoolbox/ui/mainwindow.ui +++ b/spinetoolbox/ui/mainwindow.ui @@ -151,7 +151,7 @@ - QDockWidget::DockWidgetFeature::DockWidgetClosable|QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable + QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable Event Log @@ -196,7 +196,7 @@ - Qt::ContextMenuPolicy::DefaultContextMenu + Qt::DefaultContextMenu false @@ -213,10 +213,10 @@ :/icons/check-circle.svg:/icons/check-circle.svg - QToolButton::ToolButtonPopupMode::InstantPopup + QToolButton::InstantPopup - Qt::ToolButtonStyle::ToolButtonTextBesideIcon + Qt::ToolButtonTextBesideIcon @@ -255,7 +255,7 @@ - Qt::Orientation::Vertical + Qt::Vertical false @@ -275,13 +275,13 @@ - Qt::ContextMenuPolicy::DefaultContextMenu + Qt::DefaultContextMenu Select an executing item to see its console - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter true @@ -293,6 +293,9 @@ + + QDockWidget::DockWidgetMovable + Design View @@ -319,25 +322,25 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Raised + QFrame::Raised - QPainter::RenderHint::Antialiasing|QPainter::RenderHint::TextAntialiasing + QPainter::Antialiasing|QPainter::TextAntialiasing - QGraphicsView::DragMode::RubberBandDrag + QGraphicsView::RubberBandDrag - QGraphicsView::ViewportAnchor::AnchorUnderMouse + QGraphicsView::AnchorUnderMouse - QGraphicsView::ViewportUpdateMode::FullViewportUpdate + QGraphicsView::FullViewportUpdate - Qt::ItemSelectionMode::ContainsItemBoundingRect + Qt::ContainsItemBoundingRect @@ -816,7 +819,7 @@ Execute all items in project. - F5 + Shift+F9 @@ -831,7 +834,7 @@ Execute selected items. - F6 + F9 @@ -846,7 +849,7 @@ Stop execution. - F7 + F10 diff --git a/spinetoolbox/ui/mainwindowbase.py b/spinetoolbox/ui/mainwindowbase.py index f5ede0325..d19cef479 100644 --- a/spinetoolbox/ui/mainwindowbase.py +++ b/spinetoolbox/ui/mainwindowbase.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'mainwindowbase.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/mainwindowlite.py b/spinetoolbox/ui/mainwindowlite.py index db8a1e1cb..0d217f762 100644 --- a/spinetoolbox/ui/mainwindowlite.py +++ b/spinetoolbox/ui/mainwindowlite.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'mainwindowlite.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/map_editor.py b/spinetoolbox/ui/map_editor.py index abcce8e2e..4e72d5fc9 100644 --- a/spinetoolbox/ui/map_editor.py +++ b/spinetoolbox/ui/map_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'map_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/mini_kernel_editor_dialog.py b/spinetoolbox/ui/mini_kernel_editor_dialog.py index f8a597816..2736c5f2d 100644 --- a/spinetoolbox/ui/mini_kernel_editor_dialog.py +++ b/spinetoolbox/ui/mini_kernel_editor_dialog.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'mini_kernel_editor_dialog.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -48,13 +48,13 @@ def setupUi(self, Dialog): self.verticalLayout = QVBoxLayout(self.widget) self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(self.verticalSpacer) self.label_message = QLabel(self.widget) self.label_message.setObjectName(u"label_message") - sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(2) sizePolicy.setHeightForWidth(self.label_message.sizePolicy().hasHeightForWidth()) @@ -68,14 +68,14 @@ def setupUi(self, Dialog): self.verticalLayout.addWidget(self.label_message) - self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(self.verticalSpacer_2) self.splitter.addWidget(self.widget) self.textBrowser_process = QTextBrowser(self.splitter) self.textBrowser_process.setObjectName(u"textBrowser_process") - sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(1) sizePolicy1.setHeightForWidth(self.textBrowser_process.sizePolicy().hasHeightForWidth()) @@ -86,7 +86,7 @@ def setupUi(self, Dialog): self.stackedWidget = QStackedWidget(Dialog) self.stackedWidget.setObjectName(u"stackedWidget") - sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) sizePolicy2.setHeightForWidth(self.stackedWidget.sizePolicy().hasHeightForWidth()) @@ -103,7 +103,7 @@ def setupUi(self, Dialog): self.lineEdit_python_interpreter = QLineEdit(self.stackedWidgetPage1) self.lineEdit_python_interpreter.setObjectName(u"lineEdit_python_interpreter") self.lineEdit_python_interpreter.setEnabled(True) - self.lineEdit_python_interpreter.setCursor(QCursor(Qt.IBeamCursor)) + self.lineEdit_python_interpreter.setCursor(QCursor(Qt.CursorShape.IBeamCursor)) self.lineEdit_python_interpreter.setFocusPolicy(Qt.StrongFocus) self.lineEdit_python_interpreter.setReadOnly(True) self.lineEdit_python_interpreter.setClearButtonEnabled(False) @@ -113,7 +113,7 @@ def setupUi(self, Dialog): self.stackedWidget.addWidget(self.stackedWidgetPage1) self.stackedWidgetPage2 = QWidget() self.stackedWidgetPage2.setObjectName(u"stackedWidgetPage2") - sizePolicy3 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) sizePolicy3.setHorizontalStretch(0) sizePolicy3.setVerticalStretch(0) sizePolicy3.setHeightForWidth(self.stackedWidgetPage2.sizePolicy().hasHeightForWidth()) diff --git a/spinetoolbox/ui/open_project_dialog.py b/spinetoolbox/ui/open_project_dialog.py index 68ca8a800..9364d7e9d 100644 --- a/spinetoolbox/ui/open_project_dialog.py +++ b/spinetoolbox/ui/open_project_dialog.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'open_project_dialog.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -46,7 +46,7 @@ def setupUi(self, Dialog): self.toolButton_root = QToolButton(Dialog) self.toolButton_root.setObjectName(u"toolButton_root") icon = QIcon() - icon.addFile(u":/icons/slash.svg", QSize(), QIcon.Normal, QIcon.Off) + icon.addFile(u":/icons/slash.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_root.setIcon(icon) self.horizontalLayout.addWidget(self.toolButton_root) @@ -54,7 +54,7 @@ def setupUi(self, Dialog): self.toolButton_home = QToolButton(Dialog) self.toolButton_home.setObjectName(u"toolButton_home") icon1 = QIcon() - icon1.addFile(u":/icons/home.svg", QSize(), QIcon.Normal, QIcon.Off) + icon1.addFile(u":/icons/home.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_home.setIcon(icon1) self.horizontalLayout.addWidget(self.toolButton_home) @@ -62,7 +62,7 @@ def setupUi(self, Dialog): self.toolButton_documents = QToolButton(Dialog) self.toolButton_documents.setObjectName(u"toolButton_documents") icon2 = QIcon() - icon2.addFile(u":/icons/book.svg", QSize(), QIcon.Normal, QIcon.Off) + icon2.addFile(u":/icons/book.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_documents.setIcon(icon2) self.horizontalLayout.addWidget(self.toolButton_documents) @@ -70,12 +70,12 @@ def setupUi(self, Dialog): self.toolButton_desktop = QToolButton(Dialog) self.toolButton_desktop.setObjectName(u"toolButton_desktop") icon3 = QIcon() - icon3.addFile(u":/icons/desktop.svg", QSize(), QIcon.Normal, QIcon.Off) + icon3.addFile(u":/icons/desktop.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_desktop.setIcon(icon3) self.horizontalLayout.addWidget(self.toolButton_desktop) - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout.addItem(self.horizontalSpacer) diff --git a/spinetoolbox/ui/parameter_value_editor.py b/spinetoolbox/ui/parameter_value_editor.py index a4dd2c125..7fc0b7e84 100644 --- a/spinetoolbox/ui/parameter_value_editor.py +++ b/spinetoolbox/ui/parameter_value_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'parameter_value_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -54,7 +54,7 @@ def setupUi(self, ParameterValueEditor): self.parameter_type_selector_layout.addWidget(self.parameter_type_selector) - self.parameter_type_selector_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.parameter_type_selector_spacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.parameter_type_selector_layout.addItem(self.parameter_type_selector_spacer) diff --git a/spinetoolbox/ui/plain_parameter_value_editor.py b/spinetoolbox/ui/plain_parameter_value_editor.py index f5df143c0..bd8cc2470 100644 --- a/spinetoolbox/ui/plain_parameter_value_editor.py +++ b/spinetoolbox/ui/plain_parameter_value_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'plain_parameter_value_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -83,7 +83,7 @@ def setupUi(self, PlainParameterValueEditor): self.verticalLayout.addWidget(self.groupBox) - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(self.verticalSpacer) diff --git a/spinetoolbox/ui/select_database_items.py b/spinetoolbox/ui/select_database_items.py index 14d8f41b5..6134702e9 100644 --- a/spinetoolbox/ui/select_database_items.py +++ b/spinetoolbox/ui/select_database_items.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'select_database_items.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/select_database_items_dialog.py b/spinetoolbox/ui/select_database_items_dialog.py index 39eee7a9a..59ae1e6bb 100644 --- a/spinetoolbox/ui/select_database_items_dialog.py +++ b/spinetoolbox/ui/select_database_items_dialog.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'select_database_items_dialog.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -37,7 +37,7 @@ def setupUi(self, Dialog): Dialog.resize(400, 307) self.root_layout = QVBoxLayout(Dialog) self.root_layout.setObjectName(u"root_layout") - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.root_layout.addItem(self.verticalSpacer) diff --git a/spinetoolbox/ui/settings.py b/spinetoolbox/ui/settings.py index 1cc454478..12192ba39 100644 --- a/spinetoolbox/ui/settings.py +++ b/spinetoolbox/ui/settings.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'settings.ui' ## -## Created by: Qt User Interface Compiler version 6.6.3 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -63,24 +63,24 @@ def setupUi(self, SettingsForm): self.splitter.setChildrenCollapsible(False) self.listWidget = QListWidget(self.splitter) icon = QIcon() - icon.addFile(u":/icons/sliders-h.svg", QSize(), QIcon.Normal, QIcon.Off) + icon.addFile(u":/icons/sliders-h.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) __qlistwidgetitem = QListWidgetItem(self.listWidget) __qlistwidgetitem.setIcon(icon); __qlistwidgetitem.setFlags(Qt.ItemIsSelectable|Qt.ItemIsUserCheckable|Qt.ItemIsEnabled); icon1 = QIcon() - icon1.addFile(u":/icons/project_item_icons/hammer.svg", QSize(), QIcon.Normal, QIcon.Off) + icon1.addFile(u":/icons/project_item_icons/hammer.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) __qlistwidgetitem1 = QListWidgetItem(self.listWidget) __qlistwidgetitem1.setIcon(icon1); icon2 = QIcon() - icon2.addFile(u":/icons/database.svg", QSize(), QIcon.Normal, QIcon.Off) + icon2.addFile(u":/icons/database.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) __qlistwidgetitem2 = QListWidgetItem(self.listWidget) __qlistwidgetitem2.setIcon(icon2); icon3 = QIcon() - icon3.addFile(u":/icons/wrench.svg", QSize(), QIcon.Normal, QIcon.Off) + icon3.addFile(u":/icons/wrench.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) __qlistwidgetitem3 = QListWidgetItem(self.listWidget) __qlistwidgetitem3.setIcon(icon3); icon4 = QIcon() - icon4.addFile(u":/icons/tractor.svg", QSize(), QIcon.Normal, QIcon.Off) + icon4.addFile(u":/icons/tractor.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) __qlistwidgetitem4 = QListWidgetItem(self.listWidget) __qlistwidgetitem4.setIcon(icon4); self.listWidget.setObjectName(u"listWidget") @@ -91,14 +91,14 @@ def setupUi(self, SettingsForm): self.listWidget.setSizePolicy(sizePolicy1) self.listWidget.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) self.listWidget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) - self.listWidget.setProperty("showDropIndicator", True) + self.listWidget.setProperty(u"showDropIndicator", True) self.listWidget.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop) self.listWidget.setDefaultDropAction(Qt.DropAction.CopyAction) self.listWidget.setAlternatingRowColors(False) self.listWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.listWidget.setMovement(QListView.Movement.Static) self.listWidget.setFlow(QListView.Flow.TopToBottom) - self.listWidget.setProperty("isWrapping", False) + self.listWidget.setProperty(u"isWrapping", False) self.listWidget.setResizeMode(QListView.ResizeMode.Fixed) self.listWidget.setLayoutMode(QListView.LayoutMode.SinglePass) self.listWidget.setSpacing(0) @@ -153,7 +153,7 @@ def setupUi(self, SettingsForm): sizePolicy4.setHeightForWidth(self.toolButton_browse_work.sizePolicy().hasHeightForWidth()) self.toolButton_browse_work.setSizePolicy(sizePolicy4) icon5 = QIcon() - icon5.addFile(u":/icons/menu_icons/folder-open-solid.svg", QSize(), QIcon.Normal, QIcon.Off) + icon5.addFile(u":/icons/menu_icons/folder-open-solid.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.toolButton_browse_work.setIcon(icon5) self.horizontalLayout_6.addWidget(self.toolButton_browse_work) @@ -406,8 +406,8 @@ def setupUi(self, SettingsForm): self.line_3 = QFrame(self.groupBox_julia) self.line_3.setObjectName(u"line_3") - self.line_3.setFrameShape(QFrame.VLine) - self.line_3.setFrameShadow(QFrame.Sunken) + self.line_3.setFrameShape(QFrame.Shape.VLine) + self.line_3.setFrameShadow(QFrame.Shadow.Sunken) self.horizontalLayout_14.addWidget(self.line_3) @@ -489,8 +489,8 @@ def setupUi(self, SettingsForm): self.line = QFrame(self.groupBox_julia) self.line.setObjectName(u"line") - self.line.setFrameShape(QFrame.HLine) - self.line.setFrameShadow(QFrame.Sunken) + self.line.setFrameShape(QFrame.Shape.HLine) + self.line.setFrameShadow(QFrame.Shadow.Sunken) self.verticalLayout_10.addWidget(self.line) @@ -543,8 +543,8 @@ def setupUi(self, SettingsForm): self.line_2 = QFrame(self.groupBox_python) self.line_2.setObjectName(u"line_2") - self.line_2.setFrameShape(QFrame.VLine) - self.line_2.setFrameShadow(QFrame.Sunken) + self.line_2.setFrameShape(QFrame.Shape.VLine) + self.line_2.setFrameShadow(QFrame.Shadow.Sunken) self.horizontalLayout_5.addWidget(self.line_2) @@ -909,7 +909,7 @@ def setupUi(self, SettingsForm): self.horizontalLayout_18.setObjectName(u"horizontalLayout_18") self.lineEdit_secfolder = QLineEdit(self.groupBox_4) self.lineEdit_secfolder.setObjectName(u"lineEdit_secfolder") - self.lineEdit_secfolder.setCursor(QCursor(Qt.ArrowCursor)) + self.lineEdit_secfolder.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) self.lineEdit_secfolder.setReadOnly(False) self.lineEdit_secfolder.setClearButtonEnabled(True) @@ -949,7 +949,7 @@ def setupUi(self, SettingsForm): self.spinBox_port.setFrame(True) self.spinBox_port.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.UpDownArrows) self.spinBox_port.setAccelerated(True) - self.spinBox_port.setProperty("showGroupSeparator", False) + self.spinBox_port.setProperty(u"showGroupSeparator", False) self.spinBox_port.setMinimum(49152) self.spinBox_port.setMaximum(65535) diff --git a/spinetoolbox/ui/time_pattern_editor.py b/spinetoolbox/ui/time_pattern_editor.py index 3a5c69ff4..32853f31f 100644 --- a/spinetoolbox/ui/time_pattern_editor.py +++ b/spinetoolbox/ui/time_pattern_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'time_pattern_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/time_series_fixed_resolution_editor.py b/spinetoolbox/ui/time_series_fixed_resolution_editor.py index 771aecbf7..4261c1f1b 100644 --- a/spinetoolbox/ui/time_series_fixed_resolution_editor.py +++ b/spinetoolbox/ui/time_series_fixed_resolution_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'time_series_fixed_resolution_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/time_series_variable_resolution_editor.py b/spinetoolbox/ui/time_series_variable_resolution_editor.py index 0ce459945..997657b11 100644 --- a/spinetoolbox/ui/time_series_variable_resolution_editor.py +++ b/spinetoolbox/ui/time_series_variable_resolution_editor.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'time_series_variable_resolution_editor.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/spinetoolbox/ui/tool_configuration_assistant.py b/spinetoolbox/ui/tool_configuration_assistant.py index 66757d24c..5f605cb99 100644 --- a/spinetoolbox/ui/tool_configuration_assistant.py +++ b/spinetoolbox/ui/tool_configuration_assistant.py @@ -14,7 +14,7 @@ ################################################################################ ## Form generated from reading UI file 'tool_configuration_assistant.ui' ## -## Created by: Qt User Interface Compiler version 6.5.2 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -35,7 +35,7 @@ def setupUi(self, PackagesForm): PackagesForm.setObjectName(u"PackagesForm") PackagesForm.setWindowModality(Qt.ApplicationModal) PackagesForm.resize(685, 331) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(PackagesForm.sizePolicy().hasHeightForWidth()) @@ -63,7 +63,7 @@ def setupUi(self, PackagesForm): self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.groupBox_general = QGroupBox(self.scrollAreaWidgetContents) self.groupBox_general.setObjectName(u"groupBox_general") - sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(0) sizePolicy1.setHeightForWidth(self.groupBox_general.sizePolicy().hasHeightForWidth()) diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py index ed2c30dcc..eef191264 100644 --- a/spinetoolbox/ui_main.py +++ b/spinetoolbox/ui_main.py @@ -47,6 +47,7 @@ from .config import DEFAULT_WORK_DIR, MAINWINDOW_SS, ONLINE_DOCUMENTATION_URL, SPINE_TOOLBOX_REPO_URL from .helpers import ( ChildCyclingKeyPressFilter, + add_keyboard_shortcuts_to_action_tool_tips, busy_effect, color_from_index, create_dir, @@ -126,6 +127,7 @@ def __init__(self, toolboxuibase): self.ui = Ui_MainWindow() 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() @@ -394,7 +396,7 @@ def init_tasks(self, project_dir_from_args): """ self._display_welcome_message() if sys.version_info < (3, 9): - self._display_python_38_deprecation_message() + self._display_deprecated_python_warning() self.init_project(project_dir_from_args) def _display_welcome_message(self): @@ -408,12 +410,11 @@ def _display_welcome_message(self): welcome_msg = f"Welcome to Spine Toolbox! If you need help, please read the {getting_started_anchor} guide." self.msg.emit(welcome_msg) - def _display_python_38_deprecation_message(self): - """Shows Python 3.8 deprecation message in the event log.""" + def _display_deprecated_python_warning(self): + """Shows a warning message in Event log.""" self.msg_warning.emit("Please upgrade your Python.") self.msg_warning.emit( - f"Looks like you are running Python {sys.version_info[0]}.{sys.version_info[1]}. " - f"Support for Python older than 3.9 will be dropped in September 2024." + f"Your Python version {sys.version_info[0]}.{sys.version_info[1]} is unsupported. Expect trouble." ) def init_project(self, project_dir): @@ -1235,7 +1236,7 @@ def open_specification_file(self, index): @Slot(bool) def new_db_editor(self): - editor = MultiSpineDBEditor(self.db_mngr, {}) + editor = MultiSpineDBEditor(self.db_mngr, []) editor.show() @Slot() diff --git a/spinetoolbox/widgets/add_up_spine_opt_wizard.py b/spinetoolbox/widgets/add_up_spine_opt_wizard.py index fe96373eb..40f23cfa9 100644 --- a/spinetoolbox/widgets/add_up_spine_opt_wizard.py +++ b/spinetoolbox/widgets/add_up_spine_opt_wizard.py @@ -13,7 +13,7 @@ """Classes for custom QDialogs for julia setup.""" from enum import IntEnum, auto from PySide6.QtCore import Qt, Slot -from PySide6.QtGui import QCursor +from PySide6.QtGui import QCursor, QTextCursor from PySide6.QtWidgets import ( QCheckBox, QFileDialog, @@ -26,6 +26,7 @@ QWidget, QWizard, QWizardPage, + QApplication, ) from ..config import REQUIRED_SPINE_OPT_VERSION from ..execution_managers import QProcessExecutionManager @@ -42,7 +43,6 @@ class _PageId(IntEnum): FAILURE = auto() TROUBLESHOOT_PROBLEMS = auto() TROUBLESHOOT_SOLUTION = auto() - RESET_REGISTRY = auto() ADD_UP_SPINE_OPT_AGAIN = auto() TOTAL_FAILURE = auto() @@ -69,10 +69,10 @@ def __init__(self, parent, julia_exe, julia_project): self.setPage(_PageId.FAILURE, FailurePage(self)) self.setPage(_PageId.TROUBLESHOOT_PROBLEMS, TroubleshootProblemsPage(self)) self.setPage(_PageId.TROUBLESHOOT_SOLUTION, TroubleshootSolutionPage(self)) - self.setPage(_PageId.RESET_REGISTRY, ResetRegistryPage(self)) self.setPage(_PageId.ADD_UP_SPINE_OPT_AGAIN, AddUpSpineOptAgainPage(self)) self.setPage(_PageId.TOTAL_FAILURE, TotalFailurePage(self)) self.setStartId(_PageId.INTRO) + self.setOption(QWizard.WizardOption.NoCancelButtonOnLastPage) class IntroPage(QWizardPage): @@ -94,7 +94,7 @@ def nextId(self): class SelectJuliaPage(QWizardPage): def __init__(self, parent, julia_exe, julia_project): super().__init__(parent) - self.setTitle("Select Julia project") + self.setTitle("Select Julia") self._julia_exe = julia_exe self._julia_project = julia_project self._julia_exe_line_edit = QLineEdit() @@ -103,14 +103,14 @@ def __init__(self, parent, julia_exe, julia_project): self.registerField("julia_exe*", self._julia_exe_line_edit) self.registerField("julia_project", self._julia_project_line_edit) layout = QVBoxLayout(self) - layout.addWidget(QLabel("Julia executable:")) + layout.addWidget(QLabel("Julia executable")) julia_exe_widget = QWidget() julia_exe_layout = QHBoxLayout(julia_exe_widget) julia_exe_layout.addWidget(self._julia_exe_line_edit) julia_exe_button = QPushButton("Browse") julia_exe_layout.addWidget(julia_exe_button) layout.addWidget(julia_exe_widget) - layout.addWidget(QLabel("Julia project (directory):")) + layout.addWidget(QLabel("Julia project/environment (directory)")) julia_project_widget = QWidget() julia_project_layout = QHBoxLayout(julia_project_widget) julia_project_layout.addWidget(self._julia_project_line_edit) @@ -134,7 +134,7 @@ def _select_julia_exe(self, _): @Slot(bool) def _select_julia_project(self, _): julia_project = QFileDialog.getExistingDirectory( - self, "Select Julia project (directory)", self.field("julia_project") + self, "Select Julia project/environment (directory)", self.field("julia_project") ) if not julia_project: return @@ -179,11 +179,11 @@ def initializePage(self): self._exec_mngr = QProcessExecutionManager(self, julia_exe, args, silent=True) self.completeChanged.emit() self._exec_mngr.execution_finished.connect(self._handle_check_install_finished) - qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable + QApplication.setOverrideCursor(QCursor(Qt.CursorShape.BusyCursor)) # pylint: disable=undefined-variable self._exec_mngr.start_execution() def _handle_check_install_finished(self, ret): - qApp.restoreOverrideCursor() # pylint: disable=undefined-variable + QApplication.restoreOverrideCursor() # pylint: disable=undefined-variable self._exec_mngr.execution_finished.disconnect(self._handle_check_install_finished) if self.wizard().currentPage() is not self: return @@ -212,20 +212,20 @@ def _handle_check_install_finished(self, ret): return msg = ( f"SpineOpt version {spine_opt_version} is installed, " - f"but version {REQUIRED_SPINE_OPT_VERSION} is required." + f"but version {REQUIRED_SPINE_OPT_VERSION} or higher is required." ) self.layout().addWidget(WrapLabel(msg)) self.wizard().required_action = "update" self.setFinalPage(False) self.setCommitPage(True) - self.setButtonText(QWizard.CommitButton, "Update SpineOpt") + self.setButtonText(QWizard.WizardButton.CommitButton, "Update SpineOpt") self.completeChanged.emit() return self.layout().addWidget(QLabel("SpineOpt is not installed.")) self.wizard().required_action = "add" self.setFinalPage(False) self.setCommitPage(True) - self.setButtonText(QWizard.CommitButton, "Install SpineOpt") + self.setButtonText(QWizard.WizardButton.CommitButton, "Install SpineOpt") self.completeChanged.emit() def nextId(self): @@ -239,11 +239,10 @@ def initializePage(self): processing, code, process = { "add": ( "Installing", - 'using Pkg; pkg"registry add General https://github.com/spine-tools/SpineJuliaRegistry.git"; ' - 'pkg"add SpineOpt"', + 'using Pkg; Pkg.Registry.add("General"); Pkg.add("SpineOpt")', "installation", ), - "update": ("Updating", 'using Pkg; pkg"up SpineOpt"', "update"), + "update": ("Updating", 'using Pkg; Pkg.update("SpineOpt")', "update"), }[self.wizard().required_action] self.setTitle(f"{processing} SpineOpt") julia_exe = self.field("julia_exe") @@ -255,11 +254,11 @@ def initializePage(self): self.msg_success.emit(f"SpineOpt {process} started") cmd = julia_exe + " " + " ".join(args) self.msg.emit(f"$ {cmd}") - qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable + QApplication.setOverrideCursor(QCursor(Qt.CursorShape.BusyCursor)) # pylint: disable=undefined-variable self._exec_mngr.start_execution() def _handle_spine_opt_add_up_finished(self, ret): - qApp.restoreOverrideCursor() # pylint: disable=undefined-variable + QApplication.restoreOverrideCursor() # pylint: disable=undefined-variable self._exec_mngr.execution_finished.disconnect(self._handle_spine_opt_add_up_finished) if self.wizard().currentPage() is not self: return @@ -270,9 +269,10 @@ def _handle_spine_opt_add_up_finished(self, ret): configured = {"add": "installed", "update": "updated"}[self.wizard().required_action] self.msg_success.emit(f"SpineOpt successfully {configured}") return - process = {"add": "installation", "update": "updatee"}[self.wizard().required_action] + process = {"add": "installation", "update": "update"}[self.wizard().required_action] self.msg_error.emit(f"SpineOpt {process} failed") self.wizard().process_log = self._log.toHtml() + self.wizard().process_log_plain = self._log.toPlainText() def nextId(self): if self._successful: @@ -296,13 +296,18 @@ def nextId(self): class FailurePage(QWizardPage): - def __init__(self, parent): - super().__init__(parent) + def initializePage(self): + process = {"add": "Installation", "update": "Update"}[self.wizard().required_action] + self.setTitle(f"{process} failed") check_box = QCheckBox("Troubleshoot problems") check_box.setChecked(True) self.registerField("troubleshoot", check_box) layout = QVBoxLayout(self) - msg = "Apologies." + msg = ( + "Apologies. Please see the Troubleshoot problems section " + "by clicking Next or click Cancel to close " + "the wizard." + ) layout.addWidget(WrapLabel(msg)) layout.addStretch() layout.addWidget(check_box) @@ -314,10 +319,6 @@ def __init__(self, parent): def _handle_check_box_clicked(self, checked=False): self.setFinalPage(not checked) - def initializePage(self): - process = {"add": "Installation", "update": "Update"}[self.wizard().required_action] - self.setTitle(f"{process} failed") - def nextId(self): if self.field("troubleshoot"): return _PageId.TROUBLESHOOT_PROBLEMS @@ -329,10 +330,24 @@ def __init__(self, parent): super().__init__(parent) self.setTitle("Troubleshooting") msg = "Select your problem from the list." - self._button1 = QRadioButton("Installing SpineOpt fails with one of the following messages (or similar):") - msg1a = MonoSpaceFontTextBrowser(self) - msg1b = MonoSpaceFontTextBrowser(self) - msg1a.append( + self._button1 = QRadioButton("None of the below") + self._button2 = QRadioButton("Installing SpineOpt fails with the following message (or similar):") + msg2 = MonoSpaceFontTextBrowser(self) + msg2.append( + """ + \u22ee
    + error: GitError(Code:ERROR, Class:SSL, Your Julia is built with a SSL/TLS engine that libgit2 + doesn't know how to configure to use a file or directory of certificate authority roots, + but your environment specifies one via the SSL_CERT_FILE variable. If you believe your + system's root certificates are safe to use, you can `export JULIA_SSL_CA_ROOTS_PATH=""` + in your environment to use those instead.
    + \u22ee + """ + ) + self._button3 = QRadioButton("Installing SpineOpt fails with one of the following messages (or similar):") + msg3a = MonoSpaceFontTextBrowser(self) + msg3b = MonoSpaceFontTextBrowser(self) + msg3a.append( """ \u22ee
    Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`
    @@ -341,7 +356,7 @@ def __init__(self, parent): \u22ee """ ) - msg1b.append( + msg3b.append( """ \u22ee
    Updating git-repo `https://github.com/spine-tools/SpineJuliaRegistry`
    @@ -350,9 +365,9 @@ def __init__(self, parent): \u22ee """ ) - self._button2 = QRadioButton("On Windows 7, installing SpineOpt fails with the following message (or similar):") - msg2 = MonoSpaceFontTextBrowser(self) - msg2.append( + self._button4 = QRadioButton("On Windows 7, installing SpineOpt fails with the following message (or similar):") + msg4 = MonoSpaceFontTextBrowser(self) + msg4.append( """ \u22ee
    Downloading artifact: OpenBLAS32
    @@ -371,31 +386,46 @@ def __init__(self, parent): layout.addWidget(WrapLabel(msg)) layout.addStretch() layout.addWidget(self._button1) - layout.addWidget(msg1a) - layout.addWidget(msg1b) layout.addStretch() layout.addWidget(self._button2) layout.addWidget(msg2) layout.addStretch() - button_view_log = QPushButton("View process log") + layout.addWidget(self._button3) + layout.addWidget(msg3a) + layout.addWidget(msg3b) + layout.addStretch() + layout.addWidget(self._button4) + layout.addWidget(msg4) + layout.addStretch() + button_view_log = QPushButton("View log") widget_view_log = QWidget() layout_view_log = QHBoxLayout(widget_view_log) layout_view_log.addStretch() layout_view_log.addWidget(button_view_log) layout.addWidget(widget_view_log) layout.addStretch() + cursor = QTextCursor() + cursor.movePosition(QTextCursor.MoveOperation.Start, QTextCursor.MoveMode.MoveAnchor) + msg2.setTextCursor(cursor) # Scroll to the beginning of the document + msg3a.setTextCursor(cursor) + msg3b.setTextCursor(cursor) + msg4.setTextCursor(cursor) self.registerField("problem1", self._button1) self.registerField("problem2", self._button2) + self.registerField("problem3", self._button3) + self.registerField("problem4", self._button4) self._button1.toggled.connect(lambda _: self.completeChanged.emit()) self._button2.toggled.connect(lambda _: self.completeChanged.emit()) + self._button3.toggled.connect(lambda _: self.completeChanged.emit()) + self._button4.toggled.connect(lambda _: self.completeChanged.emit()) button_view_log.clicked.connect(self._show_log) def isComplete(self): - return self.field("problem1") or self.field("problem2") + return self.field("problem1") or self.field("problem2") or self.field("problem3") or self.field("problem4") @Slot(bool) def _show_log(self, _=False): - log_widget = QWidget(self, f=Qt.Window) + log_widget = QWidget(self, f=Qt.WindowType.Window) layout = QVBoxLayout(log_widget) log = MonoSpaceFontTextBrowser(log_widget) log.append(self.wizard().process_log) @@ -412,97 +442,144 @@ def __init__(self, parent): self.setCommitPage(True) QVBoxLayout(self) - def cleanupPage(self): - super().cleanupPage() - self.wizard().reset_registry = False - def initializePage(self): _clear_layout(self.layout()) if self.field("problem1"): self._initialize_page_solution1() elif self.field("problem2"): self._initialize_page_solution2() + elif self.field("problem3"): + self._initialize_page_solution3() + elif self.field("problem4"): + self._initialize_page_solution4() def _initialize_page_solution1(self): - self.wizard().reset_registry = True + self.setFinalPage(False) + action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] + julia = self.field("julia_exe") + env = self.field("julia_project") + if not env: + install_cmds = f""" + julia> import Pkg
    + julia> Pkg.Registry.add("General")
    + julia> Pkg.add("SpineOpt")
    """ + else: + install_cmds = f""" + julia> import Pkg
    + julia> cd("{env}")
    + julia> Pkg.activate(".")
    + julia> Pkg.Registry.add("General")
    + julia> Pkg.add("SpineOpt")
    """ + if not env: + update_cmds = """ + julia> import Pkg
    + julia> Pkg.update("SpineOpt")
    """ + else: + update_cmds = f""" + julia> import Pkg
    + julia> cd("{env}")
    + julia> Pkg.activate(".")
    + julia> Pkg.update("SpineOpt")
    """ + action_cmds = {"Install SpineOpt": install_cmds, "Update SpineOpt": update_cmds} + self.setTitle("What now?") + msg_browser = MonoSpaceFontTextBrowser(self) + msg_browser.append(action_cmds[action]) + label1_txt = ( + "

      " + f"
    1. Click the {action} button to try again.
    2. " + f"
    3. {action} manually. Open your favorite terminal (ie. Command prompt) and start the " + f"Julia REPL using command:

      {julia}

      " + "In the Julia REPL, enter the following commands (gray text, not the green one):

    " + ) + label2_txt = ( + "

    See also up-to-date " + "installation " + "instructions in SpineOpt documentation.

    " + ) + self.layout().addWidget(HyperTextLabel(label1_txt)) + self.layout().addWidget(msg_browser) + self.layout().addWidget(HyperTextLabel(label2_txt)) + self.setButtonText(QWizard.WizardButton.CommitButton, action) + + def _initialize_page_solution2(self): + self.setFinalPage(True) + julia = self.field("julia_exe") + env = self.field("julia_project") + self.setTitle("Environment variable JULIA_SSL_CA_ROOTS_PATH missing") + description = ( + "

    You are most likely running Toolbox in a Conda environment and the issue " + "you're facing is due to a missing environment variable. The simplest solution " + "is to open the Julia REPL from the Anaconda Prompt, add the environment variable, " + "and then install SpineOpt.

    " + "

    To do this, open your Anaconda prompt and start the Julia REPL using " + f"command:

    {julia}

    In the Julia REPL, enter the commands below (gray text, " + "not the green one). After entering the commands, SpineOpt should be installed. If you run into " + "other problems, please open an issue " + "with SpineOpt.

    " + ) + if not env: + install_cmds = f""" + julia> using Pkg
    + julia> ENV["JULIA_SSL_CA_ROOTS_PATH"] = ""
    + julia> Pkg.Registry.add("General")
    + julia> Pkg.add("SpineOpt")
    """ + else: + install_cmds = f""" + julia> using Pkg
    + julia> cd("{env}")
    + julia> Pkg.activate(".")
    + julia> ENV["JULIA_SSL_CA_ROOTS_PATH"] = ""
    + julia> Pkg.Registry.add("General")
    + julia> Pkg.add("SpineOpt")
    """ + cmd_browser = MonoSpaceFontTextBrowser(self) + cmd_browser.append(install_cmds) + self.layout().addWidget(HyperTextLabel(description)) + self.layout().addWidget(cmd_browser) + + def _initialize_page_solution3(self): + self.setFinalPage(True) + julia = self.field("julia_exe") self.setTitle("Reset Julia General Registry") description = ( "

    The issue you're facing can be due to an error in the installation of the Julia General registry " - "from the Julia Package Server.

    " - "

    The simplest solution is to delete any trace of the registry and install it again, from GitHub.

    " - "

    However, this will also remove all your installed packages.

    " + "from the Julia Package Server. The simplest solution is to delete any trace of the registry and install " + "it again from GitHub.

    " + "

    To do this, open your favorite terminal (ie. Command prompt) and start the Julia REPL using " + f"command:

    {julia}

    In the Julia REPL, enter the commands below (gray text, " + "not the green one). Afterwards, try Add/Update SpineOpt again.

    " + "

    NOTE: this will also remove all your installed packages.

    " ) + cmds = f""" + julia> import Pkg
    + julia> Pkg.Registry.rm("General")
    + julia> Pkg.Registry.add()
    """ + cmd_browser = MonoSpaceFontTextBrowser(self) + cmd_browser.append(cmds) self.layout().addWidget(HyperTextLabel(description)) - self.setButtonText(QWizard.CommitButton, "Reset registry") + self.layout().addWidget(cmd_browser) - def _initialize_page_solution2(self): + def _initialize_page_solution4(self): + self.setFinalPage(True) action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] - self.setTitle("Update Windows Managemet Framework") + self.setTitle("Update Windows Management Framework") description = ( - "

    The issue you're facing can be solved by installing Windows Managemet Framework 3 or greater, " + "

    The issue you're facing can be solved by updating Windows Management Framework to 5.1 or greater, " "as follows:

      " - "
    • Install .NET 4.5 " - "from here.
    • " - "
    • Install Windows management framework 3 or later " - "from here.
    • " + "
    • Install latest .NET Framework (minimum 4.5) " + "from here.
    • " + "
    • Install Windows management framework 5.1 or later " + "from here.
    • " f"
    • {action} again.
    • " "

    " ) self.layout().addWidget(HyperTextLabel(description)) - self.setButtonText(QWizard.CommitButton, action) def nextId(self): - if self.field("problem1"): - return _PageId.RESET_REGISTRY + if self.field("problem2") or self.field("problem3") or self.field("problem4"): + return -1 return _PageId.ADD_UP_SPINE_OPT_AGAIN -class ResetRegistryPage(QWizardProcessPage): - def initializePage(self): - code = ( - "using Pkg; " - 'rm(joinp ath(DEPOT_PATH[1], "registries", "General"); force=true, recursive=true); ' - 'withenv("JULIA_PKG_SERVER"=>"") do pkg"registry add" end' - ) - self.setTitle("Resetting Julia General Registry") - julia_exe = self.field("julia_exe") - julia_project = self.field("julia_project") - args = [f"--project={julia_project}", "-e", code] - self._exec_mngr = QProcessExecutionManager(self, julia_exe, args, semisilent=True) - self.completeChanged.emit() - self._exec_mngr.execution_finished.connect(self._handle_registry_reset_finished) - self.msg_success.emit("Registry reset started") - cmd = julia_exe + " " + " ".join(args) - self.msg.emit(f"$ {cmd}") - qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable - self._exec_mngr.start_execution() - - def _handle_registry_reset_finished(self, ret): - qApp.restoreOverrideCursor() # pylint: disable=undefined-variable - self._exec_mngr.execution_finished.disconnect(self._handle_registry_reset_finished) - if self.wizard().currentPage() is not self: - return - self._exec_mngr = None - self._successful = ret == 0 - if self._successful: - self.msg_success.emit("Registry successfully reset") - self.setCommitPage(True) - action = {"add": "Install SpineOpt", "update": "Update SpineOpt"}[self.wizard().required_action] - self.setButtonText(QWizard.CommitButton, action) - else: - # FIXME: Rather, add a button to copy log to clipboard? - # self.wizard().process_log = self._log.toHtml() - self.msg_error.emit("Registry reset failed") - self.completeChanged.emit() - - def nextId(self): - if self._successful: - return _PageId.ADD_UP_SPINE_OPT_AGAIN - return _PageId.TOTAL_FAILURE - - class AddUpSpineOptAgainPage(AddUpSpineOptPage): def nextId(self): if self._successful: @@ -513,10 +590,31 @@ def nextId(self): class TotalFailurePage(QWizardPage): def __init__(self, parent): super().__init__(parent) + self._copy_label = QLabel() + + def initializePage(self): self.setTitle("Troubleshooting failed") - msg = "

    Please open an issue with SpineOpt." + msg = ( + "

    Please open an issue with SpineOpt." + "
    Copy the log and paste it into the issue description.

    " + ) layout = QVBoxLayout(self) layout.addWidget(HyperTextLabel(msg)) + copy_widget = QWidget() + copy_button = QPushButton("Copy log") + self._copy_label.setText("Log copied to clipboard") + self._copy_label.hide() + layout_copy = QHBoxLayout(copy_widget) + layout_copy.addWidget(copy_button) + layout_copy.addWidget(self._copy_label) + layout_copy.addStretch() + layout.addWidget(copy_widget) + copy_button.clicked.connect(self._handle_copy_clicked) + + @Slot(bool) + def _handle_copy_clicked(self, _=False): + self._copy_label.show() + QApplication.clipboard().setText(self.wizard().process_log_plain) # pylint: disable=undefined-variable def nextId(self): return -1 diff --git a/spinetoolbox/widgets/code_text_edit.py b/spinetoolbox/widgets/code_text_edit.py index cc115f7db..f8524d7e8 100644 --- a/spinetoolbox/widgets/code_text_edit.py +++ b/spinetoolbox/widgets/code_text_edit.py @@ -65,10 +65,11 @@ def setPlainText(self, text): self.setDocument(doc) def setDocument(self, doc): - doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) + if doc is not None: + doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) + doc.setDefaultFont(self.font()) super().setDocument(doc) self._highlighter.setDocument(doc) - doc.setDefaultFont(self.font()) self.setTabStopDistance(QFontMetrics(self.font()).horizontalAdvance(4 * " ")) def line_number_area_width(self): diff --git a/spinetoolbox/widgets/custom_qtextbrowser.py b/spinetoolbox/widgets/custom_qtextbrowser.py index 1524701b5..a9fa22730 100644 --- a/spinetoolbox/widgets/custom_qtextbrowser.py +++ b/spinetoolbox/widgets/custom_qtextbrowser.py @@ -47,7 +47,7 @@ def __init__(self, parent): self._frame_format.setBorder(1) self._selected_frame_format = QTextFrameFormat(self._frame_format) palette = self.palette() - self._selected_frame_format.setBackground(QBrush(palette.color(QPalette.Highlight).darker())) + self._selected_frame_format.setBackground(QBrush(palette.color(QPalette.ColorRole.Highlight).darker())) self._executions_menu.aboutToShow.connect(self._populate_executions_menu) self._executions_menu.triggered.connect(self._select_execution) @@ -236,12 +236,13 @@ def __init__(self, parent): self.setStyleSheet(TEXTBROWSER_SS) -class MonoSpaceFontTextBrowser(CustomQTextBrowser): +class MonoSpaceFontTextBrowser(QTextBrowser): def __init__(self, parent): """ Args: parent (QWidget): Parent widget """ super().__init__(parent=parent) + self.setStyleSheet(TEXTBROWSER_SS) font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) self.setFont(font) diff --git a/spinetoolbox/widgets/custom_qwidgets.py b/spinetoolbox/widgets/custom_qwidgets.py index 2a9c6ea07..3b931a0fe 100644 --- a/spinetoolbox/widgets/custom_qwidgets.py +++ b/spinetoolbox/widgets/custom_qwidgets.py @@ -239,7 +239,7 @@ def __init__(self, text, parent=None, compact=False): parent.installEventFilter(self) def eventFilter(self, obj, ev): - if ev.type() == QEvent.KeyPress: + if ev.type() == QEvent.Type.KeyPress: self._parent_key_press_event = QKeyEvent(ev.type(), ev.key(), ev.modifiers()) return super().eventFilter(obj, ev) @@ -342,7 +342,7 @@ def _align_buttons(self): for i in range(layout.count()): item = layout.itemAt(i) if item.widget() in self._buttons: - item.setAlignment(Qt.AlignBottom) + item.setAlignment(Qt.AlignmentFlag.AlignBottom) def add_frame(self, left, right, title): """Add frame around given actions, with given title. @@ -394,16 +394,16 @@ def paintEvent(self, ev): top_left = left.geometry().topLeft() bottom_right = right.geometry().bottomRight() rect = QRect(top_left, bottom_right).adjusted(-1, -fm.height() / 2, 1, 1) - painter.setPen(Qt.gray) + painter.setPen(Qt.GlobalColor.gray) painter.drawRoundedRect(rect, 1, 1) title_rect = fm.boundingRect(title).adjusted(-4, 0, 4, 0) title_rect.moveCenter(rect.center()) title_rect.moveTop(rect.top() - fm.height() / 2) - painter.setBrush(Qt.white) - painter.setPen(Qt.NoPen) + painter.setBrush(Qt.GlobalColor.white) + painter.setPen(Qt.PenStyle.NoPen) painter.drawRect(title_rect) - painter.setPen(Qt.black) - painter.drawText(title_rect, Qt.AlignHCenter | Qt.AlignTop, title) + painter.setPen(Qt.GlobalColor.black) + painter.drawText(title_rect, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, title) painter.end() def _setup_action_button(self, action): @@ -438,29 +438,29 @@ def eventFilter(self, obj, ev): """Installed on each action's QToolButton. Ignores Up and Down key press events, so they are handled by the toolbar for custom navigation. """ - if ev.type() == QEvent.KeyPress: - if ev.key() in (Qt.Key_Left, Qt.Key_Right): + if ev.type() == QEvent.Type.KeyPress: + if ev.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right): ev.accept() return True - if ev.key() in (Qt.Key_Up, Qt.Key_Down): + if ev.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): ev.ignore() return True return super().eventFilter(obj, ev) def keyPressEvent(self, ev): """Navigates over the tool bar buttons.""" - if ev.key() in (Qt.Key_Left, Qt.Key_Right): # FIXME + if ev.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right): # FIXME ev.ignore() return - if ev.key() in (Qt.Key_Up, Qt.Key_Down): + if ev.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): widgets = [self.widgetForAction(a) for a in self.actions() if not a.isSeparator() and a.isEnabled()] if self._focus_widget not in widgets: self._focus_widget = None if self._focus_widget is None: - next_index = 0 if ev.key() == Qt.Key_Down else len(widgets) - 1 + next_index = 0 if ev.key() == Qt.Key.Key_Down else len(widgets) - 1 else: index = widgets.index(self._focus_widget) - next_index = index + 1 if ev.key() == Qt.Key_Down else index - 1 + next_index = index + 1 if ev.key() == Qt.Key.Key_Down else index - 1 if 0 <= next_index < len(widgets): self._focus_widget = widgets[next_index] self._focus_widget.setFocus() @@ -760,7 +760,7 @@ def _handle_check_box_state_changed(self, _checked): ) if self._warn_checked_non_data_items: if self._item_check_boxes_widget.any_structural_item_checked(): - self._ui.warning_label.setText("Warning! Structural data items selected.") + self._ui.warning_label.setText("Warning! You are about to delete structural items.") else: self._ui.warning_label.clear() diff --git a/spinetoolbox/widgets/install_julia_wizard.py b/spinetoolbox/widgets/install_julia_wizard.py index d5956c27f..b9590a254 100644 --- a/spinetoolbox/widgets/install_julia_wizard.py +++ b/spinetoolbox/widgets/install_julia_wizard.py @@ -13,6 +13,7 @@ """Classes for custom QDialogs for julia setup.""" from enum import IntEnum, auto import os +import sys try: import jill.install as jill_install @@ -31,6 +32,7 @@ QWidget, QWizard, QWizardPage, + QApplication, ) from spine_engine.utils.helpers import resolve_current_python_interpreter from ..config import APPLICATION_PATH @@ -71,13 +73,15 @@ def __init__(self, parent): self.setStartId(_PageId.INTRO) def set_julia_exe(self): - basename = next( - (file for file in os.listdir(self.field("symlink_dir")) if file.lower().startswith("julia")), None - ) - if basename is None: + """Returns the path to the jill julia launcher, which always launches the latest Julia release.""" + if not sys.platform == "win32": + julia_launcher_path = os.path.join(self.field("symlink_dir"), "julia") + else: + julia_launcher_path = os.path.join(self.field("symlink_dir"), "julia.cmd") + if not os.path.exists(julia_launcher_path): self.julia_exe = None return - self.julia_exe = os.path.join(self.field("symlink_dir"), basename) + self.julia_exe = julia_launcher_path def accept(self): super().accept() @@ -156,7 +160,7 @@ def __init__(self, parent): install_dir_button.clicked.connect(self._select_install_dir) symlink_dir_button.clicked.connect(self._select_symlink_dir) self.setCommitPage(True) - self.setButtonText(QWizard.CommitButton, "Install Julia") + self.setButtonText(QWizard.WizardButton.CommitButton, "Install Julia") def initializePage(self): self._install_dir_line_edit.setText(jill_install.default_install_dir()) @@ -215,12 +219,12 @@ def initializePage(self): self.msg_success.emit("Julia installation started") cmd = python + " " + " ".join(args) self.msg.emit(f"$ {cmd}") - qApp.setOverrideCursor(QCursor(Qt.BusyCursor)) # pylint: disable=undefined-variable + QApplication.setOverrideCursor(QCursor(Qt.CursorShape.BusyCursor)) # pylint: disable=undefined-variable self._exec_mngr.start_execution() @Slot(int) def _handle_julia_install_finished(self, ret): - qApp.restoreOverrideCursor() # pylint: disable=undefined-variable + QApplication.restoreOverrideCursor() # pylint: disable=undefined-variable self._exec_mngr.execution_finished.disconnect(self._handle_julia_install_finished) if self.wizard().currentPage() != self: return diff --git a/spinetoolbox/widgets/multi_tab_window.py b/spinetoolbox/widgets/multi_tab_window.py index dd4baa7eb..9c0d332b3 100644 --- a/spinetoolbox/widgets/multi_tab_window.py +++ b/spinetoolbox/widgets/multi_tab_window.py @@ -70,7 +70,7 @@ def _make_new_tab(self, *args, **kwargs): """Creates a new tab. Args: - *args: positional arguments neede to make a new tab + *args: positional arguments needed to make a new tab **kwargs: keyword arguments needed to make a new tab """ raise NotImplementedError() diff --git a/spinetoolbox/widgets/plugin_manager_widgets.py b/spinetoolbox/widgets/plugin_manager_widgets.py index 2a6b7ea8a..82f1585e3 100644 --- a/spinetoolbox/widgets/plugin_manager_widgets.py +++ b/spinetoolbox/widgets/plugin_manager_widgets.py @@ -19,14 +19,14 @@ class _InstallPluginModel(QStandardItemModel): def data(self, index, role=None): - if role == Qt.SizeHintRole: + if role == Qt.ItemDataRole.SizeHintRole: return QSize(0, 40) return super().data(index, role) class _ManagePluginsModel(_InstallPluginModel): def flags(self, index): - return super().flags(index) & ~Qt.ItemIsSelectable + return super().flags(index) & ~Qt.ItemFlag.ItemIsSelectable class InstallPluginDialog(QDialog): @@ -44,7 +44,7 @@ def __init__(self, parent): self._model = QSortFilterProxyModel(self) self._source_model = _InstallPluginModel(self) self._model.setSourceModel(self._source_model) - self._model.setFilterCaseSensitivity(Qt.CaseInsensitive) + self._model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self._list_view.setModel(self._model) self._timer = QTimer(self) self._timer.setInterval(200) @@ -54,7 +54,7 @@ def __init__(self, parent): self.layout().addWidget(self._line_edit) self.layout().addWidget(self._list_view) self.layout().addWidget(self._button_box) - self.setAttribute(Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.setMinimumWidth(400) self._button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close) self._button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self._handle_ok_clicked) @@ -109,13 +109,14 @@ def __init__(self, parent): self._button_box.setStandardButtons(QDialogButtonBox.StandardButton.Close) self.layout().addWidget(self._list_view) self.layout().addWidget(self._button_box) - self.setAttribute(Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.setMinimumWidth(400) self._button_box.button(QDialogButtonBox.StandardButton.Close).clicked.connect(self.close) def populate_list(self, names): for name, can_update in names: - item = QStandardItem(name) + item = QStandardItem() # Don't put name into DisplayRole or the plugin name is shown twice + item.setData(name, role=Qt.ItemDataRole.UserRole) self._model.appendRow(item) widget = self._create_plugin_widget(name, can_update) index = self._model.indexFromItem(item) @@ -131,7 +132,7 @@ def _create_plugin_widget(self, plugin_name, can_update): def _emit_item_removed(self, plugin_name): for row in range(self._model.rowCount()): - if self._model.index(row, 0).data(Qt.ItemDataRole.DisplayRole) == plugin_name: + if self._model.index(row, 0).data(Qt.ItemDataRole.UserRole) == plugin_name: self._model.removeRow(row) break self.item_removed.emit(plugin_name) diff --git a/spinetoolbox/widgets/project_item_drag.py b/spinetoolbox/widgets/project_item_drag.py index 5991a386a..077a7ca35 100644 --- a/spinetoolbox/widgets/project_item_drag.py +++ b/spinetoolbox/widgets/project_item_drag.py @@ -30,16 +30,16 @@ def _reset(self): self.drag_start_pos = None self.pixmap = None self.mime_data = None - self.setCursor(Qt.OpenHandCursor) + self.setCursor(Qt.CursorShape.OpenHandCursor) def mousePressEvent(self, event): super().mousePressEvent(event) - self.setCursor(Qt.ClosedHandCursor) + self.setCursor(Qt.CursorShape.ClosedHandCursor) def mouseMoveEvent(self, event): """Start dragging action if needed""" super().mouseMoveEvent(event) - if not event.buttons() & Qt.LeftButton: + if not event.buttons() & Qt.MouseButton.LeftButton: return if not self.drag_start_pos: return @@ -62,7 +62,7 @@ def mouseReleaseEvent(self, event): def enterEvent(self, event): super().enterEvent(event) - self.setCursor(Qt.OpenHandCursor) + self.setCursor(Qt.CursorShape.OpenHandCursor) class NiceButton(QToolButton): @@ -73,20 +73,27 @@ def __init__(self, *args, **kwargs): self.setFont(font) def setText(self, text): - super().setText(fill(text, width=12, break_long_words=False)) + if self.toolButtonStyle() == Qt.ToolButtonStyle.ToolButtonTextUnderIcon: + super().setText(fill(text, width=12, break_long_words=False)) + elif self.toolButtonStyle() == Qt.ToolButtonStyle.ToolButtonTextBesideIcon: + txt_l = text.strip().split() # Remove all newlines + txt = " ".join(txt_l) + trunc = txt[:12] + ".." if len(txt) > 14 else txt # Truncate text to 14 characters with .. in the end + super().setText(trunc) def set_orientation(self, orientation): if orientation == Qt.Orientation.Horizontal: self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon) self.setStyleSheet("QToolButton{margin: 16px 2px 2px 2px;}") else: - self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.setStyleSheet("QToolButton{margin: 2px;}") class ProjectItemButtonBase(ProjectItemDragMixin, NiceButton): - def __init__(self, toolbox, item_type, icon, parent=None): + def __init__(self, toolbox, item_type, icon, style, parent=None): super().__init__(parent=parent) + self.setToolButtonStyle(style) self._toolbox = toolbox self.item_type = item_type self._icon = icon @@ -113,7 +120,7 @@ def _handle_drag_about_to_start(self): def mousePressEvent(self, event): """Register drag start position""" super().mousePressEvent(event) - if event.button() == Qt.LeftButton: + if event.button() == Qt.MouseButton.LeftButton: self.drag_start_pos = event.position().toPoint() self.pixmap = self.icon().pixmap(self.iconSize()) self.mime_data = QMimeData() @@ -126,11 +133,15 @@ def _make_mime_data_text(self): class ProjectItemButton(ProjectItemButtonBase): double_clicked = Signal() - def __init__(self, toolbox, item_type, icon, parent=None): - super().__init__(toolbox, item_type, icon, parent=parent) + def __init__(self, toolbox, item_type, icon, style, parent=None): + super().__init__(toolbox, item_type, icon, style, parent=parent) self.setToolTip(f"

    Drag-and-drop this onto the Design View to create a new {item_type} item.

    ") self.setText(item_type) + def set_orientation(self, orientation): + super().set_orientation(orientation) + self.setText(self.item_type) + def _make_mime_data_text(self): return ",".join([self.item_type, ""]) @@ -139,12 +150,11 @@ def mouseDoubleClickEvent(self, event): class ProjectItemSpecButton(ProjectItemButtonBase): - def __init__(self, toolbox, item_type, icon, spec_name="", parent=None): - super().__init__(toolbox, item_type, icon, parent=parent) + def __init__(self, toolbox, item_type, icon, style, spec_name="", parent=None): + super().__init__(toolbox, item_type, icon, style, parent=parent) self._spec_name = None self._index = None self.spec_name = spec_name - self.setText(self.spec_name) @property def spec_name(self): @@ -156,6 +166,10 @@ def spec_name(self, spec_name): self.setText(self._spec_name) self.setToolTip(f"

    Drag-and-drop this onto the Design View to create a new {self.spec_name} item.

    ") + def set_orientation(self, orientation): + super().set_orientation(orientation) + self.setText(self.spec_name) + def _make_mime_data_text(self): return ",".join([self.item_type, self.spec_name]) diff --git a/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py index dfb66bbb1..c8fa70cc9 100644 --- a/spinetoolbox/widgets/settings_widget.py +++ b/spinetoolbox/widgets/settings_widget.py @@ -39,6 +39,7 @@ from ..kernel_fetcher import KernelFetcher from ..link import JumpLink, Link from ..project_item_icon import ProjectItemIcon +from ..spine_db_editor.editors import db_editor_registry from ..widgets.kernel_editor import MiniJuliaKernelEditor, MiniPythonKernelEditor from .add_up_spine_opt_wizard import AddUpSpineOptWizard from .install_julia_wizard import InstallJuliaWizard @@ -234,7 +235,7 @@ def update_ui(self): @Slot(bool) def set_hide_empty_classes(self, checked=False): - for db_editor in self.db_mngr.get_all_spine_db_editors(): + for db_editor in db_editor_registry.tabs(): db_editor.entity_tree_model.hide_empty_classes = checked @Slot(bool) @@ -266,7 +267,7 @@ def set_neg_weight_exp(self, value=None): self._set_graph_property("neg_weight_exp", value) def _set_graph_property(self, name, value): - for db_editor in self.db_mngr.get_all_spine_db_editors(): + for db_editor in db_editor_registry.tabs(): db_editor.ui.graphicsView.set_property(name, value) @@ -442,6 +443,13 @@ def _show_install_julia_wizard(self, _=False): def _show_add_up_spine_opt_wizard(self, _=False): """Opens the add/update SpineOpt wizard.""" use_julia_jupyter_console, julia_path, julia_project_path, julia_kernel = self._get_julia_settings() + if julia_project_path != "@." and not dir_is_valid( + self, + julia_project_path, + "Invalid Julia Project", + "Julia project must be an existing directory, @., or empty", + ): + return settings = QSettings("SpineProject", "AddUpSpineOptWizard") settings.setValue("appSettings/useJuliaKernel", use_julia_jupyter_console) settings.setValue("appSettings/juliaPath", julia_path) diff --git a/spinetoolbox/widgets/toolbars.py b/spinetoolbox/widgets/toolbars.py index 377b4bdce..90fab68d9 100644 --- a/spinetoolbox/widgets/toolbars.py +++ b/spinetoolbox/widgets/toolbars.py @@ -74,13 +74,14 @@ def __init__(self, name, toolbox): self.setObjectName(name.replace(" ", "_")) self._toolbox = toolbox self.addWidget(_TitleWidget(self.name(), self)) + self.setIconSize(QSize(20, 20)) def name(self): return self._name def paintEvent(self, ev): super().paintEvent(ev) - if self.orientation() == Qt.Vertical: + if self.orientation() == Qt.Orientation.Vertical: return layout = self.layout() title_pos_x = ( @@ -157,9 +158,12 @@ def _make_tool_button(self, icon, text, slot=None, tip=None): QToolButton: created button """ button = NiceButton() + style = self._get_toolbutton_style() + button.setToolButtonStyle(style) button.setIcon(icon) button.setText(text) - button.setToolTip(f"

    {tip}

    ") + if tip is not None: + button.setToolTip(f"

    {tip}

    ") if slot is not None: button.clicked.connect(slot) self._add_tool_button(button) @@ -171,6 +175,11 @@ def _icon_from_factory(self, factory): icon_color = factory.icon_color().darker(120) return ColoredIcon(icon_file_name, icon_color, self.iconSize(), colored=colored) + def _get_toolbutton_style(self): + if self.orientation() == Qt.Orientation.Horizontal: + return Qt.ToolButtonStyle.ToolButtonTextUnderIcon + return Qt.ToolButtonStyle.ToolButtonTextBesideIcon + class PluginToolBar(ToolBar): """A plugin toolbar.""" @@ -202,7 +211,8 @@ def setup(self, plugin_specs, disabled_names): for spec in specs: factory = self._toolbox.item_factories[spec.item_type] icon = self._icon_from_factory(factory) - button = ProjectItemSpecButton(self._toolbox, spec.item_type, icon, spec.name) + style = self._get_toolbutton_style() + button = ProjectItemSpecButton(self._toolbox, spec.item_type, icon, style, spec.name) button.setIconSize(self.iconSize()) if spec.name in disabled_names: button.setEnabled(False) @@ -244,7 +254,8 @@ def _add_spec(self, row): next_row += 1 factory = self._toolbox.item_factories[spec.item_type] icon = self._icon_from_factory(factory) - button = ProjectItemSpecButton(self._toolbox, spec.item_type, icon, spec.name) + style = self._get_toolbutton_style() + button = ProjectItemSpecButton(self._toolbox, spec.item_type, icon, style, spec.name) button.setIconSize(self.iconSize()) action = ( self._insert_tool_button(self._actions[next_spec.name], button) @@ -288,10 +299,10 @@ def setup(self): ).setIcon(self._icon_from_factory(factory)) menu.addSeparator() menu.addAction("From specification file...", self._toolbox.import_specification).setIcon( - QIcon(CharIconEngine("\uf067", color=Qt.darkGreen)) + QIcon(CharIconEngine("\uf067", color=Qt.GlobalColor.darkGreen)) ) - button = self._make_tool_button(QIcon(CharIconEngine("\uf067", color=Qt.darkGreen)), "New...") - button.setPopupMode(QToolButton.InstantPopup) + button = self._make_tool_button(QIcon(CharIconEngine("\uf067", color=Qt.GlobalColor.darkGreen)), "New...") + button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) button.setMenu(menu) @@ -334,7 +345,8 @@ def _add_project_item_button(self, item_type, factory): if factory.is_deprecated(): return icon = self._icon_from_factory(factory) - button = ProjectItemButton(self._toolbox, item_type, icon) + style = self._get_toolbutton_style() + button = ProjectItemButton(self._toolbox, item_type, icon, style) self._add_tool_button(button) self._buttons.append(button) diff --git a/tests/mock_helpers.py b/tests/mock_helpers.py index 249e33e33..25d27783a 100644 --- a/tests/mock_helpers.py +++ b/tests/mock_helpers.py @@ -36,10 +36,10 @@ def tearDownClass(cls): def create_toolboxui(): """Returns ToolboxUI, where QSettings among others has been mocked.""" - with mock.patch( - "spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value, mock.patch( - "spinetoolbox.top_ui_main.ToolboxUIBase._set_app_style") as mock_set_app_style, mock.patch( - "spinetoolbox.plugin_manager.PluginManager.load_installed_plugins" + with ( + mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value, + mock.patch("spinetoolbox.ui_main.ToolboxUI.set_app_style") as mock_set_app_style, + mock.patch("spinetoolbox.plugin_manager.PluginManager.load_installed_plugins"), ): mock_qsettings_value.side_effect = qsettings_value_side_effect mock_set_app_style.return_value = True @@ -51,22 +51,26 @@ def create_toolboxui(): def create_project(toolbox, project_dir): """Creates a project for the given ToolboxUI.""" - with mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), mock.patch( - "spinetoolbox.ui_main.QSettings.setValue" - ), mock.patch("spinetoolbox.ui_main.QSettings.sync"): + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), + mock.patch("spinetoolbox.ui_main.QSettings.setValue"), + mock.patch("spinetoolbox.ui_main.QSettings.sync"), + ): toolbox.create_project(project_dir) def create_toolboxui_with_project(project_dir): """Returns ToolboxUI with a project instance where QSettings among others has been mocked.""" - with mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value, mock.patch( - "spinetoolbox.top_ui_main.ToolboxUIBase._set_app_style") as mock_set_app_style, mock.patch( - "spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.ui_main.QSettings.setValue"), mock.patch( - "spinetoolbox.ui_main.QSettings.sync"), mock.patch( - "spinetoolbox.plugin_manager.PluginManager.load_installed_plugins"), mock.patch( - "spinetoolbox.ui_main.QScrollArea.setWidget"): + with ( + mock.patch("spinetoolbox.ui_main.QSettings.value") as mock_qsettings_value, + mock.patch("spinetoolbox.ui_main.ToolboxUI.set_app_style") as mock_set_app_style, + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.ui_main.QSettings.setValue"), + mock.patch("spinetoolbox.ui_main.QSettings.sync"), + mock.patch("spinetoolbox.plugin_manager.PluginManager.load_installed_plugins"), + mock.patch("spinetoolbox.ui_main.QScrollArea.setWidget"), + ): mock_qsettings_value.side_effect = qsettings_value_side_effect mock_set_app_style.return_value = True toolbox = ToolboxUI(None) diff --git a/tests/mvcmodels/test_FilterCheckboxList.py b/tests/mvcmodels/test_FilterCheckboxList.py index 3c1e835c7..973de7ccf 100644 --- a/tests/mvcmodels/test_FilterCheckboxList.py +++ b/tests/mvcmodels/test_FilterCheckboxList.py @@ -47,12 +47,12 @@ def test_is_all_selected_when_not_empty_selected(self): def test_add_item_with_select_without_filter(self): new_item = ["aaaa"] self.model.set_list(self.data) - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged" + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows"), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged"), ): self.model.add_items(new_item) self.assertEqual(self.model._data, self.data + new_item) @@ -61,12 +61,12 @@ def test_add_item_with_select_without_filter(self): def test_add_item_without_select_without_filter(self): new_item = ["aaaa"] self.model.set_list(self.data) - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged" + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows"), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged"), ): self.model.add_items(new_item, selected=False) self.assertFalse(self.model._all_selected) @@ -237,12 +237,12 @@ def test_add_item_with_select_with_filter_last(self): new_item = ["bbbb"] self.model.set_list(self.data) self.model.set_filter("b") - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged" + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows"), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged"), ): self.model.add_items(new_item) self.assertEqual(self.model._data, sorted(self.data + new_item)) @@ -255,12 +255,12 @@ def test_add_item_with_select_with_filter_first(self): new_item = ["0b"] self.model.set_list(self.data) self.model.set_filter("b") - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged" + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows"), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged"), ): self.model.add_items(new_item) self.assertEqual(self.model._filter_index, [3, 4, 5, 6]) @@ -270,12 +270,12 @@ def test_add_item_with_select_with_filter_middle(self): new_item = ["b1"] self.model.set_list(self.data) self.model.set_filter("b") - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows" - ), mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged" + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginInsertRows" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endInsertRows"), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.dataChanged"), ): self.model.add_items(new_item) self.assertEqual(self.model._filter_index, [3, 4, 5, 6]) @@ -284,9 +284,12 @@ def test_add_item_with_select_with_filter_middle(self): def test_remove_items_data(self): items = set("a") self.model.set_list(self.data) - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._data, self.data[1:]) self.assertEqual(self.model._data_set, set(self.data[1:])) @@ -294,9 +297,12 @@ def test_remove_items_data(self): def test_remove_items_selected(self): items = set("a") self.model.set_list(self.data) - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._selected, set(self.data[1:])) self.assertTrue(self.model._all_selected) @@ -306,9 +312,12 @@ def test_remove_items_not_selected(self): self.model.set_list(self.data) self.model._selected.discard("a") self.model._all_selected = False - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._selected, set(self.data[1:])) self.assertTrue(self.model._all_selected) @@ -317,9 +326,12 @@ def test_remove_items_filtered_data(self): items = set("b") self.model.set_list(self.data) self.model.set_filter("b") - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._filter_index, [3, 4]) self.assertEqual(self.model._selected_filtered, set(self.data[4:])) @@ -328,9 +340,12 @@ def test_remove_items_filtered_data_middle(self): items = set("bb") self.model.set_list(self.data) self.model.set_filter("b") - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._filter_index, [3, 4]) @@ -340,9 +355,12 @@ def test_remove_items_filtered_data_not_selected(self): self.model.set_filter("b") self.model._selected_filtered.discard("a") self.model._all_selected = False - with mock.patch( - "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" - ), mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"): + with ( + mock.patch( + "spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.beginResetModel" + ), + mock.patch("spinetoolbox.mvcmodels.filter_checkbox_list_model.SimpleFilterCheckboxListModel.endResetModel"), + ): self.model.remove_items(items) self.assertEqual(self.model._selected_filtered, set(self.data[4:])) self.assertTrue(self.model._all_selected) diff --git a/tests/project_item/test_logging_connection.py b/tests/project_item/test_logging_connection.py index b57a9092a..fe4f0d8b6 100644 --- a/tests/project_item/test_logging_connection.py +++ b/tests/project_item/test_logging_connection.py @@ -103,9 +103,7 @@ def setUp(self): project.add_item(store_2) self._db_mngr_logger = MagicMock() self._url = "sqlite:///" + str(Path(self._temp_dir.name, "test_database.sqlite")) - self._db_map = self._toolbox.db_mngr.get_db_map( - self._url, self._db_mngr_logger, codename="database", create=True - ) + self._db_map = self._toolbox.db_mngr.get_db_map(self._url, self._db_mngr_logger, create=True) def tearDown(self): clean_up_toolbox(self._toolbox) diff --git a/tests/project_item/test_specification_editor_window.py b/tests/project_item/test_specification_editor_window.py index 9bcf1a437..e04f28323 100644 --- a/tests/project_item/test_specification_editor_window.py +++ b/tests/project_item/test_specification_editor_window.py @@ -56,9 +56,12 @@ def tearDown(self): self._temp_dir.cleanup() def test_init(self): - with patch.object(SpecificationEditorWindowBase, "_make_ui") as mock_make_ui, patch.object( - SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock - ) as mock_settings_group: + with ( + patch.object(SpecificationEditorWindowBase, "_make_ui") as mock_make_ui, + patch.object( + SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock + ) as mock_settings_group, + ): mock_settings_group.return_value = "settings group" window = SpecificationEditorWindowBase(self._toolbox) mock_make_ui.assert_called_once() @@ -68,9 +71,12 @@ def test_init(self): window.deleteLater() def test_init_with_existing_specification(self): - with patch.object(SpecificationEditorWindowBase, "_make_ui"), patch.object( - SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock - ) as mock_settings_group: + with ( + patch.object(SpecificationEditorWindowBase, "_make_ui"), + patch.object( + SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock + ) as mock_settings_group, + ): mock_settings_group.return_value = "settings group" specification = ProjectItemSpecification("spec name", "spec description") window = SpecificationEditorWindowBase(self._toolbox, specification) @@ -81,17 +87,16 @@ def test_init_with_existing_specification(self): window.deleteLater() def test_save_specification(self): - with patch.object(SpecificationEditorWindowBase, "_make_ui"), patch.object( - SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock - ) as mock_settings_group, patch.object( - SpecificationEditorWindowBase, "_make_new_specification" - ) as mock_make_specification, patch.object( - ProjectItemSpecification, "save" - ) as mock_save, patch.object( - ProjectItemFactory, "icon" - ) as mock_icon, patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + patch.object(SpecificationEditorWindowBase, "_make_ui"), + patch.object( + SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock + ) as mock_settings_group, + patch.object(SpecificationEditorWindowBase, "_make_new_specification") as mock_make_specification, + patch.object(ProjectItemSpecification, "save") as mock_save, + patch.object(ProjectItemFactory, "icon") as mock_icon, + patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): specification = ProjectItemSpecification("spec name", "spec description", "Mock") mock_settings_group.return_value = "settings group" mock_make_specification.return_value = specification @@ -112,19 +117,17 @@ def test_save_specification(self): window.deleteLater() def test_make_new_specification_for_item(self): - with patch.object(SpecificationEditorWindowBase, "_make_ui"), patch.object( - SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock - ) as mock_settings_group, patch.object( - SpecificationEditorWindowBase, "_make_new_specification" - ) as mock_make_specification, patch.object( - ProjectItemSpecification, "save" - ) as mock_save, patch.object( - ProjectItemFactory, "make_icon" - ) as mock_make_icon, patch.object( - ProjectItemFactory, "icon" - ) as mock_icon, patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + patch.object(SpecificationEditorWindowBase, "_make_ui"), + patch.object( + SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock + ) as mock_settings_group, + patch.object(SpecificationEditorWindowBase, "_make_new_specification") as mock_make_specification, + patch.object(ProjectItemSpecification, "save") as mock_save, + patch.object(ProjectItemFactory, "make_icon") as mock_make_icon, + patch.object(ProjectItemFactory, "icon") as mock_icon, + patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_settings_group.return_value = "settings group" mock_make_icon.return_value = ProjectItemIcon( self._toolbox, ":/icons/item_icons/hammer.svg", QColor("white") @@ -154,19 +157,17 @@ def test_make_new_specification_for_item(self): window.deleteLater() def test_rename_specification_for_item(self): - with patch.object(SpecificationEditorWindowBase, "_make_ui"), patch.object( - SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock - ) as mock_settings_group, patch.object( - SpecificationEditorWindowBase, "_make_new_specification" - ) as mock_make_specification, patch.object( - ProjectItemSpecification, "save" - ) as mock_save, patch.object( - ProjectItemFactory, "make_icon" - ) as mock_make_icon, patch.object( - ProjectItemFactory, "icon" - ) as mock_icon, patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + patch.object(SpecificationEditorWindowBase, "_make_ui"), + patch.object( + SpecificationEditorWindowBase, "settings_group", new_callable=PropertyMock + ) as mock_settings_group, + patch.object(SpecificationEditorWindowBase, "_make_new_specification") as mock_make_specification, + patch.object(ProjectItemSpecification, "save") as mock_save, + patch.object(ProjectItemFactory, "make_icon") as mock_make_icon, + patch.object(ProjectItemFactory, "icon") as mock_icon, + patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_settings_group.return_value = "settings group" mock_make_icon.return_value = ProjectItemIcon( self._toolbox, ":/icons/item_icons/hammer.svg", QColor("white") diff --git a/tests/spine_db_editor/helpers.py b/tests/spine_db_editor/helpers.py index 116b001f7..564885316 100644 --- a/tests/spine_db_editor/helpers.py +++ b/tests/spine_db_editor/helpers.py @@ -32,21 +32,24 @@ def tearDown(self): self._common_tear_down() def _common_setup(self, url, create): - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.MagicMock() mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map(url, logger, codename=self.db_codename, create=create) - self._db_editor = SpineDBEditor(self._db_mngr, {url: self.db_codename}) + self._db_map = self._db_mngr.get_db_map(url, logger, create=create) + self._db_mngr.name_registry.register(url, self.db_codename) + self._db_editor = SpineDBEditor(self._db_mngr, [url]) QApplication.processEvents() def _common_tear_down(self): - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ), mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self._db_editor.close() self._db_mngr.close_all_sessions() while not self._db_map.closed: diff --git a/tests/spine_db_editor/mvcmodels/test_alternative_model.py b/tests/spine_db_editor/mvcmodels/test_alternative_model.py index 84853733f..7f3360f85 100644 --- a/tests/spine_db_editor/mvcmodels/test_alternative_model.py +++ b/tests/spine_db_editor/mvcmodels/test_alternative_model.py @@ -30,13 +30,15 @@ def setUp(self): app_settings = MagicMock() logger = MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename) with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"): - self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": self.db_codename}) + self._db_editor = SpineDBEditor(self._db_mngr) def tearDown(self): - with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox" + with ( + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox"), ): self._db_editor.close() self._db_mngr.close_all_sessions() @@ -128,15 +130,18 @@ def setUp(self): app_settings = MagicMock() logger = MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, codename="test_db_1", create=True) + self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, create=True) url2 = "sqlite:///" + str(Path(self._temp_dir.name, "db2.sqlite")) - self._db_map2 = self._db_mngr.get_db_map(url2, logger, codename=self.db_codename, create=True) + self._db_map2 = self._db_mngr.get_db_map(url2, logger, create=True) + self._db_mngr.name_registry.register(self._db_map1.sa_url, "test_db_1") + self._db_mngr.name_registry.register(self._db_map2.sa_url, self.db_codename) with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"): - self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "test_db_1", url2: self.db_codename}) + self._db_editor = SpineDBEditor(self._db_mngr) def tearDown(self): - with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox" + with ( + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox"), ): self._db_editor.close() self._db_mngr.close_all_sessions() diff --git a/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py b/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py index 8ac64dc3e..9c9577cfd 100644 --- a/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py +++ b/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py @@ -50,7 +50,8 @@ def setUp(self): app_settings = mock.MagicMock() logger = mock.MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="mock_db", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "mock_db") import_object_classes(self._db_map, ("dog", "fish")) import_object_parameters(self._db_map, (("dog", "breed"),)) import_objects(self._db_map, (("dog", "pluto"), ("fish", "nemo"))) diff --git a/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py b/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py index 3da5f0d33..45ddf56b0 100644 --- a/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py +++ b/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py @@ -26,7 +26,8 @@ def setUp(self): app_settings = MagicMock() logger = MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename) self._parent = QObject() self._model = FrozenTableModel(self._db_mngr, self._parent) diff --git a/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py b/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py index 7e8ebeae3..7db6ea771 100644 --- a/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py +++ b/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py @@ -78,7 +78,8 @@ def setUp(self): mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map(self._url, logger, codename="database") + self._db_map = self._db_mngr.get_db_map(self._url, logger) + self._db_mngr.name_registry.register(self._db_map.sa_url, "database") QApplication.processEvents() self._db_map.fetch_all() self._model = ItemMetadataTableModel(self._db_mngr, [self._db_map], None) diff --git a/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py b/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py index e49447bf6..8869c76d5 100644 --- a/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py +++ b/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py @@ -29,7 +29,8 @@ def setUp(self): mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "database") QApplication.processEvents() self._model = MetadataTableModel(self._db_mngr, [self._db_map], None) fetch_model(self._model) @@ -94,7 +95,8 @@ def test_adding_data_to_another_database(self): database_path = Path(temp_dir, "db.sqlite") url = "sqlite:///" + str(database_path) try: - db_map_2 = self._db_mngr.get_db_map(url, logger, codename="2nd database", create=True) + db_map_2 = self._db_mngr.get_db_map(url, logger, create=True) + self._db_mngr.name_registry.register(url, "2nd database") self._model.set_db_maps([self._db_map, db_map_2]) fetch_model(self._model) index = self._model.index(1, Column.DB_MAP) diff --git a/tests/spine_db_editor/mvcmodels/test_scenario_model.py b/tests/spine_db_editor/mvcmodels/test_scenario_model.py index 79fa1ab6c..ee61a6f95 100644 --- a/tests/spine_db_editor/mvcmodels/test_scenario_model.py +++ b/tests/spine_db_editor/mvcmodels/test_scenario_model.py @@ -42,13 +42,15 @@ def setUp(self): app_settings = MagicMock() logger = MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.db_url, self.db_codename) with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"): self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": self.db_codename}) def tearDown(self): - with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox" + with ( + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox"), ): self._db_editor.close() self._db_mngr.close_all_sessions() @@ -432,15 +434,18 @@ def setUp(self): app_settings = MagicMock() logger = MagicMock() self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, codename="test_db_1", create=True) + self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map1.sa_url, "test_db_1") url2 = "sqlite:///" + str(Path(self._temp_dir.name, "db_2.sqlite")) - self._db_map2 = self._db_mngr.get_db_map(url2, logger, codename="test_db_2", create=True) + self._db_map2 = self._db_mngr.get_db_map(url2, logger, create=True) + self._db_mngr.name_registry.register(self._db_map2.sa_url, "test_db_2") with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"): self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "test_db_1", url2: "test_db_2"}) def tearDown(self): - with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox" + with ( + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.QMessageBox"), ): self._db_editor.close() self._db_mngr.close_all_sessions() diff --git a/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py b/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py index 152d88a8c..acc4d692a 100644 --- a/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py +++ b/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py @@ -78,7 +78,8 @@ class TestSingleObjectParameterValueModel(TestCaseWithQApplication): def setUp(self): self._db_mngr = TestSpineDBManager(None, None) self._logger = MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite:///", self._logger, codename="Test database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite:///", self._logger, create=True) + self._db_mngr.name_registry.register(self._db_map.db_url, "Test database") def tearDown(self): self._db_mngr.close_all_sessions() diff --git a/tests/spine_db_editor/test_graphics_items.py b/tests/spine_db_editor/test_graphics_items.py index 7d0af6977..7c67c2978 100644 --- a/tests/spine_db_editor/test_graphics_items.py +++ b/tests/spine_db_editor/test_graphics_items.py @@ -24,14 +24,16 @@ class TestEntityItem(TestCaseWithQApplication): _db_mngr = None def setUp(self): - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "database") self._spine_db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "database"}) self._spine_db_editor.pivot_table_model = mock.MagicMock() self._db_mngr.add_entity_classes({self._db_map: [{"name": "oc", "id": 1}]}) @@ -59,9 +61,12 @@ def tearDownClass(cls): QApplication.removePostedEvents(None) # Clean up unfinished fetcher signals def tearDown(self): - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ) as mock_save_w_s, mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch( + "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" + ) as mock_save_w_s, + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self._spine_db_editor.close() mock_save_w_s.assert_called_once() self._db_mngr.close_all_sessions() diff --git a/tests/spine_db_editor/widgets/spine_db_editor_test_base.py b/tests/spine_db_editor/widgets/spine_db_editor_test_base.py index ca6735ab5..7b8de19d3 100644 --- a/tests/spine_db_editor/widgets/spine_db_editor_test_base.py +++ b/tests/spine_db_editor/widgets/spine_db_editor_test_base.py @@ -23,22 +23,27 @@ class DBEditorTestBase(TestCaseWithQApplication): def setUp(self): """Makes instances of SpineDBEditor classes.""" - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwargs: 0 self.db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self.mock_db_map = self.db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self.mock_db_map = self.db_mngr.get_db_map("sqlite://", logger, create=True) + self.db_mngr.name_registry.register("sqlite://", self.db_codename) self.spine_db_editor = SpineDBEditor(self.db_mngr, {"sqlite://": self.db_codename}) self.spine_db_editor.pivot_table_model = mock.MagicMock() self.spine_db_editor.entity_tree_model.hide_empty_classes = False def tearDown(self): - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ) as mock_save_w_s, mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch( + "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" + ) as mock_save_w_s, + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self.spine_db_editor.close() mock_save_w_s.assert_called_once() self.db_mngr.close_all_sessions() diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditor.py b/tests/spine_db_editor/widgets/test_SpineDBEditor.py index 60a717eac..b142e51e7 100644 --- a/tests/spine_db_editor/widgets/test_SpineDBEditor.py +++ b/tests/spine_db_editor/widgets/test_SpineDBEditor.py @@ -255,14 +255,15 @@ def test_stacked_table_empty_row_defaults_are_updated_on_entity_rename(self): class TestClosingDBEditors(TestCaseWithQApplication): def setUp(self): self._editors = [] - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) def tearDown(self): self._db_mngr.close_all_sessions() @@ -282,11 +283,13 @@ def test_first_editor_to_close_does_not_ask_for_confirmation_on_dirty_database(s editor_2 = self._make_db_editor() self._db_mngr.add_entity_classes({self._db_map: [{"name": "my_object_class"}]}) self.assertTrue(self._db_mngr.dirty(self._db_map)) - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ), mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor._prompt_to_commit_changes" - ) as commit_changes: + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + mock.patch( + "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor._prompt_to_commit_changes" + ) as commit_changes, + ): commit_changes.return_value = QMessageBox.StandardButton.Discard editor_1.close() commit_changes.assert_not_called() @@ -299,11 +302,13 @@ def test_editor_asks_for_confirmation_even_when_non_editor_listeners_are_connect self.assertTrue(self._db_mngr.dirty(self._db_map)) non_editor_listener = object() self._db_mngr.register_listener(non_editor_listener, self._db_map) - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ), mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor._prompt_to_commit_changes" - ) as commit_changes: + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + mock.patch( + "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor._prompt_to_commit_changes" + ) as commit_changes, + ): commit_changes.return_value = QMessageBox.StandardButton.Discard editor.close() commit_changes.assert_called_once() diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py b/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py index f8d790d23..585bef02b 100644 --- a/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py +++ b/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py @@ -13,6 +13,7 @@ """Contains unit tests for the SpineDBEditorBase class.""" import unittest from unittest import mock +from sqlalchemy.engine.url import make_url from spinetoolbox.spine_db_editor.widgets.spine_db_editor import SpineDBEditorBase from tests.mock_helpers import TestCaseWithQApplication, TestSpineDBManager @@ -20,27 +21,31 @@ class TestSpineDBEditorBase(TestCaseWithQApplication): def setUp(self): """Builds a SpineDBEditorBase object.""" - with mock.patch("spinetoolbox.spine_db_worker.DatabaseMapping") as mock_DiffDBMapping, mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui" - ), mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"): + with ( + mock.patch("spinetoolbox.spine_db_worker.DatabaseMapping") as mock_DBMapping, + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), + ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwards: 0 self.db_mngr = TestSpineDBManager(mock_settings, None) - def DiffDBMapping_side_effect(url, codename=None, upgrade=False, create=False): + def DBMapping_side_effect(url, upgrade=False, create=False): mock_db_map = mock.MagicMock() - mock_db_map.codename = codename mock_db_map.db_url = url + mock_db_map.sa_url = make_url(url) return mock_db_map - mock_DiffDBMapping.side_effect = DiffDBMapping_side_effect + mock_DBMapping.side_effect = DBMapping_side_effect self.db_editor = SpineDBEditorBase(self.db_mngr) + self.db_editor.connect_signals() def tearDown(self): """Frees resources after each test.""" - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ), mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self.db_editor._parameter_models = [] self.db_editor.close() self.db_mngr.close_all_sessions() @@ -49,25 +54,31 @@ def tearDown(self): self.db_editor = None def test_import_file_recognizes_excel(self): - with mock.patch.object(self.db_editor, "qsettings"), mock.patch.object( - self.db_editor, "import_from_excel" - ) as mock_import_from_excel, mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog: + with ( + mock.patch.object(self.db_editor, "qsettings"), + mock.patch.object(self.db_editor, "import_from_excel") as mock_import_from_excel, + mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog, + ): mock_file_dialog.getOpenFileName.return_value = "my_excel_file.xlsx", "Excel files (*.xlsx)" self.db_editor.import_file() mock_import_from_excel.assert_called_once_with("my_excel_file.xlsx") def test_import_file_recognizes_sqlite(self): - with mock.patch.object(self.db_editor, "qsettings"), mock.patch.object( - self.db_editor, "import_from_sqlite" - ) as mock_import_from_sqlite, mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog: + with ( + mock.patch.object(self.db_editor, "qsettings"), + mock.patch.object(self.db_editor, "import_from_sqlite") as mock_import_from_sqlite, + mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog, + ): mock_file_dialog.getOpenFileName.return_value = "my_sqlite_file.sqlite", "SQLite files (*.sqlite)" self.db_editor.import_file() mock_import_from_sqlite.assert_called_once_with("my_sqlite_file.sqlite") def test_import_file_recognizes_json(self): - with mock.patch.object(self.db_editor, "qsettings"), mock.patch.object( - self.db_editor, "import_from_json" - ) as mock_import_from_json, mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog: + with ( + mock.patch.object(self.db_editor, "qsettings"), + mock.patch.object(self.db_editor, "import_from_json") as mock_import_from_json, + mock.patch("spinetoolbox.helpers.QFileDialog") as mock_file_dialog, + ): mock_file_dialog.getOpenFileName.return_value = "my_json_file.json", "JSON files (*.json)" self.db_editor.import_file() mock_import_from_json.assert_called_once_with("my_json_file.json") diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditorFilter.py b/tests/spine_db_editor/widgets/test_SpineDBEditorFilter.py index 0d7841a10..6690d49f5 100644 --- a/tests/spine_db_editor/widgets/test_SpineDBEditorFilter.py +++ b/tests/spine_db_editor/widgets/test_SpineDBEditorFilter.py @@ -333,9 +333,10 @@ def test_filtering_with_entity_selections(self): """Tests that the graph view filters the entities correctly based on Entity Tree selection""" entity_tree_view = self.spine_db_editor.ui.treeView_entity entity_tree_view.expandAll() - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # When nothing selected, no entities should be visible self.assertFalse(self.spine_db_editor.entity_items) @@ -383,13 +384,11 @@ def test_filtering_with_entity_selections(self): def test_filtering_with_alternative_selections(self): """Tests that the graph view filters the entities correctly based on Alternative tree selections""" alternative_tree_view = self.spine_db_editor.ui.alternative_tree_view - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object( - self.spine_db_editor.ui.dockWidget_parameter_value, "isVisible", return_value=True - ), mock.patch.object( - self.spine_db_editor.qsettings, "value" - ) as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.ui.dockWidget_parameter_value, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # Selecting the root shouldn't do anything select_item_with_index(alternative_tree_view, self.indexes["alternative_root"]) @@ -425,9 +424,10 @@ def test_filtering_with_entity_and_alternative_selections(self): """Tests that the graph view filters the entities correctly based on Entity and Alternative tree selections""" entity_tree_view = self.spine_db_editor.ui.treeView_entity alternative_tree_view = self.spine_db_editor.ui.alternative_tree_view - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # Select entity classes A and B along with alternative Alt1. select_item_with_index(entity_tree_view, self.indexes["entity_class_A"]) @@ -456,9 +456,10 @@ def test_empty_click_clears(self): """Tests that a click on empty space in one of the trees clears all selections""" entity_tree_view = self.spine_db_editor.ui.treeView_entity alternative_tree_view = self.spine_db_editor.ui.alternative_tree_view - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # Select the entity class A select_item_with_index(entity_tree_view, self.indexes["entity_class_A"]) @@ -473,9 +474,10 @@ def test_filtering_with_scenario_selections(self): """Tests that the graph view filters the entities correctly based on Scenario tree selections""" scenario_tree_view = self.spine_db_editor.ui.scenario_tree_view scenario_tree_view.expandAll() - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # When nothing selected, no entities should be visible self.assertFalse(self.spine_db_editor.entity_items) @@ -524,9 +526,10 @@ def test_scenario_deselection_with_ctrl_is_consistent(self): """Tests that deselection with ctrl pressed is consistent""" scenario_tree_view = self.spine_db_editor.ui.scenario_tree_view scenario_tree_view.expandAll() - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # When nothing selected, no entities should be visible self.assertFalse(self.spine_db_editor.entity_items) @@ -579,9 +582,10 @@ def test_filtering_with_entity_selections_with_auto_expand(self): auto-expand is enabled""" self.gv.set_property("auto_expand_entities", True) entity_tree_view = self.spine_db_editor.ui.treeView_entity - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # When nothing selected, no entities should be visible self.assertFalse(self.spine_db_editor.entity_items) @@ -620,9 +624,10 @@ def test_filtering_with_entity_and_alternative_selections_with_auto_expand(self) entity_tree_view = self.spine_db_editor.ui.treeView_entity entity_tree_view.expandAll() alternative_tree_view = self.spine_db_editor.ui.alternative_tree_view - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # Select entity ba select_item_with_index(entity_tree_view, self.indexes["entity_ba"]) @@ -649,9 +654,10 @@ def test_filtering_with_entity_and_alternative_selections_with_auto_expand(self) def test_start_connecting_entities(self): entity_tree_view = self.spine_db_editor.ui.treeView_entity - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) select_item_with_index(entity_tree_view, self.indexes["entity_root"]) entity_tree_view.fully_expand() @@ -680,9 +686,10 @@ def test_consistent_across_different_selection_order(self): entity_tree_view = self.spine_db_editor.ui.treeView_entity alternative_tree_view = self.spine_db_editor.ui.alternative_tree_view entity_tree_view.expandAll() - with mock.patch.object( - self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True - ), mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value: + with ( + mock.patch.object(self.spine_db_editor.ui.dockWidget_entity_graph, "isVisible", return_value=True), + mock.patch.object(self.spine_db_editor.qsettings, "value") as mock_value, + ): mock_value.side_effect = lambda key, defaultValue: ("false" if key == "appSettings/stickySelection" else 0) # Select entity classes A then B then Alt1. select_item_with_index(entity_tree_view, self.indexes["entity_class_A"]) diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py b/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py index 38553246d..52bf1457b 100644 --- a/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py +++ b/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py @@ -26,24 +26,29 @@ def setUp(self): """Overridden method. Runs before each test. Makes instances of SpineDBEditor classes.""" self._temp_dir = TemporaryDirectory() url = "sqlite:///" + os.path.join(self._temp_dir.name, "test.sqlite") - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwards: 0 self.db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self.db_map = self.db_mngr.get_db_map(url, logger, codename="db", create=True) - self.spine_db_editor = SpineDBEditor(self.db_mngr, {url: "db"}) + self.db_map = self.db_mngr.get_db_map(url, logger, create=True) + self.spine_db_editor = SpineDBEditor(self.db_mngr, [url]) + self.db_mngr.name_registry.register(self.db_map.sa_url, "db") self.spine_db_editor.pivot_table_model = mock.MagicMock() def tearDown(self): """Overridden method. Runs after each test. Use this to free resources after a test if needed. """ - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ) as mock_save_w_s, mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch( + "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" + ) as mock_save_w_s, + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self.spine_db_editor.close() mock_save_w_s.assert_called_once() QApplication.removePostedEvents(None) # Clean up unfinished fetcher signals diff --git a/tests/spine_db_editor/widgets/test_add_items_dialog.py b/tests/spine_db_editor/widgets/test_add_items_dialog.py index dd021f2b5..7b7afdf96 100644 --- a/tests/spine_db_editor/widgets/test_add_items_dialog.py +++ b/tests/spine_db_editor/widgets/test_add_items_dialog.py @@ -37,16 +37,18 @@ def setUp(self): logger = mock.MagicMock() self._temp_dir = TemporaryDirectory() url = "sqlite:///" + self._temp_dir.name + "/db.sqlite" - self._db_map = self._db_mngr.get_db_map(url, logger, codename="mock_db", create=True) + self._db_map = self._db_mngr.get_db_map(url, logger, create=True) + self._db_mngr.name_registry.register(url, "mock_db") self._db_editor = SpineDBEditor(self._db_mngr, {url: "mock_db"}) def tearDown(self): """Overridden method. Runs after each test. Use this to free resources after a test if needed. """ - with mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state" - ), mock.patch("spinetoolbox.spine_db_manager.QMessageBox"): + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"), + mock.patch("spinetoolbox.spine_db_manager.QMessageBox"), + ): self._db_editor.close() self._db_mngr.close_all_sessions() while not self._db_map.closed: diff --git a/tests/spine_db_editor/widgets/test_commit_viewer.py b/tests/spine_db_editor/widgets/test_commit_viewer.py index 51a4b4f2f..61d096589 100644 --- a/tests/spine_db_editor/widgets/test_commit_viewer.py +++ b/tests/spine_db_editor/widgets/test_commit_viewer.py @@ -30,7 +30,7 @@ def setUp(self): self._db_mngr = SpineDBManager(mock_settings, None, synchronous=True) logger = mock.MagicMock() url = "sqlite://" - self._db_map = self._db_mngr.get_db_map(url, logger, codename="mock_db", create=True) + self._db_map = self._db_mngr.get_db_map(url, logger, create=True) with mock.patch.object(QSplitter, "restoreState"): self._commit_viewer = CommitViewer(mock_settings, self._db_mngr, self._db_map) diff --git a/tests/spine_db_editor/widgets/test_custom_menus.py b/tests/spine_db_editor/widgets/test_custom_menus.py index dc9b0d2cd..7463d814f 100644 --- a/tests/spine_db_editor/widgets/test_custom_menus.py +++ b/tests/spine_db_editor/widgets/test_custom_menus.py @@ -14,8 +14,9 @@ import unittest from unittest import mock from PySide6.QtWidgets import QWidget +from spinetoolbox.database_display_names import NameRegistry from spinetoolbox.helpers import signal_waiter -from spinetoolbox.spine_db_editor.widgets.custom_menus import TabularViewCodenameFilterMenu +from spinetoolbox.spine_db_editor.widgets.custom_menus import TabularViewDatabaseNameFilterMenu from tests.mock_helpers import TestCaseWithQApplication @@ -28,11 +29,14 @@ def tearDown(self): def test_init_fills_filter_list_with_database_codenames(self): db_map1 = mock.MagicMock() - db_map1.codename = "db map 1" + db_map1.sa_url = "sqlite://a" db_map2 = mock.MagicMock() - db_map2.codename = "db map 2" + db_map2.sa_url = "sqlite://b" db_maps = [db_map1, db_map2] - menu = TabularViewCodenameFilterMenu(self._parent, db_maps, "database") + name_registry = NameRegistry() + name_registry.register(db_map1.sa_url, "db map 1") + name_registry.register(db_map2.sa_url, "db map 2") + menu = TabularViewDatabaseNameFilterMenu(self._parent, db_maps, "database", name_registry) self.assertIs(menu.anchor, self._parent) filter_list_model = menu._filter._filter_model filter_rows = [] @@ -42,11 +46,14 @@ def test_init_fills_filter_list_with_database_codenames(self): def test_filter_changed_signal_is_emitted_correctly(self): db_map1 = mock.MagicMock() - db_map1.codename = "db map 1" + db_map1.sa_url = "sqlite://a" db_map2 = mock.MagicMock() - db_map2.codename = "db map 2" + db_map2.sa_url = "sqlite://b" db_maps = [db_map1, db_map2] - menu = TabularViewCodenameFilterMenu(self._parent, db_maps, "database") + name_registry = NameRegistry() + name_registry.register(db_map1.sa_url, "db map 1") + name_registry.register(db_map2.sa_url, "db map 2") + menu = TabularViewDatabaseNameFilterMenu(self._parent, db_maps, "database", name_registry) with signal_waiter(menu.filterChanged, timeout=0.1) as waiter: menu._clear_filter() waiter.wait() diff --git a/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py b/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py index d9290bd3f..b66b2def2 100644 --- a/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py +++ b/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py @@ -22,14 +22,15 @@ class TestMassRemoveItemsDialog(TestCaseWithQApplication): def setUp(self): url = "sqlite:///" - with mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), mock.patch( - "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show" + with ( + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"), + mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"), ): mock_settings = mock.Mock() mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = SpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map(url, logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map(url, logger, create=True) QApplication.processEvents() def tearDown(self): 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 a264baf62..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 @@ -11,10 +11,15 @@ ###################################################################################################################### """Unit tests for SpineDBEditor classes.""" +from pathlib import Path from tempfile import TemporaryDirectory -from PySide6.QtCore import QPoint -from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor -from tests.mock_helpers import FakeDataStore, clean_up_toolbox, create_toolboxui_with_project +from unittest.mock import MagicMock, patch +from PySide6.QtCore import QPoint, QSettings +from PySide6.QtWidgets import QApplication +from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry +from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor, open_db_editor +from spinetoolbox.spine_db_manager import SpineDBManager +from tests.mock_helpers import FakeDataStore, TestCaseWithQApplication, clean_up_toolbox, create_toolboxui_with_project from .spine_db_editor_test_base import DBEditorTestBase @@ -32,7 +37,7 @@ def tearDown(self): def test_multi_spine_db_editor(self): self.db_mngr.setParent(self._toolbox) multieditor = MultiSpineDBEditor(self.db_mngr) - multieditor.add_new_tab() + multieditor.add_new_tab([]) self.assertEqual(1, multieditor.tab_widget.count()) multieditor.make_context_menu(0) multieditor.show_plus_button_context_menu(QPoint(0, 0)) @@ -40,3 +45,70 @@ def test_multi_spine_db_editor(self): self._toolbox.project._project_items = {"a": FakeDataStore("a")} multieditor.show_plus_button_context_menu(QPoint(0, 0)) multieditor._take_tab(0) + + +class TestOpenDBEditor(TestCaseWithQApplication): + def setUp(self): + self._temp_dir = TemporaryDirectory() + db_path = Path(self._temp_dir.name, "db.sqlite") + self._db_url = "sqlite:///" + str(db_path) + self._db_mngr = SpineDBManager(QSettings(), None) + self._logger = MagicMock() + self._db_editor_registry = MultiTabWindowRegistry() + + def tearDown(self): + self._db_mngr.close_all_sessions() + self._db_mngr.clean_up() + # Database connection may still be open. Retry cleanup until it succeeds. + running = True + while running: + QApplication.processEvents() + try: + self._temp_dir.cleanup() + except NotADirectoryError: + pass + else: + running = False + + def _close_windows(self): + for editor in self._db_editor_registry.windows(): + QApplication.processEvents() + editor.close() + self.assertFalse(self._db_editor_registry.has_windows()) + + def test_open_db_editor(self): + with ( + patch( + "spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.db_editor_registry", + self._db_editor_registry, + ), + patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show") as mock_show, + ): + self.assertFalse(self._db_editor_registry.has_windows()) + open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True) + mock_show.assert_called_once() + self.assertEqual(len(self._db_editor_registry.windows()), 1) + open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True) + self.assertEqual(len(self._db_editor_registry.windows()), 1) + editor = self._db_editor_registry.windows()[0] + self.assertEqual(editor.tab_widget.count(), 1) + self._close_windows() + + def test_open_db_in_tab_when_editor_has_an_empty_tab(self): + with ( + patch( + "spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.db_editor_registry", + self._db_editor_registry, + ), + patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show") as mock_show, + ): + self.assertFalse(self._db_editor_registry.has_windows()) + window = MultiSpineDBEditor(self._db_mngr, []) + self.assertEqual(window.tab_widget.count(), 1) + tab = window.tab_widget.widget(0) + self.assertEqual(tab.db_urls, []) + open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True) + self.assertEqual(window.tab_widget.count(), 2) + tab = window.tab_widget.widget(1) + self.assertEqual(tab.db_urls, [self._db_url]) + self._close_windows() diff --git a/tests/spine_db_editor/widgets/test_toolbar.py b/tests/spine_db_editor/widgets/test_toolbar.py index 1fb920468..b76ab0ba0 100644 --- a/tests/spine_db_editor/widgets/test_toolbar.py +++ b/tests/spine_db_editor/widgets/test_toolbar.py @@ -33,7 +33,7 @@ def test_toolbar(self): self.db_mngr.setParent(self._toolbox) tb = DBEditorToolBar(self.spine_db_editor) self.assertEqual([{"database": "sqlite://"}], self.spine_db_editor._history) - with mock.patch("spinetoolbox.spine_db_editor.widgets.toolbar._UrlFilterDialog.show") as mock_show_dialog: + with mock.patch("spinetoolbox.spine_db_editor.widgets.toolbar._URLDialog.show") as mock_show_dialog: mock_show_dialog.show.return_value = True - tb._show_filter_menu() + tb._show_url_codename_widget() mock_show_dialog.assert_called() diff --git a/tests/test_ProjectUpgrader.py b/tests/test_ProjectUpgrader.py index 563f53e16..433ed4dcb 100644 --- a/tests/test_ProjectUpgrader.py +++ b/tests/test_ProjectUpgrader.py @@ -119,15 +119,12 @@ def test_upgrade_v1_to_v2(self): proj_v1 = make_v1_project_dict() self.assertTrue(pu.is_valid(1, proj_v1)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 2 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 2), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): # Upgrade to version 2 mock_mb.return_value = QMessageBox.StandardButton.Yes proj_v2 = pu.upgrade(proj_v1, project_dir) @@ -150,15 +147,12 @@ def test_upgrade_v2_to_v3(self): proj_v2 = make_v2_project_dict() self.assertTrue(pu.is_valid(2, proj_v2)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 3 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 3), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir # Make temp preprocessing_tool.json tool spec file @@ -184,15 +178,12 @@ def test_upgrade_v3_to_v4(self): proj_v3 = make_v3_project_dict() self.assertTrue(pu.is_valid(3, proj_v3)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 4 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 4), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir # Make temp preprocessing_tool.json tool spec file @@ -218,15 +209,12 @@ def test_upgrade_v4_to_v5(self): proj_v4 = make_v4_project_dict() self.assertTrue(pu.is_valid(4, proj_v4)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 5 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 5), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir # Make temp preprocessing_tool.json tool spec file @@ -261,15 +249,12 @@ def test_upgrade_v9_to_v10(self): proj_v9 = make_v9_project_dict() self.assertTrue(pu.is_valid(9, proj_v9)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 10 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 10), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir # Make temp preprocessing_tool.json tool spec file @@ -305,15 +290,12 @@ def test_upgrade_v10_to_v11(self): proj_v10 = make_v10_project_dict() self.assertTrue(pu.is_valid(10, proj_v10)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 11 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 11), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir proj_v11 = pu.upgrade(proj_v10, project_dir) @@ -333,15 +315,12 @@ def test_upgrade_v11_to_v12(self): proj_v11 = make_v11_project_dict() self.assertTrue(pu.is_valid(11, proj_v11)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 12 - ), mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION", 12), + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir proj_v12 = pu.upgrade(proj_v11, project_dir) @@ -357,13 +336,11 @@ def test_upgrade_v1_to_latest(self): proj_v1 = make_v1_project_dict() self.assertTrue(pu.is_valid(1, proj_v1)) with TemporaryDirectory() as project_dir: - with mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" - ) as mock_backup, mock.patch( - "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" - ) as mock_force_save, mock.patch( - "spinetoolbox.project_upgrader.QMessageBox.question" - ) as mock_mb: + with ( + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file") as mock_backup, + mock.patch("spinetoolbox.project_upgrader.ProjectUpgrader.force_save") as mock_force_save, + mock.patch("spinetoolbox.project_upgrader.QMessageBox.question") as mock_mb, + ): mock_mb.return_value = QMessageBox.StandardButton.Yes os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir # Make temp preprocessing_tool.json tool spec file diff --git a/tests/test_SpineDBManager.py b/tests/test_SpineDBManager.py index 34c44635f..3f13f7401 100644 --- a/tests/test_SpineDBManager.py +++ b/tests/test_SpineDBManager.py @@ -318,7 +318,8 @@ def setUp(self): self.editor = MagicMock() self._temp_dir = TemporaryDirectory() url = "sqlite:///" + self._temp_dir.name + "/db.sqlite" - self._db_map = self._db_mngr.get_db_map(url, logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map(url, logger, create=True) + self._db_mngr.name_registry.register(url, "test_import_export_data_db") def tearDown(self): self._db_mngr.close_all_sessions() @@ -402,51 +403,6 @@ def test_import_parameter_value_lists(self): ) -class TestOpenDBEditor(TestCaseWithQApplication): - def setUp(self): - self._temp_dir = TemporaryDirectory() - db_path = Path(self._temp_dir.name, "db.sqlite") - self._db_url = "sqlite:///" + str(db_path) - self._db_mngr = SpineDBManager(QSettings(), None) - self._logger = MagicMock() - - def test_open_db_editor(self): - editors = list(self._db_mngr.get_all_multi_spine_db_editors()) - self.assertFalse(editors) - self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True) - editors = list(self._db_mngr.get_all_multi_spine_db_editors()) - self.assertEqual(len(editors), 1) - self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True) - editors = list(self._db_mngr.get_all_multi_spine_db_editors()) - self.assertEqual(len(editors), 1) - self._db_mngr.open_db_editor({self._db_url: "not_the_same"}, reuse_existing_editor=True) - self.assertEqual(len(editors), 1) - editor = editors[0] - self.assertEqual(editor.tab_widget.count(), 1) - # Finally try to open the first tab again - self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True) - editors = list(self._db_mngr.get_all_multi_spine_db_editors()) - editor = editors[0] - self.assertEqual(editor.tab_widget.count(), 1) - for editor in self._db_mngr.get_all_multi_spine_db_editors(): - QApplication.processEvents() - editor.close() - - def tearDown(self): - self._db_mngr.close_all_sessions() - self._db_mngr.clean_up() - # Database connection may still be open. Retry cleanup until it succeeds. - running = True - while running: - QApplication.processEvents() - try: - self._temp_dir.cleanup() - except NotADirectoryError: - pass - else: - running = False - - class TestDuplicateEntity(TestCaseWithQApplication): @classmethod def setUpClass(cls): @@ -456,7 +412,8 @@ def setUpClass(cls): def setUp(self): self._db_mngr = SpineDBManager(QSettings(), None) logger = MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename) def tearDown(self): self._db_mngr.close_all_sessions() @@ -525,7 +482,8 @@ def setUp(self): mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = SpineDBManager(mock_settings, None) self._logger = MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "test_update_expanded_parameter_values_db") def tearDown(self): self._db_mngr.close_all_sessions() @@ -570,7 +528,8 @@ def setUp(self): mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = SpineDBManager(mock_settings, None) self._logger = MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="database", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "test_remove_scenario_alternative_db") def tearDown(self): self._db_mngr.close_all_sessions() diff --git a/tests/test_SpineToolboxProject.py b/tests/test_SpineToolboxProject.py index 6ef213b22..abd75049e 100644 --- a/tests/test_SpineToolboxProject.py +++ b/tests/test_SpineToolboxProject.py @@ -249,9 +249,10 @@ def _execute_project(self, names=None): """ waiter = SignalWaiter() 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: + 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, + ): # Make sure that the test uses LocalSpineEngineManager # This mocks the check for engineSettings/remoteEngineEnabled in SpineToolboxProject.execute_dags() mock_qsettings_value.side_effect = qsettings_value_side_effect @@ -557,8 +558,9 @@ def test_update_connection(self): def test_save_when_storing_item_local_data(self): project = self.toolbox.project item = _MockItemWithLocalData(project) - with mock.patch.object(self.toolbox, "project_item_properties_ui"), mock.patch.object( - self.toolbox, "project_item_icon" + with ( + mock.patch.object(self.toolbox, "project_item_properties_ui"), + mock.patch.object(self.toolbox, "project_item_icon"), ): project.add_item(item) project.save() @@ -585,16 +587,19 @@ def test_save_when_storing_item_local_data(self): def test_load_when_storing_item_local_data(self): project = self.toolbox.project item = _MockItemWithLocalData(project) - with mock.patch.object(self.toolbox, "project_item_properties_ui"), mock.patch.object( - self.toolbox, "project_item_icon" + with ( + mock.patch.object(self.toolbox, "project_item_properties_ui"), + mock.patch.object(self.toolbox, "project_item_icon"), ): project.add_item(item) project.save() self.assertTrue(self.toolbox.close_project(ask_confirmation=False)) self.toolbox.item_factories = {"Tester": _MockItemFactoryForLocalDataTests()} - with mock.patch.object(self.toolbox, "update_recent_projects"), mock.patch.object( - self.toolbox, "project_item_properties_ui" - ), mock.patch.object(self.toolbox, "project_item_icon"): + with ( + mock.patch.object(self.toolbox, "update_recent_projects"), + mock.patch.object(self.toolbox, "project_item_properties_ui"), + 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") self.assertEqual(item.kwargs, {"type": "Tester", "a": {"b": 1, "c": 2, "d": 3}}) @@ -603,9 +608,10 @@ def test_add_and_save_specification(self): project = self.toolbox.project self.toolbox.item_factories = {"Tester": ProjectItemFactory()} specification = _MockSpecification("a specification", "Specification for testing.", "Tester") - with mock.patch.object(ProjectItemFactory, "icon") as mock_icon, mock.patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + mock.patch.object(ProjectItemFactory, "icon") as mock_icon, + mock.patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_icon.return_value = ":/icons/item_icons/hammer.svg" mock_icon_color.return_value = QColor("white") project.add_specification(specification) @@ -629,9 +635,10 @@ def test_add_and_save_specification_with_local_data(self): specification = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" ) - with mock.patch.object(ProjectItemFactory, "icon") as mock_icon, mock.patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + mock.patch.object(ProjectItemFactory, "icon") as mock_icon, + mock.patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_icon.return_value = ":/icons/item_icons/hammer.svg" mock_icon_color.return_value = QColor("white") project.add_specification(specification) @@ -662,9 +669,10 @@ def test_renaming_specification_with_local_data_updates_local_data_file(self): original_specification = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" ) - with mock.patch.object(ProjectItemFactory, "icon") as mock_icon, mock.patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + mock.patch.object(ProjectItemFactory, "icon") as mock_icon, + mock.patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_icon.return_value = ":/icons/item_icons/hammer.svg" mock_icon_color.return_value = QColor("white") project.add_specification(original_specification) @@ -697,9 +705,10 @@ def test_replace_specification_with_local_data_by_one_without_removes_local_data specification_with_local_data = _MockSpecificationWithLocalData( "a specification", "Specification for testing.", "Tester", "my precious data" ) - with mock.patch.object(ProjectItemFactory, "icon") as mock_icon, mock.patch.object( - ProjectItemFactory, "icon_color" - ) as mock_icon_color: + with ( + mock.patch.object(ProjectItemFactory, "icon") as mock_icon, + mock.patch.object(ProjectItemFactory, "icon_color") as mock_icon_color, + ): mock_icon.return_value = ":/icons/item_icons/hammer.svg" mock_icon_color.return_value = QColor("white") project.add_specification(specification_with_local_data) diff --git a/tests/test_ToolboxUI.py b/tests/test_ToolboxUI.py index 9b6c8cd4a..5c4910a03 100644 --- a/tests/test_ToolboxUI.py +++ b/tests/test_ToolboxUI.py @@ -91,10 +91,11 @@ 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) - with mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.project.create_dir" - ), mock.patch("spinetoolbox.project_item.project_item.create_dir"), mock.patch( - "spinetoolbox.ui_main.ToolboxUI.update_recent_projects" + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.project.create_dir"), + mock.patch("spinetoolbox.project_item.project_item.create_dir"), + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.open_project(project_dir) self.assertIsInstance(self.toolbox.project, SpineToolboxProject) @@ -149,10 +150,11 @@ 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) - with mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.project.create_dir" - ), mock.patch("spinetoolbox.project_item.project_item.create_dir"), mock.patch( - "spinetoolbox.ui_main.ToolboxUI.update_recent_projects" + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.project.create_dir"), + mock.patch("spinetoolbox.project_item.project_item.create_dir"), + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.init_project(project_dir) self.assertIsNotNone(self.toolbox.project) @@ -160,9 +162,11 @@ def test_init_project(self): def test_new_project(self): self._temp_dir = TemporaryDirectory() - with mock.patch("spinetoolbox.ui_main.QSettings.setValue"), mock.patch( - "spinetoolbox.ui_main.QSettings.sync" - ), mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter: + with ( + mock.patch("spinetoolbox.ui_main.QSettings.setValue"), + mock.patch("spinetoolbox.ui_main.QSettings.sync"), + mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter, + ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() self.assertIsNotNone(self.toolbox.project) @@ -170,9 +174,11 @@ def test_new_project(self): def test_save_project(self): self._temp_dir = TemporaryDirectory() - with mock.patch("spinetoolbox.ui_main.QSettings.setValue"), mock.patch( - "spinetoolbox.ui_main.QSettings.sync" - ), mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter: + with ( + mock.patch("spinetoolbox.ui_main.QSettings.setValue"), + mock.patch("spinetoolbox.ui_main.QSettings.sync"), + mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter, + ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() add_dc_trough_undo_stack(self.toolbox, "DC") @@ -184,10 +190,11 @@ def test_save_project(self): mock_qsettings_value.side_effect = qsettings_value_side_effect self.assertTrue(self.toolbox.close_project()) mock_qsettings_value.assert_called() - with mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.project.create_dir" - ), mock.patch("spinetoolbox.project_item.project_item.create_dir"), mock.patch( - "spinetoolbox.ui_main.ToolboxUI.update_recent_projects" + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.project.create_dir"), + mock.patch("spinetoolbox.project_item.project_item.create_dir"), + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.open_project(self._temp_dir.name) self.assertIsNotNone(self.toolbox.project) @@ -195,9 +202,11 @@ def test_save_project(self): def test_prevent_project_closing_with_unsaved_changes(self): self._temp_dir = TemporaryDirectory() - with mock.patch("spinetoolbox.ui_main.QSettings.setValue"), mock.patch( - "spinetoolbox.ui_main.QSettings.sync" - ), mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter: + with ( + mock.patch("spinetoolbox.ui_main.QSettings.setValue"), + mock.patch("spinetoolbox.ui_main.QSettings.sync"), + mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter, + ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() add_dc_trough_undo_stack(self.toolbox, "DC1") @@ -213,12 +222,12 @@ def test_prevent_project_closing_with_unsaved_changes(self): with mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.Cancel): self.assertFalse(self.toolbox.close_project()) mock_qsettings_value.assert_called() - with mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.project.create_dir" - ), mock.patch("spinetoolbox.project_item.project_item.create_dir"), mock.patch( - "spinetoolbox.ui_main.ToolboxUI.update_recent_projects" - ), mock.patch.object( - QMessageBox, "exec", return_value=QMessageBox.Cancel + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.project.create_dir"), + mock.patch("spinetoolbox.project_item.project_item.create_dir"), + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), + mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.Cancel), ): # Selecting cancel on the project close confirmation with mock.patch("spinetoolbox.ui_main.ToolboxUI.add_warning_message") as warning_msg: @@ -247,9 +256,11 @@ def test_close_project(self): def test_show_project_or_item_context_menu(self): self._temp_dir = TemporaryDirectory() - with mock.patch("spinetoolbox.ui_main.QSettings.setValue") as mock_set_value, mock.patch( - "spinetoolbox.ui_main.QSettings.sync" - ) as mock_sync, mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter: + with ( + mock.patch("spinetoolbox.ui_main.QSettings.setValue") as mock_set_value, + mock.patch("spinetoolbox.ui_main.QSettings.sync") as mock_sync, + mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter, + ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() mock_set_value.assert_called() @@ -277,9 +288,11 @@ def test_refresh_edit_action_states(self): self.assertFalse(self.toolbox.ui.actionRemove_all.isEnabled()) # Make project self._temp_dir = TemporaryDirectory() - with mock.patch("spinetoolbox.ui_main.QSettings.setValue") as mock_set_value, mock.patch( - "spinetoolbox.ui_main.QSettings.sync" - ) as mock_sync, mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter: + with ( + mock.patch("spinetoolbox.ui_main.QSettings.setValue") as mock_set_value, + mock.patch("spinetoolbox.ui_main.QSettings.sync") as mock_sync, + mock.patch("PySide6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir_getter, + ): mock_dir_getter.return_value = self._temp_dir.name self.toolbox.new_project() mock_set_value.assert_called() @@ -474,11 +487,11 @@ def test_drop_invalid_drag_on_design_view(self): gv = self.toolbox.ui.graphicsView pos = QPoint(0, 0) event = QDropEvent(pos, Qt.CopyAction, mime_data, Qt.NoButton, Qt.NoModifier) - with mock.patch( - "PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source" - ) as mock_drop_event_source, mock.patch.object(self.toolbox, "project"), mock.patch.object( - self.toolbox, "show_add_project_item_form" - ) as mock_show_add_project_item_form: + with ( + mock.patch("PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source") as mock_drop_event_source, + mock.patch.object(self.toolbox, "project"), + mock.patch.object(self.toolbox, "show_add_project_item_form") as mock_show_add_project_item_form, + ): mock_drop_event_source.return_value = "Invalid source" gv.dropEvent(event) mock_show_add_project_item_form.assert_not_called() @@ -493,11 +506,11 @@ def test_drop_project_item_on_design_view(self): scene_pos = QPointF(44, 20) pos = gv.mapFromScene(scene_pos) event = QDropEvent(pos, Qt.CopyAction, mime_data, Qt.NoButton, Qt.NoModifier) - with mock.patch( - "PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source" - ) as mock_drop_event_source, mock.patch.object(self.toolbox, "project"), mock.patch.object( - self.toolbox, "show_add_project_item_form" - ) as mock_show_add_project_item_form: + with ( + mock.patch("PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source") as mock_drop_event_source, + mock.patch.object(self.toolbox, "project"), + mock.patch.object(self.toolbox, "show_add_project_item_form") as mock_show_add_project_item_form, + ): mock_drop_event_source.return_value = MockDraggableButton() gv.dropEvent(event) mock_show_add_project_item_form.assert_called_once() @@ -544,8 +557,9 @@ def test_add_and_remove_specification(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) - with mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), mock.patch( - "spinetoolbox.ui_main.ToolboxUI.update_recent_projects" + with ( + mock.patch("spinetoolbox.ui_main.ToolboxUI.save_project"), + mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), ): self.toolbox.open_project(project_dir) # Tool spec model must be empty at this point @@ -555,9 +569,10 @@ def test_add_and_remove_specification(self): ) self.assertTrue(os.path.exists(tool_spec_path)) # Add a Tool spec to 'project.json' file - with mock.patch("spinetoolbox.ui_main.QFileDialog.getOpenFileName") as mock_filename, mock.patch( - "spine_items.tool.tool_specifications.ToolSpecification.save" - ) as mock_save_specification: + with ( + mock.patch("spinetoolbox.ui_main.QFileDialog.getOpenFileName") as mock_filename, + mock.patch("spine_items.tool.tool_specifications.ToolSpecification.save") as mock_save_specification, + ): mock_filename.return_value = [tool_spec_path] mock_save_specification.return_value = True self.toolbox.import_specification() diff --git a/tests/test_database_display_names.py b/tests/test_database_display_names.py new file mode 100644 index 000000000..0697dc219 --- /dev/null +++ b/tests/test_database_display_names.py @@ -0,0 +1,102 @@ +###################################################################################################################### +# 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 . +###################################################################################################################### +import sys +import unittest +from unittest import mock +from sqlalchemy.engine.url import make_url +from spinetoolbox.database_display_names import NameRegistry, suggest_display_name +from spinetoolbox.helpers import signal_waiter +from tests.mock_helpers import TestCaseWithQApplication + + +class TestNameRegistry(TestCaseWithQApplication): + def test_display_name_for_unregistered_url(self): + registry = NameRegistry() + self.assertEqual(registry.display_name("mysql://db.example.com/best_database"), "best_database") + sa_url = make_url("mysql://db.example.com/even_better_database") + self.assertEqual(registry.display_name(sa_url), "even_better_database") + + def test_display_name_for_registered_url(self): + registry = NameRegistry() + url = "mysql://db.example.com/best_database" + registry.register(url, "Best database") + self.assertEqual(registry.display_name(url), "Best database") + sa_url = make_url("mysql://db.example.com/even_better_database") + registry.register(sa_url, "Even better database") + self.assertEqual(registry.display_name(sa_url), "Even better database") + + def test_multiple_registered_names_gives_simple_database_name(self): + registry = NameRegistry() + url = "mysql://db.example.com/best_database" + with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter: + registry.register(url, "Best database") + self.assertEqual(waiter.args, (url, "Best database")) + with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter: + registry.register(url, "Even better database") + self.assertEqual(waiter.args, (url, "best_database")) + self.assertEqual(registry.display_name(url), "best_database") + + def test_unregister(self): + registry = NameRegistry() + url = "mysql://db.example.com/best_database" + with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter: + registry.register(url, "Best database") + self.assertEqual(waiter.args, (url, "Best database")) + self.assertEqual(registry.display_name(url), "Best database") + with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter: + registry.unregister(url, "Best database") + self.assertEqual(waiter.args, (url, "best_database")) + self.assertEqual(registry.display_name(url), "best_database") + + def test_unregister_one_of_two_names(self): + registry = NameRegistry() + url = "mysql://db.example.com/best_database" + registry.register(url, "Database 1") + registry.register(url, "Database 2") + self.assertEqual(registry.display_name(url), "best_database") + with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter: + registry.unregister(url, "Database 1") + self.assertEqual(waiter.args, (url, "Database 2")) + self.assertEqual(registry.display_name(url), "Database 2") + + def test_unregister_one_of_three_names(self): + registry = NameRegistry() + url = "mysql://db.example.com/best_database" + registry.register(url, "Database 1") + registry.register(url, "Database 2") + registry.register(url, "Database 3") + self.assertEqual(registry.display_name(url), "best_database") + with mock.patch.object(registry, "display_name_changed") as name_changed_signal: + registry.unregister(url, "Database 3") + name_changed_signal.emit.assert_not_called() + self.assertEqual(registry.display_name(url), "best_database") + + +class TestSuggestDisplayName(unittest.TestCase): + def test_mysql_url_returns_database_name(self): + sa_url = make_url("mysql://db.example.com/my_lovely_db") + self.assertEqual(suggest_display_name(sa_url), "my_lovely_db") + + def test_sqlite_url_returns_file_name_without_extension(self): + path = "c:\path\to\my_lovely_db.sqlite" if sys.platform == "win32" else "/path/to/my_lovely_db.sqlite" + sa_url = make_url(r"sqlite:///" + path) + self.assertEqual(suggest_display_name(sa_url), "my_lovely_db") + + def test_in_memory_sqlite_url_returns_random_hash(self): + sa_url = make_url(r"sqlite://") + name = suggest_display_name(sa_url) + self.assertTrue(isinstance(name, str)) + self.assertTrue(bool(name)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 201f7aa98..0aa83e185 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -19,11 +19,14 @@ import unittest from unittest.mock import MagicMock, patch from PySide6.QtCore import QSettings +from PySide6.QtGui import QAction, QKeySequence from PySide6.QtWidgets import QLineEdit, QWidget from spine_engine.load_project_items import load_item_specification_factories from spinetoolbox.config import PROJECT_FILENAME, PROJECT_LOCAL_DATA_DIR_NAME, PROJECT_LOCAL_DATA_FILENAME from spinetoolbox.helpers import ( HTMLTagFilter, + add_keyboard_shortcut_to_tool_tip, + add_keyboard_shortcuts_to_action_tool_tips, copy_files, create_dir, dir_is_valid, @@ -54,7 +57,7 @@ tuple_itemgetter, unique_name, ) -from tests.mock_helpers import TestCaseWithQApplication +from tests.mock_helpers import TestCaseWithQApplication, q_object class TestHelpers(TestCaseWithQApplication): @@ -289,9 +292,10 @@ def test_initial_dir_for_python_open_dialogs(self): # initial dir should be according to the text in line edit self.assertEqual(mock_native_dialog.call_args[1]["File"], str(executable)) self.assertEqual(mock_native_dialog.call_args[1]["InitialDir"], home_dir()) - with patch("spinetoolbox.helpers.os.path.exists") as mock_exists, patch( - "spinetoolbox.helpers.os.path.abspath" - ) as mock_abspath: + with ( + patch("spinetoolbox.helpers.os.path.exists") as mock_exists, + patch("spinetoolbox.helpers.os.path.abspath") as mock_abspath, + ): mock_exists.return_value = True mock_abspath.return_value = python_in_path line_edit.clear() @@ -313,9 +317,10 @@ def test_initial_dir_for_python_open_dialogs(self): select_python_interpreter(None, line_edit) # initial dir should be according to the text in line edit mock_open_file_dialog.assert_called_with(None, "Select Python Interpreter", str(executable)) - with patch("spinetoolbox.helpers.os.path.exists") as mock_exists, patch( - "spinetoolbox.helpers.os.path.abspath" - ) as mock_abspath: + with ( + patch("spinetoolbox.helpers.os.path.exists") as mock_exists, + patch("spinetoolbox.helpers.os.path.abspath") as mock_abspath, + ): mock_exists.return_value = True mock_abspath.return_value = python_in_path line_edit.clear() @@ -462,5 +467,72 @@ def test_makes_tool_tips(self): self.assertEqual(plain_to_tool_tip("Is not None."), plain_to_rich("Is not None.")) +class TestAddKeyboardShortcutToToolTip(TestCaseWithQApplication): + def test_tool_tip_remains_unchanged_without_shortcut(self): + with q_object(QAction()) as action: + text = "

    A useful action

    " + action.setToolTip(text) + add_keyboard_shortcut_to_tool_tip(action) + self.assertEqual(action.toolTip(), text) + + def test_shortcut_get_added_to_html_tool_tip(self): + with q_object(QAction()) as action: + text = "

    A useful action

    " + action.setToolTip(text) + action.setShortcut(QKeySequence("g")) + add_keyboard_shortcut_to_tool_tip(action) + self.assertEqual(action.toolTip(), "

    A useful action

    G

    ") + + def test_shortcut_get_added_to_plain_text_tool_tip(self): + with q_object(QAction()) as action: + text = "A useful action" + action.setToolTip(text) + action.setShortcut(QKeySequence("g")) + add_keyboard_shortcut_to_tool_tip(action) + self.assertEqual(action.toolTip(), "

    A useful action

    G

    ") + + def test_html_formatting_within_tool_tip_is_preserved(self): + with q_object(QAction()) as action: + text = "

    A useful action

    " + action.setToolTip(text) + action.setShortcut(QKeySequence("g")) + add_keyboard_shortcut_to_tool_tip(action) + self.assertEqual(action.toolTip(), "

    A useful action

    G

    ") + + +class TestAddKeyboardShortcutsToActionToolTips(TestCaseWithQApplication): + def test_ui_without_actions_works(self): + class Ui: + no_action = object() + + ui = Ui() + add_keyboard_shortcuts_to_action_tool_tips(ui) + + def test_ui_with_action_without_shortscuts_leaves_tool_tips_as_is(self): + class Ui: + no_action = object() + action = None + + ui = Ui() + with q_object(QAction()) as action: + action.setToolTip("A highly useful thing") + ui.action = action + add_keyboard_shortcuts_to_action_tool_tips(ui) + self.assertEqual(action.toolTip(), "A highly useful thing") + + def test_shortcut_gets_appended_to_tool_tip(self): + class Ui: + no_action = object() + action = None + + ui = Ui() + with q_object(QAction()) as action: + action.setToolTip("A highly useful thing") + action.setShortcut(QKeySequence("w")) + ui.action = action + add_keyboard_shortcuts_to_action_tool_tips(ui) + self.assertEqual(action.toolTip(), "

    A highly useful thing

    W

    ") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_multi_tab_windows.py b/tests/test_multi_tab_windows.py new file mode 100644 index 000000000..702002259 --- /dev/null +++ b/tests/test_multi_tab_windows.py @@ -0,0 +1,56 @@ +###################################################################################################################### +# 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 . +###################################################################################################################### +import unittest +from unittest import mock +from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry + + +class TestMultiTabWindowRegistry(unittest.TestCase): + def test_initialization(self): + registry = MultiTabWindowRegistry() + self.assertFalse(registry.has_windows()) + self.assertEqual(registry.windows(), []) + self.assertEqual(registry.tabs(), []) + self.assertIsNone(registry.get_some_window()) + + def test_register_window(self): + registry = MultiTabWindowRegistry() + window = mock.MagicMock() + registry.register_window(window) + self.assertEqual(registry.windows(), [window]) + + def test_unregister_window(self): + registry = MultiTabWindowRegistry() + window = mock.MagicMock() + registry.register_window(window) + self.assertTrue(registry.has_windows()) + registry.unregister_window(window) + self.assertEqual(registry.windows(), []) + + def test_get_some_window(self): + registry = MultiTabWindowRegistry() + window = mock.MagicMock() + registry.register_window(window) + self.assertIs(registry.get_some_window(), window) + + def test_tabs(self): + registry = MultiTabWindowRegistry() + window = mock.MagicMock() + window.tab_widget.count.return_value = 1 + tab = mock.MagicMock() + window.tab_widget.widget.return_value = tab + registry.register_window(window) + self.assertEqual(registry.tabs(), [tab]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_parameter_type_validation.py b/tests/test_parameter_type_validation.py index 5aca06561..8f4c20db9 100644 --- a/tests/test_parameter_type_validation.py +++ b/tests/test_parameter_type_validation.py @@ -29,7 +29,8 @@ def setUp(self): mock_settings.value.side_effect = lambda *args, **kwargs: 0 self._db_mngr = TestSpineDBManager(mock_settings, None) logger = mock.MagicMock() - self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename) self._db_mngr.parameter_type_validator.set_interval(0) def tearDown(self): diff --git a/tests/test_spine_db_fetcher.py b/tests/test_spine_db_fetcher.py index 4990c6980..99d8748c3 100644 --- a/tests/test_spine_db_fetcher.py +++ b/tests/test_spine_db_fetcher.py @@ -31,7 +31,8 @@ def setUp(self): app_settings = MagicMock() self._logger = MagicMock() # Collects error messages therefore handy for debugging. self._db_mngr = TestSpineDBManager(app_settings, None) - self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="db_fetcher_test_db", create=True) + self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True) + self._db_mngr.name_registry.register(self._db_map.sa_url, "db_fetcher_test_db") def tearDown(self): self._db_mngr.close_all_sessions() diff --git a/tests/widgets/test_AddProjectItemWidget.py b/tests/widgets/test_AddProjectItemWidget.py index 5473cdbae..782dc14f5 100644 --- a/tests/widgets/test_AddProjectItemWidget.py +++ b/tests/widgets/test_AddProjectItemWidget.py @@ -26,9 +26,10 @@ class TestAddProjectItemWidget(TestCaseWithQApplication): def setUp(self): """Set up toolbox.""" self._temp_dir = TemporaryDirectory() - with patch("spinetoolbox.ui_main.JumpPropertiesWidget") as mock_jump_props_widget, patch( - "spinetoolbox.ui_main.load_project_items" - ) as mock_load_project_items: + with ( + patch("spinetoolbox.ui_main.JumpPropertiesWidget") as mock_jump_props_widget, + patch("spinetoolbox.ui_main.load_project_items") as mock_load_project_items, + ): mock_jump_props_widget.return_value = QWidget() mock_load_project_items.return_value = {TestProjectItem.item_type(): TestItemFactory} self._toolbox = create_toolboxui_with_project(self._temp_dir.name) @@ -51,11 +52,11 @@ class TestAddProjectItemWidgetWithSpecifications(TestCaseWithQApplication): def setUp(self): """Set up toolbox.""" self._temp_dir = TemporaryDirectory() - with patch("spinetoolbox.ui_main.JumpPropertiesWidget") as mock_jump_props_widget, patch( - "spinetoolbox.ui_main.load_project_items" - ) as mock_load_project_items, patch( - "spinetoolbox.ui_main.load_item_specification_factories" - ) as mock_load_specification_factories: + with ( + patch("spinetoolbox.ui_main.JumpPropertiesWidget") as mock_jump_props_widget, + patch("spinetoolbox.ui_main.load_project_items") as mock_load_project_items, + patch("spinetoolbox.ui_main.load_item_specification_factories") as mock_load_specification_factories, + ): mock_jump_props_widget.return_value = QWidget() mock_load_project_items.return_value = {TestProjectItem.item_type(): TestItemFactory} mock_load_specification_factories.return_value = {TestProjectItem.item_type(): TestSpecificationFactory} diff --git a/tests/widgets/test_AddUpSpineOptWizard.py b/tests/widgets/test_AddUpSpineOptWizard.py index a73bfb19f..f762c04eb 100644 --- a/tests/widgets/test_AddUpSpineOptWizard.py +++ b/tests/widgets/test_AddUpSpineOptWizard.py @@ -22,11 +22,14 @@ class TestAddUpSpineOptWizard(TestCaseWithQApplication): def setUp(self): """Set up toolbox.""" self.toolbox = create_toolboxui() - with mock.patch( - "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_python_kernels" - ) as mock_fetch_python_kernels, mock.patch( - "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_julia_kernels" - ) as mock_fetch_julia_kernels: + with ( + mock.patch( + "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_python_kernels" + ) as mock_fetch_python_kernels, + mock.patch( + "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_julia_kernels" + ) as mock_fetch_julia_kernels, + ): self.settings_widget = SettingsWidget(self.toolbox) mock_fetch_python_kernels.assert_called() mock_fetch_julia_kernels.assert_called() @@ -41,7 +44,7 @@ def test_spine_opt_installation_succeeds(self): wizard.restart() self.assertEqual("Welcome", wizard.currentPage().title()) wizard.next() - self.assertEqual("Select Julia project", wizard.currentPage().title()) + self.assertEqual("Select Julia", wizard.currentPage().title()) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) wizard.next() @@ -60,7 +63,7 @@ def test_spine_opt_update_succeeds(self): wizard.restart() self.assertEqual("Welcome", wizard.currentPage().title()) wizard.next() - self.assertEqual("Select Julia project", wizard.currentPage().title()) + self.assertEqual("Select Julia", wizard.currentPage().title()) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: # We need the process to return a version that's lower than required curr_ver_split = [int(x) for x in REQUIRED_SPINE_OPT_VERSION.split(".")] @@ -84,7 +87,7 @@ def test_spine_opt_already_up_to_date(self): wizard.restart() self.assertEqual("Welcome", wizard.currentPage().title()) wizard.next() - self.assertEqual("Select Julia project", wizard.currentPage().title()) + self.assertEqual("Select Julia", wizard.currentPage().title()) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: stdout = REQUIRED_SPINE_OPT_VERSION.encode() MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit), stdout=stdout) @@ -97,7 +100,7 @@ def _make_failed_wizard(self): wizard.restart() self.assertEqual("Welcome", wizard.currentPage().title()) wizard.next() - self.assertEqual("Select Julia project", wizard.currentPage().title()) + self.assertEqual("Select Julia", wizard.currentPage().title()) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) wizard.next() @@ -114,101 +117,67 @@ def _make_failed_wizard(self): def test_spine_opt_installation_fails(self): wizard = self._make_failed_wizard() wizard.setField("troubleshoot", False) + self.assertEqual("Installation failed", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) - def test_registry_reset_succeeds(self): + def test_troubleshoot_solution_page1_and_retry_spineopt_install_fails(self): wizard = self._make_failed_wizard() wizard.next() self.assertEqual("Troubleshooting", wizard.currentPage().title()) wizard.setField("problem1", True) wizard.next() - self.assertEqual("Reset Julia General Registry", wizard.currentPage().title()) - self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Reset registry", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) - with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) - wizard.next() - self.assertEqual("Resetting Julia General Registry", wizard.currentPage().title()) + self.assertEqual("What now?", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isCommitPage()) self.assertEqual("Install SpineOpt", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) + MockQProcess.return_value = MockInstantQProcess(finished_args=(-1, MockQProcess.NormalExit)) wizard.next() self.assertEqual("Installing SpineOpt", wizard.currentPage().title()) wizard.next() - self.assertEqual("Installation successful", wizard.currentPage().title()) + self.assertEqual("Troubleshooting failed", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) - def test_registry_reset_fails(self): + def test_troubleshoot_solution_page1_and_retry_spineopt_install_succeeds(self): wizard = self._make_failed_wizard() wizard.next() self.assertEqual("Troubleshooting", wizard.currentPage().title()) wizard.setField("problem1", True) wizard.next() - self.assertEqual("Reset Julia General Registry", wizard.currentPage().title()) + self.assertEqual("What now?", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Reset registry", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) + self.assertFalse(wizard.currentPage().isFinalPage()) + self.assertEqual("Install SpineOpt", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(-1, MockQProcess.NormalExit)) + MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) wizard.next() - self.assertEqual("Resetting Julia General Registry", wizard.currentPage().title()) + self.assertEqual("Installing SpineOpt", wizard.currentPage().title()) wizard.next() - self.assertEqual("Troubleshooting failed", wizard.currentPage().title()) + self.assertEqual("Installation successful", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) - def test_registry_reset_succeeds_but_installing_spine_opt_fails_again_afterwards(self): + def test_troubleshoot_solution_page2(self): wizard = self._make_failed_wizard() wizard.next() self.assertEqual("Troubleshooting", wizard.currentPage().title()) - wizard.setField("problem1", True) - wizard.next() - self.assertEqual("Reset Julia General Registry", wizard.currentPage().title()) - self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Reset registry", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) - with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) - wizard.next() - self.assertEqual("Resetting Julia General Registry", wizard.currentPage().title()) - self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Install SpineOpt", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) - with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(-1, MockQProcess.NormalExit)) - wizard.next() - self.assertEqual("Installing SpineOpt", wizard.currentPage().title()) + wizard.setField("problem2", True) wizard.next() - self.assertEqual("Troubleshooting failed", wizard.currentPage().title()) + self.assertEqual("Environment variable JULIA_SSL_CA_ROOTS_PATH missing", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) - def test_updating_wmf_succeeds(self): + def test_troubleshoot_solution_page3(self): wizard = self._make_failed_wizard() wizard.next() self.assertEqual("Troubleshooting", wizard.currentPage().title()) - wizard.setField("problem2", True) + wizard.setField("problem3", True) wizard.next() - self.assertEqual("Update Windows Managemet Framework", wizard.currentPage().title()) - self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Install SpineOpt", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) - with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(0, MockQProcess.NormalExit)) - wizard.next() - self.assertEqual("Installing SpineOpt", wizard.currentPage().title()) - wizard.next() - self.assertEqual("Installation successful", wizard.currentPage().title()) + self.assertEqual("Reset Julia General Registry", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) - def test_updating_wmf_fails(self): + def test_troubleshoot_solution_page4(self): wizard = self._make_failed_wizard() wizard.next() self.assertEqual("Troubleshooting", wizard.currentPage().title()) - wizard.setField("problem2", True) - wizard.next() - self.assertEqual("Update Windows Managemet Framework", wizard.currentPage().title()) - self.assertTrue(wizard.currentPage().isCommitPage()) - self.assertEqual("Install SpineOpt", wizard.currentPage().buttonText(QWizard.WizardButton.CommitButton)) - with mock.patch("spinetoolbox.execution_managers.QProcess") as MockQProcess: - MockQProcess.return_value = MockInstantQProcess(finished_args=(-1, MockQProcess.NormalExit)) - wizard.next() - self.assertEqual("Installing SpineOpt", wizard.currentPage().title()) + wizard.setField("problem4", True) wizard.next() - self.assertEqual("Troubleshooting failed", wizard.currentPage().title()) + self.assertEqual("Update Windows Management Framework", wizard.currentPage().title()) self.assertTrue(wizard.currentPage().isFinalPage()) diff --git a/tests/widgets/test_InstallJuliaWizard.py b/tests/widgets/test_InstallJuliaWizard.py index 6e02dc7bc..952fe8d14 100644 --- a/tests/widgets/test_InstallJuliaWizard.py +++ b/tests/widgets/test_InstallJuliaWizard.py @@ -22,11 +22,14 @@ class TestInstallJuliaWizard(TestCaseWithQApplication): def setUp(self): """Set up toolbox.""" self.toolbox = create_toolboxui() - with mock.patch( - "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_python_kernels" - ) as mock_fetch_python_kernels, mock.patch( - "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_julia_kernels" - ) as mock_fetch_julia_kernels: + with ( + mock.patch( + "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_python_kernels" + ) as mock_fetch_python_kernels, + mock.patch( + "spinetoolbox.widgets.settings_widget.SettingsWidget.start_fetching_julia_kernels" + ) as mock_fetch_julia_kernels, + ): self._settings_widget = SettingsWidget(self.toolbox) mock_fetch_python_kernels.assert_called() mock_fetch_julia_kernels.assert_called() diff --git a/tests/widgets/test_custom_qwidgets.py b/tests/widgets/test_custom_qwidgets.py index 46300aa55..036986ced 100644 --- a/tests/widgets/test_custom_qwidgets.py +++ b/tests/widgets/test_custom_qwidgets.py @@ -99,7 +99,7 @@ def test_warning_label(self): with _select_database_items_dialog(None, None) as dialog: self.assertEqual(dialog._ui.warning_label.text(), "") dialog._item_check_boxes_widget._item_check_boxes["entity_class"].setChecked(True) - self.assertEqual(dialog._ui.warning_label.text(), "Warning! Structural data items selected.") + self.assertEqual(dialog._ui.warning_label.text(), "Warning! You are about to delete structural items.") @contextmanager diff --git a/tests/widgets/test_kernel_editor.py b/tests/widgets/test_kernel_editor.py index ab53730e1..4dcb109bf 100644 --- a/tests/widgets/test_kernel_editor.py +++ b/tests/widgets/test_kernel_editor.py @@ -51,10 +51,11 @@ def test_make_python_kernel(self): python_exec = "python.exe" if sys.platform == "win32" else "python" python_path = pathlib.Path(environment_dir, "Scripts", python_exec) kernel_name = "spinetoolbox_test_make_python_kernel" - with patch("spinetoolbox.widgets.kernel_editor.QMessageBox") as mock_message_box, patch.object( - KernelEditorBase, "_python_interpreter_name", return_value=str(python_path) - ), patch.object(KernelEditorBase, "_python_kernel_name", return_value=kernel_name), patch.object( - KernelEditorBase, "_python_kernel_display_name", return_value="Test kernel" + with ( + patch("spinetoolbox.widgets.kernel_editor.QMessageBox") as mock_message_box, + patch.object(KernelEditorBase, "_python_interpreter_name", return_value=str(python_path)), + patch.object(KernelEditorBase, "_python_kernel_name", return_value=kernel_name), + patch.object(KernelEditorBase, "_python_kernel_display_name", return_value="Test kernel"), ): mock_message_box.exec.return_value = QMessageBox.StandardButton.Ok editor = KernelEditorBase(self._settings_widget, "python") @@ -81,10 +82,11 @@ def test_make_julia_kernel(self): self.skipTest("Julia not found in PATH.") kernel_name = "spinetoolbox_test_make_julia_kernel" # with TemporaryDirectory() as julia_project_dir: - with patch("spinetoolbox.widgets.kernel_editor.QMessageBox") as mock_message_box, patch.object( - KernelEditorBase, "_julia_kernel_name", return_value=kernel_name - ), patch.object(KernelEditorBase, "_julia_executable", return_value=julia_exec), patch.object( - KernelEditorBase, "_julia_project", return_value="@." + with ( + patch("spinetoolbox.widgets.kernel_editor.QMessageBox") as mock_message_box, + patch.object(KernelEditorBase, "_julia_kernel_name", return_value=kernel_name), + patch.object(KernelEditorBase, "_julia_executable", return_value=julia_exec), + patch.object(KernelEditorBase, "_julia_project", return_value="@."), ): mock_message_box.exec.return_value = QMessageBox.StandardButton.Ok editor = KernelEditorBase(self._settings_widget, "julia")