@@ -106,6 +106,7 @@ public static Bvv getInstance() {
106106 // 2nd row of #sntToolbar(): tracks the A* search started by Tracer#traceSegmentAsync, if any
107107 private JProgressBar tracingStatusBar ;
108108 private JButton tracingCancelButton ;
109+ private JButton tracingUndoButton ;
109110 private JToggleButton slabAnnotationsToggle ; // Slab Annotations toggle injected into BookmarkManager's toolbar
110111 private PathOverlay pathOverlay ;
111112 private AnnotationOverlay annotationOverlay ;
@@ -1465,6 +1466,8 @@ private void attachControlPanel(final BvvSource source) {
14651466 sntAMap .put ("snt-finish-path" , tracer .getFinishPathAction ());
14661467 sntIMap .put (KeyStroke .getKeyStroke (java .awt .event .KeyEvent .VK_ESCAPE , 0 ), "snt-discard-path" );
14671468 sntAMap .put ("snt-discard-path" , tracer .getDiscardPathAction ());
1469+ sntIMap .put (KeyStroke .getKeyStroke (java .awt .event .KeyEvent .VK_Z , 0 ), "snt-undo-segment" ); //See Tracer#undoLastSegment
1470+ sntAMap .put ("snt-undo-segment" , tracer .getUndoSegmentAction ());
14681471 }
14691472 sntIMap .put (KeyStroke .getKeyStroke (java .awt .event .KeyEvent .VK_K , 0 ), "snt-capture-keyframe" );
14701473 sntAMap .put ("snt-capture-keyframe" , new AbstractAction () {
@@ -1780,14 +1783,14 @@ private JToolBar sourceTransformsToolbar(final BvvActions actions) {
17801783
17811784 private JComponent sntToolbar (final BvvActions actions ) {
17821785 final JToolBar toolbar = createToolbar ();
1783-
1786+ final float SCALING_FACTOR = 1.1f ;
17841787 // group 1: tracing controls (auto/manual)
17851788 if (tracer != null ) {
17861789 final ButtonGroup bg1 = GuiUtils .Buttons .noneSelectedButtonGroup ();
1787- final JToggleButton b1 = GuiUtils . Buttons . toolbarToggleButton (tracer .getToggleAction (true ),
1788- "Start/stop manual tracing" , IconFactory . GLYPH . PEN , IconFactory . GLYPH . PEN );
1789- final JToggleButton b2 = GuiUtils . Buttons . toolbarToggleButton (tracer .getToggleAction (false ),
1790- "Start/stop interactive tracing" , IconFactory . GLYPH . ROUTE , IconFactory . GLYPH . ROUTE );
1790+ final JToggleButton b1 = scaledToggleButton (tracer .getToggleAction (true ),
1791+ IconFactory . GLYPH . PEN , "Start/stop manual tracing" );
1792+ final JToggleButton b2 = scaledToggleButton (tracer .getToggleAction (false ),
1793+ IconFactory . GLYPH . ROUTE , "Start/stop interactive tracing" );
17911794 bg1 .add (b1 );
17921795 bg1 .add (b2 );
17931796 toolbar .add (b1 );
@@ -1806,7 +1809,7 @@ private JComponent sntToolbar(final BvvActions actions) {
18061809 }
18071810 toolbar .addSeparator ();
18081811 final GuiUtils .Buttons .OptionsButton optionsButton =
1809- GuiUtils .Buttons .OptionsButton (IconFactory .GLYPH .CROSSHAIR , 1f , popupMenu );
1812+ GuiUtils .Buttons .OptionsButton (IconFactory .GLYPH .CROSSHAIR , SCALING_FACTOR , popupMenu );
18101813 optionsButton .setToolTipText ("<html>Choose when to recenter the view on a tracing click:"
18111814 + "<br> <b>Never</b>: Keep the current view as-is"
18121815 + "<br> <b>Always</b>: Recenter on every click (best for in-core data)"
@@ -1818,38 +1821,31 @@ private JComponent sntToolbar(final BvvActions actions) {
18181821 }
18191822
18201823 // group 2: annotations/paths
1821- final JToggleButton toggleAroundCursor = GuiUtils .Buttons .toolbarToggleButton (actions .togglePersistentAnnotationsAction (),
1822- "Restrict display of annotations around cursor" ,
1823- IconFactory .GLYPH .COMPUTER_MOUSE , IconFactory .GLYPH .COMPUTER_MOUSE );
1824- final JToggleButton offsetActivate = GuiUtils .Buttons .toolbarToggleButton (actions .setCanvasOffsetAction (),
1825- "Translate annotations from original signal" ,
1826- IconFactory .GLYPH .MOVE , IconFactory .GLYPH .MOVE );
1824+ final JToggleButton toggleAroundCursor = scaledToggleButton (actions .togglePersistentAnnotationsAction (),
1825+ IconFactory .GLYPH .COMPUTER_MOUSE , "Restrict display of annotations around cursor" );
1826+ final JToggleButton offsetActivate = scaledToggleButton (actions .setCanvasOffsetAction (),
1827+ IconFactory .GLYPH .MOVE , "Translate annotations from original signal" );
18271828 offsetActivate .setDisabledIcon (IconFactory .buttonIcon (IconFactory .GLYPH .MOVE , GuiUtils .getDisabledComponentColor (), 1f ));
18281829 final JToggleButton toggleVisibility = GuiUtils .Buttons .toolbarToggleButton (actions .toggleVisibilityAction (
18291830 toggleAroundCursor , offsetActivate ),
18301831 "<html>Show/hide annotations.<br>Hold H to temporarily hide annotations." ,
18311832 IconFactory .GLYPH .EYE , IconFactory .GLYPH .EYE_SLASH );
1833+ // rescale assigned icons to SCALING_FACTOR
1834+ IconFactory .assignIcon (toggleVisibility , IconFactory .GLYPH .EYE , IconFactory .GLYPH .EYE_SLASH , SCALING_FACTOR );
18321835
18331836 toolbar .add (toggleVisibility );
18341837 toolbar .addSeparator ();
18351838 toolbar .add (toggleAroundCursor );
18361839 toolbar .addSeparator ();
18371840 toolbar .add (offsetActivate );
18381841 toolbar .addSeparator ();
1839- if (tracer != null ) {
1840- toolbar .add (GuiUtils .Buttons .toolbarButton (actions .syncPathManagerAction (),
1841- "Sync Path Manager changes" ));
1842- toolbar .addSeparator ();
1843- }
18441842 toolbar .add (Box .createHorizontalGlue ());
18451843 toolbar .addSeparator ();
18461844
1847- // group 3: markers
1848- final JToggleButton markerButton = GuiUtils .Buttons .toolbarToggleButton (
1849- actions .showMarkerManagerAction (),
1850- "<html>Show/hide the Markers table.<br>"
1851- + "Press M in the viewer to place a marker at the cursor position." ,
1852- IconFactory .GLYPH .MARKER , IconFactory .GLYPH .MARKER );
1845+ // group 3: non-tracing actions
1846+ final JToggleButton markerButton = scaledToggleButton (actions .showMarkerManagerAction (),
1847+ IconFactory .GLYPH .MARKER , "<html>Show/hide the Markers table.<br>"
1848+ + "Press M in the viewer to place a marker at the cursor position." );
18531849 // Keep button state in sync with frame visibility
18541850 getMarkerManager ().getViewerDialogPanel ().addComponentListener (new java .awt .event .ComponentAdapter () {
18551851 @ Override
@@ -1867,7 +1863,11 @@ public void componentHidden(java.awt.event.ComponentEvent e) {
18671863 toolbar .add (Box .createHorizontalGlue ());
18681864 toolbar .addSeparator ();
18691865
1870- // group 4: options and settings
1866+ // group 4: options and settings: these buttons are made more discrete and not scaled
1867+ if (tracer != null ) {
1868+ toolbar .add (GuiUtils .Buttons .toolbarButton (actions .syncPathManagerAction (),
1869+ "Sync Path Manager changes" ));
1870+ }
18711871 toolbar .add (GuiUtils .Buttons .toolbarButton (actions .PathRenderingOptionsAction (),
18721872 "Set rendering options of annotations" ));
18731873
@@ -1882,20 +1882,37 @@ public void componentHidden(java.awt.event.ComponentEvent e) {
18821882 }
18831883 }
18841884
1885+ private JToggleButton scaledToggleButton (final Action action , final IconFactory .GLYPH glyph , final String tooltipText ) {
1886+ final JToggleButton button = new JToggleButton (action );
1887+ button .setText (null );
1888+ IconFactory .assignIcon (button , glyph , (Color ) null , (float ) 1.1 );
1889+ button .setToolTipText (tooltipText );
1890+ return button ;
1891+ }
1892+
18851893 /**
1886- * Builds the fixed second row shown below the main SNT toolbar: an indeterminate progress bar (empty when no search
1887- * is running) plus a Cancel button (enabled only while a search is ongoing). Kept as a constant row. Driven
1888- * by {@link #setTracingStatus(boolean, String)}, called from {@code Tracer#traceSegmentAsync}.
1894+ * Builds the second row below the main SNT toolbar: an Undo button (see {@code Tracer#undoLastSegment()}), an
1895+ * indeterminate progress bar (empty when no search is running), and a Cancel button (enabled only while a
1896+ * search is ongoing). Driven by {@link #setTracingStatus(boolean, String)}, called from
1897+ * {@code Tracer#traceSegmentAsync}.
18891898 */
18901899 private JComponent tracingStatusRow () {
1900+ assert tracer != null ;
18911901 tracingStatusBar = new JProgressBar ();
18921902 tracingStatusBar .setStringPainted (true );
18931903 tracingStatusBar .setString ("" );
18941904 tracingCancelButton = GuiUtils .Buttons .toolbarButton (IconFactory .GLYPH .CIRCLE_XMARK , IconFactory .defaultColor (), 1f );
18951905 tracingCancelButton .setToolTipText ("<HTML>Interactive tracing:<br>Cancel the current search" );
18961906 tracingCancelButton .setEnabled (false );
18971907 tracingCancelButton .addActionListener (e -> tracer .cancelCurrentSearch ());
1908+ tracingUndoButton = new JButton (tracer .getUndoSegmentAction ());
1909+ tracingUndoButton .setText (null );
1910+ IconFactory .assignIcon (tracingUndoButton , IconFactory .GLYPH .UNDO , (Color ) null , .9f );
1911+ tracingUndoButton .setToolTipText ("<HTML>Tracing:Undo last segment (or press Z)" );
1912+ tracer .updateUndoButtonState ();
18981913 final JToolBar row = new JToolBar ();
1914+ row .add (tracingUndoButton );
1915+ row .addSeparator ();
18991916 row .add (tracingStatusBar );
19001917 row .add (tracingCancelButton );
19011918 return row ;
@@ -3348,6 +3365,13 @@ private class Tracer extends MouseAdapter {
33483365 private Path .PathNode previousNode ;
33493366 private PointInImage previousForkPoint ;
33503367
3368+ // Undo mechanism, ported from SNT#confirmedSegmentSizes/SNT#undoLastSegment: each traced
3369+ // segment auto-confirms into tempPath (there is no separate temporary/confirm step here, as
3370+ // in the classic 2D canvas), so the "confirmed" node count of each segment is pushed here as
3371+ // it lands, and popped by undoLastSegment() to walk the path back one click at a time. Single-
3372+ // step undo only; no redo.
3373+ private final Deque <Integer > confirmedSegmentSizes = new ArrayDeque <>();
3374+
33513375 // Guards against a click being processed while a segment is still being traced in the background
33523376 // This keeps tempPath mutations single-threaded
33533377 private volatile boolean computing ;
@@ -3435,6 +3459,9 @@ private void handleClick(final MouseEvent e) {
34353459 }
34363460 }
34373461
3462+ confirmedSegmentSizes .clear (); // just in case of abnormal prior state
3463+ updateUndoButtonState ();
3464+
34383465 // this is the first click: we are starting a new Path. Lock in the channel/frame (and, for A*,
34393466 // the pixel data) for the whole path now, rather than re-checking on every segment, so a single
34403467 // path can't silently span two channels if the user switches BVV's active source mid-trace
@@ -3586,6 +3613,8 @@ private void finishPath() {
35863613 syncPathManagerList ();
35873614 tempPath = null ;
35883615 previousNode = null ;
3616+ confirmedSegmentSizes .clear ();
3617+ updateUndoButtonState ();
35893618 showViewerMessage ("Path finished" );
35903619 }
35913620
@@ -3638,6 +3667,8 @@ void discardCurrentPath() {
36383667 tempPath = null ;
36393668 pendingFinish = false ;
36403669 computing = false ;
3670+ confirmedSegmentSizes .clear ();
3671+ updateUndoButtonState ();
36413672 setTracingStatus (false , null );
36423673 if (tracingOverlay != null ) tracingOverlay .clear ();
36433674 if (pathOverlay != null ) pathOverlay .updatePaths (); // get rid of the stale preview segment
@@ -3697,8 +3728,11 @@ protected void done() {
36973728 ? "Search cancelled" : "Segment could not be traced (out of bounds?)" );
36983729 } else if (tempPath == null ) {
36993730 tempPath = result ;
3731+ pushConfirmedSegmentSize (result .size ());
37003732 } else {
3733+ final int sizeBefore = tempPath .size ();
37013734 tempPath .add (result );
3735+ pushConfirmedSegmentSize (tempPath .size () - sizeBefore );
37023736 }
37033737 if (result != null ) drawSegment (tempPath );
37043738 } catch (final Exception ex ) {
@@ -3728,6 +3762,77 @@ void cancelCurrentSearch() {
37283762 if (f != null ) f .cancel (true );
37293763 }
37303764
3765+ /**
3766+ * Records the node count of a segment that was just auto-confirmed into {@code tempPath}, for
3767+ * {@link #undoLastSegment()} to later pop. Mirrors {@code SNT#confirmedSegmentSizes}/{@code
3768+ * SNT#confirmTemporary}, capped the same way at {@link SNTPrefs#MAX_UNDO_STEPS}.
3769+ *
3770+ * @param nodesAdded number of nodes the segment actually contributed to {@code tempPath}
3771+ */
3772+ private void pushConfirmedSegmentSize (final int nodesAdded ) {
3773+ confirmedSegmentSizes .push (nodesAdded );
3774+ if (confirmedSegmentSizes .size () > SNTPrefs .MAX_UNDO_STEPS )
3775+ confirmedSegmentSizes .removeLast (); // drop oldest
3776+ updateUndoButtonState ();
3777+ }
3778+
3779+ /**
3780+ * Keeps {@link Bvv#tracingUndoButton} in sync with whether there is anything to undo.
3781+ * Safe to call even before the toolbar is built ({@code tracingUndoButton} null then).
3782+ */
3783+ private void updateUndoButtonState () {
3784+ if (tracingUndoButton != null ) tracingUndoButton .setEnabled (!confirmedSegmentSizes .isEmpty ());
3785+ }
3786+
3787+ /**
3788+ * Undoes the most recently confirmed segment, one click at a time (single-step; no redo).
3789+ * Ported from {@code SNT#undoLastSegment()}: pops the last segment's node count off {@link
3790+ * #confirmedSegmentSizes} and removes that many trailing nodes from {@code tempPath}. If that
3791+ * empties {@code tempPath} entirely (undone back to the very first point), this falls back to
3792+ * {@link #discardCurrentPath()}, exactly as the classic 2D canvas cancels the whole path in
3793+ * that case. No-op if tracing is disabled, nothing has been confirmed yet, or a segment is
3794+ * currently being traced (undoing mid-search would race with {@code done()}).
3795+ */
3796+ void undoLastSegment () {
3797+ if (!tracingEnabled ) return ;
3798+ if (confirmedSegmentSizes .isEmpty ()) {
3799+ showViewerMessage ("No segment to undo" );
3800+ return ;
3801+ }
3802+ if (computing ) {
3803+ showViewerMessage ("A segment is still being traced; please wait before undoing" );
3804+ return ;
3805+ }
3806+ final int nodesToRemove = confirmedSegmentSizes .pop ();
3807+ for (int i = 0 ; i < nodesToRemove ; i ++)
3808+ tempPath .removeNode (tempPath .size () - 1 );
3809+
3810+ if (tempPath .size () == 0 ) {
3811+ // Undone back to the very first point: equivalent to discarding the path outright
3812+ discardCurrentPath ();
3813+ return ;
3814+ }
3815+ previousNode = new Path .PathNode (tempPath .lastNode ());
3816+ previousForkPoint = null ;
3817+ updateUndoButtonState ();
3818+ drawSegment (tempPath );
3819+ showViewerMessage ("Segment undone" );
3820+ }
3821+
3822+ /**
3823+ * Builds the undo action shared by {@link Bvv#tracingUndoButton} and the Cmd/Ctrl+Z hotkey.
3824+ *
3825+ * @see #undoLastSegment()
3826+ */
3827+ private AbstractAction getUndoSegmentAction () {
3828+ return new AbstractAction ("Undo Last Segment" ) {
3829+ @ Override
3830+ public void actionPerformed (final java .awt .event .ActionEvent e ) {
3831+ undoLastSegment ();
3832+ }
3833+ };
3834+ }
3835+
37313836 private void initializeTracingOverlay (final BigVolumeViewer bvv ) {
37323837 if (tracingOverlay != null ) tracingOverlay .dispose ();
37333838 tracingOverlay = new AnnotationOverlay (adapt (bvv .getViewer ()), Bvv .this , renderingOptions );
@@ -3754,6 +3859,8 @@ void exit() {
37543859 tempPath = null ;
37553860 computing = false ;
37563861 pendingFinish = false ;
3862+ confirmedSegmentSizes .clear ();
3863+ updateUndoButtonState ();
37573864 setTracingStatus (false , null );
37583865 if (tracingOverlay != null ) tracingOverlay .dispose ();
37593866 tracingOverlay = null ;
0 commit comments