From 20f8583e7c39257af2ac87be08b5b72e25937af0 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 14 May 2026 12:06:05 -0700 Subject: [PATCH 1/4] improve labels --- .../lightsheetmanager/gui/tabs/AcquisitionTab.java | 2 +- .../gui/tabs/channels/ChannelTablePanel.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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..dd6186d 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 @@ -43,8 +43,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); From 4c251de4534263d579a4fdcf968872b1cb495891 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 19 May 2026 15:24:52 -0700 Subject: [PATCH 2/4] make close method to simplify pycromanager shutdown --- .../lightsheetmanager/LightSheetManager.java | 59 +++++++++++++++++-- .../LightSheetManagerFrame.java | 10 ++-- .../LightSheetManagerPlugin.java | 8 +-- .../gui/tabs/channels/ChannelTablePanel.java | 3 +- .../model/DeviceManager.java | 8 +-- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java index 925c106..8312b16 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,63 @@ 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) { + if (positionUpdater_.isPolling()) { + positionUpdater_.stopPolling(); + if (studio_ != null) { + studio_.logs().logMessage("Polling stopped."); + } + } + } + } catch (Exception e) { + // Log the error but don't rethrow, so we can still try to save 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..9ffd7f5 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java @@ -54,12 +54,10 @@ public void onPluginSelected() { frame_.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); WindowUtils.registerWindowClosingEvent(frame_, event -> { - // no need to clean up for the error ui + // only close when the main ui was loaded, + // the error ui does not need to call close if (isLoaded) { - if (model_.positions().isPolling()) { - model_.positions().stopPolling(); - } - model_.userSettings().save(); + model_.close(); } }); 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 dd6186d..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; /** 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; From a40941e08ee8fcce6eb8d80514b0e21cd58e14e6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 19 May 2026 15:49:51 -0700 Subject: [PATCH 3/4] always print the polling stopped message --- .../lightsheetmanager/LightSheetManager.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java index 8312b16..3966e3b 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManager.java @@ -110,15 +110,17 @@ 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(); - if (studio_ != null) { - studio_.logs().logMessage("Polling stopped."); - } } } } catch (Exception e) { - // Log the error but don't rethrow, so we can still try to save settings! + // 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."); } From 7ec7995b5a4048fa87de00e23ffa451d8e9cbf97 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 19 May 2026 18:15:02 -0700 Subject: [PATCH 4/4] add pycromanager script --- .../LightSheetManagerPlugin.java | 6 +- src/python/lsm_pycromanager.py | 91 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/python/lsm_pycromanager.py diff --git a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java index 9ffd7f5..c42f1fe 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java +++ b/src/main/java/org/micromanager/lightsheetmanager/LightSheetManagerPlugin.java @@ -53,14 +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 -> { - // only close when the main ui was loaded, - // the error ui does not need to call close if (isLoaded) { 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/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()