Skip to content

Commit 67f38ca

Browse files
committed
Implement undo in Bvv
1 parent d7f1add commit 67f38ca

1 file changed

Lines changed: 134 additions & 27 deletions

File tree

  • src/main/java/sc/fiji/snt/viewer

‎src/main/java/sc/fiji/snt/viewer/Bvv.java‎

Lines changed: 134 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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>&nbsp;&nbsp;<b>Never</b>: Keep the current view as-is"
18121815
+ "<br>&nbsp;&nbsp;<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

Comments
 (0)