From 73d93948fe77b56903584f6dfc7febf955df6039 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 3 Nov 2026 16:11:48 -0800 Subject: [PATCH 1/3] add a run progress tracker to the acquisition engine --- .../model/acquisitions/AcquisitionEngine.java | 15 + .../model/acquisitions/RunProgress.java | 301 ++++++++++++++++++ .../acquisitions/RunProgressTracker.java | 121 +++++++ 3 files changed, 437 insertions(+) create mode 100644 src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java create mode 100644 src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java index df873e94..a9ef7f37 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngine.java @@ -74,6 +74,8 @@ public abstract class AcquisitionEngine implements AcquisitionManager, MMAcquist // true while finish() restores the system after a run, for the stop and abort log lines private volatile boolean cleaningUp_ = false; + final RunProgressTracker tracker_ = new RunProgressTracker(() -> this.datastore_); + private final AutofocusAdapter autofocus_; // visibility: written on the acquisition thread, read on the edt when a window asks to close @@ -296,6 +298,13 @@ public boolean isRunning() { return acquisitionRequested_; } + /** + * A snapshot of where the run in flight is, or of how the last run ended. + */ + public RunProgress progress() { + return tracker_.now(); + } + /** * Records whether a series is waiting between time points, for the stop and abort log lines. * @@ -425,6 +434,8 @@ private Future requestRun(boolean testAcquisition) { model_.logging().reportError("Acquisition is already running."); return; } + // in the task: the executor runs one at a time, so this cannot reach a run in flight + tracker_.start(); // marker data long runId = -1; // set at START, so STOP is logged only after a START @@ -477,6 +488,8 @@ private Future requestRun(boolean testAcquisition) { model_.logging().reportError(e, "Error during acquisition cleanup"); } finally { cleaningUp_ = false; + // before the acquisition is let go: it says how the run ended + tracker_.end(currentAcquisition_, stopRequested_); // must ALWAYS run: if currentAcquisition_ is left set, every future // acquisition is rejected until the plugin restarts currentAcquisition_ = null; @@ -488,6 +501,8 @@ private Future requestRun(boolean testAcquisition) { // free the datastore so a large store isn't kept in memory (matches MM's // AcqEngJAdapter.onAcquisitionEnded); also what the save guard checks to skip aborted/empty runs datastore_ = null; + // after the run is let go, so the log cannot hold it + tracker_.logEnd(studio_.logs()); // LSM-ACQ-STOP in the innermost finally: fires on completion, error, abort, throwing finish() if (runId != -1) { final long elapsedMs = (System.nanoTime() - startNs) / 1_000_000L; diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java new file mode 100644 index 00000000..43aec596 --- /dev/null +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java @@ -0,0 +1,301 @@ +package org.micromanager.lightsheetmanager.model.acquisitions; + +import java.text.NumberFormat; +import java.util.Objects; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +/** + * Where a run is, as the status line of the acquisition tab shows it. It comes from the count of + * stored images: a run acquires its time points in order and its positions in order inside each, + * so the count says which one is in progress. + * + *

Immutable. The acquisition thread publishes a {@link Source} per dataset, and the interface + * thread takes a snapshot from it when it redraws. + */ +public final class RunProgress implements Supplier { + + /** The states of the status line. */ + public enum Status { + /** no run, or the last one opened no dataset */ + NONE, + /** no dataset open yet, so the run's size is not known */ + STARTING, + /** a run is in progress */ + ACQUIRING, + /** waiting for the next time point */ + WAITING, + /** ended with every image */ + DONE, + /** ended short of images, with no stop and no error */ + SHORT, + /** stopped before every image was in */ + STOPPED, + /** ended on an error */ + FAILED + } + + private static final RunProgress NONE = new RunProgress(Status.NONE, null, 0, 0, 0L, 0L); + private static final RunProgress STARTING = + new RunProgress(Status.STARTING, null, 0, 0, 0L, 0L); + + private final Status status; + // null until the run's size is known + private final Shape shape; + // from 1: where the run is or where it ended; position is 0 while waiting for a time point + private final int timePoint; + private final int position; + private final long msToNextTimePoint; + private final long images; + + private RunProgress(final Status status, final Shape shape, final int timePoint, + final int position, final long msToNextTimePoint, final long images) { + this.status = status; + this.shape = shape; + this.timePoint = timePoint; + this.position = position; + this.msToNextTimePoint = msToNextTimePoint; + this.images = images; + } + + public static RunProgress none() { + return NONE; + } + + public static RunProgress starting() { + return STARTING; + } + + /** + * The progress of a run in flight. + * + * @param firstTimePoint the time point the dataset starts at, from 0 + * @param imagesBefore the images of the datasets already complete + * @param inDataset the images the dataset holds + */ + static RunProgress during(final Shape shape, final int firstTimePoint, final long imagesBefore, + final long inDataset, final Schedule schedule) { + final long perTimePoint = shape.imagesPerTimePoint(); + final long saved = Math.max(0L, inDataset); + // the first time point not complete, or the last one once every image is in + final int timeIndex = (int) Math.min(firstTimePoint + saved / perTimePoint, + shape.numTimePoints - 1); + // whole past the last time point, so the line stays on its last position + final long inTimePoint = saved - (timeIndex - firstTimePoint) * perTimePoint; + final int positionIndex = + (int) Math.min(inTimePoint / shape.imagesPerPosition, shape.numPositions - 1); + final long images = Math.max(0L, imagesBefore) + saved; + // nothing of this time point yet: waiting for it if it is not due + if (timeIndex > 0 && inTimePoint == 0) { + final long ms = schedule.msUntil(timeIndex); + if (ms > 0) { + return new RunProgress(Status.WAITING, shape, timeIndex + 1, 0, ms, images); + } + } + return new RunProgress(Status.ACQUIRING, shape, timeIndex + 1, positionIndex + 1, 0L, + images); + } + + /** + * How the run ended, from where it was. A run that opened no dataset ended with nothing. + */ + RunProgress ended(final boolean failed, final boolean stopped) { + if (shape == null) { + return NONE; + } + final Status end; + if (failed) { + end = Status.FAILED; + } else if (images >= imagesExpected()) { + // every image is in, whenever the stop came + end = Status.DONE; + } else if (stopped) { + end = Status.STOPPED; + } else { + end = Status.SHORT; + } + return new RunProgress(end, shape, timePoint, position, 0L, images); + } + + /** + * A snapshot never changes, so it is its own source. + */ + @Override + public RunProgress get() { + return this; + } + + public Status status() { + return status; + } + + /** + * The images so far, over all the run's datasets. + */ + public long images() { + return images; + } + + /** + * The images the run is built for, or 0 until its size is known. + */ + public long imagesExpected() { + return shape == null ? 0L : shape.numTimePoints * shape.imagesPerTimePoint(); + } + + /** + * The images so far against the images expected, or empty until the run's size is known. + */ + public String describeImages() { + final long expected = imagesExpected(); + return expected == 0 ? "" : count(images) + " of " + count(expected) + " images"; + } + + /** + * The text of the status line. + */ + public String describe() { + switch (status) { + case STARTING: + return "Starting"; + case ACQUIRING: + return "Acquiring" + where(); + case WAITING: + return "Next time point (" + count(timePoint) + " of " + count(shape.numTimePoints) + + ") in " + count(msToNextTimePoint / 1000) + " s"; + case DONE: + return "Finished with " + amount(shape.numTimePoints, "time point") + " and " + + amount(shape.numPositions, "position"); + case SHORT: + return "Finished with " + amount(imagesExpected() - images, "image") + " missing"; + case STOPPED: + return "Stopped" + whereItEnded(); + case FAILED: + return "Failed" + whereItEnded(); + default: + return "No acquisition in progress"; + } + } + + // a count is named only when the run has more than one + private String where() { + final StringBuilder text = new StringBuilder(); + if (shape.numTimePoints > 1) { + text.append(" time point ").append(count(timePoint)) + .append(" of ").append(count(shape.numTimePoints)); + } + if (shape.numPositions > 1) { + if (shape.numTimePoints > 1) { + text.append(';'); + } + text.append(" position ").append(count(position)) + .append(" of ").append(count(shape.numPositions)); + } + return text.toString(); + } + + private String whereItEnded() { + if (position == 0) { + return " before time point " + count(timePoint) + " of " + count(shape.numTimePoints); + } + final String where = where(); + return where.isEmpty() ? "" : " at" + where; + } + + private static String amount(final long n, final String noun) { + return count(n) + " " + noun + (n == 1 ? "" : "s"); + } + + // a new instance each time: a NumberFormat is not thread safe + private static String count(final long n) { + return NumberFormat.getIntegerInstance().format(n); + } + + /** + * The size of a run. + */ + static final class Shape { + final int numTimePoints; + final int numPositions; + // slices x channels x cameras + final int imagesPerPosition; + + Shape(final int numTimePoints, final int numPositions, final int imagesPerPosition) { + this.numTimePoints = Math.max(1, numTimePoints); + this.numPositions = Math.max(1, numPositions); + this.imagesPerPosition = Math.max(1, imagesPerPosition); + } + + long imagesPerTimePoint() { + return (long) numPositions * imagesPerPosition; + } + } + + /** + * When each time point of a run is due. + */ + interface Schedule { + /** Nothing waits: the controller paces the time points. */ + Schedule NONE = timeIndex -> 0L; + + /** + * @param timeIndex the time point, from 0 + * @return the milliseconds until it is due, zero or less once it is due or with no start + */ + long msUntil(int timeIndex); + } + + /** + * The schedule of a run in one dataset: time point t is due t intervals after the + * acquisition's start time, on the wall clock AcqEngJ waits on. + * + * @param startMs the acquisition's start time, negative until the first image + */ + static Schedule fromAcquisitionStart(final LongSupplier startMs, final double intervalMs, + final LongSupplier nowMs) { + return timeIndex -> { + final long start = startMs.getAsLong(); + if (start < 0) { + return 0L; + } + return start + (long) (timeIndex * intervalMs) - nowMs.getAsLong(); + }; + } + + /** + * The schedule of a separate time point series: time point t is due t intervals after the + * series started, on the monotonic clock the series waits on. + */ + static Schedule fromSeriesStart(final long startNs, final long intervalMs, + final LongSupplier nowNs) { + return timeIndex -> (startNs + timeIndex * intervalMs * 1_000_000L - nowNs.getAsLong()) + / 1_000_000L; + } + + /** + * Reads a run's progress from the count of its current dataset. + */ + static final class Source implements Supplier { + private final Shape shape; + private final int firstTimePoint; + private final long imagesBefore; + private final LongSupplier inDataset; + private final Schedule schedule; + + Source(final Shape shape, final int firstTimePoint, final long imagesBefore, + final LongSupplier inDataset, final Schedule schedule) { + this.shape = Objects.requireNonNull(shape); + this.firstTimePoint = firstTimePoint; + this.imagesBefore = imagesBefore; + this.inDataset = Objects.requireNonNull(inDataset); + this.schedule = Objects.requireNonNull(schedule); + } + + // the count first: AcqEngJ sets its start time before the first image is stored, so a + // schedule read after a count above zero sees it + @Override + public RunProgress get() { + return during(shape, firstTimePoint, imagesBefore, inDataset.getAsLong(), schedule); + } + } +} diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java new file mode 100644 index 00000000..bc496b60 --- /dev/null +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java @@ -0,0 +1,121 @@ +package org.micromanager.lightsheetmanager.model.acquisitions; + +import org.micromanager.LogManager; +import org.micromanager.acqj.main.Acquisition; +import org.micromanager.data.Datastore; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Follows a run for the status line. The acquisition thread reports what happens, and the + * interface thread asks where the run is. + */ +final class RunProgressTracker { + + private final Supplier datastore; + + // visibility: written on the acquisition thread, read on the edt by the status line + private volatile Supplier progress = RunProgress.none(); + + /** + * @param datastore the dataset in flight, or null with no dataset open + */ + RunProgressTracker(final Supplier datastore) { + this.datastore = Objects.requireNonNull(datastore); + } + + /** + * A snapshot of where the run in flight is, or of how the last run ended. + */ + RunProgress now() { + return progress.get(); + } + + /** + * A run starts, so the line leaves the last run's end. + */ + void start() { + progress = RunProgress.starting(); + } + + /** + * Publishes where the status line reads the progress from. Call this as each dataset starts + * and between the datasets of a series; an engine that never does shows only Starting. + */ + void show(final Supplier source) { + progress = Objects.requireNonNull(source); + } + + /** + * Keeps the progress as it is now, so the status line stops reading a complete dataset's + * store before it is let go. + */ + void freeze() { + progress = progress.get(); + } + + /** + * The images the dataset in flight holds so far, or 0 with no dataset open. + */ + long imagesInDataset() { + final Datastore store = datastore.get(); + if (store == null) { + return 0L; + } + try { + // -1 before the store has storage + return Math.max(0L, store.getNumImages()); + } catch (RuntimeException e) { + // a store closes with no lock, and a read landing in that must not fail the redraw or + // the run's cleanup + return 0L; + } + } + + /** + * Leaves the status line on how the run ended. A failure also sets the abort flag, so a + * failure wins over a stop. + * + * @param acquisition the run's acquisition, or null if it never had one + * @param stopRequested true if a stop was requested + */ + void end(final Acquisition acquisition, final boolean stopRequested) { + final boolean failed = acquisition != null && keptAnException(acquisition); + final boolean stopped = stopRequested + || (acquisition != null && acquisition.isAbortRequested()); + progress = progress.get().ended(failed, stopped); + } + + /** + * Logs the images of a run that ended short, stopped or failed, since the status line keeps + * them only until the next run starts. + */ + void logEnd(final LogManager logs) { + final RunProgress end = progress.get(); + final String stored = "stored " + end.images() + " of " + end.imagesExpected() + " images"; + switch (end.status()) { + case SHORT: + logs.logError("Acquisition finished short: " + stored); + break; + case FAILED: + logs.logError("Acquisition failed: " + stored); + break; + case STOPPED: + // the user's own choice, so not an error + logs.logMessage("Acquisition stopped: " + stored); + break; + default: + break; + } + } + + private static boolean keptAnException(final Acquisition acquisition) { + try { + acquisition.checkForExceptions(); + return false; + } catch (Exception e) { + return true; + } + } +} From 86485f110b56d89fd87d78287c758afaab11fc90 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 3 Nov 2026 18:06:10 -0800 Subject: [PATCH 2/3] report run progress from the scape engine --- .../acquisitions/AcquisitionEngineScape.java | 40 +++++++++++++++++ .../model/acquisitions/RunProgress.java | 13 +++--- .../acquisitions/RunProgressTracker.java | 44 +++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java index b0ee80c4..3a9bb40c 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/AcquisitionEngineScape.java @@ -352,7 +352,11 @@ boolean run() { // never wait on an Acquisition that was not finished: the wait is unbounded return false; } + tracker_.show(new RunProgress.Source(progressShape(settings, cameraNames), 0, 0L, + tracker_::imagesInDataset, runSchedule(settings, currentAcquisition_))); currentAcquisition_.waitForCompletion(); + // the dataset is complete, so its count is final + tracker_.freeze(); return true; } @@ -368,6 +372,31 @@ private static boolean isSeparatingTimePoints(final ScapeAcquisitionSettings set return settings.isUsingTimePoints() && settings.isUsingSeparateTimePoints(); } + /** + * The size of the run the status line counts against, as the events are built. + */ + private RunProgress.Shape progressShape(final ScapeAcquisitionSettings settings, + final String[] cameraNames) { + final int numPositions = settings.isUsingMultiplePositions() + ? positionList_.getNumberOfPositions() : 1; + return new RunProgress.Shape(numTimePointsToAcquire(settings), numPositions, + settings.volume().slicesPerView() * settings.channels().count() + * cameraNames.length); + } + + /** + * When each time point of a run in one dataset is due, on AcqEngJ's schedule. Under hardware + * time points the controller paces them and nothing waits. + */ + private static RunProgress.Schedule runSchedule(final ScapeAcquisitionSettings settings, + final Acquisition acquisition) { + if (settings.isUsingHardwareTimePoints()) { + return RunProgress.Schedule.NONE; + } + return RunProgress.fromAcquisitionStart(acquisition::getStartTimeMs, + settings.timePointIntervalSec() * 1000.0, System::currentTimeMillis); + } + /** * Resolves the camera device names the events are addressed to. * @@ -972,6 +1001,10 @@ private boolean runSeparateTimePoints(final ScapeAcquisitionSettings settings, } final long seriesStartNs = System.nanoTime(); + // the status line: the run's size, on the clock the wait below uses + final RunProgressTracker.Series status = tracker_.series( + progressShape(settings, cameraNames), + RunProgress.fromSeriesStart(seriesStartNs, intervalMs, System::nanoTime)); // directory names of the completed datasets final List datasetNames = new ArrayList<>(); // the time points that failed, reported together when the series ends @@ -984,6 +1017,9 @@ private boolean runSeparateTimePoints(final ScapeAcquisitionSettings settings, int inFlightIndex = -1; try { for (int timeIndex = 0; timeIndex < numTimePoints; timeIndex++) { + if (timeIndex > 0) { + status.awaiting(timeIndex); + } if (!awaitTimePointSlot(seriesStartNs, timeIndex, intervalMs)) { break; // stop requested during the wait } @@ -998,12 +1034,14 @@ private boolean runSeparateTimePoints(final ScapeAcquisitionSettings settings, timeIndex)) { break; // nothing started, so nothing to wait for } + status.acquiring(timeIndex); if (previousStore != null) { closeDisplaysOnInterfaceThread(previousStore, timeIndex - 1); previousStore = null; } // returns once the store is frozen, so the dataset is complete on disk currentAcquisition_.waitForCompletion(); + status.datasetComplete(); final String datasetDir = datastore_.getSavePath(); previousStore = datastore_; @@ -1015,6 +1053,8 @@ private boolean runSeparateTimePoints(final ScapeAcquisitionSettings settings, } } } finally { + // where the series stopped, not where the countdown is once the cleanup ends + tracker_.freeze(); // no save path means no directory was created, so there is nothing to mark final String inFlightDir = datastore_ == null ? null : datastore_.getSavePath(); if (inFlightIndex >= 0 && inFlightDir != null) { diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java index 43aec596..e9dff06e 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgress.java @@ -38,6 +38,8 @@ public enum Status { private static final RunProgress NONE = new RunProgress(Status.NONE, null, 0, 0, 0L, 0L); private static final RunProgress STARTING = new RunProgress(Status.STARTING, null, 0, 0, 0L, 0L); + private static final RunProgress FAILED_START = + new RunProgress(Status.FAILED, null, 0, 0, 0L, 0L); private final Status status; // null until the run's size is known @@ -97,11 +99,12 @@ static RunProgress during(final Shape shape, final int firstTimePoint, final lon } /** - * How the run ended, from where it was. A run that opened no dataset ended with nothing. + * How the run ended, from where it was. A run that opened no dataset ended with nothing, + * unless its start failed. */ RunProgress ended(final boolean failed, final boolean stopped) { if (shape == null) { - return NONE; + return failed ? FAILED_START : NONE; } final Status end; if (failed) { @@ -171,7 +174,7 @@ public String describe() { case STOPPED: return "Stopped" + whereItEnded(); case FAILED: - return "Failed" + whereItEnded(); + return shape == null ? "Failed to start" : "Failed" + whereItEnded(); default: return "No acquisition in progress"; } @@ -291,8 +294,8 @@ static final class Source implements Supplier { this.schedule = Objects.requireNonNull(schedule); } - // the count first: AcqEngJ sets its start time before the first image is stored, so a - // schedule read after a count above zero sees it + // the count first: AcqEngJ sets its start time before the first image is stored, and a late + // read of it costs one tick @Override public RunProgress get() { return during(shape, firstTimePoint, imagesBefore, inDataset.getAsLong(), schedule); diff --git a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java index bc496b60..69ef86c0 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java +++ b/src/main/java/org/micromanager/lightsheetmanager/model/acquisitions/RunProgressTracker.java @@ -55,6 +55,10 @@ void freeze() { progress = progress.get(); } + Series series(final RunProgress.Shape shape, final RunProgress.Schedule schedule) { + return new Series(shape, schedule); + } + /** * The images the dataset in flight holds so far, or 0 with no dataset open. */ @@ -110,6 +114,46 @@ void logEnd(final LogManager logs) { } } + /** + * Follows a series of separate time points: one dataset per time point, with a countdown + * between them and the images of the datasets complete. + */ + final class Series { + private final RunProgress.Shape shape; + private final RunProgress.Schedule schedule; + // the images of the datasets complete so far, whole or not + private long imagesComplete; + + private Series(final RunProgress.Shape shape, final RunProgress.Schedule schedule) { + this.shape = shape; + this.schedule = schedule; + } + + /** + * The dataset is not open yet, so the line counts down to it. + */ + void awaiting(final int timeIndex) { + show(new RunProgress.Source(shape, timeIndex, imagesComplete, () -> 0L, schedule)); + } + + /** + * The dataset is open, so the line follows its count. + */ + void acquiring(final int timeIndex) { + show(new RunProgress.Source(shape, timeIndex, imagesComplete, + RunProgressTracker.this::imagesInDataset, schedule)); + } + + /** + * Its count is final, so it joins the total and the line stops reading the store before + * it is let go. + */ + void datasetComplete() { + imagesComplete += imagesInDataset(); + freeze(); + } + } + private static boolean keptAnException(final Acquisition acquisition) { try { acquisition.checkForExceptions(); From 7b91d99b06830b760f2b482470e831a2733cbaeb Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 3 Nov 2026 18:19:51 -0800 Subject: [PATCH 3/3] add a status line and progress bar to the acquisition tab --- .../gui/tabs/AcquisitionTab.java | 7 ++ .../gui/tabs/acquisition/StatusPanel.java | 86 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/StatusPanel.java 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 fc5c9768..c90633a3 100644 --- a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/AcquisitionTab.java @@ -15,6 +15,7 @@ import org.micromanager.lightsheetmanager.gui.tabs.acquisition.PositionPanel; import org.micromanager.lightsheetmanager.gui.tabs.acquisition.SavePanel; import org.micromanager.lightsheetmanager.gui.tabs.acquisition.SlicePanel; +import org.micromanager.lightsheetmanager.gui.tabs.acquisition.StatusPanel; import org.micromanager.lightsheetmanager.gui.tabs.acquisition.TimePointsPanel; import org.micromanager.lightsheetmanager.gui.tabs.acquisition.DurationPanel; import org.micromanager.lightsheetmanager.gui.tabs.acquisition.VolumePanel; @@ -49,6 +50,9 @@ public class AcquisitionTab extends Panel implements ListeningPanel, SettingsLis // durations private DurationPanel pnlDurations_; + // where the run is + private StatusPanel pnlStatus_; + // time points private TimePointsPanel pnlTimePoints_; private CheckBox cbxUseTimePoints_; @@ -113,6 +117,7 @@ private void createUserInterface() { ); pnlDurations_ = new DurationPanel(model_); + pnlStatus_ = new StatusPanel(model_); pnlVolumeSettings_ = new VolumePanel(model_); // switch between these two panels @@ -219,6 +224,8 @@ private void createUserInterface() { pnlCenter.add(pnlChannelTable_, "wrap"); pnlCenter.add(new JLabel("Acquisition Mode:"), "split 2"); pnlCenter.add(cmbAcquisitionModes_, ""); + // as wide as the channel table, so a long message wraps instead of widening the column + pnlCenter.add(pnlStatus_, "newline, w 280!"); pnlRight_.add(pnlVolumeSettings_, "growx, wrap"); pnlRight_.add(pnlSliceSettings_, "growx, wrap"); diff --git a/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/StatusPanel.java b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/StatusPanel.java new file mode 100644 index 00000000..4956822c --- /dev/null +++ b/src/main/java/org/micromanager/lightsheetmanager/gui/tabs/acquisition/StatusPanel.java @@ -0,0 +1,86 @@ +package org.micromanager.lightsheetmanager.gui.tabs.acquisition; + +import org.micromanager.lightsheetmanager.LightSheetManager; +import org.micromanager.lightsheetmanager.gui.components.Panel; +import org.micromanager.lightsheetmanager.model.acquisitions.AcquisitionEngine; +import org.micromanager.lightsheetmanager.model.acquisitions.RunProgress; + +import javax.swing.JLabel; +import javax.swing.JProgressBar; +import javax.swing.SwingConstants; +import javax.swing.Timer; +import java.util.Objects; + +/** + * The status line of the acquisition tab: where the run is, how long until the next time point + * and how the last run ended, with a bar of the images so far. + * + *

Polls the engine on the interface thread while the panel is displayable, so the + * acquisition's threads never touch Swing, and a run started from a script shows too. + */ +public class StatusPanel extends Panel { + + // often enough that the countdown steps once a second + private static final int POLL_MS = 250; + + // the bar's resolution + private static final int BAR_STEPS = 1000; + + private final AcquisitionEngine engine; + private final JLabel lblStatus; + private final JProgressBar barImages; + private final JLabel lblImages; + private final Timer timer; + + public StatusPanel(final LightSheetManager model) { + super("Status"); + engine = Objects.requireNonNull(model).acquisitions(); + + // the bar takes the width the image count leaves + setMigLayout("", "[grow, fill][]", ""); + + lblStatus = new JLabel(); + // two lines high, so a long message wraps instead of resizing the panel + lblStatus.setVerticalAlignment(SwingConstants.TOP); + final int lineHeight = lblStatus.getFontMetrics(lblStatus.getFont()).getHeight(); + + barImages = new JProgressBar(0, BAR_STEPS); + // the count goes beside the bar: some themes paint text on a bar in nearly the color of + // its filled part + lblImages = new JLabel(); + + add(lblStatus, "span 2, wmin 0, h " + 2 * lineHeight + "!, wrap"); + add(barImages, "wmin 0"); + add(lblImages, ""); + + timer = new Timer(POLL_MS, e -> refresh()); + refresh(); + } + + @Override + public void addNotify() { + super.addNotify(); + timer.start(); + } + + @Override + public void removeNotify() { + // the window is being disposed: a running timer would keep the panel alive + timer.stop(); + super.removeNotify(); + } + + /** + * Shows the engine's progress as it is now. The timer calls this while the panel is showing. + * A label set to the text it already shows is neither laid out again nor repainted. + */ + public void refresh() { + final RunProgress now = engine.progress(); + // html, so a message longer than the panel wraps onto the second line + lblStatus.setText("" + now.describe() + ""); + final long expected = now.imagesExpected(); + barImages.setValue(expected == 0 ? 0 + : (int) (BAR_STEPS * Math.min(now.images(), expected) / expected)); + lblImages.setText(now.describeImages()); + } +}