Skip to content

Commit 81adef4

Browse files
authored
Merge pull request micro-manager#2392 from nicost/multiMDAPositions
Studio: MultiMDA: fix confusing behavior when use positions was not checked in the acquisition settings used in the MultiMDA.
2 parents cc1078b + e4322a7 commit 81adef4

3 files changed

Lines changed: 219 additions & 66 deletions

File tree

mmstudio/src/main/java/org/micromanager/acquisition/internal/acqengjcompat/multimda/MDASettingData.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
/**
99
* Data structure to remember the AcqSettings file, as well as
1010
* PositionList.
11+
*
12+
* <p>Note: the acqSettings_.usePositionList() flag is always derived from whether a
13+
* position list with positions is currently loaded for this row; it is never taken
14+
* directly from a user toggle. Both setPositionListFile() and setAcqSettings()
15+
* re-derive it so the GUI explanation and the executor stay consistent.
1116
*/
1217
public class MDASettingData {
1318
private final Studio studio_;
@@ -25,11 +30,32 @@ public MDASettingData(Studio studio, File acqSettingFile, SequenceSettings acqSe
2530
}
2631

2732
public void setPositionListFile(File positionListFile) {
28-
positionList_ = new PositionList();
33+
// Load into a temporary list so a failed load does not leave a stale
34+
// positionListFile_ around (which would otherwise be persisted on shutdown
35+
// and keep the UI showing the old filename even though positions are disabled).
36+
PositionList positionList = new PositionList();
2937
try {
30-
positionList_.load(positionListFile);
31-
positionListFile_ = positionListFile;
38+
positionList.load(positionListFile);
39+
if (positionList.getNumberOfPositions() > 0) {
40+
positionList_ = positionList;
41+
positionListFile_ = positionListFile;
42+
} else {
43+
// An empty list is treated the same as "no position list": keep both
44+
// references null so the UI shows "current position" instead of a filename
45+
// that would not actually be used at run time.
46+
positionList_ = null;
47+
positionListFile_ = null;
48+
}
49+
// A separate position list file does not flip the usePositionList flag that
50+
// came from the acquisition settings file, so set it here based on the loaded
51+
// list. Both the GUI explanation and the executor read this flag.
52+
acqSettings_ = new SequenceSettings.Builder(acqSettings_)
53+
.usePositionList(positionList_ != null).build();
3254
} catch (Exception e) {
55+
positionList_ = null;
56+
positionListFile_ = null;
57+
acqSettings_ = new SequenceSettings.Builder(acqSettings_)
58+
.usePositionList(false).build();
3359
studio_.logs().showError(e);
3460
}
3561
}
@@ -52,7 +78,11 @@ public File getAcqSettingFile() {
5278

5379
public void setAcqSettings(File acqFile, SequenceSettings acqSettings) {
5480
acqSettingFile_ = acqFile;
55-
acqSettings_ = acqSettings;
81+
// A newly loaded acquisition settings file would clobber the usePositionList flag,
82+
// so re-apply it based on whether a position list is currently loaded for this row.
83+
acqSettings_ = new SequenceSettings.Builder(acqSettings)
84+
.usePositionList(positionList_ != null
85+
&& positionList_.getNumberOfPositions() > 0).build();
5686
}
5787

5888
public void setPresetGroup(String presetGroup) {

mmstudio/src/main/java/org/micromanager/acquisition/internal/acqengjcompat/multimda/MultiMDAFrame.java

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import javax.swing.JTextField;
4444
import javax.swing.SpinnerModel;
4545
import javax.swing.SpinnerNumberModel;
46+
import javax.swing.SwingUtilities;
4647
import net.miginfocom.swing.MigLayout;
4748
import org.micromanager.Studio;
4849
import 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

Comments
 (0)