Skip to content

Commit

Permalink
Redirect legacy commands to the ImageJ UI
Browse files Browse the repository at this point in the history
  • Loading branch information
gselzer committed May 5, 2023
1 parent 3b31818 commit ecdd5e4
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 100 deletions.
10 changes: 10 additions & 0 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,20 @@ def Types(self):
def UIComponent(self):
return "org.scijava.widget.UIComponent"

@blocking_import
def UIShownEvent(self):
return "org.scijava.ui.event.UIShownEvent"

@blocking_import
def UserInterface(self):
return "org.scijava.ui.UserInterface"

# ImageJ Legacy Types

@blocking_import
def LegacyCommandInfo(self):
return "net.imagej.legacy.command.LegacyCommandInfo"

# ImgLib2 Types

@blocking_import
Expand Down
202 changes: 111 additions & 91 deletions src/napari_imagej/widgets/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,97 +42,36 @@ def __init__(self, viewer: Viewer):
if settings["jvm_mode"].get(str) == "headless":
self.gui_button.clicked.connect(self.gui_button.disable_popup)
else:
self.gui_button.clicked.connect(self._showUI)

@property
def gui(self) -> "jc.UserInterface":
"""
Convenience function for obtaining the default UserInterface
NB this field is a property so we can lazily evaluate the field.
This means we don't have to call ij() and start the JVM until we need it.
"""
return ij().ui().getDefaultUI()

def _showUI(self):
"""
NB: This must be its own function to prevent premature calling of ij()
"""
# First time showing
if not self.gui.isVisible():

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()
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:
# 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"""
ij().IJ.getInstance().exitWhenQuitting(False)

def _ij2_UI_setup(self):
"""Configures the ImageJ2 Swing GUI behavior"""
# Overwrite the WindowListeners so we control closing behavior
self._kill_window_listeners(self._get_AWT_frame())

def _get_AWT_frame(self):
appFrame = self.gui.getApplicationFrame()
if isinstance(appFrame, jc.Window):
return appFrame
elif isinstance(appFrame, jc.UIComponent):
return appFrame.getComponent()

def _kill_window_listeners(self, window):
"""Replaces the WindowListeners present on window with our own"""
# Remove all preset WindowListeners
for listener in window.getWindowListeners():
window.removeWindowListener(listener)

# Add our own behavior for WindowEvents
@JImplements("java.awt.event.WindowListener")
class NapariAdapter(object):
@JOverride
def windowOpened(self, event):
pass

@JOverride
def windowClosing(self, event):
# We don't want to shut down anything, we just want to hide the window.
window.setVisible(False)

@JOverride
def windowClosed(self, event):
pass

@JOverride
def windowIconified(self, event):
pass

@JOverride
def windowDeiconified(self, event):
pass

@JOverride
def windowActivated(self, event):
pass

@JOverride
def windowDeactivated(self, event):
pass

window.addWindowListener(NapariAdapter())
# NB We need to call ij().ui().showUI() on the GUI thread.
# TODO: Use PyImageJ functionality
# see https://github.com/imagej/pyimagej/pull/260
def show_ui():
if ij().ui().isVisible():
ij().thread().queue(
lambda: ij()
.ui()
.getDefaultUI()
.getApplicationFrame()
.setVisible(True)
)
else:
ij().thread().queue(lambda: ij().ui().showUI())

self.gui_button.clicked.connect(show_ui)

def post_init_setup():
# HACK: Tap into the EventBus to obtain SciJava Module debug info.
# See https://github.com/scijava/scijava-common/issues/452
event_bus_field = ij().event().getClass().getDeclaredField("eventBus")
event_bus_field.setAccessible(True)
event_bus = event_bus_field.get(ij().event())

subscriber = UIShownListener()
# NB We need to retain a reference to this object or GC will delete it
ij().object().addObject(subscriber)
event_bus.subscribe(jc.UIShownEvent.class_, subscriber)

java_signals.when_ij_ready(post_init_setup)


class ToIJButton(QPushButton):
Expand Down Expand Up @@ -432,3 +371,84 @@ def __init__(self, rich_message: str, exec: bool = False):
self.setTextInteractionFlags(Qt.TextBrowserInteraction)
if exec:
self.exec()


@JImplements(["org.scijava.event.EventSubscriber"], deferred=True)
class UIShownListener(object):
def __init__(self):
self.initialized = False

@JOverride
def onEvent(self, event):
if not self.initialized:
# add our custom settings to the User Interface
if ij().legacy and ij().legacy.isActive():
self._ij1_UI_setup()
self._ij2_UI_setup(event.getUI())
self.initialized = True

@JOverride
def getEventClass(self):
return jc.UIShownEvent.class_

@JOverride
def equals(self, other):
return isinstance(other, UIShownListener)

def _ij1_UI_setup(self):
"""Configures the ImageJ Legacy GUI"""
ij().IJ.getInstance().exitWhenQuitting(False)

def _ij2_UI_setup(self, ui: "jc.UserInterface"):
"""Configures the ImageJ2 Swing GUI behavior"""
# Overwrite the WindowListeners so we control closing behavior
self._kill_window_listeners(self._get_AWT_frame(ui))

def _get_AWT_frame(self, ui: "jc.UserInterface"):
appFrame = ui.getApplicationFrame()
if isinstance(appFrame, jc.Window):
return appFrame
elif isinstance(appFrame, jc.UIComponent):
return appFrame.getComponent()

def _kill_window_listeners(self, window):
"""Replaces the WindowListeners present on window with our own"""
# Remove all preset WindowListeners
for listener in window.getWindowListeners():
window.removeWindowListener(listener)

# Add our own behavior for WindowEvents
@JImplements("java.awt.event.WindowListener")
class NapariAdapter(object):
@JOverride
def windowOpened(self, event):
pass

@JOverride
def windowClosing(self, event):
# We don't want to shut down anything, we just want to hide the window.
window.setVisible(False)

@JOverride
def windowClosed(self, event):
pass

@JOverride
def windowIconified(self, event):
pass

@JOverride
def windowDeiconified(self, event):
pass

@JOverride
def windowActivated(self, event):
pass

@JOverride
def windowDeactivated(self, event):
pass

listener = NapariAdapter()
ij().object().addObject(listener)
window.addWindowListener(listener)
3 changes: 2 additions & 1 deletion src/napari_imagej/widgets/result_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ def _setText(self, text: Union[str, "jc.String"]):

def _buttons_for(self, result: "jc.SearchResult") -> List[ActionButton]:
return [
ActionButton(*a) for a in python_actions_for(result, self.output_signal)
ActionButton(*a)
for a in python_actions_for(result, self.output_signal, self)
]
2 changes: 1 addition & 1 deletion src/napari_imagej/widgets/result_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def _create_custom_menu(self, pos):
return
menu: QMenu = QMenu(self)

for name, action in python_actions_for(item.result, self.output_signal):
for name, action in python_actions_for(item.result, self.output_signal, self):
newAct = QAction(name, self)
newAct.triggered.connect(action)
menu.addAction(newAct)
Expand Down
30 changes: 27 additions & 3 deletions src/napari_imagej/widgets/widget_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from magicgui import magicgui
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QMessageBox, QWidget

from napari_imagej.java import ij, jc
from napari_imagej.utilities._module_utils import (
Expand All @@ -10,21 +11,25 @@
from napari_imagej.utilities.logging import log_debug


def python_actions_for(result: "jc.SearchResult", output_signal: Signal):
def python_actions_for(
result: "jc.SearchResult", output_signal: Signal, parent_widget: QWidget = None
):
actions = []
# Iterate over all available python actions
searchService = ij().get("org.scijava.search.SearchService")
for action in searchService.actions(result):
action_name = str(action.toString())
# Add buttons for the java action
if action_name == "Run":
actions.extend(_run_actions_for(result, output_signal))
actions.extend(_run_actions_for(result, output_signal, parent_widget))
else:
actions.append((action_name, action.run))
return actions


def _run_actions_for(result: "jc.SearchResult", output_signal: Signal):
def _run_actions_for(
result: "jc.SearchResult", output_signal: Signal, parent_widget: QWidget
):
def execute_result(modal: bool):
"""Helper function to perform module execution."""
log_debug("Creating module...")
Expand All @@ -35,6 +40,25 @@ def execute_result(modal: bool):
log_debug(f"Search Result {result} cannot be run!")
return []

if (
ij().legacy
and ij().legacy.isActive()
and isinstance(moduleInfo, jc.LegacyCommandInfo)
):
reply = QMessageBox.question(
parent_widget,
"Warning: ImageJ PlugIn",
(
f'"{name}" is an original ImageJ PlugIn "\
"and should be run from the ImageJ UI. " \
"Would you like to launch the ImageJ UI?'
),
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.Yes:
ij().ui().showUI()
return

module = ij().module().createModule(moduleInfo)

# preprocess using napari GUI
Expand Down
9 changes: 5 additions & 4 deletions tests/widgets/test_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,12 @@ def test_image_plus_to_napari(asserter, qtbot, ij, gui_widget: NapariImageJMenu)
def test_opening_and_closing_gui(asserter, qtbot, ij, gui_widget: NapariImageJMenu):
# Open the GUI
qtbot.mouseClick(gui_widget.gui_button, Qt.LeftButton, delay=1)
# Wait for the Frame to be created
asserter(lambda: gui_widget._get_AWT_frame() is not None)
frame = gui_widget._get_AWT_frame()
asserter(lambda: ij.ui().getDefaultUI().getApplicationFrame() is not None)
frame = ij.ui().getDefaultUI().getApplicationFrame()
if isinstance(frame, jc.UIComponent):
frame = frame.getComponent()
# Wait for the Frame to be visible
asserter(lambda: frame.isVisible())
asserter(lambda: ij.ui().isVisible())

# Close the GUI
ij.thread().queue(
Expand Down

0 comments on commit ecdd5e4

Please sign in to comment.