@@ -154,6 +154,7 @@ public abstract class AbstractBigViewer {
154154 protected JProgressBar tracingStatusBar ;
155155 protected JButton tracingCancelButton ;
156156 protected JButton tracingUndoButton ;
157+ protected JToggleButton tracingExtendButton ;
157158 protected JToggleButton secondaryLayerIndicator ; // Persistent indicator of SNT#isTracingOnSecondaryImageActive()
158159
159160 protected AbstractBigViewer () {
@@ -1058,6 +1059,14 @@ protected JComponent tracingStatusRow(final Actions actions, final AbstractTrace
10581059 IconFactory .assignIcon (tracingUndoButton , IconFactory .GLYPH .UNDO , (Color ) null , .9f );
10591060 tracingUndoButton .setToolTipText ("<HTML>Tracing: Undo last segment (or press Z)" );
10601061 tracer .updateUndoButtonState ();
1062+ tracingExtendButton = new JToggleButton (tracer .getExtendSelectedPathAction ());
1063+ tracingExtendButton .setText (null );
1064+ IconFactory .assignIcon (tracingExtendButton , IconFactory .GLYPH .CIRCLE_CHEVRON_RIGHT , IconFactory .GLYPH .CIRCLE_CHEVRON_RIGHT );
1065+ tracingExtendButton .setToolTipText ("Continue extending the selected path" );
1066+ // Same reasoning as b0/b1/b2 in Bvv#sntToolbar/Bdv#sntAnnotationsCard: non-focusable so
1067+ // Space (hold-to-toggle-tracing) isn't also captured as this button's own activation key
1068+ tracingExtendButton .setFocusable (false );
1069+ tracer .installExtendPathButton (tracingExtendButton );
10611070 secondaryLayerIndicator = new JToggleButton ();
10621071 secondaryLayerIndicator .setEnabled (snt != null );
10631072 secondaryLayerIndicator .setSelected (snt != null && snt .isTracingOnSecondaryImageActive ());
@@ -1068,6 +1077,8 @@ protected JComponent tracingStatusRow(final Actions actions, final AbstractTrace
10681077 row .setFloatable (false );
10691078 row .add (tracingUndoButton );
10701079 row .addSeparator ();
1080+ row .add (tracingExtendButton );
1081+ row .addSeparator ();
10711082 row .add (secondaryLayerIndicator );
10721083 row .addSeparator ();
10731084 row .add (tracingStatusBar );
@@ -2056,12 +2067,31 @@ protected abstract class AbstractTracer extends MouseAdapter {
20562067 // #installExtendPathButton
20572068 private JToggleButton extendPathButton ;
20582069
2059- // Whether a global SNT_PAUSED/TRACING_PAUSED state is currently forcing "No tracing" here (see
2060- // #setLockedByPause). While true, manualTracingButton/interactiveTracingButton are disabled
2061- // outright - not just deselected - so the toolbar can't show tracing as active while every click
2062- // is actually a no-op (see #handleClick's own busy/paused early-return, which bails on the same
2063- // two states).
2070+ // Whether a global SNT_PAUSED/TRACING_PAUSED state is currently forcing "No tracing" here (see #setLockedByPause).
2071+ // While true, manualTracingButton/interactiveTracingButton are disabled - not just deselected - so the toolbar
2072+ // can't show tracing as active while every click is actually a no-op (see #handleClick's busy/paused early-return,
2073+ // which bails on the same two states)
20642074 private boolean lockedByPause ;
2075+ // Space key tap-vs-hold state (see #getToggleTracingHoldPressAction()/ #getToggleTracingHoldReleaseAction()): a
2076+ // quick press+release commits a permanent flip of tracingEnabled (like clicking the button); a press held past
2077+ // SPACE_HOLD_THRESHOLD_MS previews "No tracing" for as long as it's held and snaps back on release. All 4 fields
2078+ // below are only ever touched on the EDT, so no synchronization is needed despite the Timer involved
2079+ private static final int SPACE_HOLD_THRESHOLD_MS = 200 ;
2080+ // True from the first "pressed" KeyStroke event until the matching "released" one is processed. Guards against
2081+ // OS key-repeat re-firing the press handler while the key stays down, and against a stray release (e.g. focus
2082+ // lost mid-press) being processed twice
2083+ private boolean spaceKeyDown ;
2084+ // Set once SPACE_HOLD_THRESHOLD_MS has elapsed while the key is still down: distinguishes a genuine hold from a
2085+ // quick tap at release time, regardless of whether the hold actually changed anything (see #spaceHoldEngaged)
2086+ private boolean spaceHoldThresholdReached ;
2087+ // Set only if the hold threshold was reached AND tracing was actually on at that point (so the preview actually
2088+ // suspended it). Holding while already "No tracing" reaches the threshold but never sets this, matching the
2089+ // pre-tap/hold behavior of that combination being a no-op either way (tap or hold)
2090+ private boolean spaceHoldEngaged ;
2091+ // tracingEnabled's value at the moment the hold threshold was reached, so release can restore it exactly once
2092+ // the preview ends
2093+ private boolean tracingEnabledBeforeHold ;
2094+ private javax .swing .Timer spaceHoldThresholdTimer ;
20652095 // The Future for the A* search currently active (if any), so a Cancel button (if the viewer has
20662096 // one; see Bvv#tracingStatusRow()) can stop it. Null when idle, or during manual tracing
20672097 private volatile Future <?> currentSearchFuture ;
@@ -2159,6 +2189,7 @@ private void handleClick(final MouseEvent e) {
21592189 // its workers are reading this SNT instance's shared image-data fields concurrently.
21602190 // Refuse to start a new path on a *different* channel/frame than the batch is using.
21612191 // That would trigger the resync below and race with it
2192+ assert snt != null ;
21622193 final int [] batchLock = snt .getBatchRetraceChannelFrame ();
21632194 if (batchLock != null ) {
21642195 final int [] candidate = peekActiveChannelFrame ();
@@ -2864,6 +2895,110 @@ private void disableTracing() {
28642895 showViewerMessage ("Tracing disabled" );
28652896 }
28662897
2898+ /**
2899+ * @return the action bound to Space's key-down in Bdv/Bvv. Doesn't act immediately: starts a
2900+ * {@value #SPACE_HOLD_THRESHOLD_MS}ms timer to tell a quick tap from a genuine hold
2901+ * (decided at key-up by {@link #getToggleTracingHoldReleaseAction()}) - a tap commits
2902+ * a permanent flip of {@code tracingEnabled} (like clicking the button), a hold past
2903+ * the threshold previews "No tracing" for as long as it's held (mirrors the classic
2904+ * canvas's own hold-Space-to-pan convention, see {@code
2905+ * InteractiveTracerCanvas#mousePressed}) and snaps back on release. No-op (repeated
2906+ * OS key-repeat "pressed" events while already down are ignored) if a press is
2907+ * already in progress.
2908+ */
2909+ protected AbstractAction getToggleTracingHoldPressAction () {
2910+ return new AbstractAction ("Toggle/suspend tracing (Space)" ) {
2911+ @ Override
2912+ public void actionPerformed (final java .awt .event .ActionEvent e ) {
2913+ if (spaceKeyDown ) return ; // OS key-repeat while already held; ignore
2914+ spaceKeyDown = true ;
2915+ spaceHoldThresholdReached = false ;
2916+ spaceHoldEngaged = false ;
2917+ SNTUtils .log ("Space pressed (tracingEnabled=" + tracingEnabled + ")" );
2918+ spaceHoldThresholdTimer = new javax .swing .Timer (SPACE_HOLD_THRESHOLD_MS , ev -> {
2919+ spaceHoldThresholdReached = true ;
2920+ SNTUtils .log ("Space hold threshold reached (tracingEnabled=" + tracingEnabled + ")" );
2921+ if (tracingEnabled ) {
2922+ // Only a hold starting from a tracing mode has anything to preview; a
2923+ // hold starting from "No tracing" is left a no-op, same as before tap
2924+ // detection existed
2925+ spaceHoldEngaged = true ;
2926+ tracingEnabledBeforeHold = true ;
2927+ setTracingActiveForHold (false );
2928+ }
2929+ });
2930+ spaceHoldThresholdTimer .setRepeats (false );
2931+ spaceHoldThresholdTimer .start ();
2932+ }
2933+ };
2934+ }
2935+
2936+ /**
2937+ * @return the action bound to Space's key-up. Stops the pending threshold timer (see {@link
2938+ * #getToggleTracingHoldPressAction()}) and either restores the pre-hold state (if the
2939+ * threshold was reached and actually engaged a preview), does nothing (threshold
2940+ * reached but nothing was previewed - a hold that started from "No tracing"), or, for
2941+ * a quick tap (threshold never reached), commits a permanent flip of {@code
2942+ * tracingEnabled}. The permanent-flip branch is a no-op (with a status message,
2943+ * instead of {@link #getToggleAction(boolean)}'s blocking error dialog) if turning
2944+ * tracing on isn't currently possible. No-op overall if no press was seen (e.g. focus
2945+ * was lost mid-press, so this release has no matching press).
2946+ */
2947+ protected AbstractAction getToggleTracingHoldReleaseAction () {
2948+ return new AbstractAction ("Commit/resume tracing state (Space)" ) {
2949+ @ Override
2950+ public void actionPerformed (final java .awt .event .ActionEvent e ) {
2951+ if (!spaceKeyDown ) return ;
2952+ spaceKeyDown = false ;
2953+ if (spaceHoldThresholdTimer != null ) {
2954+ spaceHoldThresholdTimer .stop ();
2955+ spaceHoldThresholdTimer = null ;
2956+ }
2957+ if (spaceHoldThresholdReached ) {
2958+ SNTUtils .log ("Space released after a hold (engaged=" + spaceHoldEngaged + ")" );
2959+ if (spaceHoldEngaged ) {
2960+ spaceHoldEngaged = false ;
2961+ setTracingActiveForHold (tracingEnabledBeforeHold );
2962+ }
2963+ // else: was held while already "No tracing" - stays a no-op
2964+ return ;
2965+ }
2966+ // Quick tap: commit a permanent flip
2967+ SNTUtils .log ("Space released as a tap (tracingEnabled=" + tracingEnabled + " -> " + !tracingEnabled + ")" );
2968+ if (!tracingEnabled ) {
2969+ final boolean sntAware = snt != null && snt .getPathAndFillManager () != null ;
2970+ final boolean tracingPossible = manualTrace || (sntAware && snt .accessToValidImageData ());
2971+ if (!tracingPossible ) {
2972+ showViewerMessage ("Tracing not available" );
2973+ return ;
2974+ }
2975+ }
2976+ setTracingActiveForHold (!tracingEnabled );
2977+ }
2978+ };
2979+ }
2980+
2981+ /**
2982+ * Shared by {@link #getToggleTracingHoldPressAction()}/{@link
2983+ * #getToggleTracingHoldReleaseAction()}: applies {@code active} to {@link #tracingEnabled}
2984+ * ({@code AbstractTracer}'s own field, not the button-driven flow), the extend-path button,
2985+ * the tracing-mode {@link ButtonGroup}'s selection, and a status message - without any of
2986+ * {@link #getToggleAction(boolean)}/{@link #disableTracing()}'s prompting or {@link #exit()}
2987+ * bookkeeping, since both the tap and the hold-preview paths bypass that heavier flow.
2988+ */
2989+ private void setTracingActiveForHold (final boolean active ) {
2990+ tracingEnabled = active ;
2991+ if (extendPathButton != null ) extendPathButton .setEnabled (active );
2992+ if (active ) {
2993+ final JToggleButton modeButton = (manualTrace ) ? manualTracingButton : interactiveTracingButton ;
2994+ if (modeButton != null ) modeButton .setSelected (true );
2995+ showViewerMessage ("Tracing mode active" );
2996+ } else {
2997+ if (noTracingButton != null ) noTracingButton .setSelected (true );
2998+ showViewerMessage ("Panning mode active" );
2999+ }
3000+ }
3001+
28673002 /**
28683003 * Wires the three mutually-exclusive tracing-mode buttons built by the viewer's own toolbar code
28693004 * (e.g. {@code Bvv#sntToolbar}/{@code Bdv#sntAnnotationsCard}) into this tracer, so
0 commit comments