diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java index 925c106..3966e3b 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java @@ -18,7 +18,7 @@ /** * This is the container for all the data needed to operate a microscope with light sheet manager. */ -public class LightSheetManager implements LightSheetManagerApi { +public class LightSheetManager implements LightSheetManagerApi, AutoCloseable { private final Studio studio_; private final CMMCore core_; @@ -35,7 +35,8 @@ public class LightSheetManager implements LightSheetManagerApi { //private final AcquisitionTableData acqTableData_; public LightSheetManager(final Studio studio) { - studio_ = Objects.requireNonNull(studio); + studio_ = Objects.requireNonNull(studio, + "Micro-Manager Studio context cannot be null!"); core_ = studio_.core(); pluginSettings_ = new PluginSettings(); @@ -101,15 +102,65 @@ public boolean setup() { } /** - * This sets the text to be displayed in the error ui when an error occurs during setup. + * Save the settings and stop polling, should be called before exiting. + */ + @Override + public void close() { + + // disable position polling + try { + if (positionUpdater_ != null) { + // message should always be printed + if (studio_ != null) { + studio_.logs().logMessage("Stopping position updater polling..."); + } + + if (positionUpdater_.isPolling()) { + positionUpdater_.stopPolling(); + } + } + } catch (Exception e) { + // log the error so we can still try to save the settings! + if (studio_ != null) { + studio_.logs().logError(e, "Failed to stop position updater polling during close."); + } + } + + // save settings + try { + if (userSettings_ != null) { + userSettings_.save(); + if (studio_ != null) { + studio_.logs().logMessage("User settings saved."); + } + } + } catch (Exception e) { + if (studio_ != null) { + studio_.logs().logError(e, "Failed to save user settings during close."); + } + } + + if (studio_ != null) { + studio_.logs().logMessage("Light Sheet Manager Shutdown"); + } + } + + /** + * Sets the text in the error ui when an error occurs during setup. * * @param text the error message */ - public void setErrorText(final String text) { + public void setupErrorMessage(final String text) { errorText_ = text; } - public String getErrorText() { + /** + * Returns the error message from the setup method, it will be empty + * in the case of no errors detected. + * + * @return the error message + */ + public String setupErrorMessage() { return errorText_; } diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerFrame.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerFrame.java index abd58d4..f5a63b4 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerFrame.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerFrame.java @@ -43,18 +43,18 @@ public LightSheetManagerFrame(final LightSheetManager model, final boolean isLoa break; case SCAPE: if (model_.devices().adapter().numImagingPaths() > 1) { - model_.setErrorText("SCAPE geometry does not support multiple imaging paths. " + model_.setupErrorMessage("SCAPE geometry does not support multiple imaging paths. " + " Use the \"SimultaneousCameras\" property to support multiple cameras."); createErrorUserInterface(); return; } if (model_.devices().adapter().numIlluminationPaths() > 1) { - model_.setErrorText("SCAPE geometry can only have a single illumination path."); + model_.setupErrorMessage("SCAPE geometry can only have a single illumination path."); createErrorUserInterface(); return; } if (model_.devices().adapter().lightSheetType() == LightSheetType.SCANNED) { - model_.setErrorText("Scanned light sheets are not implemented for SCAPE geometry, " + + model_.setupErrorMessage("Scanned light sheets are not implemented for SCAPE geometry, " + "please contact the developers if you need this feature."); createErrorUserInterface(); return; @@ -64,7 +64,7 @@ public LightSheetManagerFrame(final LightSheetManager model, final boolean isLoa model_.acquisitions().updateDurationLabels(); break; default: - model_.setErrorText("Microscope geometry type " + geometry + " is not supported yet."); + model_.setupErrorMessage("Microscope geometry type " + geometry + " is not supported yet."); createErrorUserInterface(); break; } @@ -86,7 +86,7 @@ private void createErrorUserInterface() { )); final Label lblTitle = new Label(LightSheetManagerPlugin.menuName, Font.BOLD, 16); - final Label lblError = new Label(model_.getErrorText(), Font.BOLD, 14); + final Label lblError = new Label(model_.setupErrorMessage(), Font.BOLD, 14); add(lblTitle, "wrap"); add(lblError, ""); diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java index b7fa05c..c42f1fe 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java @@ -53,16 +53,16 @@ public void onPluginSelected() { frame_ = new LightSheetManagerFrame(model_, isLoaded); frame_.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + // clean up resources before the window is fully closed + // call the shutdown code if the main ui was loaded, skip if error ui WindowUtils.registerWindowClosingEvent(frame_, event -> { - // no need to clean up for the error ui if (isLoaded) { - if (model_.positions().isPolling()) { - model_.positions().stopPolling(); - } - model_.userSettings().save(); + model_.close(); } }); + // clear references after the window is fully closed + // prevent memory leaks from closed plugin instances WindowUtils.registerWindowClosedEvent(frame_, event -> { frame_ = null; model_ = null; diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java index 7b70572..e8bb88c 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java @@ -207,7 +207,7 @@ private void createUserInterface() { pnlLeft.add(pnlCameras_, "growx, span 2"); pnlCenter.add(pnlChannelTable_, "wrap"); - pnlCenter.add(new JLabel("Acquisition mode:"), "split 2"); + pnlCenter.add(new JLabel("Acquisition Mode:"), "split 2"); pnlCenter.add(cmbAcquisitionModes_, ""); pnlRight_.add(pnlVolumeSettings_, "growx, wrap"); diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/channels/ChannelTablePanel.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/channels/ChannelTablePanel.java index 566981d..1eb037e 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/channels/ChannelTablePanel.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/channels/ChannelTablePanel.java @@ -12,7 +12,8 @@ import org.micromanager.lightsheetmanager.gui.utils.DialogUtils; import org.micromanager.lightsheetmanager.model.channels.ChannelSpec; -import javax.swing.*; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; import java.util.Objects; /** @@ -43,8 +44,8 @@ public ChannelTablePanel(final LightSheetManager model, final CheckBox checkBox) } private void createUserInterface() { - lblChannelGroup_ = new JLabel("Channel group:"); - lblChangeChannel_ = new JLabel("Change channel:"); + lblChannelGroup_ = new JLabel("Channel Group:"); + lblChangeChannel_ = new JLabel("Channel Mode:"); btnAddChannel_ = new Button("Add", 74, 24); btnRemoveChannel_ = new Button("Remove", 74, 24); diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/DeviceManager.java b/src/main/java/org/micromanager/lightsheetmanager/model/DeviceManager.java index 906adb8..81ac159 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/DeviceManager.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/DeviceManager.java @@ -368,7 +368,7 @@ public boolean validateCameras() { useDefaultImagingCameraOrder(); return true; } else { - model_.setErrorText(message); + model_.setupErrorMessage(message); return false; } } @@ -382,7 +382,7 @@ public boolean validateCameras() { final String message = "Camera in settings not found in hardware: " + camera.name() + ", consider creating a new user profile if the pre-init properties changed."; model_.studio().logs().logError(message); - model_.setErrorText(message); + model_.setupErrorMessage(message); return false; } } @@ -457,7 +457,7 @@ public boolean hasDeviceAdapter() { deviceAdapterName_ = device; count++; if (count > 1) { - model_.setErrorText("You have multiple instances of the LightSheetManager " + + model_.setupErrorMessage("You have multiple instances of the LightSheetManager " + "device adapter in your hardware configuration."); break; // exit loop because this a failure condition } @@ -469,7 +469,7 @@ public boolean hasDeviceAdapter() { } // no device adapters found if (count == 0) { - model_.setErrorText("Please add the LightSheetManager device adapter to your " + + model_.setupErrorMessage("Please add the LightSheetManager device adapter to your " + "hardware configuration to use this plugin."); } return count == 1; diff --git a/src/python/lsm_pycromanager.py b/src/python/lsm_pycromanager.py new file mode 100644 index 0000000..4c52e0c --- /dev/null +++ b/src/python/lsm_pycromanager.py @@ -0,0 +1,91 @@ +# /// script +# requires-python = ">=3.14" +# dependencies = ["pycromanager>=1.0.2"] +# /// + +from pycromanager import Studio, JavaObject +from types import TracebackType +from typing import Optional, Type +import logging + +# Set up a logger specific to your light-sheet plugin +logger = logging.getLogger("LightSheetManager") + +# uncomment to see errors +# logging.basicConfig(level=logging.DEBUG) + +class LightSheetManager: + """ + A high-level Python wrapper for the Micro-Manager Light Sheet Manager plugin. + + Handles the cross-language bridge between Python and Java, providing + automatic lifecycle management using the Python 'with' statement. + + Explicit lifecycle management is provided though manual calls to 'open' and 'close'. + """ + def __init__(self) -> None: + self.lsm: Optional[JavaObject] = None + + def open(self) -> JavaObject: + """Connect to Micro-Manager and initialize Light Sheet Manager. + + Returns: + JavaObject - The initialized Java Light Sheet Manager instance. + + Raises: + ConnectionError - the pycromanager bridge or Java initialization failed + """ + if self.lsm is not None: + return self.lsm + + try: + studio = Studio() + self.lsm = JavaObject("org.micromanager.lightsheetmanager.LightSheetManager", args=[studio]) + + if not self.lsm.setup(): + raise RuntimeError("Java LightSheetManager setup() returned False.") + + logger.info("Light Sheet Manager initialized.") + return self.lsm + except Exception as e: + self.close() # cleanup if initialization fails + raise ConnectionError(f"Could not initialize Light Sheet Manager: {e}") from e + + def close(self) -> None: + """Call the Java AutoCloseable close routine on the LSM JavaObject.""" + if self.lsm is None: + return + + logger.info("Requesting Light Sheet Manager resource cleanup...") + try: + self.lsm.close() + logger.info("Java close() request sent successfully.") + except Exception as e: + logger.warning(f"Failed to communicate with Java close() routine: {e}") + finally: + self.lsm = None # help garbage collection + + def __enter__(self) -> JavaObject: + """Entering the context block opens the bridge and returns the LSM JavaObject.""" + return self.open() + + def __exit__(self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType]) -> bool: + """Automatically clean up LSM resources resources when the exiting the context block.""" + if exc_type: + logger.error(f"Context block exited with an error: {exc_val}", exc_info=True) + + self.close() + return False # do not suppress exceptions + + +def main() -> None: + with LightSheetManager() as lsm: + active_camera = lsm.devices().first_active_camera_name() + print(f"Active Camera: {active_camera}") + + +if __name__ == "__main__": + main()