Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dialog to select config at startup + store configs paths #323

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
508fdd4
feta: add startup dialog
fdrgsp Mar 6, 2024
8070b9f
feat: add wizard
fdrgsp Mar 6, 2024
ca6a053
fix: _handle_system_configuration
fdrgsp Mar 6, 2024
9d0e539
fix: _center_dialog_in_viewer
fdrgsp Mar 6, 2024
dc129b2
fix: add_path_to_json
fdrgsp Mar 6, 2024
b04006e
fix: rename
fdrgsp Mar 7, 2024
1ec8c21
fix: move config_dialog
fdrgsp Mar 7, 2024
31762b4
fix: rename
fdrgsp Mar 7, 2024
81c1552
fix: move logic into ConfigurationsHandler
fdrgsp Mar 7, 2024
5bd0340
fix: rename
fdrgsp Mar 7, 2024
4743ef9
feat: subclass ConfigWizard
fdrgsp Mar 7, 2024
9ba4e61
fix: accept
fdrgsp Mar 7, 2024
b47fd5e
fix: update _get_micromanager_cfg_files
fdrgsp Mar 7, 2024
891bc0c
fix: move resize into _initialize
fdrgsp Mar 7, 2024
04de0fa
fix: subclass GroupPresetTableWidget
fdrgsp Mar 7, 2024
59a3784
fix: docstring
fdrgsp Mar 7, 2024
986fc28
fix: todo
fdrgsp Mar 7, 2024
302e77a
fix: docstrings and comments
fdrgsp Mar 7, 2024
b2b6b94
feat: nove login into InitializeSystemConfigurations object
fdrgsp Mar 7, 2024
0772d14
fix: comment
fdrgsp Mar 7, 2024
4cefd8d
feat: add menue + update save and load methods
fdrgsp Mar 7, 2024
34afce8
fix: docstings and comments
fdrgsp Mar 7, 2024
e390b1a
test: add init test
fdrgsp Mar 8, 2024
21e0728
test: fix
fdrgsp Mar 8, 2024
8d5fffa
test: update
fdrgsp Mar 8, 2024
4aba582
fix: add option to init the confics
fdrgsp Mar 8, 2024
0e75583
test: fix
fdrgsp Mar 8, 2024
dafc357
fix: avoid error when loading a cfg + update test
fdrgsp Mar 8, 2024
d6686f7
fix: import annotations
fdrgsp Mar 8, 2024
097b135
test: update test_config_init
fdrgsp Mar 8, 2024
4d6980c
test: todo
fdrgsp Mar 8, 2024
f6ffbfc
test: fix test_config_init
fdrgsp Mar 8, 2024
544bf1c
fix: _load_cfg
fdrgsp Mar 13, 2024
bbaa209
fix: revert
fdrgsp Mar 14, 2024
f4f0482
fix: move selected cfg to top
fdrgsp Mar 14, 2024
308dd78
Merge branch 'main' into startup
fdrgsp Mar 14, 2024
1a6523a
fix: spelling
fdrgsp Mar 16, 2024
142f036
Merge branch 'startup' of https://github.com/fdrgsp/napari-micromanag…
fdrgsp Mar 16, 2024
12802d4
Merge branch 'main' into startup
fdrgsp Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions src/napari_micromanager/_gui_objects/_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
CameraRoiWidget,
ChannelGroupWidget,
ChannelWidget,
ConfigurationWidget,
DefaultCameraExposureWidget,
GroupPresetTableWidget,
LiveButton,
Expand All @@ -18,6 +17,11 @@
SnapButton,
)

from napari_micromanager._util import (
load_sys_config_dialog,
save_sys_config_dialog,
)

try:
# this was renamed
from pymmcore_widgets import ObjectivesPixelConfigurationWidget
Expand Down Expand Up @@ -51,10 +55,31 @@
TOOL_SIZE = 35


class GroupsAndPresets(GroupPresetTableWidget):
"""Subclass of GroupPresetTableWidget.

Overwrite the save and load methods to store the saved 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:
"""Open file dialog to save the current configuration."""
save_sys_config_dialog(parent=self, mmcore=self._mmc)

Check warning on line 72 in src/napari_micromanager/_gui_objects/_toolbar.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_gui_objects/_toolbar.py#L72

Added line #L72 was not covered by tests

def _load_cfg(self) -> None:
"""Open file dialog to select a config file."""
load_sys_config_dialog(parent=self, mmcore=self._mmc)

Check warning on line 76 in src/napari_micromanager/_gui_objects/_toolbar.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_gui_objects/_toolbar.py#L76

Added line #L76 was not covered by tests


# 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),
Expand Down Expand Up @@ -103,14 +128,13 @@
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:
Expand Down Expand Up @@ -241,13 +265,6 @@
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)
Expand Down
256 changes: 256 additions & 0 deletions src/napari_micromanager/_init_system_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import cast
from warnings import warn

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,
QDialogButtonBox,
QFileDialog,
QGridLayout,
QLabel,
QPushButton,
QSizePolicy,
QWidget,
)

from napari_micromanager._util import (
USER_CONFIGS_PATHS,
USER_DIR,
add_path_to_config_json,
load_sys_config,
)

FIXED = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
NEW = "New Hardware Configuration"


class InitializeSystemConfigurations(QObject):
def __init__(
self,
parent: QObject | None = None,
config: Path | str | None = None,
mmcore: CMMCorePlus | None = None,
) -> None:
super().__init__(parent)

self._mmc = mmcore or CMMCorePlus.instance()

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)
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(
parent=self.parent(), config=config, mmcore=self._mmc
)
self._startup_dialog.show()

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
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():
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()

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

Check warning on line 99 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L98-L99

Added lines #L98 - L99 were not covered by tests

# 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 = []
warn("Error reading the json file.", stacklevel=2)

Check warning on line 112 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L110-L112

Added lines #L110 - L112 were not covered by tests

return paths

def _get_micromanager_cfg_files(self) -> list[Path]:
"""Return all the .cfg files from all the MicroManager folders."""
mm: list = find_micromanager(False)
cfg_files: list[Path] = []
for mm_dir in mm:
cfg_files.extend(Path(mm_dir).glob("*.cfg"))

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 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)
self._cfg_wizard.show()
else:
load_sys_config(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

Check warning on line 195 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L195

Added line #L195 was not covered by tests

# Read the existing data
try:
with open(USER_CONFIGS_PATHS) as f:
configs_paths = json.load(f)
except json.JSONDecodeError:
configs_paths = {"paths": []}

Check warning on line 202 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L201-L202

Added lines #L201 - L202 were not covered by tests

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.

If a file path is provided, it is added to the USER_CONFIGS_PATHS json file and
to the combo box.
"""
path, _ = QFileDialog.getOpenFileName(

Check warning on line 217 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L217

Added line #L217 was not covered by tests
self, "Open file", "", "MicroManager files (*.cfg)"
)
if path:

Check warning on line 220 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L220

Added line #L220 was not covered by tests
# using insert so we leave the empty string at the end
self.cfg_combo.insertItem(0, path)
self.cfg_combo.setCurrentText(path)

Check warning on line 223 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L222-L223

Added lines #L222 - L223 were not covered by tests
# add the config to the system configurations json and set it as the
# current configuration path.
add_path_to_config_json(path)

Check warning on line 226 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L226

Added line #L226 was not covered by tests


class HardwareConfigWizard(ConfigWizard):
"""A wizard to create a new Micro-Manager hardware configuration file.

Subclassing to load the newly created configuration file and to add it to the
USER_CONFIGS_PATHS json file.
"""

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.
"""
super().accept()
dest = self.field(DEST_CONFIG)

Check warning on line 253 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L252-L253

Added lines #L252 - L253 were not covered by tests
# add the path to the USER_CONFIGS_PATHS list
add_path_to_config_json(dest)
load_sys_config(dest)

Check warning on line 256 in src/napari_micromanager/_init_system_configs.py

View check run for this annotation

Codecov / codecov/patch

src/napari_micromanager/_init_system_configs.py#L255-L256

Added lines #L255 - L256 were not covered by tests
Loading
Loading