From 508fdd4c26993a05e208e051e95f3740d07371d2 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 16:47:45 -0500 Subject: [PATCH 01/36] feta: add startup dialog --- .../_gui_objects/_startup_widget.py | 166 ++++++++++++++++++ src/napari_micromanager/main_window.py | 42 ++++- 2 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 src/napari_micromanager/_gui_objects/_startup_widget.py diff --git a/src/napari_micromanager/_gui_objects/_startup_widget.py b/src/napari_micromanager/_gui_objects/_startup_widget.py new file mode 100644 index 00000000..84557e17 --- /dev/null +++ b/src/napari_micromanager/_gui_objects/_startup_widget.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import json +import warnings +from pathlib import Path +from typing import cast + +from platformdirs import user_config_dir +from pymmcore_plus import find_micromanager +from qtpy.QtWidgets import ( + QComboBox, + QDialog, + QDialogButtonBox, + QFileDialog, + QGridLayout, + QLabel, + QPushButton, + QSizePolicy, + QWidget, +) + +USER_DIR = Path(user_config_dir("napari_micromanager")) +USER_CONFIGS_PATHS = USER_DIR / "system_configurations.json" +FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) +NEW = "New Configuration" + + +class StartupDialog(QDialog): + """A dialog to select the MicroManager configuration files.""" + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setWindowTitle("System Configurations") + + # label + cfg_lbl = QLabel("Configuration file:") + cfg_lbl.setSizePolicy(FIXED) + + # combo box + self.cfg_combo = QComboBox() + # `AdjustToMinimumContents` is not available in all qtpy backends so using + # `AdjustToMinimumContentsLengthWithIcon` instead + self.cfg_combo.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + + # browse button + self.browse_btn = QPushButton("...") + self.browse_btn.setSizePolicy(FIXED) + self.browse_btn.clicked.connect(self._on_browse_clicked) + + # Create OK and Cancel buttons + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + # add widgets to layout + wdg_layout = QGridLayout(self) + wdg_layout.addWidget(cfg_lbl, 0, 0) + wdg_layout.addWidget(self.cfg_combo, 0, 1) + wdg_layout.addWidget(self.browse_btn, 0, 2) + wdg_layout.addWidget(button_box, 2, 0, 1, 3) + + self._initialize() + + def value(self) -> str: + """Return the selected value.""" + return str(self.cfg_combo.currentText()) + + def add_path_to_json(self, path: Path | str) -> None: + """Uopdate the json file with the new path.""" + if isinstance(path, Path): + path = str(path) + + # Read the existing data + try: + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + except json.JSONDecodeError: + data = {"paths": []} + + # Append the new path. using insert so we leave the empty string at the end + paths = cast(list, data.get("paths", [])) + paths.insert(0, path) + + # Write the data back to the file + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": paths}, f) + + def _initialize(self) -> None: + """Initialize the dialog with the configuration files.""" + # create USER_CONFIGS_PATHS if it doesn't exist + if not USER_CONFIGS_PATHS.exists(): + USER_DIR.mkdir(parents=True, exist_ok=True) + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": []}, f) + + # get the paths from the json file + configs_paths = self._get_config_paths() + + # add the paths to the combo box + self.cfg_combo.addItems([*configs_paths, NEW]) + + # write the data back to the file + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": configs_paths}, f) + + def _get_config_paths(self) -> list[str]: + """Return the paths from the json file. + + If a file stored in the json file doesn't exist, it is removed from the list. + + The method also adds all the .cfg files in the MicroManager folder to the list + if they are not already there. + """ + try: + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + + # get path list from json file + paths = cast(list, data.get("paths", [])) + + # remove any path that doesn't exist + for path in paths: + if not Path(path).exists(): + paths.remove(path) + + # get all the .cfg files in the MicroManager folder + cfg_files = self._get_micromanager_cfg_files() + + # add all the .cfg files to the list if they are not already there + for cfg in reversed(cfg_files): + if str(cfg) not in paths: + # using insert so we leave the empty string at the end + paths.insert(0, str(cfg)) + + except json.JSONDecodeError: + paths = [] + warnings.warn("Error reading the json file.", stacklevel=2) + + return paths + + def _get_micromanager_cfg_files(self) -> list[Path]: + """Return all the .cfg files in the MicroManager folders.""" + mm_dir = find_micromanager() + + if mm_dir is None: + return [] + + cfg_files: list[Path] = [] + cfg_files.extend(Path(mm_dir).glob("*.cfg")) + + return cfg_files + + def _on_browse_clicked(self) -> None: + """Open a file dialog to select a file.""" + path, _ = QFileDialog.getOpenFileName( + self, "Open file", "", "MicroManager files (*.cfg)" + ) + if path: + # using insert so we leave the empty string at the end + self.cfg_combo.insertItem(0, path) + self.cfg_combo.setCurrentText(path) + self.add_path_to_json(path) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 4d3ba523..02512509 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -12,6 +12,7 @@ from pymmcore_plus import CMMCorePlus from ._core_link import CoreViewerLink +from ._gui_objects._startup_widget import NEW, StartupDialog from ._gui_objects._toolbar import MicroManagerToolbar if TYPE_CHECKING: @@ -55,12 +56,43 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) + self._startup = StartupDialog(self.viewer.window._qt_window) + + # if a config is passed, load it if config is not None: - try: - self._mmc.loadSystemConfiguration(config) - except FileNotFoundError: - # don't crash if the user passed an invalid config - warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) + self._load_system_configuration(config) + # add the path to the json file + self._startup.add_path_to_json(config) + return + + # if no config is passed, show the startup dialog + self._center_startup_dialog() + if self._startup.exec_(): + config = self._startup.value() + # if the user selected NEW, show the config wizard + if config == NEW: + ... # TODO: CONFIG WIZARD + else: + self._load_system_configuration(config) + + def _load_system_configuration(self, config: str | Path) -> None: + """Load a Micro-Manager system configuration file.""" + try: + self._mmc.loadSystemConfiguration(config) + except FileNotFoundError: + # don't crash if the user passed an invalid config + warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) + + def _center_startup_dialog(self) -> None: + """Center the startup dialog in the viewer window.""" + self._startup.move( + self.viewer.window.qt_viewer.geometry().center() + - self._startup.geometry().center() + ) + self._startup.resize( + int(self.viewer.window.qt_viewer.geometry().width() / 2), + self._startup.sizeHint().height(), + ) def _cleanup(self) -> None: for signal, slot in self._connections: From 8070b9faef9c46ca5f9dd47da695c91efc8510d7 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 16:55:16 -0500 Subject: [PATCH 02/36] feat: add wizard --- src/napari_micromanager/main_window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 02512509..321d468f 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -10,6 +10,7 @@ import napari.layers import napari.viewer from pymmcore_plus import CMMCorePlus +from pymmcore_widgets import ConfigWizard from ._core_link import CoreViewerLink from ._gui_objects._startup_widget import NEW, StartupDialog @@ -71,7 +72,10 @@ def __init__( config = self._startup.value() # if the user selected NEW, show the config wizard if config == NEW: - ... # TODO: CONFIG WIZARD + # TODO: subclass to load the new cfg if created and to add it to the + # json file. instead of show() should use exec_() and check the return + self._cfg_wizard = ConfigWizard(parent=self.viewer.window._qt_window) + self._cfg_wizard.show() else: self._load_system_configuration(config) From ca6a053e66589f088663b59423b70ead1f159a1c Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 16:58:44 -0500 Subject: [PATCH 03/36] fix: _handle_system_configuration --- src/napari_micromanager/main_window.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 321d468f..ef96302e 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -57,19 +57,23 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - self._startup = StartupDialog(self.viewer.window._qt_window) - # if a config is passed, load it + self._handle_system_configuration(config) + + def _handle_system_configuration(self, config: str | Path | None) -> None: + """Handle the system configuration file. If None, show the startup dialog.""" + startup = StartupDialog(self.viewer.window._qt_window) + if config is not None: self._load_system_configuration(config) # add the path to the json file - self._startup.add_path_to_json(config) + startup.add_path_to_json(config) return # if no config is passed, show the startup dialog self._center_startup_dialog() - if self._startup.exec_(): - config = self._startup.value() + if startup.exec_(): + config = startup.value() # if the user selected NEW, show the config wizard if config == NEW: # TODO: subclass to load the new cfg if created and to add it to the From 9d0e5393b57061c02d32296cb854bb06711c66c1 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 17:00:24 -0500 Subject: [PATCH 04/36] fix: _center_dialog_in_viewer --- src/napari_micromanager/main_window.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index ef96302e..b2968ca5 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -20,6 +20,7 @@ from pathlib import Path from pymmcore_plus.core.events._protocol import PSignalInstance + from qtpy.QtWidgets import QDialog # this is very verbose @@ -71,7 +72,7 @@ def _handle_system_configuration(self, config: str | Path | None) -> None: return # if no config is passed, show the startup dialog - self._center_startup_dialog() + self._center_dialog_in_viewer(startup) if startup.exec_(): config = startup.value() # if the user selected NEW, show the config wizard @@ -91,15 +92,15 @@ def _load_system_configuration(self, config: str | Path) -> None: # don't crash if the user passed an invalid config warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) - def _center_startup_dialog(self) -> None: - """Center the startup dialog in the viewer window.""" - self._startup.move( + def _center_dialog_in_viewer(self, startup: QDialog) -> None: + """Center the dialog in the viewer window.""" + startup.move( self.viewer.window.qt_viewer.geometry().center() - - self._startup.geometry().center() + - startup.geometry().center() ) - self._startup.resize( + startup.resize( int(self.viewer.window.qt_viewer.geometry().width() / 2), - self._startup.sizeHint().height(), + startup.sizeHint().height(), ) def _cleanup(self) -> None: From dc129b27958da05b4e25928b1da13af7b41cf121 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 17:01:43 -0500 Subject: [PATCH 05/36] fix: add_path_to_json --- src/napari_micromanager/_gui_objects/_startup_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_widget.py b/src/napari_micromanager/_gui_objects/_startup_widget.py index 84557e17..6772851a 100644 --- a/src/napari_micromanager/_gui_objects/_startup_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_widget.py @@ -83,7 +83,8 @@ def add_path_to_json(self, path: Path | str) -> None: # Append the new path. using insert so we leave the empty string at the end paths = cast(list, data.get("paths", [])) - paths.insert(0, path) + if path not in paths: + paths.insert(0, path) # Write the data back to the file with open(USER_CONFIGS_PATHS, "w") as f: From b04006e96bcc7d18dc36c8e6fb5715b6917558e7 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 20:41:50 -0500 Subject: [PATCH 06/36] fix: rename --- .../_gui_objects/_startup_widget.py | 2 +- src/napari_micromanager/main_window.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_widget.py b/src/napari_micromanager/_gui_objects/_startup_widget.py index 6772851a..98d00bd5 100644 --- a/src/napari_micromanager/_gui_objects/_startup_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_widget.py @@ -25,7 +25,7 @@ NEW = "New Configuration" -class StartupDialog(QDialog): +class ConfigurationsDialog(QDialog): """A dialog to select the MicroManager configuration files.""" def __init__(self, parent: QWidget | None = None) -> None: diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index b2968ca5..c94c1df1 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -13,7 +13,7 @@ from pymmcore_widgets import ConfigWizard from ._core_link import CoreViewerLink -from ._gui_objects._startup_widget import NEW, StartupDialog +from ._gui_objects._startup_widget import NEW, ConfigurationsDialog from ._gui_objects._toolbar import MicroManagerToolbar if TYPE_CHECKING: @@ -63,18 +63,18 @@ def __init__( def _handle_system_configuration(self, config: str | Path | None) -> None: """Handle the system configuration file. If None, show the startup dialog.""" - startup = StartupDialog(self.viewer.window._qt_window) + config_dialog = ConfigurationsDialog(self.viewer.window._qt_window) if config is not None: self._load_system_configuration(config) # add the path to the json file - startup.add_path_to_json(config) + config_dialog.add_path_to_json(config) return # if no config is passed, show the startup dialog - self._center_dialog_in_viewer(startup) - if startup.exec_(): - config = startup.value() + self._center_dialog_in_viewer(config_dialog) + if config_dialog.exec_(): + config = config_dialog.value() # if the user selected NEW, show the config wizard if config == NEW: # TODO: subclass to load the new cfg if created and to add it to the From 1ec8c21150512ee763e90ebe5516ca67ac7dfd54 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 20:44:41 -0500 Subject: [PATCH 07/36] fix: move config_dialog --- src/napari_micromanager/main_window.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index c94c1df1..16512c96 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -58,23 +58,23 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) + self.config_dialog = ConfigurationsDialog(self.viewer.window._qt_window) + # if a config is passed, load it self._handle_system_configuration(config) def _handle_system_configuration(self, config: str | Path | None) -> None: """Handle the system configuration file. If None, show the startup dialog.""" - config_dialog = ConfigurationsDialog(self.viewer.window._qt_window) - if config is not None: self._load_system_configuration(config) # add the path to the json file - config_dialog.add_path_to_json(config) + self.config_dialog.add_path_to_json(config) return # if no config is passed, show the startup dialog - self._center_dialog_in_viewer(config_dialog) - if config_dialog.exec_(): - config = config_dialog.value() + self._center_dialog_in_viewer(self.config_dialog) + if self.config_dialog.exec_(): + config = self.config_dialog.value() # if the user selected NEW, show the config wizard if config == NEW: # TODO: subclass to load the new cfg if created and to add it to the From 31762b4ac9cafdf3aa2fe625f0b00e5dfff2cfbd Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 20:45:31 -0500 Subject: [PATCH 08/36] fix: rename --- .../_gui_objects/_startup_widget.py | 2 +- src/napari_micromanager/main_window.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_widget.py b/src/napari_micromanager/_gui_objects/_startup_widget.py index 98d00bd5..1125fa97 100644 --- a/src/napari_micromanager/_gui_objects/_startup_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_widget.py @@ -25,7 +25,7 @@ NEW = "New Configuration" -class ConfigurationsDialog(QDialog): +class ConfigurationsHandler(QDialog): """A dialog to select the MicroManager configuration files.""" def __init__(self, parent: QWidget | None = None) -> None: diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 16512c96..67c7af8d 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -13,7 +13,7 @@ from pymmcore_widgets import ConfigWizard from ._core_link import CoreViewerLink -from ._gui_objects._startup_widget import NEW, ConfigurationsDialog +from ._gui_objects._startup_widget import NEW, ConfigurationsHandler from ._gui_objects._toolbar import MicroManagerToolbar if TYPE_CHECKING: @@ -58,7 +58,7 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - self.config_dialog = ConfigurationsDialog(self.viewer.window._qt_window) + self.configs_handler = ConfigurationsHandler(self.viewer.window._qt_window) # if a config is passed, load it self._handle_system_configuration(config) @@ -68,13 +68,13 @@ def _handle_system_configuration(self, config: str | Path | None) -> None: if config is not None: self._load_system_configuration(config) # add the path to the json file - self.config_dialog.add_path_to_json(config) + self.configs_handler.add_path_to_json(config) return # if no config is passed, show the startup dialog - self._center_dialog_in_viewer(self.config_dialog) - if self.config_dialog.exec_(): - config = self.config_dialog.value() + self._center_dialog_in_viewer(self.configs_handler) + if self.configs_handler.exec_(): + config = self.configs_handler.value() # if the user selected NEW, show the config wizard if config == NEW: # TODO: subclass to load the new cfg if created and to add it to the From 81c1552772ca7d9cd2d9112f48baf57b8012f4f8 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 21:16:39 -0500 Subject: [PATCH 09/36] fix: move logic into ConfigurationsHandler --- ...t.py => _startup_configurations_widget.py} | 105 ++++++++++++------ .../_gui_objects/_toolbar.py | 17 +-- src/napari_micromanager/main_window.py | 51 +-------- 3 files changed, 82 insertions(+), 91 deletions(-) rename src/napari_micromanager/_gui_objects/{_startup_widget.py => _startup_configurations_widget.py} (67%) diff --git a/src/napari_micromanager/_gui_objects/_startup_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py similarity index 67% rename from src/napari_micromanager/_gui_objects/_startup_widget.py rename to src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 1125fa97..1b3156ea 100644 --- a/src/napari_micromanager/_gui_objects/_startup_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -1,12 +1,13 @@ from __future__ import annotations import json -import warnings from pathlib import Path from typing import cast +from warnings import warn from platformdirs import user_config_dir -from pymmcore_plus import find_micromanager +from pymmcore_plus import CMMCorePlus, find_micromanager +from pymmcore_widgets import ConfigWizard from qtpy.QtWidgets import ( QComboBox, QDialog, @@ -28,9 +29,18 @@ class ConfigurationsHandler(QDialog): """A dialog to select the MicroManager configuration files.""" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + config: Path | str | None = None, + mmcore: CMMCorePlus | None = None, + ) -> None: super().__init__(parent) - self.setWindowTitle("System Configurations") + self.setWindowTitle("Micro-Manager System Configurations") + + self._mmc = mmcore or CMMCorePlus.instance() + self._config = config # label cfg_lbl = QLabel("Configuration file:") @@ -65,33 +75,33 @@ def __init__(self, parent: QWidget | None = None) -> None: self._initialize() - def value(self) -> str: - """Return the selected value.""" - return str(self.cfg_combo.currentText()) - - def add_path_to_json(self, path: Path | str) -> None: - """Uopdate the json file with the new path.""" - if isinstance(path, Path): - path = str(path) - - # Read the existing data - try: - with open(USER_CONFIGS_PATHS) as f: - data = json.load(f) - except json.JSONDecodeError: - data = {"paths": []} - - # Append the new path. using insert so we leave the empty string at the end - paths = cast(list, data.get("paths", [])) - if path not in paths: - paths.insert(0, path) - - # Write the data back to the file - with open(USER_CONFIGS_PATHS, "w") as f: - json.dump({"paths": paths}, f) + self.resize(500, self.minimumSizeHint().height()) + + # if a config was not passed, show the dialog + if config is None: + if self.exec_(): + config = self.cfg_combo.currentText() + # if the user selected NEW, show the config wizard + if config == NEW: + # TODO: subclass to load the new cfg if created and to add it to the + # json file. use if exec_() instead of show() and check the return + self._cfg_wizard = ConfigWizard(parent=self) + self._cfg_wizard.show() + # otherwise load the selected config + else: + self._load_system_configuration(config) + # if a config was passed, load it + else: + self._load_system_configuration(config) def _initialize(self) -> None: - """Initialize the dialog with the configuration files.""" + """Initialize the dialog with the configuration files. + + This method reads the stored paths in the USER_CONFIGS_PATHS jason file (or + create one if it doesn't exist) and adds them to the combo box. It also adds the + Micro-Manager configuration files form the Micro-Manager folder if they are not + already in the list. + """ # create USER_CONFIGS_PATHS if it doesn't exist if not USER_CONFIGS_PATHS.exists(): USER_DIR.mkdir(parents=True, exist_ok=True) @@ -137,9 +147,13 @@ def _get_config_paths(self) -> list[str]: # using insert so we leave the empty string at the end paths.insert(0, str(cfg)) + # if a config was passed, add it to the list if it's not already there + if self._config is not None and str(self._config) not in paths: + paths.insert(0, str(self._config)) + except json.JSONDecodeError: paths = [] - warnings.warn("Error reading the json file.", stacklevel=2) + warn("Error reading the json file.", stacklevel=2) return paths @@ -155,6 +169,14 @@ def _get_micromanager_cfg_files(self) -> list[Path]: return cfg_files + def _load_system_configuration(self, config: str | Path) -> None: + """Load a Micro-Manager system configuration file.""" + try: + self._mmc.loadSystemConfiguration(config) + except FileNotFoundError: + # don't crash if the user passed an invalid config + warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) + def _on_browse_clicked(self) -> None: """Open a file dialog to select a file.""" path, _ = QFileDialog.getOpenFileName( @@ -164,4 +186,25 @@ def _on_browse_clicked(self) -> None: # using insert so we leave the empty string at the end self.cfg_combo.insertItem(0, path) self.cfg_combo.setCurrentText(path) - self.add_path_to_json(path) + self._add_path_to_json(path) + + def _add_path_to_json(self, path: Path | str) -> None: + """Uopdate the json file with the new path.""" + if isinstance(path, Path): + path = str(path) + + # Read the existing data + try: + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + except json.JSONDecodeError: + data = {"paths": []} + + # Append the new path. using insert so we leave the empty string at the end + paths = cast(list, data.get("paths", [])) + if path not in paths: + paths.insert(0, path) + + # Write the data back to the file + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": paths}, f) diff --git a/src/napari_micromanager/_gui_objects/_toolbar.py b/src/napari_micromanager/_gui_objects/_toolbar.py index dd551a43..dec1a08f 100644 --- a/src/napari_micromanager/_gui_objects/_toolbar.py +++ b/src/napari_micromanager/_gui_objects/_toolbar.py @@ -9,7 +9,6 @@ CameraRoiWidget, ChannelGroupWidget, ChannelWidget, - ConfigurationWidget, DefaultCameraExposureWidget, GroupPresetTableWidget, LiveButton, @@ -103,14 +102,13 @@ def __init__(self, viewer: napari.viewer.Viewer) -> None: self._dock_widgets: dict[str, QDockWidget] = {} # add toolbar items toolbar_items = [ - ConfigToolBar(self), - ChannelsToolBar(self), ObjectivesToolBar(self), - None, - ShuttersToolBar(self), - SnapLiveToolBar(self), + ChannelsToolBar(self), ExposureToolBar(self), + SnapLiveToolBar(self), ToolsToolBar(self), + None, + ShuttersToolBar(self), ] for item in toolbar_items: if item: @@ -241,13 +239,6 @@ def addSubWidget(self, wdg: QWidget) -> None: cast("QHBoxLayout", self.frame.layout()).addWidget(wdg) -class ConfigToolBar(MMToolBar): - def __init__(self, parent: QWidget) -> None: - super().__init__("Configuration", parent) - self.addSubWidget(ConfigurationWidget()) - self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - - class ObjectivesToolBar(MMToolBar): def __init__(self, parent: QWidget) -> None: super().__init__("Objectives", parent=parent) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 67c7af8d..f748a61c 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -4,23 +4,20 @@ import contextlib import logging from typing import TYPE_CHECKING, Any, Callable -from warnings import warn import napari import napari.layers import napari.viewer from pymmcore_plus import CMMCorePlus -from pymmcore_widgets import ConfigWizard from ._core_link import CoreViewerLink -from ._gui_objects._startup_widget import NEW, ConfigurationsHandler +from ._gui_objects._startup_configurations_widget import ConfigurationsHandler from ._gui_objects._toolbar import MicroManagerToolbar if TYPE_CHECKING: from pathlib import Path from pymmcore_plus.core.events._protocol import PSignalInstance - from qtpy.QtWidgets import QDialog # this is very verbose @@ -58,49 +55,9 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - self.configs_handler = ConfigurationsHandler(self.viewer.window._qt_window) - - # if a config is passed, load it - self._handle_system_configuration(config) - - def _handle_system_configuration(self, config: str | Path | None) -> None: - """Handle the system configuration file. If None, show the startup dialog.""" - if config is not None: - self._load_system_configuration(config) - # add the path to the json file - self.configs_handler.add_path_to_json(config) - return - - # if no config is passed, show the startup dialog - self._center_dialog_in_viewer(self.configs_handler) - if self.configs_handler.exec_(): - config = self.configs_handler.value() - # if the user selected NEW, show the config wizard - if config == NEW: - # TODO: subclass to load the new cfg if created and to add it to the - # json file. instead of show() should use exec_() and check the return - self._cfg_wizard = ConfigWizard(parent=self.viewer.window._qt_window) - self._cfg_wizard.show() - else: - self._load_system_configuration(config) - - def _load_system_configuration(self, config: str | Path) -> None: - """Load a Micro-Manager system configuration file.""" - try: - self._mmc.loadSystemConfiguration(config) - except FileNotFoundError: - # don't crash if the user passed an invalid config - warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) - - def _center_dialog_in_viewer(self, startup: QDialog) -> None: - """Center the dialog in the viewer window.""" - startup.move( - self.viewer.window.qt_viewer.geometry().center() - - startup.geometry().center() - ) - startup.resize( - int(self.viewer.window.qt_viewer.geometry().width() / 2), - startup.sizeHint().height(), + # handle the system configurations at startup + self._configs_handler = ConfigurationsHandler( + self.viewer.window._qt_window, config=config, mmcore=self._mmc ) def _cleanup(self) -> None: From 5bd034005b6f7d0e99e1585acdca4e41f9d7bd3c Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 21:18:13 -0500 Subject: [PATCH 10/36] fix: rename --- .../_gui_objects/_startup_configurations_widget.py | 2 +- src/napari_micromanager/main_window.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 1b3156ea..51f7476d 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -26,7 +26,7 @@ NEW = "New Configuration" -class ConfigurationsHandler(QDialog): +class StartupConfigurations(QDialog): """A dialog to select the MicroManager configuration files.""" def __init__( diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index f748a61c..5bb6cde0 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -11,7 +11,7 @@ from pymmcore_plus import CMMCorePlus from ._core_link import CoreViewerLink -from ._gui_objects._startup_configurations_widget import ConfigurationsHandler +from ._gui_objects._startup_configurations_widget import StartupConfigurations from ._gui_objects._toolbar import MicroManagerToolbar if TYPE_CHECKING: @@ -56,8 +56,8 @@ def __init__( atexit.register(self._cleanup) # handle the system configurations at startup - self._configs_handler = ConfigurationsHandler( - self.viewer.window._qt_window, config=config, mmcore=self._mmc + self._startup_configs = StartupConfigurations( + parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc ) def _cleanup(self) -> None: From 4743ef97bcfd3534f5c03f29fcb60e7e2fa0ffa1 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 21:51:18 -0500 Subject: [PATCH 11/36] feat: subclass ConfigWizard --- .../_startup_configurations_widget.py | 107 ++++++++++++------ 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 51f7476d..2f0d6042 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -8,6 +8,7 @@ from platformdirs import user_config_dir from pymmcore_plus import CMMCorePlus, find_micromanager from pymmcore_widgets import ConfigWizard +from pymmcore_widgets.hcwizard.finish_page import DEST_CONFIG from qtpy.QtWidgets import ( QComboBox, QDialog, @@ -23,11 +24,48 @@ USER_DIR = Path(user_config_dir("napari_micromanager")) USER_CONFIGS_PATHS = USER_DIR / "system_configurations.json" FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) -NEW = "New Configuration" +NEW = "New Hardware Configuration" + + +def _add_path_to_json(path: Path | str) -> None: + """Uopdate the json file with the new path.""" + if isinstance(path, Path): + path = str(path) + + # create USER_CONFIGS_PATHS if it doesn't exist + if not USER_CONFIGS_PATHS.exists(): + USER_DIR.mkdir(parents=True, exist_ok=True) + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": []}, f) + + # Read the existing data + try: + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + except json.JSONDecodeError: + data = {"paths": []} + + # Append the new path. using insert so we leave the empty string at the end + paths = cast(list, data.get("paths", [])) + if path not in paths: + paths.insert(0, path) + + # Write the data back to the file + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": paths}, f) + + +def _load_system_configuration(mmcore: CMMCorePlus, config: str | Path) -> None: + """Load a Micro-Manager system configuration file.""" + try: + mmcore.loadSystemConfiguration(config) + except FileNotFoundError: + # don't crash if the user passed an invalid config + warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) class StartupConfigurations(QDialog): - """A dialog to select the MicroManager configuration files.""" + """A dialog to select the Micro-Manager Hardware configuration files at startup.""" def __init__( self, @@ -37,7 +75,7 @@ def __init__( mmcore: CMMCorePlus | None = None, ) -> None: super().__init__(parent) - self.setWindowTitle("Micro-Manager System Configurations") + self.setWindowTitle("Micro-Manager Hardware System Configurations") self._mmc = mmcore or CMMCorePlus.instance() self._config = config @@ -75,7 +113,7 @@ def __init__( self._initialize() - self.resize(500, self.minimumSizeHint().height()) + self.resize(600, self.minimumSizeHint().height()) # if a config was not passed, show the dialog if config is None: @@ -83,16 +121,14 @@ def __init__( config = self.cfg_combo.currentText() # if the user selected NEW, show the config wizard if config == NEW: - # TODO: subclass to load the new cfg if created and to add it to the - # json file. use if exec_() instead of show() and check the return - self._cfg_wizard = ConfigWizard(parent=self) + self._cfg_wizard = HardwareConfigWizard(parent=self) self._cfg_wizard.show() # otherwise load the selected config else: - self._load_system_configuration(config) + _load_system_configuration(self._mmc, config) # if a config was passed, load it else: - self._load_system_configuration(config) + _load_system_configuration(self._mmc, config) def _initialize(self) -> None: """Initialize the dialog with the configuration files. @@ -169,14 +205,6 @@ def _get_micromanager_cfg_files(self) -> list[Path]: return cfg_files - def _load_system_configuration(self, config: str | Path) -> None: - """Load a Micro-Manager system configuration file.""" - try: - self._mmc.loadSystemConfiguration(config) - except FileNotFoundError: - # don't crash if the user passed an invalid config - warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) - def _on_browse_clicked(self) -> None: """Open a file dialog to select a file.""" path, _ = QFileDialog.getOpenFileName( @@ -186,25 +214,34 @@ def _on_browse_clicked(self) -> None: # using insert so we leave the empty string at the end self.cfg_combo.insertItem(0, path) self.cfg_combo.setCurrentText(path) - self._add_path_to_json(path) + _add_path_to_json(path) - def _add_path_to_json(self, path: Path | str) -> None: - """Uopdate the json file with the new path.""" - if isinstance(path, Path): - path = str(path) - # Read the existing data - try: - with open(USER_CONFIGS_PATHS) as f: - data = json.load(f) - except json.JSONDecodeError: - data = {"paths": []} +class HardwareConfigWizard(ConfigWizard): + """A wizard to create a new Micro-Manager hardware configuration file. - # Append the new path. using insert so we leave the empty string at the end - paths = cast(list, data.get("paths", [])) - if path not in paths: - paths.insert(0, path) + Subclassing to load the newly created configuration file and to add it to the + USER_CONFIGS_PATHS json file. + """ - # Write the data back to the file - with open(USER_CONFIGS_PATHS, "w") as f: - json.dump({"paths": paths}, f) + def __init__( + self, + config_file: str = "", + core: CMMCorePlus | None = None, + parent: QWidget | None = None, + ): + super().__init__(config_file, core, parent) + + self.setWindowTitle("Micro-Manager Hardware Configuration Wizard") + + def accept(self) -> None: + """Accept the wizard and save the configuration to a file. + + Overriding to add the new configuration file to the USER_CONFIGS_PATHS json file + and to load it. + """ + dest = self.field(DEST_CONFIG) + dest_path = Path(dest) + _add_path_to_json(dest_path) + super().accept() + _load_system_configuration(self._core, dest_path) From 9ba4e61eb393b16b77dec4045b01e6510ba07900 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 21:55:30 -0500 Subject: [PATCH 12/36] fix: accept --- .../_gui_objects/_startup_configurations_widget.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 2f0d6042..35f9b2e8 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -241,7 +241,6 @@ def accept(self) -> None: and to load it. """ dest = self.field(DEST_CONFIG) - dest_path = Path(dest) - _add_path_to_json(dest_path) + _add_path_to_json(dest) super().accept() - _load_system_configuration(self._core, dest_path) + _load_system_configuration(self._core, dest) From b47fd5e9cc3e81379e1132d8395ce37b05124ab3 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 22:01:55 -0500 Subject: [PATCH 13/36] fix: update _get_micromanager_cfg_files --- .../_gui_objects/_startup_configurations_widget.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 35f9b2e8..23a29c70 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -194,14 +194,11 @@ def _get_config_paths(self) -> list[str]: return paths def _get_micromanager_cfg_files(self) -> list[Path]: - """Return all the .cfg files in the MicroManager folders.""" - mm_dir = find_micromanager() - - if mm_dir is None: - return [] - + """Return all the .cfg files from all the MicroManager folders.""" + mm: list = find_micromanager(False) cfg_files: list[Path] = [] - cfg_files.extend(Path(mm_dir).glob("*.cfg")) + for mm_dir in mm: + cfg_files.extend(Path(mm_dir).glob("*.cfg")) return cfg_files From 891bc0c8ce407efdf86f30a313905f6bdf8894d0 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 22:03:31 -0500 Subject: [PATCH 14/36] fix: move resize into _initialize --- .../_gui_objects/_startup_configurations_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 23a29c70..1bb45ae2 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -113,8 +113,6 @@ def __init__( self._initialize() - self.resize(600, self.minimumSizeHint().height()) - # if a config was not passed, show the dialog if config is None: if self.exec_(): @@ -154,6 +152,8 @@ def _initialize(self) -> None: with open(USER_CONFIGS_PATHS, "w") as f: json.dump({"paths": configs_paths}, f) + self.resize(600, self.minimumSizeHint().height()) + def _get_config_paths(self) -> list[str]: """Return the paths from the json file. From 04de0fa8b1b46f2027f8c3fcc842c5e99f7add73 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 22:48:27 -0500 Subject: [PATCH 15/36] fix: subclass GroupPresetTableWidget --- .../_startup_configurations_widget.py | 57 ++++--------------- .../_gui_objects/_toolbar.py | 39 ++++++++++++- src/napari_micromanager/_util.py | 52 ++++++++++++++++- 3 files changed, 99 insertions(+), 49 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 1bb45ae2..e30c25dd 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -5,7 +5,6 @@ from typing import cast from warnings import warn -from platformdirs import user_config_dir from pymmcore_plus import CMMCorePlus, find_micromanager from pymmcore_widgets import ConfigWizard from pymmcore_widgets.hcwizard.finish_page import DEST_CONFIG @@ -21,49 +20,17 @@ QWidget, ) -USER_DIR = Path(user_config_dir("napari_micromanager")) -USER_CONFIGS_PATHS = USER_DIR / "system_configurations.json" +from napari_micromanager._util import ( + USER_CONFIGS_PATHS, + USER_DIR, + add_path_to_config_json, + load_system_configuration, +) + FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) NEW = "New Hardware Configuration" -def _add_path_to_json(path: Path | str) -> None: - """Uopdate the json file with the new path.""" - if isinstance(path, Path): - path = str(path) - - # create USER_CONFIGS_PATHS if it doesn't exist - if not USER_CONFIGS_PATHS.exists(): - USER_DIR.mkdir(parents=True, exist_ok=True) - with open(USER_CONFIGS_PATHS, "w") as f: - json.dump({"paths": []}, f) - - # Read the existing data - try: - with open(USER_CONFIGS_PATHS) as f: - data = json.load(f) - except json.JSONDecodeError: - data = {"paths": []} - - # Append the new path. using insert so we leave the empty string at the end - paths = cast(list, data.get("paths", [])) - if path not in paths: - paths.insert(0, path) - - # Write the data back to the file - with open(USER_CONFIGS_PATHS, "w") as f: - json.dump({"paths": paths}, f) - - -def _load_system_configuration(mmcore: CMMCorePlus, config: str | Path) -> None: - """Load a Micro-Manager system configuration file.""" - try: - mmcore.loadSystemConfiguration(config) - except FileNotFoundError: - # don't crash if the user passed an invalid config - warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) - - class StartupConfigurations(QDialog): """A dialog to select the Micro-Manager Hardware configuration files at startup.""" @@ -123,10 +90,10 @@ def __init__( self._cfg_wizard.show() # otherwise load the selected config else: - _load_system_configuration(self._mmc, config) + load_system_configuration(self._mmc, config) # if a config was passed, load it else: - _load_system_configuration(self._mmc, config) + load_system_configuration(self._mmc, config) def _initialize(self) -> None: """Initialize the dialog with the configuration files. @@ -211,7 +178,7 @@ def _on_browse_clicked(self) -> None: # using insert so we leave the empty string at the end self.cfg_combo.insertItem(0, path) self.cfg_combo.setCurrentText(path) - _add_path_to_json(path) + add_path_to_config_json(path) class HardwareConfigWizard(ConfigWizard): @@ -238,6 +205,6 @@ def accept(self) -> None: and to load it. """ dest = self.field(DEST_CONFIG) - _add_path_to_json(dest) + add_path_to_config_json(dest) super().accept() - _load_system_configuration(self._core, dest) + load_system_configuration(self._core, dest) diff --git a/src/napari_micromanager/_gui_objects/_toolbar.py b/src/napari_micromanager/_gui_objects/_toolbar.py index dec1a08f..ea056ef3 100644 --- a/src/napari_micromanager/_gui_objects/_toolbar.py +++ b/src/napari_micromanager/_gui_objects/_toolbar.py @@ -17,6 +17,11 @@ SnapButton, ) +from napari_micromanager._util import ( + add_path_to_config_json, + load_system_configuration, +) + try: # this was renamed from pymmcore_widgets import ObjectivesPixelConfigurationWidget @@ -26,6 +31,7 @@ from qtpy.QtCore import QEvent, QObject, QSize, Qt from qtpy.QtWidgets import ( QDockWidget, + QFileDialog, QFrame, QHBoxLayout, QLabel, @@ -50,10 +56,41 @@ TOOL_SIZE = 35 +class GroupsAndPresets(GroupPresetTableWidget): + """Subclass of GroupPresetTableWidget. + + Overwrite the save and load methods to store the saced or loaded configuration in + the USER_CONFIGS_PATHS json config file. + """ + + def __init__( + self, *, parent: QWidget | None = None, mmcore: CMMCorePlus | None = None + ) -> None: + super().__init__(parent=parent, mmcore=mmcore) + + def _save_cfg(self) -> None: + (filename, _) = QFileDialog.getSaveFileName( + self, "Save Micro-Manager Configuration." + ) + if filename: + filename = filename if str(filename).endswith(".cfg") else f"{filename}.cfg" + self._mmc.saveSystemConfiguration(filename) + add_path_to_config_json(filename) + + def _load_cfg(self) -> None: + """Open file dialog to select a config file.""" + (filename, _) = QFileDialog.getOpenFileName( + self, "Select a Micro-Manager configuration file", "", "cfg(*.cfg)" + ) + if filename: + add_path_to_config_json(filename) + load_system_configuration(mmcore=self._mmc, config=filename) + + # Dict for QObject and its QPushButton icon DOCK_WIDGETS: Dict[str, Tuple[type[QWidget], str | None]] = { # noqa: U006 "Device Property Browser": (PropertyBrowser, MDI6.table_large), - "Groups and Presets Table": (GroupPresetTableWidget, MDI6.table_large_plus), + "Groups and Presets Table": (GroupsAndPresets, MDI6.table_large_plus), "Illumination Control": (IlluminationWidget, MDI6.lightbulb_on), "Stages Control": (MMStagesWidget, MDI6.arrow_all), "Camera ROI": (CameraRoiWidget, MDI6.crop), diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index 686459f2..5da8d92a 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -1,11 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, cast +from warnings import warn -if TYPE_CHECKING: - from pathlib import Path +from platformdirs import user_config_dir +if TYPE_CHECKING: import useq + from pymmcore_plus import CMMCorePlus + +USER_DIR = Path(user_config_dir("napari_micromanager")) +USER_CONFIGS_PATHS = USER_DIR / "system_configurations.json" # key in MDASequence.metadata to store napari-micromanager metadata # note that this is also used in napari layer metadata @@ -68,3 +74,43 @@ def ensure_unique(path: Path, extension: str = ".tif", ndigits: int = 3) -> Path # build new path name number = f"_{current_max+1:0{ndigits}d}" return path.parent / f"{stem}{number}{extension}" + + +def add_path_to_config_json(path: Path | str) -> None: + """Uopdate the st=ystem configurations json file with the new path.""" + import json + + if isinstance(path, Path): + path = str(path) + + # create USER_CONFIGS_PATHS if it doesn't exist + if not USER_CONFIGS_PATHS.exists(): + USER_DIR.mkdir(parents=True, exist_ok=True) + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": []}, f) + + # Read the existing data + try: + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + except json.JSONDecodeError: + data = {"paths": []} + + # Append the new path. using insert so we leave the empty string at the end + paths = cast(list, data.get("paths", [])) + if path not in paths: + paths.insert(0, path) + + # Write the data back to the file + with open(USER_CONFIGS_PATHS, "w") as f: + json.dump({"paths": paths}, f) + + +def load_system_configuration(mmcore: CMMCorePlus, config: str | Path) -> None: + """Load a Micro-Manager system configuration file.""" + try: + mmcore.unloadAllDevices() + mmcore.loadSystemConfiguration(config) + except FileNotFoundError: + # don't crash if the user passed an invalid config + warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) From 59a378460b92ce95fdcedd4a76ec559946e815cd Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 22:54:21 -0500 Subject: [PATCH 16/36] fix: docstring --- .../_gui_objects/_startup_configurations_widget.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index e30c25dd..35a70ff4 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -98,10 +98,10 @@ def __init__( def _initialize(self) -> None: """Initialize the dialog with the configuration files. - This method reads the stored paths in the USER_CONFIGS_PATHS jason file (or - create one if it doesn't exist) and adds them to the combo box. It also adds the - Micro-Manager configuration files form the Micro-Manager folder if they are not - already in the list. + This method reads the paths in the USER_CONFIGS_PATHS json file (it creates + one if it doesn't exist) and adds them to the combo box. It also adds the + Micro-Manager configuration files form all the Micro-Manager folder if they are + not already in the list. """ # create USER_CONFIGS_PATHS if it doesn't exist if not USER_CONFIGS_PATHS.exists(): From 986fc2892244dfbab966007ab5f068aae122c3a1 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 6 Mar 2024 22:56:22 -0500 Subject: [PATCH 17/36] fix: todo --- .../_gui_objects/_startup_configurations_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index 35a70ff4..e2c38618 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -103,6 +103,8 @@ def _initialize(self) -> None: Micro-Manager configuration files form all the Micro-Manager folder if they are not already in the list. """ + # TODO: move this method to main_window.py and leave here only the combo update + # create USER_CONFIGS_PATHS if it doesn't exist if not USER_CONFIGS_PATHS.exists(): USER_DIR.mkdir(parents=True, exist_ok=True) From 302e77a92ec266a18ac1ff64a8f9331143e4c103 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 10:04:10 -0500 Subject: [PATCH 18/36] fix: docstrings and comments --- .../_startup_configurations_widget.py | 27 +++++++++++-------- src/napari_micromanager/main_window.py | 4 ++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py index e2c38618..7ce5c088 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py @@ -96,15 +96,13 @@ def __init__( load_system_configuration(self._mmc, config) def _initialize(self) -> None: - """Initialize the dialog with the configuration files. + """Initialize the dialog with the Micro-Manager configuration files. - This method reads the paths in the USER_CONFIGS_PATHS json file (it creates - one if it doesn't exist) and adds them to the combo box. It also adds the - Micro-Manager configuration files form all the Micro-Manager folder if they are - not already in the list. + This method is called everytime the widget is created (and so when + napari-micromanage is loaded) and it updates (or create if does not yet exists) + the list of Micro-Manager configurations paths saved in the USER_CONFIGS_PATHS + json file. """ - # TODO: move this method to main_window.py and leave here only the combo update - # create USER_CONFIGS_PATHS if it doesn't exist if not USER_CONFIGS_PATHS.exists(): USER_DIR.mkdir(parents=True, exist_ok=True) @@ -114,13 +112,14 @@ def _initialize(self) -> None: # get the paths from the json file configs_paths = self._get_config_paths() - # add the paths to the combo box - self.cfg_combo.addItems([*configs_paths, NEW]) - # write the data back to the file with open(USER_CONFIGS_PATHS, "w") as f: json.dump({"paths": configs_paths}, f) + # add the paths to the combo box + self.cfg_combo.addItems([*configs_paths, NEW]) + + # resize the widget so its width is not too small self.resize(600, self.minimumSizeHint().height()) def _get_config_paths(self) -> list[str]: @@ -172,7 +171,11 @@ def _get_micromanager_cfg_files(self) -> list[Path]: return cfg_files def _on_browse_clicked(self) -> None: - """Open a file dialog to select a file.""" + """Open a file dialog to select a file. + + If a file path is provided, it is added to the USER_CONFIGS_PATHS json file and + to the combo box. + """ path, _ = QFileDialog.getOpenFileName( self, "Open file", "", "MicroManager files (*.cfg)" ) @@ -180,6 +183,7 @@ def _on_browse_clicked(self) -> None: # using insert so we leave the empty string at the end self.cfg_combo.insertItem(0, path) self.cfg_combo.setCurrentText(path) + # add the path to the USER_CONFIGS_PATHS list add_path_to_config_json(path) @@ -207,6 +211,7 @@ def accept(self) -> None: and to load it. """ dest = self.field(DEST_CONFIG) + # add the path to the USER_CONFIGS_PATHS list add_path_to_config_json(dest) super().accept() load_system_configuration(self._core, dest) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 5bb6cde0..f448a121 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -55,7 +55,9 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - # handle the system configurations at startup + # handle the system configurations at startup. with this we also create/update + # the list of the Micro-Manager system configurations files path stored a s a + # json file in the user's configuration file directory (USER_CONFIGS_PATHS) self._startup_configs = StartupConfigurations( parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc ) From b2b6b94e6989188bf4b8a9068a703c21abe718e8 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 10:46:38 -0500 Subject: [PATCH 19/36] feat: nove login into InitializeSystemConfigurations object --- ...ions_widget.py => _init_system_configs.py} | 160 ++++++++++-------- src/napari_micromanager/_util.py | 6 +- src/napari_micromanager/main_window.py | 12 +- 3 files changed, 104 insertions(+), 74 deletions(-) rename src/napari_micromanager/{_gui_objects/_startup_configurations_widget.py => _init_system_configs.py} (78%) diff --git a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py b/src/napari_micromanager/_init_system_configs.py similarity index 78% rename from src/napari_micromanager/_gui_objects/_startup_configurations_widget.py rename to src/napari_micromanager/_init_system_configs.py index 7ce5c088..74d364d0 100644 --- a/src/napari_micromanager/_gui_objects/_startup_configurations_widget.py +++ b/src/napari_micromanager/_init_system_configs.py @@ -8,6 +8,7 @@ from pymmcore_plus import CMMCorePlus, find_micromanager from pymmcore_widgets import ConfigWizard from pymmcore_widgets.hcwizard.finish_page import DEST_CONFIG +from qtpy.QtCore import QObject from qtpy.QtWidgets import ( QComboBox, QDialog, @@ -31,77 +32,34 @@ NEW = "New Hardware Configuration" -class StartupConfigurations(QDialog): - """A dialog to select the Micro-Manager Hardware configuration files at startup.""" - +class InitializeSystemConfigurations(QObject): def __init__( self, - parent: QWidget | None = None, - *, + parent: QObject | None = None, config: Path | str | None = None, mmcore: CMMCorePlus | None = None, ) -> None: super().__init__(parent) - self.setWindowTitle("Micro-Manager Hardware System Configurations") self._mmc = mmcore or CMMCorePlus.instance() - self._config = config - - # label - cfg_lbl = QLabel("Configuration file:") - cfg_lbl.setSizePolicy(FIXED) - - # combo box - self.cfg_combo = QComboBox() - # `AdjustToMinimumContents` is not available in all qtpy backends so using - # `AdjustToMinimumContentsLengthWithIcon` instead - self.cfg_combo.setSizeAdjustPolicy( - QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon - ) - - # browse button - self.browse_btn = QPushButton("...") - self.browse_btn.setSizePolicy(FIXED) - self.browse_btn.clicked.connect(self._on_browse_clicked) - - # Create OK and Cancel buttons - button_box = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - ) - button_box.accepted.connect(self.accept) - button_box.rejected.connect(self.reject) - - # add widgets to layout - wdg_layout = QGridLayout(self) - wdg_layout.addWidget(cfg_lbl, 0, 0) - wdg_layout.addWidget(self.cfg_combo, 0, 1) - wdg_layout.addWidget(self.browse_btn, 0, 2) - wdg_layout.addWidget(button_box, 2, 0, 1, 3) self._initialize() - # if a config was not passed, show the dialog - if config is None: - if self.exec_(): - config = self.cfg_combo.currentText() - # if the user selected NEW, show the config wizard - if config == NEW: - self._cfg_wizard = HardwareConfigWizard(parent=self) - self._cfg_wizard.show() - # otherwise load the selected config - else: - load_system_configuration(self._mmc, config) - # if a config was passed, load it - else: + if config is not None: + add_path_to_config_json(config) load_system_configuration(self._mmc, config) + else: + self._startup_dialog = StartupConfigurationsDialog( + parent=self.parent(), config=config, mmcore=self._mmc + ) + self._startup_dialog.show() def _initialize(self) -> None: - """Initialize the dialog with the Micro-Manager configuration files. + """Create or update the list of Micro-Manager hardware configurations paths. - This method is called everytime the widget is created (and so when - napari-micromanage is loaded) and it updates (or create if does not yet exists) - the list of Micro-Manager configurations paths saved in the USER_CONFIGS_PATHS - json file. + This method is called everytime napari-micromanager is loaded and it updates (or + create if does not yet exists) the list of Micro-Manager configurations paths + saved in the USER_CONFIGS_PATHS as a json file. """ # create USER_CONFIGS_PATHS if it doesn't exist if not USER_CONFIGS_PATHS.exists(): @@ -116,12 +74,6 @@ def _initialize(self) -> None: with open(USER_CONFIGS_PATHS, "w") as f: json.dump({"paths": configs_paths}, f) - # add the paths to the combo box - self.cfg_combo.addItems([*configs_paths, NEW]) - - # resize the widget so its width is not too small - self.resize(600, self.minimumSizeHint().height()) - def _get_config_paths(self) -> list[str]: """Return the paths from the json file. @@ -151,10 +103,6 @@ def _get_config_paths(self) -> list[str]: # using insert so we leave the empty string at the end paths.insert(0, str(cfg)) - # if a config was passed, add it to the list if it's not already there - if self._config is not None and str(self._config) not in paths: - paths.insert(0, str(self._config)) - except json.JSONDecodeError: paths = [] warn("Error reading the json file.", stacklevel=2) @@ -170,6 +118,86 @@ def _get_micromanager_cfg_files(self) -> list[Path]: return cfg_files + +class StartupConfigurationsDialog(QDialog): + """A dialog to select the Micro-Manager Hardware configuration files at startup.""" + + def __init__( + self, + parent: QWidget | None = None, + *, + config: Path | str | None = None, + mmcore: CMMCorePlus | None = None, + ) -> None: + super().__init__(parent) + self.setWindowTitle("Micro-Manager Hardware System Configurations") + + self._mmc = mmcore or CMMCorePlus.instance() + self._config = config + + # label + cfg_lbl = QLabel("Configuration file:") + cfg_lbl.setSizePolicy(FIXED) + + # combo box + self.cfg_combo = QComboBox() + # `AdjustToMinimumContents` is not available in all qtpy backends so using + # `AdjustToMinimumContentsLengthWithIcon` instead + self.cfg_combo.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon + ) + + # browse button + self.browse_btn = QPushButton("...") + self.browse_btn.setSizePolicy(FIXED) + self.browse_btn.clicked.connect(self._on_browse_clicked) + + # Create OK and Cancel buttons + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + # add widgets to layout + wdg_layout = QGridLayout(self) + wdg_layout.addWidget(cfg_lbl, 0, 0) + wdg_layout.addWidget(self.cfg_combo, 0, 1) + wdg_layout.addWidget(self.browse_btn, 0, 2) + wdg_layout.addWidget(button_box, 2, 0, 1, 3) + + self._initialize() + + def accept(self) -> None: + super().accept() + config = self.cfg_combo.currentText() + # if the user selected NEW, show the config wizard + if config == NEW: + self._cfg_wizard = HardwareConfigWizard(parent=self) + self._cfg_wizard.show() + else: + load_system_configuration(self._mmc, config) + + def _initialize(self) -> None: + """Initialize the dialog with the Micro-Manager configuration files.""" + # return if the json file doesn't exist + if not USER_CONFIGS_PATHS.exists(): + return + + # Read the existing data + try: + with open(USER_CONFIGS_PATHS) as f: + configs_paths = json.load(f) + except json.JSONDecodeError: + configs_paths = {"paths": []} + + configs_paths = cast(list, configs_paths.get("paths", [])) + # add the paths to the combo box + self.cfg_combo.addItems([*configs_paths, NEW]) + + # resize the widget so its width is not too small + self.resize(600, self.minimumSizeHint().height()) + def _on_browse_clicked(self) -> None: """Open a file dialog to select a file. diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index 5da8d92a..d7ed9e05 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -92,12 +92,12 @@ def add_path_to_config_json(path: Path | str) -> None: # Read the existing data try: with open(USER_CONFIGS_PATHS) as f: - data = json.load(f) + configs_paths = json.load(f) except json.JSONDecodeError: - data = {"paths": []} + configs_paths = {"paths": []} # Append the new path. using insert so we leave the empty string at the end - paths = cast(list, data.get("paths", [])) + paths = cast(list, configs_paths.get("paths", [])) if path not in paths: paths.insert(0, path) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index f448a121..e5c2babe 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -11,8 +11,8 @@ from pymmcore_plus import CMMCorePlus from ._core_link import CoreViewerLink -from ._gui_objects._startup_configurations_widget import StartupConfigurations from ._gui_objects._toolbar import MicroManagerToolbar +from ._init_system_configs import InitializeSystemConfigurations if TYPE_CHECKING: from pathlib import Path @@ -55,10 +55,12 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - # handle the system configurations at startup. with this we also create/update - # the list of the Micro-Manager system configurations files path stored a s a - # json file in the user's configuration file directory (USER_CONFIGS_PATHS) - self._startup_configs = StartupConfigurations( + # handle the system configurations at startup. with this we create/update the + # list of the Micro-Manager hardware system configurations files path stored as + # a json file in the user's configuration file directory (USER_CONFIGS_PATHS). + # a dialog will be also displayed if no system configuration file is provided + # to select one from the list of available ones or to create a new one + self._init_cfg = InitializeSystemConfigurations( parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc ) From 0772d140da9b18851002f61042018dbdf15e1f26 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 10:48:06 -0500 Subject: [PATCH 20/36] fix: comment --- src/napari_micromanager/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index e5c2babe..37968d1e 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -59,7 +59,7 @@ def __init__( # list of the Micro-Manager hardware system configurations files path stored as # a json file in the user's configuration file directory (USER_CONFIGS_PATHS). # a dialog will be also displayed if no system configuration file is provided - # to select one from the list of available ones or to create a new one + # to either select one from the list of available ones or to create a new one. self._init_cfg = InitializeSystemConfigurations( parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc ) From 4cefd8d8eca87cc8583d23a2e52b0291b4c85b5f Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 13:51:24 -0500 Subject: [PATCH 21/36] feat: add menue + update save and load methods --- .../_gui_objects/_toolbar.py | 21 ++------ .../_init_system_configs.py | 16 +++--- src/napari_micromanager/_util.py | 44 ++++++++++----- src/napari_micromanager/main_window.py | 54 ++++++++++++++++++- 4 files changed, 96 insertions(+), 39 deletions(-) diff --git a/src/napari_micromanager/_gui_objects/_toolbar.py b/src/napari_micromanager/_gui_objects/_toolbar.py index ea056ef3..bc62face 100644 --- a/src/napari_micromanager/_gui_objects/_toolbar.py +++ b/src/napari_micromanager/_gui_objects/_toolbar.py @@ -18,8 +18,8 @@ ) from napari_micromanager._util import ( - add_path_to_config_json, - load_system_configuration, + load_sys_config_dialog, + save_sys_config_dialog, ) try: @@ -31,7 +31,6 @@ from qtpy.QtCore import QEvent, QObject, QSize, Qt from qtpy.QtWidgets import ( QDockWidget, - QFileDialog, QFrame, QHBoxLayout, QLabel, @@ -69,22 +68,12 @@ def __init__( super().__init__(parent=parent, mmcore=mmcore) def _save_cfg(self) -> None: - (filename, _) = QFileDialog.getSaveFileName( - self, "Save Micro-Manager Configuration." - ) - if filename: - filename = filename if str(filename).endswith(".cfg") else f"{filename}.cfg" - self._mmc.saveSystemConfiguration(filename) - add_path_to_config_json(filename) + """Open file dialog to save the current configuration.""" + save_sys_config_dialog(parent=self, mmcore=self._mmc) def _load_cfg(self) -> None: """Open file dialog to select a config file.""" - (filename, _) = QFileDialog.getOpenFileName( - self, "Select a Micro-Manager configuration file", "", "cfg(*.cfg)" - ) - if filename: - add_path_to_config_json(filename) - load_system_configuration(mmcore=self._mmc, config=filename) + load_sys_config_dialog(parent=self, mmcore=self._mmc) # Dict for QObject and its QPushButton icon diff --git a/src/napari_micromanager/_init_system_configs.py b/src/napari_micromanager/_init_system_configs.py index 74d364d0..d13a96e0 100644 --- a/src/napari_micromanager/_init_system_configs.py +++ b/src/napari_micromanager/_init_system_configs.py @@ -25,7 +25,6 @@ USER_CONFIGS_PATHS, USER_DIR, add_path_to_config_json, - load_system_configuration, ) FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) @@ -46,8 +45,10 @@ def __init__( self._initialize() if config is not None: + # add the config to the system configurations json and set it as the + # current configuration path. add_path_to_config_json(config) - load_system_configuration(self._mmc, config) + self._mmc.loadSystemConfiguration(config) else: self._startup_dialog = StartupConfigurationsDialog( parent=self.parent(), config=config, mmcore=self._mmc @@ -58,7 +59,7 @@ def _initialize(self) -> None: """Create or update the list of Micro-Manager hardware configurations paths. This method is called everytime napari-micromanager is loaded and it updates (or - create if does not yet exists) the list of Micro-Manager configurations paths + create if does not yet exists) the list of Micro-Manager configurations paths saved in the USER_CONFIGS_PATHS as a json file. """ # create USER_CONFIGS_PATHS if it doesn't exist @@ -176,7 +177,7 @@ def accept(self) -> None: self._cfg_wizard = HardwareConfigWizard(parent=self) self._cfg_wizard.show() else: - load_system_configuration(self._mmc, config) + self._mmc.loadSystemConfiguration(config) def _initialize(self) -> None: """Initialize the dialog with the Micro-Manager configuration files.""" @@ -211,7 +212,8 @@ def _on_browse_clicked(self) -> None: # using insert so we leave the empty string at the end self.cfg_combo.insertItem(0, path) self.cfg_combo.setCurrentText(path) - # add the path to the USER_CONFIGS_PATHS list + # add the config to the system configurations json and set it as the + # current configuration path. add_path_to_config_json(path) @@ -238,8 +240,8 @@ def accept(self) -> None: Overriding to add the new configuration file to the USER_CONFIGS_PATHS json file and to load it. """ + super().accept() dest = self.field(DEST_CONFIG) # add the path to the USER_CONFIGS_PATHS list add_path_to_config_json(dest) - super().accept() - load_system_configuration(self._core, dest) + self._core.loadSystemConfiguration(dest) diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index d7ed9e05..7bcd0663 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -2,13 +2,13 @@ from pathlib import Path from typing import TYPE_CHECKING, cast -from warnings import warn from platformdirs import user_config_dir +from pymmcore_plus import CMMCorePlus +from qtpy.QtWidgets import QFileDialog, QWidget if TYPE_CHECKING: import useq - from pymmcore_plus import CMMCorePlus USER_DIR = Path(user_config_dir("napari_micromanager")) USER_CONFIGS_PATHS = USER_DIR / "system_configurations.json" @@ -77,7 +77,7 @@ def ensure_unique(path: Path, extension: str = ".tif", ndigits: int = 3) -> Path def add_path_to_config_json(path: Path | str) -> None: - """Uopdate the st=ystem configurations json file with the new path.""" + """Update the stystem configurations json file with the new path.""" import json if isinstance(path, Path): @@ -92,12 +92,12 @@ def add_path_to_config_json(path: Path | str) -> None: # Read the existing data try: with open(USER_CONFIGS_PATHS) as f: - configs_paths = json.load(f) + data = json.load(f) except json.JSONDecodeError: - configs_paths = {"paths": []} + data = {"paths": []} # Append the new path. using insert so we leave the empty string at the end - paths = cast(list, configs_paths.get("paths", [])) + paths = cast(list, data.get("paths", [])) if path not in paths: paths.insert(0, path) @@ -106,11 +106,27 @@ def add_path_to_config_json(path: Path | str) -> None: json.dump({"paths": paths}, f) -def load_system_configuration(mmcore: CMMCorePlus, config: str | Path) -> None: - """Load a Micro-Manager system configuration file.""" - try: - mmcore.unloadAllDevices() - mmcore.loadSystemConfiguration(config) - except FileNotFoundError: - # don't crash if the user passed an invalid config - warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) +def save_sys_config_dialog( + parent: QWidget | None = None, mmcore: CMMCorePlus | None = None +) -> None: + (filename, _) = QFileDialog.getSaveFileName( + parent, "Save Micro-Manager Configuration." + ) + if filename: + filename = filename if str(filename).endswith(".cfg") else f"{filename}.cfg" + mmcore = mmcore or CMMCorePlus.instance() + mmcore.saveSystemConfiguration(filename) + add_path_to_config_json(filename) + + +def load_sys_config_dialog( + parent: QWidget | None = None, mmcore: CMMCorePlus | None = None +) -> None: + """Open file dialog to select a config file.""" + (filename, _) = QFileDialog.getOpenFileName( + parent, "Select a Micro-Manager configuration file", "", "cfg(*.cfg)" + ) + if filename: + add_path_to_config_json(filename) + mmcore = mmcore or CMMCorePlus.instance() + mmcore.loadSystemConfiguration(filename) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 37968d1e..aa44d907 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -3,16 +3,23 @@ import atexit import contextlib import logging -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast import napari import napari.layers import napari.viewer from pymmcore_plus import CMMCorePlus +from pymmcore_widgets.hcwizard.intro_page import SRC_CONFIG +from qtpy.QtWidgets import QAction, QMenuBar + +from napari_micromanager._util import ( + load_sys_config_dialog, + save_sys_config_dialog, +) from ._core_link import CoreViewerLink from ._gui_objects._toolbar import MicroManagerToolbar -from ._init_system_configs import InitializeSystemConfigurations +from ._init_system_configs import HardwareConfigWizard, InitializeSystemConfigurations if TYPE_CHECKING: from pathlib import Path @@ -33,6 +40,8 @@ def __init__( ) -> None: super().__init__(viewer) + self._add_menu() + # get global CMMCorePlus instance self._mmc = CMMCorePlus.instance() # this object mediates the connection between the viewer and core events @@ -55,6 +64,9 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) + # Micro-Manager HArdware Configuration Wizard + self._wiz = HardwareConfigWizard(parent=self.viewer.window._qt_window) + # handle the system configurations at startup. with this we create/update the # list of the Micro-Manager hardware system configurations files path stored as # a json file in the user's configuration file directory (USER_CONFIGS_PATHS). @@ -77,3 +89,41 @@ def _update_max_min(self, *_: Any) -> None: self.minmax.update_from_layers( lr for lr in visible if isinstance(lr, napari.layers.Image) ) + + def _add_menu(self) -> None: + if (win := getattr(self.viewer.window, "_qt_window", None)) is None: + return + + menubar = cast(QMenuBar, win.menuBar()) + + # main Micro-Manager menu + mm_menu = menubar.addMenu("Micro-Manager") + + # Configurations Sub-Menu + configurations_menu = mm_menu.addMenu("System Configurations") + self.act_save_configuration = QAction("Save Configuration", self) + self.act_save_configuration.triggered.connect(self._save_cfg) + configurations_menu.addAction(self.act_save_configuration) + self.act_load_configuration = QAction("Load Configuration", self) + self.act_load_configuration.triggered.connect(self._load_cfg) + configurations_menu.addAction(self.act_load_configuration) + self.act_cfg_wizard = QAction("Hardware Configuration Wizard", self) + self.act_cfg_wizard.triggered.connect(self._show_config_wizard) + configurations_menu.addAction(self.act_cfg_wizard) + + def _save_cfg(self) -> None: + """Save the current Micro-Manager system configuration.""" + save_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) + + def _load_cfg(self) -> None: + """Load a Micro-Manager system configuration.""" + load_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) + + def _show_config_wizard(self) -> None: + """Show the Micro-Manager Hardware Configuration Wizard.""" + if self._wiz.isVisible(): + self._wiz.raise_() + else: + current_cfg = self._mmc.systemConfigurationFile() or "" + self._wiz.setField(SRC_CONFIG, current_cfg) + self._wiz.show() From 34afce82e0ff9b46bc9e327ddf9d328f70ebf778 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 13:55:27 -0500 Subject: [PATCH 22/36] fix: docstings and comments --- src/napari_micromanager/_util.py | 11 ++++++++++- src/napari_micromanager/main_window.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index 7bcd0663..2b19c110 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -109,6 +109,11 @@ def add_path_to_config_json(path: Path | str) -> None: def save_sys_config_dialog( parent: QWidget | None = None, mmcore: CMMCorePlus | None = None ) -> None: + """Open file dialog to save a config file. + + The file will be also saved in the USER_CONFIGS_PATHS jason file if it doesn't + yet exist. + """ (filename, _) = QFileDialog.getSaveFileName( parent, "Save Micro-Manager Configuration." ) @@ -122,7 +127,11 @@ def save_sys_config_dialog( def load_sys_config_dialog( parent: QWidget | None = None, mmcore: CMMCorePlus | None = None ) -> None: - """Open file dialog to select a config file.""" + """Open file dialog to select a config file. + + The loaded file will be also saved in the USER_CONFIGS_PATHS jason file if it + doesn't yet exist. + """ (filename, _) = QFileDialog.getOpenFileName( parent, "Select a Micro-Manager configuration file", "", "cfg(*.cfg)" ) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index aa44d907..683864f4 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -101,12 +101,15 @@ def _add_menu(self) -> None: # Configurations Sub-Menu configurations_menu = mm_menu.addMenu("System Configurations") + # save cfg self.act_save_configuration = QAction("Save Configuration", self) self.act_save_configuration.triggered.connect(self._save_cfg) configurations_menu.addAction(self.act_save_configuration) + # load cfg self.act_load_configuration = QAction("Load Configuration", self) self.act_load_configuration.triggered.connect(self._load_cfg) configurations_menu.addAction(self.act_load_configuration) + # cfg wizard self.act_cfg_wizard = QAction("Hardware Configuration Wizard", self) self.act_cfg_wizard.triggered.connect(self._show_config_wizard) configurations_menu.addAction(self.act_cfg_wizard) From e390b1a1a73a5321aa4ba0f4f861d3ed127d68e7 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 20:21:49 -0500 Subject: [PATCH 23/36] test: add init test --- .../_init_system_configs.py | 2 + tests/test_configurations.py | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/test_configurations.py diff --git a/src/napari_micromanager/_init_system_configs.py b/src/napari_micromanager/_init_system_configs.py index d13a96e0..79703b64 100644 --- a/src/napari_micromanager/_init_system_configs.py +++ b/src/napari_micromanager/_init_system_configs.py @@ -44,11 +44,13 @@ def __init__( self._initialize() + # if a config is provided, load it if config is not None: # add the config to the system configurations json and set it as the # current configuration path. add_path_to_config_json(config) self._mmc.loadSystemConfiguration(config) + # if no config is provided, show a dialog to select one or to create a new one else: self._startup_dialog = StartupConfigurationsDialog( parent=self.parent(), config=config, mmcore=self._mmc diff --git a/tests/test_configurations.py b/tests/test_configurations.py new file mode 100644 index 00000000..bff5f00b --- /dev/null +++ b/tests/test_configurations.py @@ -0,0 +1,48 @@ +import json +from pathlib import Path + +import pytest +from napari_micromanager._init_system_configs import ( + InitializeSystemConfigurations, +) +from napari_micromanager._util import USER_CONFIGS_PATHS +from pymmcore_plus import CMMCorePlus +from pytestqt.qtbot import QtBot + +DEMO = "MMConfig_demo.cfg" + + +configs = [None, Path(__file__).parent / "test_config.cfg"] + + +@pytest.mark.parametrize("config", configs) +def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None): + init = InitializeSystemConfigurations(mmcore=core, config=config) + + with open(USER_CONFIGS_PATHS) as f: + data = json.load(f) + # the default config should be in the json file + assert DEMO in [Path(path).name for path in data["paths"]] + + if config is None: + assert len(data["paths"]) == 1 + assert init._startup_dialog.isVisible() + combo = init._startup_dialog.cfg_combo + current_cfg = combo.currentText() + assert Path(current_cfg).name == DEMO + assert [Path(combo.itemText(i)).name for i in range(combo.count())] == [ + DEMO, + "New Hardware Configuration", + ] + # simulate click on ok + init._startup_dialog.accept() + + else: + assert len(data["paths"]) == 2 + assert not hasattr(init, "_startup_dialog") + current_cfg = config + assert str(current_cfg) in data["paths"] + + assert core.systemConfigurationFile() == str(current_cfg) + + USER_CONFIGS_PATHS.unlink() From 21e07282b526e7c355fad9663eec5e4fd25315f7 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 20:23:45 -0500 Subject: [PATCH 24/36] test: fix --- tests/test_configurations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index bff5f00b..addc3455 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -10,7 +10,7 @@ from pytestqt.qtbot import QtBot DEMO = "MMConfig_demo.cfg" - +NEW = "New Hardware Configuration" configs = [None, Path(__file__).parent / "test_config.cfg"] @@ -30,9 +30,8 @@ def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None): combo = init._startup_dialog.cfg_combo current_cfg = combo.currentText() assert Path(current_cfg).name == DEMO - assert [Path(combo.itemText(i)).name for i in range(combo.count())] == [ - DEMO, - "New Hardware Configuration", + assert [DEMO, NEW] == [ + Path(combo.itemText(i)).name for i in range(combo.count()) ] # simulate click on ok init._startup_dialog.accept() From 8d5fffa30baa0018424224c0aeeebcd1ae5a68e5 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 20:26:03 -0500 Subject: [PATCH 25/36] test: update --- tests/test_configurations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index addc3455..019f89c5 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -17,6 +17,8 @@ @pytest.mark.parametrize("config", configs) def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None): + assert not USER_CONFIGS_PATHS.exists() + init = InitializeSystemConfigurations(mmcore=core, config=config) with open(USER_CONFIGS_PATHS) as f: From 4aba582de98c972f30d020ed87bc7284e18ad565 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 21:36:48 -0500 Subject: [PATCH 26/36] fix: add option to init the confics --- src/napari_micromanager/_util.py | 9 +++----- src/napari_micromanager/main_window.py | 31 ++++++++++++++++---------- tests/conftest.py | 2 +- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index 2b19c110..dea7a7a5 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -80,15 +80,12 @@ def add_path_to_config_json(path: Path | str) -> None: """Update the stystem configurations json file with the new path.""" import json + if not USER_CONFIGS_PATHS.exists(): + return + if isinstance(path, Path): path = str(path) - # create USER_CONFIGS_PATHS if it doesn't exist - if not USER_CONFIGS_PATHS.exists(): - USER_DIR.mkdir(parents=True, exist_ok=True) - with open(USER_CONFIGS_PATHS, "w") as f: - json.dump({"paths": []}, f) - # Read the existing data try: with open(USER_CONFIGS_PATHS) as f: diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 683864f4..131d226b 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -36,7 +36,11 @@ class MainWindow(MicroManagerToolbar): """The main napari-micromanager widget that gets added to napari.""" def __init__( - self, viewer: napari.viewer.Viewer, config: str | Path | None = None + self, + viewer: napari.viewer.Viewer, + config: str | Path | None = None, + *, + init_configs: bool = True, ) -> None: super().__init__(viewer) @@ -64,17 +68,20 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - # Micro-Manager HArdware Configuration Wizard - self._wiz = HardwareConfigWizard(parent=self.viewer.window._qt_window) - - # handle the system configurations at startup. with this we create/update the - # list of the Micro-Manager hardware system configurations files path stored as - # a json file in the user's configuration file directory (USER_CONFIGS_PATHS). - # a dialog will be also displayed if no system configuration file is provided - # to either select one from the list of available ones or to create a new one. - self._init_cfg = InitializeSystemConfigurations( - parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc - ) + if init_configs: + # Micro-Manager HArdware Configuration Wizard + self._wiz = HardwareConfigWizard(parent=self.viewer.window._qt_window) + + # handle the system configurations at startup. with this we create/update + # the list of the Micro-Manager hardware system configurations files path + # stored as a json file in the user's configuration file directory + # (USER_CONFIGS_PATHS). + # a dialog will be also displayed if no system configuration file is + # provided to either select one from the list of available ones or to create + # a new one. + self._init_cfg = InitializeSystemConfigurations( + parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc + ) def _cleanup(self) -> None: for signal, slot in self._connections: diff --git a/tests/conftest.py b/tests/conftest.py index cde3cad5..6010a2e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def core(monkeypatch): @pytest.fixture def main_window(core: CMMCorePlus, make_napari_viewer): viewer = make_napari_viewer() - win = MainWindow(viewer=viewer) + win = MainWindow(viewer=viewer, init_configs=False) assert core == win._mmc return win From 0e75583b411c42933f84255a719375f50c894df6 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 21:38:27 -0500 Subject: [PATCH 27/36] test: fix --- tests/test_main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main_window.py b/tests/test_main_window.py index ef4d3a1f..3ed01b0c 100644 --- a/tests/test_main_window.py +++ b/tests/test_main_window.py @@ -17,7 +17,7 @@ def test_main_window(qtbot: QtBot, core: CMMCorePlus) -> None: This test should remain fast. """ viewer = MagicMock() - wdg = MainWindow(viewer) + wdg = MainWindow(viewer, init_configs=False) qtbot.addWidget(wdg) viewer.layers.events.connect.assert_called_once_with(wdg._update_max_min) From dafc357759d0d66ece36cd24fa28996e2c9f88c3 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 22:05:03 -0500 Subject: [PATCH 28/36] fix: avoid error when loading a cfg + update test --- src/napari_micromanager/_init_system_configs.py | 7 ++++--- src/napari_micromanager/_util.py | 11 +++++++++++ src/napari_micromanager/main_window.py | 14 +++++++++++--- tests/test_main.py | 4 ++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/napari_micromanager/_init_system_configs.py b/src/napari_micromanager/_init_system_configs.py index 79703b64..7dedb3a9 100644 --- a/src/napari_micromanager/_init_system_configs.py +++ b/src/napari_micromanager/_init_system_configs.py @@ -25,6 +25,7 @@ USER_CONFIGS_PATHS, USER_DIR, add_path_to_config_json, + load_sys_config, ) FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) @@ -49,7 +50,7 @@ def __init__( # add the config to the system configurations json and set it as the # current configuration path. add_path_to_config_json(config) - self._mmc.loadSystemConfiguration(config) + load_sys_config(config) # if no config is provided, show a dialog to select one or to create a new one else: self._startup_dialog = StartupConfigurationsDialog( @@ -179,7 +180,7 @@ def accept(self) -> None: self._cfg_wizard = HardwareConfigWizard(parent=self) self._cfg_wizard.show() else: - self._mmc.loadSystemConfiguration(config) + load_sys_config(config) def _initialize(self) -> None: """Initialize the dialog with the Micro-Manager configuration files.""" @@ -246,4 +247,4 @@ def accept(self) -> None: dest = self.field(DEST_CONFIG) # add the path to the USER_CONFIGS_PATHS list add_path_to_config_json(dest) - self._core.loadSystemConfiguration(dest) + load_sys_config(dest) diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index dea7a7a5..5c1d49fa 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import TYPE_CHECKING, cast +from warnings import warn from platformdirs import user_config_dir from pymmcore_plus import CMMCorePlus @@ -136,3 +137,13 @@ def load_sys_config_dialog( add_path_to_config_json(filename) mmcore = mmcore or CMMCorePlus.instance() mmcore.loadSystemConfiguration(filename) + + +def load_sys_config(config: Path | str, mmcore: CMMCorePlus | None = None) -> None: + """Load a system configuration with a warning if the file is not found.""" + mmcore = mmcore or CMMCorePlus.instance() + try: + mmcore.loadSystemConfiguration(config) + except FileNotFoundError: + # don't crash if the user passed an invalid config + warn(f"Config file {config} not found. Nothing loaded.", stacklevel=2) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 131d226b..d77fc8cd 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -13,6 +13,7 @@ from qtpy.QtWidgets import QAction, QMenuBar from napari_micromanager._util import ( + load_sys_config, load_sys_config_dialog, save_sys_config_dialog, ) @@ -68,10 +69,10 @@ def __init__( self.destroyed.connect(self._cleanup) atexit.register(self._cleanup) - if init_configs: - # Micro-Manager HArdware Configuration Wizard - self._wiz = HardwareConfigWizard(parent=self.viewer.window._qt_window) + # Micro-Manager Hardware Configuration Wizard + self._wiz: HardwareConfigWizard | None = None + if init_configs: # handle the system configurations at startup. with this we create/update # the list of the Micro-Manager hardware system configurations files path # stored as a json file in the user's configuration file directory @@ -82,6 +83,10 @@ def __init__( self._init_cfg = InitializeSystemConfigurations( parent=self.viewer.window._qt_window, config=config, mmcore=self._mmc ) + return + + if config: + load_sys_config(config) def _cleanup(self) -> None: for signal, slot in self._connections: @@ -131,6 +136,9 @@ def _load_cfg(self) -> None: def _show_config_wizard(self) -> None: """Show the Micro-Manager Hardware Configuration Wizard.""" + if self._wiz is None: + self._wiz = HardwareConfigWizard(parent=self.viewer.window._qt_window) + if self._wiz.isVisible(): self._wiz.raise_() else: diff --git a/tests/test_main.py b/tests/test_main.py index 5cae6d6b..ba614d10 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import pytest from napari_micromanager.__main__ import main +from napari_micromanager._util import USER_CONFIGS_PATHS from pymmcore_plus import CMMCorePlus @@ -35,3 +36,6 @@ def test_cli_main(argv: list) -> None: # this is to prevent a leaked widget error in the NEXT test napari.current_viewer().close() QtViewer._instances.clear() + + if USER_CONFIGS_PATHS.exists(): + USER_CONFIGS_PATHS.unlink() From d6686f78505690b82fc02fdee683de2f71bacfa7 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 22:22:05 -0500 Subject: [PATCH 29/36] fix: import annotations --- tests/test_configurations.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index 019f89c5..e32d3f3a 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import json from pathlib import Path +from typing import TYPE_CHECKING import pytest from napari_micromanager._init_system_configs import ( InitializeSystemConfigurations, ) from napari_micromanager._util import USER_CONFIGS_PATHS -from pymmcore_plus import CMMCorePlus -from pytestqt.qtbot import QtBot + +if TYPE_CHECKING: + from pymmcore_plus import CMMCorePlus + from pytestqt.qtbot import QtBot DEMO = "MMConfig_demo.cfg" NEW = "New Hardware Configuration" From 097b135e05af9e16dc24c9d779b50725aaed526b Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 22:39:16 -0500 Subject: [PATCH 30/36] test: update test_config_init --- tests/test_configurations.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index e32d3f3a..20e709cd 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -21,7 +21,8 @@ @pytest.mark.parametrize("config", configs) -def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None): +@pytest.mark.parametrize("wiz", [True, False]) +def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None, wiz: bool): assert not USER_CONFIGS_PATHS.exists() init = InitializeSystemConfigurations(mmcore=core, config=config) @@ -32,23 +33,32 @@ def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None): assert DEMO in [Path(path).name for path in data["paths"]] if config is None: - assert len(data["paths"]) == 1 assert init._startup_dialog.isVisible() combo = init._startup_dialog.cfg_combo current_cfg = combo.currentText() assert Path(current_cfg).name == DEMO - assert [DEMO, NEW] == [ + assert DEMO and NEW in [ Path(combo.itemText(i)).name for i in range(combo.count()) ] + + # set the combo to new so that after accepting the config wizard should + # be visible + combo.setCurrentText(NEW if wiz else DEMO) + # simulate click on ok init._startup_dialog.accept() + # only if DEMO cfg was selected, the config wizard should be visible + if wiz: + assert init._startup_dialog._cfg_wizard.isVisible() + else: - assert len(data["paths"]) == 2 assert not hasattr(init, "_startup_dialog") current_cfg = config assert str(current_cfg) in data["paths"] - assert core.systemConfigurationFile() == str(current_cfg) + # a config should have been loaded only if DEMO cfg was selected + if not wiz: + assert core.systemConfigurationFile() == str(current_cfg) USER_CONFIGS_PATHS.unlink() From 4d6980cef44d38fc84fe0f7f6d6d9f065a6c0c10 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 22:43:41 -0500 Subject: [PATCH 31/36] test: todo --- tests/test_configurations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index 20e709cd..e682741f 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -62,3 +62,6 @@ def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None, wiz: assert core.systemConfigurationFile() == str(current_cfg) USER_CONFIGS_PATHS.unlink() + + +# TODO: test the config wizard and the menu actions From f6ffbfcb8fc8fc5299e81b9d90e6febf7a52f9d3 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Thu, 7 Mar 2024 22:45:11 -0500 Subject: [PATCH 32/36] test: fix test_config_init --- tests/test_configurations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index e682741f..8eb2e347 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -35,8 +35,6 @@ def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None, wiz: if config is None: assert init._startup_dialog.isVisible() combo = init._startup_dialog.cfg_combo - current_cfg = combo.currentText() - assert Path(current_cfg).name == DEMO assert DEMO and NEW in [ Path(combo.itemText(i)).name for i in range(combo.count()) ] @@ -44,6 +42,7 @@ def test_config_init(qtbot: QtBot, core: CMMCorePlus, config: Path | None, wiz: # set the combo to new so that after accepting the config wizard should # be visible combo.setCurrentText(NEW if wiz else DEMO) + current_cfg = combo.currentText() # simulate click on ok init._startup_dialog.accept() From 544bf1cc4adfe59cd965a0c604e7ae8f0c99f565 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 13 Mar 2024 13:00:48 -0400 Subject: [PATCH 33/36] fix: _load_cfg --- src/napari_micromanager/main_window.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index d77fc8cd..2c5781aa 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -14,7 +14,6 @@ from napari_micromanager._util import ( load_sys_config, - load_sys_config_dialog, save_sys_config_dialog, ) @@ -132,7 +131,10 @@ def _save_cfg(self) -> None: def _load_cfg(self) -> None: """Load a Micro-Manager system configuration.""" - load_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) + # load_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) + InitializeSystemConfigurations( + parent=self.viewer.window._qt_window, mmcore=self._mmc + ) def _show_config_wizard(self) -> None: """Show the Micro-Manager Hardware Configuration Wizard.""" From bbaa2097bd2435c92f109efeea4a00ab383f64f8 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 13 Mar 2024 21:46:28 -0400 Subject: [PATCH 34/36] fix: revert --- src/napari_micromanager/main_window.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 2c5781aa..d77fc8cd 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -14,6 +14,7 @@ from napari_micromanager._util import ( load_sys_config, + load_sys_config_dialog, save_sys_config_dialog, ) @@ -131,10 +132,7 @@ def _save_cfg(self) -> None: def _load_cfg(self) -> None: """Load a Micro-Manager system configuration.""" - # load_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) - InitializeSystemConfigurations( - parent=self.viewer.window._qt_window, mmcore=self._mmc - ) + load_sys_config_dialog(parent=self.viewer.window._qt_window, mmcore=self._mmc) def _show_config_wizard(self) -> None: """Show the Micro-Manager Hardware Configuration Wizard.""" From f4f048288501e5a5be000b2e6b38b57db0f54de0 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Wed, 13 Mar 2024 21:52:03 -0400 Subject: [PATCH 35/36] fix: move selected cfg to top --- src/napari_micromanager/_init_system_configs.py | 6 ++++++ src/napari_micromanager/_util.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/napari_micromanager/_init_system_configs.py b/src/napari_micromanager/_init_system_configs.py index 7dedb3a9..5d793ffd 100644 --- a/src/napari_micromanager/_init_system_configs.py +++ b/src/napari_micromanager/_init_system_configs.py @@ -175,6 +175,12 @@ def __init__( def accept(self) -> None: super().accept() config = self.cfg_combo.currentText() + # if current text is not at index 0, update the json file and insert it at the + # first position so it will be shown as the fort option in the combo box next + # time the dialog is shown. + if config != self.cfg_combo.itemText(0): + add_path_to_config_json(config) + # if the user selected NEW, show the config wizard if config == NEW: self._cfg_wizard = HardwareConfigWizard(parent=self) diff --git a/src/napari_micromanager/_util.py b/src/napari_micromanager/_util.py index 5c1d49fa..60c426c8 100644 --- a/src/napari_micromanager/_util.py +++ b/src/napari_micromanager/_util.py @@ -96,8 +96,9 @@ def add_path_to_config_json(path: Path | str) -> None: # Append the new path. using insert so we leave the empty string at the end paths = cast(list, data.get("paths", [])) - if path not in paths: - paths.insert(0, path) + if path in paths: + paths.remove(path) + paths.insert(0, path) # Write the data back to the file with open(USER_CONFIGS_PATHS, "w") as f: From 1a6523a63a8978abec810b4fe474a4b0b3188907 Mon Sep 17 00:00:00 2001 From: fdrgsp Date: Sat, 16 Mar 2024 08:50:13 -0400 Subject: [PATCH 36/36] fix: spelling --- src/napari_micromanager/_gui_objects/_toolbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_micromanager/_gui_objects/_toolbar.py b/src/napari_micromanager/_gui_objects/_toolbar.py index bc62face..7f638e8e 100644 --- a/src/napari_micromanager/_gui_objects/_toolbar.py +++ b/src/napari_micromanager/_gui_objects/_toolbar.py @@ -58,7 +58,7 @@ class GroupsAndPresets(GroupPresetTableWidget): """Subclass of GroupPresetTableWidget. - Overwrite the save and load methods to store the saced or loaded configuration in + Overwrite the save and load methods to store the saved or loaded configuration in the USER_CONFIGS_PATHS json config file. """