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

Use dynaconf for user preferences #99

Merged
merged 22 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,6 @@ venv/
# Scripts
/scripts/*
!/scripts/examples

# Ignore dynaconf secret files
gselzer marked this conversation as resolved.
Show resolved Hide resolved
.secrets.*
1 change: 1 addition & 0 deletions dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ channels:
- defaults
dependencies:
# Project dependencies
- confuse
- labeling >= 0.1.12
- magicgui >= 0.5.1
- napari
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ channels:
- defaults
dependencies:
# Project depenencies
- confuse
- labeling >= 0.1.12
- magicgui >= 0.5.1
- napari
Expand Down
15 changes: 0 additions & 15 deletions settings.yml

This file was deleted.

2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ package_dir =

# add your package requirements here
install_requires =
dynaconf
gselzer marked this conversation as resolved.
Show resolved Hide resolved
labeling >= 0.1.12
napari
magicgui >= 0.5.1
Expand All @@ -59,6 +60,7 @@ dev =
autopep8
black
build
confuse
gselzer marked this conversation as resolved.
Show resolved Hide resolved
flake8
isort
pyqt5
Expand Down
4 changes: 4 additions & 0 deletions src/napari_imagej/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
gselzer marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 18 additions & 0 deletions src/napari_imagej/config_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# napari-imagej Default Settings

# USERS BEWARE:
# 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 (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
22 changes: 5 additions & 17 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
"""
import os
import sys
from functools import lru_cache
from multiprocessing.pool import AsyncResult, ThreadPool
from typing import Any, Callable, Dict
from typing import Callable

import imagej
import yaml
from jpype import JClass
from scyjava import config, jimport

from napari_imagej import settings
from napari_imagej.utilities.logging import log_debug

# -- ImageJ API -- #
Expand All @@ -48,17 +47,6 @@ def ensure_jvm_started() -> None:
ij_future.wait()


def setting(name: str):
"""Gets the value of setting name"""
return settings().get(name, None)


@lru_cache(maxsize=None)
def settings() -> Dict[Any, Any]:
"""Gets all plugin settings as a dictionary"""
return yaml.safe_load(open("settings.yml", "r"))


def get_mode() -> str:
"""
Returns the mode ImageJ will be run in
Expand Down Expand Up @@ -86,14 +74,14 @@ def _imagej_init():
log_debug("Completed JVM Configuration")

# Configure PyImageJ settings
settings = {
"ij_dir_or_version_or_endpoint": setting("imagej_installation"),
ij_settings = {
"ij_dir_or_version_or_endpoint": settings["imagej_directory_or_endpoint"].get(),
gselzer marked this conversation as resolved.
Show resolved Hide resolved
"mode": get_mode(),
gselzer marked this conversation as resolved.
Show resolved Hide resolved
"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
Expand Down
79 changes: 63 additions & 16 deletions src/napari_imagej/widgets/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@
from napari import Viewer
from napari._qt.qt_resources import QColoredSVGIcon
from napari.layers import Layer
from qtpy.QtCore import Qt
from qtpy.QtCore import Qt, Signal
from qtpy.QtGui import QIcon, QPixmap
from qtpy.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QWidget

from napari_imagej.java import (
ensure_jvm_started,
ij,
jc,
log_debug,
running_headless,
setting,
)
from napari_imagej import settings
from napari_imagej.java import ensure_jvm_started, ij, jc, log_debug, running_headless
from napari_imagej.utilities._module_utils import _get_layers_hack
from napari_imagej.widgets.resources import resource_path


class NapariImageJMenu(QWidget):
Expand All @@ -40,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:
Expand Down Expand Up @@ -133,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 setting("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):
Expand Down Expand Up @@ -178,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 setting("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):
Expand Down Expand Up @@ -259,19 +259,19 @@ def post_setup():
Thread(target=post_setup).start()

def _setup_headful(self):
self._set_icon("resources/16x16-flat-disabled.png")
self._set_icon(resource_path("imagej2-16x16-flat-disabled"))
self.setToolTip("Display ImageJ2 GUI (loading)")

def post_setup():
ensure_jvm_started()
self._set_icon("resources/16x16-flat.png")
self._set_icon(resource_path("imagej2-16x16-flat"))
self.setEnabled(True)
self.setToolTip("Display ImageJ2 GUI")

Thread(target=post_setup).start()

def _setup_headless(self):
self._set_icon("resources/16x16-flat-disabled.png")
self._set_icon(resource_path("imagej2-16x16-flat-disabled"))
self.setToolTip("ImageJ2 GUI unavailable!")

def disable_popup(self):
Expand All @@ -286,3 +286,50 @@ def disable_popup(self):
msg.setTextFormat(Qt.RichText)
msg.setTextInteractionFlags(Qt.TextBrowserInteraction)
msg.exec()


class SettingsButton(QPushButton):

# Signal used to identify changes to user settings
setting_change = Signal()

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)
self.setting_change.connect(self._notify_settings_change)

def _update_settings(self):
args = {}
default_source = next(s for s in settings.sources if s.default)
gselzer marked this conversation as resolved.
Show resolved Hide resolved
for k, v in default_source.items():
gselzer marked this conversation as resolved.
Show resolved Hide resolved
args[k] = {}
args[k]["value"] = settings[k].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] = v

if any_changed:
self.setting_change.emit()
output = settings.dump()
with open(settings.user_config_path(), "w") as f:
f.write(output)

def _notify_settings_change(self):
"""
Notifies (using a popup) that a restart is required for settings changes
to take effect
"""
msg: QMessageBox = QMessageBox()
msg.setText(
"Please restart napari for napari-imagej settings changes to take effect!"
)
msg.exec()
16 changes: 16 additions & 0 deletions src/napari_imagej/widgets/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
A module used to help find napari-imagej widget resources
gselzer marked this conversation as resolved.
Show resolved Hide resolved
"""
from pathlib import Path

PATH = Path(__file__).parent.resolve()
RESOURCES = {x.stem: str(x) for x in PATH.iterdir() if x.suffix != ".py"}


def resource_path(name: str) -> str:
"""Return path to a resource in this folder."""
if name not in RESOURCES:
raise ValueError(
f"{name} is not a known resource! Known resources: {RESOURCES}"
)
return RESOURCES[name]
Loading