diff --git a/dev-environment.yml b/dev-environment.yml index 7787150f..05cb681b 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -14,7 +14,7 @@ channels: - defaults dependencies: # Project dependencies - - dynaconf + - confuse - labeling >= 0.1.12 - magicgui >= 0.5.1 - napari diff --git a/environment.yml b/environment.yml index d08593e4..cf5de899 100644 --- a/environment.yml +++ b/environment.yml @@ -14,7 +14,7 @@ channels: - defaults dependencies: # Project depenencies - - dynaconf + - confuse - labeling >= 0.1.12 - magicgui >= 0.5.1 - napari diff --git a/setup.cfg b/setup.cfg index 64a7daee..09570596 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,7 @@ dev = autopep8 black build + confuse flake8 isort pyqt5 diff --git a/src/napari_imagej/__init__.py b/src/napari_imagej/__init__.py index 35d5b8a7..6faa58a9 100644 --- a/src/napari_imagej/__init__.py +++ b/src/napari_imagej/__init__.py @@ -17,4 +17,8 @@ napari-imagej is built upon the PyImageJ project: https://pyimagej.readthedocs.io/en/latest/ """ +import confuse + __version__ = "0.0.1.dev0" + +settings = confuse.Configuration(appname="napari-imagej", modname=__name__) diff --git a/src/napari_imagej/settings.toml b/src/napari_imagej/config_default.yaml similarity index 72% rename from src/napari_imagej/settings.toml rename to src/napari_imagej/config_default.yaml index 46feab23..b71f3903 100644 --- a/src/napari_imagej/settings.toml +++ b/src/napari_imagej/config_default.yaml @@ -1,18 +1,18 @@ -# napari-imagej Settings +# napari-imagej Default Settings # USERS BEWARE: -# This toml file will soon be replaced with napari's contribution configuration. +# This yaml file will soon be replaced with napari's contribution configuration. # Path to a local ImageJ2 installation (e.g. /Applications/Fiji.app), # OR version of net.imagej:imagej artifact to launch (e.g. 2.3.0), # OR endpoint of another artifact built on ImageJ2 (e.g. sc.fiji:fiji), # OR list of Maven artifacts to include (e.g. # ['net.imagej:imagej:2.3.0', 'net.imagej:imagej-legacy', 'net.preibisch:BigStitcher']). -# The default is the latest version of ImageJ2. -imagej_directory_or_endpoint = "net.imagej:imagej" +# The default (null) will use the latest version of ImageJ2, downloading it if needed. +imagej_directory_or_endpoint: 'net.imagej:imagej' # This can be used to identify whether transferred data between ImageJ2 and napari # should be selected via activation or by user selection via a dialog. # By default, the active layer/window is chosen for transfer between applications. # By setting this value to false, a popup will be shown instead. -choose_active_layer = true \ No newline at end of file +choose_active_layer: true \ No newline at end of file diff --git a/src/napari_imagej/java.py b/src/napari_imagej/java.py index e016032c..19291388 100644 --- a/src/napari_imagej/java.py +++ b/src/napari_imagej/java.py @@ -26,7 +26,7 @@ from jpype import JClass from scyjava import config, jimport -from napari_imagej.settings import preferences +from napari_imagej import settings from napari_imagej.utilities.logging import log_debug # -- ImageJ API -- # @@ -74,14 +74,14 @@ def _imagej_init(): log_debug("Completed JVM Configuration") # Configure PyImageJ settings - settings = { - "ij_dir_or_version_or_endpoint": preferences.imagej_directory_or_endpoint, + ij_settings = { + "ij_dir_or_version_or_endpoint": settings["imagej_directory_or_endpoint"].get(), "mode": get_mode(), "add_legacy": False, } # Launch PyImageJ - _ij = imagej.init(**settings) + _ij = imagej.init(**ij_settings) log_debug(f"Initialized at version {_ij.getVersion()}") # Return the ImageJ gateway diff --git a/src/napari_imagej/settings.py b/src/napari_imagej/settings.py deleted file mode 100644 index 3ab55af7..00000000 --- a/src/napari_imagej/settings.py +++ /dev/null @@ -1,13 +0,0 @@ -from pathlib import Path - -from dynaconf import Dynaconf - -# Preferences settings - -# The settings are in the root directory, three directories up -SETTINGS_PATH = (Path(__file__).parent / "settings.toml").resolve() - -preferences = Dynaconf( - envvar_prefix="DYNACONF", - settings_files=SETTINGS_PATH, -) diff --git a/src/napari_imagej/widgets/menu.py b/src/napari_imagej/widgets/menu.py index 4772951c..c22909fc 100644 --- a/src/napari_imagej/widgets/menu.py +++ b/src/napari_imagej/widgets/menu.py @@ -14,8 +14,8 @@ from qtpy.QtGui import QIcon, QPixmap from qtpy.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QWidget +from napari_imagej import settings from napari_imagej.java import ensure_jvm_started, ij, jc, log_debug, running_headless -from napari_imagej.settings import preferences from napari_imagej.utilities._module_utils import _get_layers_hack from napari_imagej.widgets.resources import resource_path @@ -35,6 +35,9 @@ def __init__(self, viewer: Viewer): self.gui_button: GUIButton = GUIButton() self.layout().addWidget(self.gui_button) + self.settings_button: SettingsButton = SettingsButton(viewer) + self.layout().addWidget(self.settings_button) + if running_headless(): self.gui_button.clicked.connect(self.gui_button.disable_popup) else: @@ -128,10 +131,11 @@ def __init__(self, viewer: Viewer): self.setEnabled(False) icon = QColoredSVGIcon.from_resources("long_right_arrow") self.setIcon(icon.colored(theme=viewer.theme)) - self.setToolTip("Export active napari layer to ImageJ2") - if preferences.choose_active_layer: + if settings["choose_active_layer"].get(): + self.setToolTip("Export active napari layer to ImageJ2") self.clicked.connect(self.send_active_layer) else: + self.setToolTip("Export napari layer to ImageJ2") self.clicked.connect(self.send_chosen_layer) def _set_icon(self, path: str): @@ -173,10 +177,11 @@ def __init__(self, viewer: Viewer): self.setEnabled(False) icon = QColoredSVGIcon.from_resources("long_left_arrow") self.setIcon(icon.colored(theme=viewer.theme)) - self.setToolTip("Import active ImageJ2 Dataset to napari") - if preferences.choose_active_layer: + if settings["choose_active_layer"].get(): + self.setToolTip("Import active ImageJ2 Dataset to napari") self.clicked.connect(self.get_active_layer) else: + self.setToolTip("Import ImageJ2 Dataset to napari") self.clicked.connect(self.get_chosen_layer) def _set_icon(self, path: str): @@ -281,3 +286,32 @@ def disable_popup(self): msg.setTextFormat(Qt.RichText) msg.setTextInteractionFlags(Qt.TextBrowserInteraction) msg.exec() + + +class SettingsButton(QPushButton): + def __init__(self, viewer: Viewer): + super().__init__() + self.viewer = viewer + + icon = QColoredSVGIcon(resource_path("gear")) + self.setIcon(icon.colored(theme=viewer.theme)) + + self.clicked.connect(self._update_settings) + + def _update_settings(self): + args = {} + for k, v in settings.items(): + args[k] = {} + args[k]["value"] = v.get() + choices = request_values(title="napari-imagej Settings", values=args) + if choices is not None: + any_changed = False + for k, v in choices.items(): + if v != settings[k].get(): + any_changed = True + settings[k].set(v) + + if any_changed: + output = settings.dump() + with open(settings.user_config_path(), "w") as f: + f.write(output) diff --git a/src/napari_imagej/widgets/resources/gear.svg b/src/napari_imagej/widgets/resources/gear.svg new file mode 100644 index 00000000..2c5a2a7c --- /dev/null +++ b/src/napari_imagej/widgets/resources/gear.svg @@ -0,0 +1,193 @@ + + + + diff --git a/tests/conftest.py b/tests/conftest.py index 33fa6822..bd14d099 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,14 +4,26 @@ import os from typing import Callable, Generator +import confuse import pytest from napari import Viewer -from napari_imagej.settings import preferences +import napari_imagej from napari_imagej.widgets.menu import NapariImageJMenu from napari_imagej.widgets.napari_imagej import NapariImageJWidget +@pytest.fixture(scope="session", autouse=True) +def confuse_settings(): + # Overwrite all settings with defaults + napari_imagej.settings = confuse.Configuration( + appname="napari-imagej", modname=__name__, read=False + ) + napari_imagej.settings.read(user=False) + + yield + + @pytest.fixture(scope="module") def ij(): """Fixture providing the ImageJ2 Gateway""" @@ -46,8 +58,8 @@ def gui_widget(viewer) -> Generator[NapariImageJMenu, None, None]: # Define GUIWidget settings for this particular feature. # In particular, we want to enforce active layer selection - previous = preferences.choose_active_layer - preferences.choose_active_layer = True + previous = napari_imagej.settings["choose_active_layer"] + napari_imagej.settings["choose_active_layer"] = True # Create widget widget: NapariImageJMenu = NapariImageJMenu(viewer) @@ -56,7 +68,7 @@ def gui_widget(viewer) -> Generator[NapariImageJMenu, None, None]: # Cleanup -> Close the widget, trigger ImageJ shutdown widget.close() - preferences.choose_active_layer = previous + napari_imagej.settings["choose_active_layer"] = previous @pytest.fixture @@ -67,8 +79,8 @@ def gui_widget_chooser(viewer) -> Generator[NapariImageJMenu, None, None]: # Define GUIWidget settings for this particular feature. # In particular, we want to enforce user layer selection via Dialog - previous = preferences.choose_active_layer - preferences.choose_active_layer = False + previous = napari_imagej.settings["choose_active_layer"] + napari_imagej.settings["choose_active_layer"] = False # Create widget widget: NapariImageJMenu = NapariImageJMenu(viewer) @@ -77,7 +89,7 @@ def gui_widget_chooser(viewer) -> Generator[NapariImageJMenu, None, None]: # Cleanup -> Close the widget, trigger ImageJ shutdown widget.close() - preferences.choose_active_layer = previous + napari_imagej.settings["choose_active_layer"] = previous @pytest.fixture() diff --git a/tests/widgets/test_menu.py b/tests/widgets/test_menu.py index 583761a4..8d9eb071 100644 --- a/tests/widgets/test_menu.py +++ b/tests/widgets/test_menu.py @@ -16,6 +16,7 @@ FromIJButton, GUIButton, NapariImageJMenu, + SettingsButton, ToIJButton, ) from napari_imagej.widgets.resources import resource_path @@ -101,7 +102,7 @@ def clean_layers_and_Displays(asserter, ij, viewer: Viewer): def test_widget_layout(gui_widget: NapariImageJMenu): """Tests the number and expected order of imagej_widget children""" subwidgets = gui_widget.children() - assert len(subwidgets) == 4 + assert len(subwidgets) == 5 assert isinstance(subwidgets[0], QHBoxLayout) assert isinstance(subwidgets[1], FromIJButton) @@ -112,6 +113,8 @@ def test_widget_layout(gui_widget: NapariImageJMenu): assert isinstance(subwidgets[3], GUIButton) + assert isinstance(subwidgets[4], SettingsButton) + @pytest.mark.skipif( running_headless(), reason="Only applies when not running headlessly"