@@ -775,8 +775,8 @@ private void clearWarningCanvasLabel() {
775775 *
776776 * @param cmd The command to be run, exactly as listed in SNTUI's menu bar
777777 * @param args the option(s) that would fill the command's prompt. e.g.,
778- * 'runCommand("Load Demo Dataset...", "4. Hippocampal neuron (DIC
779- * timelapse)")'
778+ * 'runCommand("Load Demo Dataset/Tutorial ...", "Hippocampal
779+ * neuron (DIC timelapse)")'
780780 * @throws IllegalArgumentException if {@code cmd} is not found or supported.
781781 */
782782 public void runCommand (final String cmd , final String ... args ) throws IllegalArgumentException {
@@ -1401,9 +1401,18 @@ public void windowOpened(final WindowEvent e) {
14011401 public void windowClosing (final WindowEvent e ) {
14021402 if (plugin .isStreamMode ()) {
14031403 exitRequested ();
1404- } else if (bvvSNT != null && bvvSNT .getViewerFrame () != null ) {
1405- bvvSNT .getViewerFrame ().getViewerPanel ().stop ();
1406- bvvSNT .getViewerFrame ().dispose ();
1404+ return ;
1405+ }
1406+ // Always stop/dispose *this* window's own viewer, regardless of whether it is still
1407+ // the instance tracked in bvvSNT: an older Bvv left orphaned by a later setBvv() call
1408+ // (e.g. a second demo/tutorial opening its own instance without closing this one first)
1409+ // must still close itself when its own close button is clicked, rather than reaching
1410+ // for whatever instance bvvSNT currently happens to hold.
1411+ if (bvv .getViewerFrame () != null ) {
1412+ bvv .getViewerFrame ().getViewerPanel ().stop ();
1413+ bvv .getViewerFrame ().dispose ();
1414+ }
1415+ if (bvvSNT == bvv ) { // this window is (still) the tethered/active one; clear UI bookkeeping too
14071416 bvvSNT = null ;
14081417 if (bvvSeedHandler != null ) { bvvSeedHandler .dispose (); bvvSeedHandler = null ; }
14091418 syncBookmarksTabContent ();
@@ -1464,9 +1473,15 @@ public void windowOpened(final WindowEvent e) {
14641473 public void windowClosing (final WindowEvent e ) {
14651474 if (plugin .isStreamMode ()) {
14661475 exitRequested ();
1467- } else if (bdvSNT != null && bdvSNT .getViewerFrame () != null ) {
1468- bdvSNT .getViewerFrame ().getViewerPanel ().stop ();
1469- bdvSNT .getViewerFrame ().dispose ();
1476+ return ;
1477+ }
1478+ // See the twin comment in setBvvOnEDT's windowClosing: always close *this* window's own
1479+ // viewer, regardless of whether it is still the instance tracked in bdvSNT.
1480+ if (bdv .getViewerFrame () != null ) {
1481+ bdv .getViewerFrame ().getViewerPanel ().stop ();
1482+ bdv .getViewerFrame ().dispose ();
1483+ }
1484+ if (bdvSNT == bdv ) { // this window is (still) the tethered/active one; clear UI bookkeeping too
14701485 bdvSNT = null ;
14711486 if (bdvSeedHandler != null ) { bdvSeedHandler .dispose (); bdvSeedHandler = null ; }
14721487 syncBookmarksTabContent ();
@@ -3750,8 +3765,8 @@ private JMenu fileMenu(final ScriptInstaller installer) {
37503765 fileMenu .add (changeImpMenu );
37513766
37523767 // Load
3753- fileMenu .addSeparator ();
37543768 fileMenu .add (importSubmenu );
3769+ fileMenu .addSeparator ();
37553770 final JMenuItem fromDemo = getImportActionMenuItem (ImportAction .DEMO );
37563771 fromDemo .setIcon (IconFactory .menuIcon (GLYPH .GRADUATION_CAP ));
37573772 fromDemo .setToolTipText ("Load sample images and/or reconstructions" );
@@ -3937,7 +3952,7 @@ private JMenu fileMenu(final ScriptInstaller installer) {
39373952 urlItem .setIcon (IconFactory .menuIcon (GLYPH .GLOBE ));
39383953 importSubmenu .add (urlItem );
39393954 CalloutManager .add (fileMenu , CalloutManager .AUTO ,
3940- "Use <i>File > Load Demo Dataset...</i> to explore SNT<br>using sample images and reconstructions ." ,
3955+ "Use <i>File > Load Demo Dataset/Tutorial ...</i> to explore SNT<br>using sample images, reconstructions, and tutorials ." ,
39413956 calloutGroup (), 1 );
39423957 return fileMenu ;
39433958 }
@@ -4191,8 +4206,10 @@ private JMenu viewMenu() {
41914206 arrangeDialogsMenuItem .addActionListener (e -> {
41924207 final DialogLayout layout = arrangeCoreDialogs (true );
41934208 if (layout == null ) return ; // error already shown (corrupt prefs)
4194- // Classic mode: unchanged behavior: nothing else to do. Arrange the BVV/BDV viewer frame:
4195- if (plugin .isStreamMode ()) arrangeStreamViewer (layout );
4209+ if (plugin .isStreamMode ())
4210+ arrangeStreamViewer (layout ); // display-canvas placeholder + viewer frame
4211+ else
4212+ arrangeBigViewerOnly (layout ); // canvas is handled by arrangeCanvases(): just the viewer
41964213 });
41974214 viewMenu .add (arrangeDialogsMenuItem );
41984215 final JMenuItem arrangeWindowsMenuItem = new JMenuItem ("Arrange Tracing Views" );
@@ -5718,7 +5735,8 @@ private DialogLayout arrangeCoreDialogs(final boolean warnOnError) {
57185735 }
57195736
57205737 /**
5721- * Stream-mode continuation of {@link #arrangeCoreDialogs(boolean)}.
5738+ * Stream-mode continuation of {@link #arrangeCoreDialogs(boolean)}: positions the display-canvas
5739+ * placeholder (if any) above the BVV/BDV viewer frame, both in the "next column" it reserved
57225740 */
57235741 private void arrangeStreamViewer (final DialogLayout layout ) {
57245742 int viewerY = layout .usableY ();
@@ -5730,6 +5748,23 @@ private void arrangeStreamViewer(final DialogLayout layout) {
57305748 imp .getWindow ().toFront ();
57315749 viewerY += placeholderHeight + InternalUtils .MARGIN ;
57325750 }
5751+ arrangeBigViewer (layout , viewerY );
5752+ }
5753+
5754+ /**
5755+ * Classic-mode counterpart of {@link #arrangeStreamViewer(DialogLayout)}: the tracing canvas is
5756+ * already handled by {@link #arrangeCanvases(boolean)}/"Arrange Tracing Views", so this only
5757+ * places an active BVV/BDV viewer frame, if any, in the "next column" {@code layout} reserved
5758+ */
5759+ private void arrangeBigViewerOnly (final DialogLayout layout ) {
5760+ arrangeBigViewer (layout , layout .usableY ());
5761+ }
5762+
5763+ /**
5764+ * Positions the active BVV/BDV viewer frame, if any, in the "next column" {@code layout}
5765+ * reserved, starting at {@code viewerY} and filling the remaining usable area
5766+ */
5767+ private void arrangeBigViewer (final DialogLayout layout , final int viewerY ) {
57335768 final AbstractBigViewer viewer = getActiveBigViewer ();
57345769 if (viewer == null ) return ;
57355770 final JFrame viewerFrame = viewer .getViewerFrame ();
@@ -6144,6 +6179,54 @@ protected void reset() {
61446179 showStatus ("Resetting" , true );
61456180 }
61466181
6182+ /**
6183+ * Closes any tethered {@link Bvv}/{@link Bdv} viewer(s), if open. Unlike {@link #resetUI()}, this does not
6184+ * touch the loaded image or paths: closing a big-viewer *window* isn't data loss the way clearing paths is
6185+ * (the underlying {@code PathAndFillManager} is untouched), so this needs no confirmation and is safe to call
6186+ * unconditionally - e.g. from {@code DemoRunner} before loading a new demo, so a viewer left over from a
6187+ * previous one is never silently orphaned (see {@code AbstractBigViewer}/{@code setBvvOnEDT}'s
6188+ * {@code windowClosing} handler for what "orphaned" means here).
6189+ */
6190+ public void closeBigViewers () {
6191+ closeBigViewer (bvvSNT );
6192+ closeBigViewer (bdvSNT );
6193+ }
6194+
6195+ /**
6196+ * Full reset: closes any tethered {@link Bvv}/{@link Bdv} viewer (via {@link #closeBigViewers()}) and the
6197+ * current image, and clears all paths, restoring the UI to its initial, blank-canvas state. Intended for
6198+ * callers that need a guaranteed-fresh session regardless of whatever was open before.
6199+ *
6200+ * @return true if the reset completed; false if it was aborted (e.g. the current image has unsaved changes and the
6201+ * user did not resolve ImageJ's own "Save Changes?" prompt), in which case nothing else was changed
6202+ */
6203+ public boolean resetUI () {
6204+ abortCurrentOperation ();
6205+ closeBigViewers ();
6206+ if (plugin .getImagePlus () != null ) {
6207+ plugin .getImagePlus ().close ();
6208+ if (plugin .getImagePlus () != null ) // user did not resolve the 'Save Changes?' prompt
6209+ return false ;
6210+ }
6211+ plugin .closeAndResetAllPanes ();
6212+ plugin .getPathAndFillManager ().clear ();
6213+ resetState ();
6214+ showStatus ("Resetting" , true );
6215+ return true ;
6216+ }
6217+
6218+ /**
6219+ * Closes {@code viewer} (if non-null) by dispatching {@link WindowEvent#WINDOW_CLOSING} at its frame,
6220+ * rather than disposing it directly, so this runs the exact same stop()+dispose()+SNTUI-detach path
6221+ * that closing the window by hand would trigger (see {@code setBvvOnEDT}/{@code setBdvOnEDT}'s
6222+ * {@code windowClosing} handler).
6223+ */
6224+ private void closeBigViewer (final AbstractBigViewer viewer ) {
6225+ if (viewer != null && viewer .getViewerFrame () != null ) {
6226+ viewer .getViewerFrame ().dispatchEvent (new WindowEvent (viewer .getViewerFrame (), WindowEvent .WINDOW_CLOSING ));
6227+ }
6228+ }
6229+
61476230 protected void ctPositionChanged () {
61486231 final ImagePlus imp = plugin .getImagePlus ();
61496232 final JPanel newSourcePanel = sourcePanel (imp );
@@ -6908,7 +6991,7 @@ static String getImportActionName(final int type) {
69086991 case ImportAction .IMAGE -> "From File..." ;
69096992 case ImportAction .ANY_RECONSTRUCTION -> "Guess File Type..." ;
69106993 case ImportAction .JSON -> "JSON..." ;
6911- case ImportAction .DEMO -> "Load Demo Dataset..." ;
6994+ case ImportAction .DEMO -> "Load Demo Dataset/Tutorial ..." ;
69126995 case ImportAction .NDF -> "NDF..." ;
69136996 case ImportAction .NEUROLUCIDA -> "Neurolucida XML..." ;
69146997 case ImportAction .TRACES -> "TRACES..." ;
@@ -6928,7 +7011,7 @@ static int getImportActionType(final String name) {
69287011 case "From File..." -> ImportAction .IMAGE ;
69297012 case "Guess File Type..." -> ImportAction .ANY_RECONSTRUCTION ;
69307013 case "JSON..." -> ImportAction .JSON ;
6931- case "Load Demo Dataset..." -> ImportAction .DEMO ;
7014+ case "Load Demo Dataset/Tutorial ..." , "Load Demo Dataset..." -> ImportAction .DEMO ; // 2nd form: backwards compatibility
69327015 case "NDF..." -> ImportAction .NDF ;
69337016 case "Neurolucida XML..." -> ImportAction .NEUROLUCIDA ;
69347017 case "TRACES..." -> ImportAction .TRACES ;
@@ -7488,10 +7571,10 @@ private void run() {
74887571 }
74897572 final DemoRunner demoRunner = new DemoRunner (SNTUI .this , plugin );
74907573 if (file != null ) { // recorded command
7491- try ( final Scanner scanner = new Scanner ( file . getName ())) {
7492- demoRunner .load (scanner . useDelimiter ( " \\ D+" ). nextInt ());
7574+ try {
7575+ demoRunner .load (file . getName ()); // resolves label or legacy/bare id
74937576 return ;
7494- } catch (final NoSuchElementException | IllegalStateException | IllegalArgumentException ex ) {
7577+ } catch (final IllegalArgumentException ex ) {
74957578 throw new IllegalArgumentException ("Invalid recorded option " + ex .getMessage ());
74967579 }
74977580 }
0 commit comments