4343import javax .swing .JTextField ;
4444import javax .swing .SpinnerModel ;
4545import javax .swing .SpinnerNumberModel ;
46+ import javax .swing .SwingUtilities ;
4647import net .miginfocom .swing .MigLayout ;
4748import org .micromanager .Studio ;
4849import org .micromanager .acquisition .ChannelSpec ;
@@ -68,13 +69,22 @@ public class MultiMDAFrame extends JFrame {
6869 private final List <JLabel > acqExplanations_ = new ArrayList <>();
6970 private final List <JComboBox <String >> presetCombos_ = new ArrayList <>();
7071 private static final String USE_TIME_POINTS = "UseTimePoints" ;
72+ private static final String NR_FRAMES = "NumberOfFrames" ;
73+ private static final String INTERVAL = "Interval" ;
74+ private static final String TIME_UNIT = "TimeUnit" ;
7175 private static final String USE_AUTOFOCUS = "UseAutofocus" ;
76+ private static final String AF_SKIP_INTERVAL = "AutofocusSkipInterval" ;
7277 private static final String USE_PRESET = "UsePreset" ;
7378 private static final String PRESET_GROUP = "PresetGroup" ;
7479 private static final String NR_ACQ_SETTINGS = "NumberOfSettings" ;
7580 private static final String ACQ_PATHS = "AcquisitionPaths" ;
7681 private static final String POSITION_LIST_PATHS = "PositionListPaths" ;
7782 private final JSpinner nrSpinner_ ;
83+ private JButton acquireButton_ ;
84+ // Written on the acquisition thread, read on the EDT (button handler); volatile so
85+ // the EDT sees the start/clear promptly and the Run/Stop behavior stays consistent.
86+ private volatile MultiAcqEngJAdapter currentAcqj_ ;
87+ private final Color runButtonColor_ ;
7888 private final CheckBoxPanel framesPanel_ ;
7989 private final CheckBoxPanel autoFocusPanel_ ;
8090 private final CheckBoxPanel presetPanel_ ;
@@ -180,15 +190,27 @@ public MultiMDAFrame(Studio studio) {
180190 super .add (new JLabel ("" ), "growx" );
181191
182192 // Run an acquisition using the current MDA parameters.
183- JButton acquireButton = new JButton ("Run Multi MDA" );
184- acquireButton .addActionListener (e -> {
193+ acquireButton_ = new JButton ("Run Multi MDA" );
194+ runButtonColor_ = acquireButton_ .getForeground ();
195+ acquireButton_ .addActionListener (e -> {
196+ // This handler runs on the EDT. If an acquisition is already running (or one
197+ // is in the process of starting), this button acts as a Stop button.
198+ if (currentAcqj_ != null ) {
199+ currentAcqj_ .abortRequest ();
200+ return ;
201+ }
202+ // Claim the "running" slot here on the EDT, before starting the background
203+ // thread, so a fast double-click cannot pass the guard above twice and start
204+ // two acquisitions. The same adapter is reused inside run().
205+ final MultiAcqEngJAdapter acqj = new MultiAcqEngJAdapter (studio_ );
206+ currentAcqj_ = acqj ;
207+ setRunningState (true );
185208 // All GUI event handlers are invoked on the EDT (Event Dispatch
186209 // Thread). Acquisitions are not allowed to be started from the
187210 // EDT. Therefore, we must make a new thread to run this.
188211 Thread acqThread = new Thread (new Runnable () {
189212 @ Override
190213 public void run () {
191- final MultiAcqEngJAdapter acqj = new MultiAcqEngJAdapter (studio_ );
192214 SequenceSettings .Builder sb = new SequenceSettings .Builder ();
193215 double multiplier = 1 ;
194216 String token = (String ) timeUnitCombo_ .getSelectedItem ();
@@ -208,13 +230,26 @@ public void run() {
208230 studio_ .logs ().logError (ex );
209231 }
210232 SequenceSettings baseSettings = sb .build ();
211- acqj .runAcquisition (baseSettings , acqs_ );
233+ try {
234+ acqj .runAcquisition (baseSettings , acqs_ );
235+ // runAcquisition() returns once events are submitted, but the engine
236+ // keeps running on its own threads. Wait for it to actually finish (or
237+ // be aborted) before reverting the button to "Run".
238+ while (acqj .isAcquisitionRunning ()) {
239+ Thread .sleep (250 );
240+ }
241+ } catch (InterruptedException ex ) {
242+ Thread .currentThread ().interrupt ();
243+ } finally {
244+ currentAcqj_ = null ;
245+ setRunningState (false );
246+ }
212247 }
213248 });
214249 acqThread .start ();
215250 });
216251
217- super .add (acquireButton , "align right, push, gap 10, wrap" );
252+ super .add (acquireButton_ , "align right, push, gap 10, wrap" );
218253
219254 super .setIconImage (Toolkit .getDefaultToolkit ().getImage (
220255 getClass ().getResource ("/org/micromanager/icons/microscope.gif" )));
@@ -224,30 +259,42 @@ public void run() {
224259 // restore settings from previous session
225260 final MutablePropertyMapView settings = studio_ .profile ().getSettings (this .getClass ());
226261 framesPanel_ .setSelected (settings .getBoolean (USE_TIME_POINTS , false ));
262+ numFrames_ .setValue (settings .getInteger (NR_FRAMES , (Integer ) numFrames_ .getValue ()));
263+ interval_ .setValue (settings .getDouble (INTERVAL ,
264+ ((Number ) interval_ .getValue ()).doubleValue ()));
265+ timeUnitCombo_ .setSelectedItem (settings .getString (TIME_UNIT ,
266+ (String ) timeUnitCombo_ .getSelectedItem ()));
227267 autoFocusPanel_ .setSelected (settings .getBoolean (USE_AUTOFOCUS , false ));
268+ afSkipInterval_ .setValue (settings .getInteger (AF_SKIP_INTERVAL ,
269+ (Integer ) afSkipInterval_ .getValue ()));
228270 presetPanel_ .setSelected (settings .getBoolean (USE_PRESET , false ));
229271 int nrAcquisitions = settings .getInteger (NR_ACQ_SETTINGS , 0 );
230272 if (nrAcquisitions > 0 ) {
231273 List <String > acqPaths = settings .getStringList (ACQ_PATHS );
232274 List <String > positionListFiles = settings .getStringList (POSITION_LIST_PATHS );
233- int count = 0 ;
234- for (String acqPath : acqPaths ) {
235- File f = new File (acqPath );
275+ for (int origIndex = 0 ; origIndex < acqPaths .size (); origIndex ++) {
276+ File f = new File (acqPaths .get (origIndex ));
236277 SequenceSettings seqSb ;
237278 try {
238279 seqSb = studio_ .acquisitions ().loadSequenceSettings (f .getPath ());
239280 } catch (IOException ex ) {
240281 studio_ .logs ().logError (ex , "Failed to load Acquisition setting file" );
241282 continue ;
242283 }
243- acqs_ .add (count , new MDASettingData (studio_ , f , seqSb ));
244- if (positionListFiles .size () > count ) {
245- File posFile = new File (positionListFiles .get (count ));
246- acqs_ .get (count ).setPositionListFile (posFile );
284+ MDASettingData mdaSettingData = new MDASettingData (studio_ , f , seqSb );
285+ acqs_ .add (mdaSettingData );
286+ // The position list paths are persisted parallel to acqPaths, so index them
287+ // by the original position (not by the number of successfully loaded rows);
288+ // otherwise a failed acquisition load would shift every later pairing.
289+ // Rows without a position list are persisted as an empty path (see the
290+ // save logic). Skip those, otherwise setPositionListFile() tries to load
291+ // "" and throws FileNotFoundException on every startup.
292+ if (positionListFiles .size () > origIndex
293+ && !positionListFiles .get (origIndex ).isEmpty ()) {
294+ mdaSettingData .setPositionListFile (new File (positionListFiles .get (origIndex )));
247295 }
248- count ++;
249296 }
250- nrSpinner_ .setValue (count );
297+ nrSpinner_ .setValue (acqs_ . size () );
251298 }
252299
253300 super .pack ();
@@ -271,6 +318,12 @@ public void run() {
271318 private int adjustNrSettings (int nr ) {
272319 acqPanel_ .removeAll ();
273320 acqLabels_ .clear ();
321+ // These are rebuilt below alongside acqLabels_; clear them too or they keep
322+ // growing and the *.get(lineNr) lookups below return stale entries from a
323+ // previous build (e.g. a newly added acquisition showing an earlier row's
324+ // explanation/preset).
325+ acqExplanations_ .clear ();
326+ presetCombos_ .clear ();
274327 // add headers to the table
275328 acqPanel_ .add (new JLabel ("Acquisition Settings File" ), "span 2, alignx center" );
276329 acqPanel_ .add (new JLabel ("Preset" ), "alignx center" );
@@ -348,12 +401,15 @@ private int adjustNrSettings(int nr) {
348401 presetCombos_ .add (presetCombo );
349402 acqPanel_ .add (presetCombo , "gapx 20" );
350403
351- String positionListText = "current position" ;
404+ String positionListText = "current position (at run time) " ;
352405 if (acqs_ .get (lineNr ).getPositionList () != null
353406 && acqs_ .get (lineNr ).getPositionListFile () != null ) {
354407 positionListText = acqs_ .get (lineNr ).getPositionListFile ().getName ();
355408 }
356409 final JLabelC positionListLabel = new JLabelC (positionListText );
410+ positionListLabel .setToolTipText ("<html>When no position list is selected, the stage "
411+ + "position<br>at the time the acquisition is run is used "
412+ + "(not the<br>position when this acquisition was added).</html>" );
357413 acqPanel_ .add (positionListLabel , "gapx 20" );
358414
359415 JButton selectPosFile = new JButton ("..." );
@@ -363,12 +419,17 @@ private int adjustNrSettings(int nr) {
363419 "Load position list" , POSITION_LIST_FILE );
364420 if (f != null ) {
365421 acqs_ .get (lineNr ).setPositionListFile (f );
422+ // The load may have failed or yielded an empty list, in which case
423+ // getPositionList()/getPositionListFile() are null; reflect that by
424+ // showing "current position" rather than leaving a stale filename.
366425 if (acqs_ .get (lineNr ).getPositionList () != null ) {
367426 positionListLabel .setText (acqs_ .get (lineNr ).getPositionListFile ().getName ());
368- JLabel explanation = acqExplanations_ .get (lineNr );
369- if (explanation != null ) {
370- explanation .setText (oneLineSummary (acqs_ .get (lineNr )));
371- }
427+ } else {
428+ positionListLabel .setText ("current position (at run time)" );
429+ }
430+ JLabel explanation = acqExplanations_ .get (lineNr );
431+ if (explanation != null ) {
432+ explanation .setText (oneLineSummary (acqs_ .get (lineNr )));
372433 }
373434 }
374435 });
@@ -435,10 +496,6 @@ private CheckBoxPanel createTimePoints() {
435496 timeUnitCombo_ .setEnabled (framesPanel .isEnabled ());
436497 defaultTimesPanel .add (timeUnitCombo_ , "pad 0 -15 0 0, wrap" );
437498
438- JLabelC overrideLabel = new JLabelC ("Custom time intervals enabled" );
439- overrideLabel .setFont (new Font ("Arial" , Font .BOLD , 12 ));
440- overrideLabel .setForeground (Color .red );
441-
442499 return framesPanel ;
443500 }
444501
@@ -515,6 +572,30 @@ private CheckBoxPanel createPresetPanel() {
515572 return presetPanel ;
516573 }
517574
575+ /**
576+ * Switches the Run/Stop button between its two states. Safe to call from any
577+ * thread; the actual Swing mutation is marshalled onto the EDT.
578+ *
579+ * @param running true while an acquisition is running (button becomes a red
580+ * "Stop Multi MDA"), false otherwise ("Run Multi MDA").
581+ */
582+ private void setRunningState (boolean running ) {
583+ Runnable r = () -> {
584+ if (running ) {
585+ acquireButton_ .setText ("Stop Multi MDA" );
586+ acquireButton_ .setForeground (Color .RED );
587+ } else {
588+ acquireButton_ .setText ("Run Multi MDA" );
589+ acquireButton_ .setForeground (runButtonColor_ );
590+ }
591+ };
592+ if (SwingUtilities .isEventDispatchThread ()) {
593+ r .run ();
594+ } else {
595+ SwingUtilities .invokeLater (r );
596+ }
597+ }
598+
518599 private String oneLineSummary (MDASettingData mdaSettingData ) {
519600 StringBuilder sb = new StringBuilder ();
520601 if (mdaSettingData .getSequenceSettings ().useSlices ()) {
@@ -530,12 +611,13 @@ private String oneLineSummary(MDASettingData mdaSettingData) {
530611 }
531612 }
532613 if (mdaSettingData .getSequenceSettings ().usePositionList ()
614+ && mdaSettingData .getPositionListFile () != null
533615 && mdaSettingData .getPositionList () != null
534616 && mdaSettingData .getPositionList ().getNumberOfPositions () > 0 ) {
535617 sb .append ("Positions: " ).append (mdaSettingData .getPositionList ().getNumberOfPositions ());
536618 sb .append ("." );
537619 } else {
538- sb .append ("Positions: current only ." );
620+ sb .append ("Positions: current at run time ." );
539621 }
540622 if (mdaSettingData .getSequenceSettings ().save ()) {
541623 sb .append (" Saving." );
@@ -557,7 +639,11 @@ public void onApplicationShuttingDown(ShutdownCommencingEvent sce) {
557639 MutablePropertyMapView settings = studio_ .profile ().getSettings (this .getClass ());
558640 settings .putInteger (NR_ACQ_SETTINGS , (Integer ) nrSpinner_ .getValue ());
559641 settings .putBoolean (USE_TIME_POINTS , framesPanel_ .isSelected ());
642+ settings .putInteger (NR_FRAMES , (Integer ) numFrames_ .getValue ());
643+ settings .putDouble (INTERVAL , ((Number ) interval_ .getValue ()).doubleValue ());
644+ settings .putString (TIME_UNIT , (String ) timeUnitCombo_ .getSelectedItem ());
560645 settings .putBoolean (USE_AUTOFOCUS , autoFocusPanel_ .isSelected ());
646+ settings .putInteger (AF_SKIP_INTERVAL , (Integer ) afSkipInterval_ .getValue ());
561647 settings .putBoolean (USE_PRESET , presetPanel_ .isSelected ());
562648
563649 List <String > acqPaths = new ArrayList <>(acqs_ .size ());
0 commit comments