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

Avoid FlatLaf crashing the GUI #135

Merged
merged 12 commits into from
Nov 18, 2022
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ jobs:
with:
configuration: --check-only

test-imagej-legacy:
name: Test legacy inclusion
test-ij2:
name: Test pure IJ2
runs-on: ubuntu-latest
defaults:
# Steps that rely on the activated environment must be run with this shell setup.
Expand All @@ -124,7 +124,7 @@ jobs:

- name: Setup Testing Environment Variables
run: |
echo "NAPARI_IMAGEJ_INCLUDE_IMAGEJ_LEGACY=true" >> $GITHUB_ENV
echo "NAPARI_IMAGEJ_INCLUDE_IMAGEJ_LEGACY=false" >> $GITHUB_ENV

- name: Test napari-imagej
uses: GabrielBB/xvfb-action@v1
Expand Down
2 changes: 1 addition & 1 deletion dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies:
- magicgui >= 0.5.1
- napari
- openjdk=8
- pyimagej >= 1.2.0
- pyimagej >= 1.3.0
- scyjava >= 1.7.0
# Test dependencies
- numpy
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies:
- napari
- numpy
- openjdk=8
- pyimagej >= 1.2.0
- pyimagej >= 1.3.0
- scyjava >= 1.7.0
# Project from source
- pip
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ install_requires =
napari
numpy
magicgui >= 0.5.1
pyimagej >= 1.2.0
pyimagej >= 1.3.0
scyjava >= 1.7.0

[options.packages.find]
Expand Down
5 changes: 3 additions & 2 deletions src/napari_imagej/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ imagej_base_directory: '.'

# This can be used to include original ImageJ functionality.
# Iff true, original ImageJ functionality (ij.* packages) will be available.
# Defaults to false.
include_imagej_legacy: false
# Iff false, many ImageJ2 rewrites of original ImageJ functionality are available.
# Defaults to true as the ImageJ legacy UI is most popular and familiar.
include_imagej_legacy: true

# Designates the mode of execution for ImageJ2.
# Allowed options are 'headless' and 'interactive'.
Expand Down
112 changes: 92 additions & 20 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- object whose fields are lazily-loaded Java Class instances.
"""
from threading import Lock
from typing import Callable, List, Tuple
from typing import Any, Callable, Dict, List, Tuple

import imagej
from jpype import JClass
Expand Down Expand Up @@ -89,11 +89,95 @@ def initialize(self):
"""
Creates the ImageJ instance
"""
# Initialize ImageJ
log_debug("Initializing ImageJ2")

# -- IMAGEJ CONFIG -- #
# determine whether imagej is already running
imagej_already_initialized: bool = hasattr(imagej, "gateway") and imagej.gateway

# -- CONFIGURATION -- #

# Configure pyimagej
if imagej_already_initialized:
self._update_imagej_settings()
else:
ij_settings = self._configure_imagej()

# Configure napari-imagej
from napari_imagej.types.converters import install_converters

install_converters()

log_debug("Completed JVM Configuration")

# -- INITIALIZATION -- #

# Launch ImageJ
if imagej_already_initialized:
self.ij = imagej.gateway
else:
self.ij = imagej.init(**ij_settings)

# Log initialization
log_debug(f"Initialized at version {self.ij.getVersion()}")

# -- VALIDATION -- #

# Validate PyImageJ
self._validate_imagej()

# HACK: Avoid FlatLaf with ImageJ2 Swing UI;
# it doesn't work for reasons unknown.
# NB this SHOULD NOT be moved.
# This code must be in place before ANY swing components get created.
# Swing components could be created by any Java functionality (e.g. Commands).
# Therefore, we can't move it to e.g. the menu file
try:
ui = self.ij.ui().getDefaultUI().getInfo().getName()
log_debug(f"Default SciJava UI is {ui}.")
if ui == "swing":
SwingLookAndFeelService = jimport(
"org.scijava.ui.swing.laf.SwingLookAndFeelService"
)
laf = self.ij.prefs().get(SwingLookAndFeelService, "lookAndFeel")
log_debug(f"Preferred Look+Feel is {laf}.")
if laf is None or laf.startsWith("FlatLaf"):
UIManager = jimport("javax.swing.UIManager")
fallback_laf = UIManager.getSystemLookAndFeelClassName()
log_debug(
f"Detected FlatLaf. Falling back to {fallback_laf} "
"instead to avoid problems."
)
self.ij.prefs().put(
SwingLookAndFeelService, "lookAndFeel", fallback_laf
)
except Exception as exc:
from scyjava import jstacktrace

# NB: The hack failed, but no worries, just try to keep going.
print(jstacktrace(exc))

def _update_imagej_settings(self) -> None:
"""
Updates napari-imagej's settings to reflect an active ImageJ instance.
"""
# Scrape the JVM mode off of the active ImageJ instance
settings["jvm_mode"] = (
"headless" if imagej.gateway.ui().isHeadless() else "interactive"
)
# Determine if legacy is active on the active ImageJ instance
# NB bool is needed to coerce Nones into booleans.
settings["add_legacy"] = bool(
imagej.gateway.legacy and imagej.gateway.legacy.isActive()
)

def _configure_imagej(self) -> Dict[str, Any]:
"""
Configures scyjava and pyimagej.
This function returns the settings that must be passed in the
actual initialization call.

:return: kwargs that should be passed to imagej.init()
"""
# ScyJava configuration
# TEMP: Avoid issues caused by
# https://github.com/imagej/pyimagej/issues/160
Expand All @@ -103,26 +187,14 @@ def initialize(self):
config.add_option(f"-Dimagej2.dir={settings['imagej_base_directory'].get(str)}")

# PyImageJ configuration
ij_settings = {}
ij_settings["ij_dir_or_version_or_endpoint"] = settings[
init_settings = {}
init_settings["ij_dir_or_version_or_endpoint"] = settings[
"imagej_directory_or_endpoint"
].get(str)
ij_settings["mode"] = settings["jvm_mode"].get(str)
ij_settings["add_legacy"] = settings["include_imagej_legacy"].get(bool)
init_settings["mode"] = settings["jvm_mode"].get(str)
init_settings["add_legacy"] = settings["include_imagej_legacy"].get(bool)

# napari-imagej configuration
from napari_imagej.types.converters import install_converters

install_converters()

log_debug("Completed JVM Configuration")

# Launch PyImageJ
self.ij = imagej.init(**ij_settings)
# Validate PyImageJ
self._validate_imagej()
# Log initialization
log_debug(f"Initialized at version {self.ij.getVersion()}")
return init_settings

def _validate_imagej(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/napari_imagej/types/converters/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _dataset_view_to_image(image: Any) -> Image:
data=ij().py.from_java(view.getData().getImgPlus().getImg()),
name=view.getData().getName(),
)
if view.getColorTables().size() > 0:
if view.getColorTables() and view.getColorTables().size() > 0:
if not jc.ColorTables.isGrayColorTable(view.getColorTables().get(0)):
kwargs["colormap"] = _color_table_to_colormap(view.getColorTables().get(0))
return Image(**kwargs)
Expand Down Expand Up @@ -58,7 +58,7 @@ def _dataset_to_image(image: Any) -> Image:
def _image_to_dataset(image: Image) -> "jc.Dataset":
# Construct a dataset from the data
data = image.data
dataset: "jc.Dataset" = ij().py._numpy_to_dataset(data)
dataset: "jc.Dataset" = ij().py.to_dataset(data)
# Add name
dataset.setName(image.name)
# Add color table, if the image uses a custom colormap
Expand Down
3 changes: 2 additions & 1 deletion src/napari_imagej/types/converters/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
scyjava Converters for converting between ImgLib2 ImgLabelings
and napari Labels
"""
from imagej.convert import imglabeling_to_labeling
from labeling.Labeling import Labeling
from napari.layers import Labels
from scyjava import Priority
Expand Down Expand Up @@ -39,7 +40,7 @@ def _imglabeling_to_layer(imgLabeling: "jc.ImgLabeling") -> Labels:
:param imgLabeling: the Java ImgLabeling
:return: a Labels layer
"""
labeling: Labeling = ij().py._imglabeling_to_labeling(imgLabeling)
labeling: Labeling = imglabeling_to_labeling(ij(), imgLabeling)
return _labeling_to_layer(labeling)


Expand Down
3 changes: 2 additions & 1 deletion src/napari_imagej/utilities/_module_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from inspect import Parameter, Signature, _empty, isclass, signature
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from imagej.images import is_arraylike
from jpype import JException
from magicgui.widgets import Container, Label, LineEdit, Widget, request_values
from magicgui.widgets._bases import CategoricalWidget
Expand Down Expand Up @@ -306,7 +307,7 @@ def _pure_module_outputs(
continue
output = ij().py.from_java(output_entry.getValue())
# Add arraylike outputs as images
if ij().py._is_arraylike(output):
if is_arraylike(output):
layer = Layer.create(data=output, meta={"name": name}, layer_type="image")
layer_outputs.append(layer)
# Add Layers directly
Expand Down
33 changes: 23 additions & 10 deletions src/napari_imagej/widgets/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,24 @@ def _showUI(self):
"""
# First time showing
if not self.gui.isVisible():
# First things first, show the GUI
ij().ui().showUI(self.gui)
# Then, add our custom settings to the User Interface
if ij().legacy and ij().legacy.isActive():
self._ij1_UI_setup()
else:
self._ij2_UI_setup()

def ui_setup():
# First things first, show the GUI
ij().ui().showUI(self.gui)
# Then, add our custom settings to the User Interface
if ij().legacy and ij().legacy.isActive():
self._ij1_UI_setup()
else:
self._ij2_UI_setup()

# Queue UI call on the EDT
# TODO: Use EventQueue.invokeLater scyjava wrapper, once it exists
ij().thread().queue(ui_setup)
# Later shows - the GUI is "visible", but the appFrame probably isn't
else:
self.gui.getApplicationFrame().setVisible(True)
# Queue UI call on the EDT
# TODO: Use EventQueue.invokeLater scyjava wrapper, once it exists
ij().thread().queue(lambda: self.gui.getApplicationFrame().setVisible(True))

def _ij1_UI_setup(self):
"""Configures the ImageJ Legacy GUI"""
Expand Down Expand Up @@ -148,7 +156,7 @@ def _set_icon(self, path: str):
def send_active_layer(self):
active_layer: Optional[Layer] = self.viewer.layers.selection.active
if active_layer:
ij().ui().show(ij().py.to_java(active_layer))
self._show(active_layer)
else:
log_debug("There is no active layer to export to ImageJ2")

Expand All @@ -165,7 +173,12 @@ def send_chosen_layer(self):
layer = choices["layer"]
if isinstance(layer, Layer):
# Pass the relevant data to ImageJ2
ij().ui().show(ij().py.to_java(layer))
self._show(layer)

def _show(self, layer):
# Queue UI call on the EDT
# TODO: Use EventQueue.invokeLater scyjava wrapper, once it exists
ij().thread().queue(lambda: ij().ui().show(ij().py.to_java(layer)))


class FromIJButton(QPushButton):
Expand Down