Skip to content

Commit db2545d

Browse files
committed
Register stream mode commands in Command Palette
1 parent 4ef6747 commit db2545d

7 files changed

Lines changed: 242 additions & 5 deletions

File tree

‎src/main/java/sc/fiji/snt/SNTUI.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,13 @@ protected void runSNTCommandFinderCommand(final String cmd) {
749749
commandFinder.runCommand(cmd);
750750
}
751751

752+
/**
753+
* Returns the command palette instance, so that other components can register their own entries into it.
754+
*/
755+
public SNTCommandFinder getCommandFinder() {
756+
return commandFinder;
757+
}
758+
752759
protected boolean runCustomCommand(final String cmd) {
753760
if ("validateImgDimensions".equals(cmd)) {
754761
validateImgDimensions();
@@ -1117,6 +1124,9 @@ private JLabel materializedCropBadge(final float badgeScaling) {
11171124
private void updateMaterializationDependentControls() {
11181125
if (plugin == null || !plugin.isStreamMode()) return;
11191126
final boolean enable = plugin.isMaterializedCrop();
1127+
// Canvas contextual menu only exists for a materialized crop (getTracingCanvasPopupMenu());
1128+
// its entries may now need to be added to, or dropped from, commandFinder's scraped index
1129+
commandFinder.invalidateIndex();
11201130
if (showPathsSelectedRow != null) GuiUtils.enableComponents(showPathsSelectedRow, enable);
11211131
else showPathsSelected.setEnabled(enable);
11221132
partsNearbyCSpinner.setEnabled(enable && isStackAvailable());

‎src/main/java/sc/fiji/snt/gui/GuiUtils.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2400,7 +2400,7 @@ private static String keystrokeToString(final javax.swing.KeyStroke ks) {
24002400
/**
24012401
* Converts an action-name string to Title Case.
24022402
*/
2403-
private static String toTitleCase(final String s) {
2403+
public static String toTitleCase(final String s) {
24042404
if (s == null || s.isEmpty()) return s;
24052405
final String[] words = s.replaceAll("[-_]", " ").split("\\s+");
24062406
final StringBuilder sb = new StringBuilder();

‎src/main/java/sc/fiji/snt/gui/IconFactory.java‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,31 @@ public static Icon accentIcon(final Color color, final boolean squarify) {
496496
return new AccentIcon(color, (squarify) ? size : size * 2, size);
497497
}
498498

499+
/**
500+
* Wraps an icon in a fixed-width icon, centering it within a common reference.
501+
* @param icon the icon to be matched to reference
502+
* @param refIcon reference icon, setting the target width
503+
* @return the centered fixed-width icon
504+
*/
505+
public static Icon fixedWidthIcon(final Icon icon, final Icon refIcon) {
506+
final int refWidth = refIcon.getIconWidth();
507+
final int width = Math.max(icon.getIconWidth(), refWidth);
508+
return new Icon() {
509+
@Override
510+
public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
511+
icon.paintIcon(c, g, x + (width - icon.getIconWidth()) / 2, y);
512+
}
513+
@Override
514+
public int getIconWidth() {
515+
return width;
516+
}
517+
@Override
518+
public int getIconHeight() {
519+
return icon.getIconHeight();
520+
}
521+
};
522+
}
523+
499524
public static int defaultSize() {
500525
return FADerivedIcon.defSize();
501526
}

‎src/main/java/sc/fiji/snt/gui/SNTCommandFinder.java‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public class SNTCommandFinder {
8787
private boolean reveal;
8888
private Timer revealTimer;
8989
private Timer msgTimer;
90+
// Guards the two setText() calls in displayTempMsg() so the temporary clear/restore
91+
// of the field does not itself fire PromptDocumentListener and re-run a search
92+
private boolean suppressDocListener = false;
9093

9194
/**
9295
* Constructs a new SNTCommandFinder for the specified SNTUI instance.
@@ -284,14 +287,18 @@ private void displayTempMsg(final String msg, final boolean warning) {
284287
final String existingPlaceholder = searchField.getClientProperty(FlatClientProperties.PLACEHOLDER_TEXT).toString();
285288
final String existingText = searchField.getText();
286289
searchField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, msg);
290+
suppressDocListener = true;
287291
searchField.setText(null);
292+
suppressDocListener = false;
288293
if (warning) searchField.setBackground(GuiUtils.warningColor());
289294
msgTimer = new Timer((warning) ? 3000 : 6000, null) {
290295
@Override
291296
public void stop() {
292297
super.stop();
293298
searchField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, existingPlaceholder);
299+
suppressDocListener = true;
294300
searchField.setText(existingText);
301+
suppressDocListener = false;
295302
if (warning) searchField.setBackground(searchFieldBackground);
296303
}
297304
};
@@ -788,6 +795,21 @@ private static boolean isOnScreen(final Point p) {
788795
return false;
789796
}
790797

798+
/**
799+
* Returns the keyboard shortcut that toggles this palette's visibility.
800+
*/
801+
public KeyStroke getAccelerator() {
802+
return ACCELERATOR;
803+
}
804+
805+
/**
806+
* Returns the {@link Action} that {@link #getAccelerator()} triggers (toggle visibility; bring to
807+
* front and focus the search field if already visible; Shift+accelerator also re-centers the frame).
808+
*/
809+
public Action getToggleVisibilityAction() {
810+
return getAction();
811+
}
812+
791813
public void attach(final JDialog dialog) {
792814
final int condition = JPanel.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT; //JPanel.WHEN_IN_FOCUSED_WINDOW;
793815
final InputMap inputMap = ((JPanel) dialog.getContentPane()).getInputMap(condition);
@@ -847,6 +869,16 @@ public AbstractButton getRegisteredComponent(final String label) {
847869
return (cmd == null) ? null : cmd.button;
848870
}
849871

872+
/**
873+
* Marks the scraped index stale, e.g., after the tracing canvas's contextual menu
874+
* ({@link SNTUI#getTracingCanvasPopupMenu()}) becomes available or unavailable, such as when
875+
* a Stream mode materialized crop is created or discarded. The next lazy scrape (palette
876+
* open, {@link #getShortcuts()}, etc.) picks up the change; no immediate rescrape happens here
877+
*/
878+
public void invalidateIndex() {
879+
cmdScrapper.invalidate();
880+
}
881+
850882
private void registerMenu(final JMenu menu, final List<String> path) {
851883
for (final Component component : menu.getMenuComponents()) {
852884
if (component instanceof JMenu) {
@@ -1106,15 +1138,15 @@ final void addProHint(final JToolBar parent) {
11061138

11071139
private class PromptDocumentListener implements DocumentListener {
11081140
public void insertUpdate(final DocumentEvent e) {
1109-
populateList(currentQuery());
1141+
if (!suppressDocListener) populateList(currentQuery());
11101142
}
11111143

11121144
public void removeUpdate(final DocumentEvent e) {
1113-
populateList(currentQuery());
1145+
if (!suppressDocListener) populateList(currentQuery());
11141146
}
11151147

11161148
public void changedUpdate(final DocumentEvent e) {
1117-
populateList(currentQuery());
1149+
if (!suppressDocListener) populateList(currentQuery());
11181150
}
11191151
}
11201152

@@ -1518,6 +1550,8 @@ private class CmdScrapper {
15181550
private TreeMap<String, CmdAction> combinedCache;
15191551
/** Synthetic entries that survive scrape() cycles (see {@link SNTCommandFinder#registerKeywords}). */
15201552
private final List<CmdAction> extras = new ArrayList<>();
1553+
/** Set by {@link #invalidate()} when a scraped component may have changed outside a UI edit */
1554+
private boolean stale;
15211555

15221556
CmdScrapper() {
15231557
cmdMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@@ -1573,12 +1607,18 @@ CmdAction getCmdAction(final String identifier) {
15731607
}
15741608

15751609
boolean scrapeFailed() {
1576-
return cmdMap.isEmpty();
1610+
return stale || cmdMap.isEmpty();
1611+
}
1612+
1613+
/** Marks the index stale so the next lazy {@link #scrapeFailed()} check triggers a rescrape */
1614+
void invalidate() {
1615+
stale = true;
15771616
}
15781617

15791618
void scrape() {
15801619
cmdMap.clear();
15811620
combinedCache = null; // force merge rebuild on next getCmdMap() call
1621+
stale = false;
15821622
for (final AnnotatedComponent ac : getComponents()) {
15831623
if (ac != null) scrapeComponent(ac);
15841624
}

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import sc.fiji.snt.*;
3131
import sc.fiji.snt.gui.GuiUtils;
3232
import sc.fiji.snt.gui.IconFactory;
33+
import sc.fiji.snt.gui.SNTCommandFinder;
3334
import sc.fiji.snt.tracing.SearchInterface;
3435
import sc.fiji.snt.util.BoundingBox;
3536
import sc.fiji.snt.util.PointInImage;
@@ -40,6 +41,8 @@
4041
import javax.swing.*;
4142
import java.awt.*;
4243
import java.awt.event.ActionEvent;
44+
import java.awt.event.InputEvent;
45+
import java.awt.event.KeyEvent;
4346
import java.awt.event.MouseAdapter;
4447
import java.awt.event.MouseEvent;
4548
import java.io.File;
@@ -496,6 +499,129 @@ public boolean flyTo(final BoundingBox box) {
496499
*/
497500
protected abstract Action getViewerAction(String name);
498501

502+
/**
503+
* Keystrokes bound to BDV/BVV native commands intentionally kept out of the command palette by
504+
* {@link #registerNativeCommands}: per-source selection/visibility digits, continuous pan/zoom/
505+
* rotate stepping, timepoint stepping, and bookmark set/recall/rotation. These read as noise in
506+
* a command finder
507+
*/
508+
protected static final KeyStroke[] NATIVE_KEYS_EXCLUDED_FROM_PALETTE = buildExcludedNativeKeys();
509+
510+
private static KeyStroke[] buildExcludedNativeKeys() {
511+
final List<KeyStroke> keys = new ArrayList<>();
512+
for (int vk = KeyEvent.VK_0; vk <= KeyEvent.VK_9; vk++) {
513+
keys.add(KeyStroke.getKeyStroke(vk, 0));
514+
keys.add(KeyStroke.getKeyStroke(vk, InputEvent.SHIFT_DOWN_MASK));
515+
}
516+
for (final int vk : new int[]{KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN,
517+
KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_OPEN_BRACKET, KeyEvent.VK_CLOSE_BRACKET,
518+
KeyEvent.VK_N, KeyEvent.VK_M, KeyEvent.VK_B, KeyEvent.VK_O}) {
519+
keys.add(KeyStroke.getKeyStroke(vk, 0));
520+
}
521+
keys.add(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.SHIFT_DOWN_MASK));
522+
return keys.toArray(new KeyStroke[0]);
523+
}
524+
525+
/**
526+
* Registers this viewer's native (library-level) single-shot commands -- i.e., entries of the BDV/BVV keybindings
527+
* {@link ActionMap}, as opposed to SNT's own overlaid commands (see {@link Actions}) -- in {@code commandFinder},
528+
* so they become searchable/runnable from the command palette. Meant to be called once per viewer instance, only
529+
* while {@link SNT#isStreamMode() Stream mode} is active
530+
* <p>
531+
* Continuous, held-key or mouse-drag behaviors (pan, rotate, zoom, scroll) are not picked up by this scrape: they
532+
* live elsewhere so they are excluded by construction. {@code excludedTriggers} additionally drops named actions
533+
* bound to specific keys that clutter the palette.
534+
* </p>
535+
*
536+
* @param commandFinder the palette to register into; a no-op if null
537+
* @param actionMap the viewer's concatenated keybindings {@link ActionMap}
538+
* @param inputMap the viewer's concatenated keybindings {@link InputMap}, used only to
539+
* resolve {@code excludedTriggers} to the action keys they are bound to
540+
* @param path palette category shown for every registered entry, e.g. {@code List.of("Bvv Viewer")}
541+
* @param icon icon applied to every registered entry via {@code Action.SMALL_ICON}; may be null
542+
* @param excludedNames action-map keys to skip outright, matched case-insensitively against the raw
543+
* (un-prettified) key -- for contextual/"abort this in-progress gesture" natives
544+
* that have no single dedicated trigger key, or SNT press/release-pair overlays
545+
* (e.g., "hide annotations (hold)") that only make sense as a held key, never as
546+
* a one-shot palette command
547+
* @param excludedTriggers keystrokes whose bound action should be skipped (the keystroke itself
548+
* does not need to be a valid trigger for this viewer; unresolved ones are ignored)
549+
*/
550+
protected void registerNativeCommands(final SNTCommandFinder commandFinder, final ActionMap actionMap,
551+
final InputMap inputMap, final List<String> path, final Icon icon, final Set<String> excludedNames,
552+
final KeyStroke... excludedTriggers) {
553+
if (commandFinder == null || actionMap == null) return;
554+
final Set<Object> excludedKeys = new HashSet<>();
555+
if (inputMap != null) {
556+
for (final KeyStroke ks : excludedTriggers) {
557+
final Object actionKey = inputMap.get(ks);
558+
if (actionKey != null) excludedKeys.add(actionKey);
559+
}
560+
}
561+
final Object[] keys = actionMap.allKeys();
562+
if (keys == null) return;
563+
for (final Object key : keys) {
564+
if (!(key instanceof final String name) || excludedKeys.contains(key)) continue;
565+
if (excludedNames != null && excludedNames.contains(name.toLowerCase())) continue;
566+
final Action nativeAction = actionMap.get(key);
567+
if (nativeAction == null || !nativeAction.isEnabled()) continue;
568+
final String label = humanizeActionMapKey(name);
569+
final AbstractAction wrapper = new AbstractAction(label) {
570+
@Override
571+
public void actionPerformed(final ActionEvent e) {
572+
nativeAction.actionPerformed(e);
573+
}
574+
};
575+
if (icon != null) wrapper.putValue(Action.SMALL_ICON, icon);
576+
commandFinder.registerKeywords(label, path, null, wrapper, null);
577+
}
578+
}
579+
580+
/**
581+
* Turns a raw action-map key (e.g., "align XY plane", "snt-capture-keyframe") into a palette-friendly
582+
* label ("Align XY Plane", "Capture Keyframe"). Reuses {@link GuiUtils#toTitleCase(String)}, but --
583+
* unlike that method, which upper-cases a leading "snt" word to flag SNT's own bindings among BDV/BVV's
584+
* defaults in the plain keyboard-shortcuts dialog -- drops the "snt" prefix entirely here: in the
585+
* palette, SNT's own commands are already distinguished by their {@code path} category and icon, so
586+
* repeating "SNT" in every label would just be noise.
587+
*/
588+
private static String humanizeActionMapKey(final String rawKey) {
589+
return GuiUtils.toTitleCase(rawKey.replaceFirst("(?i)^snt[- _]+", ""));
590+
}
591+
592+
/**
593+
* Action-map keys that {@link #registerNativeCommands} should always skip, shared by every
594+
* BDV/BVV-family viewer: natives that only make sense mid-gesture (aborting an in-progress
595+
* bookmark placement or manual transform, neither of which has a dedicated trigger key of its
596+
* own), and SNT's own press/release-pair or modal-pick overlays -- present under the same literal
597+
* keys in both {@code Bvv} and {@code Bdv} -- which are meaningless as a one-shot palette command
598+
* (see, e.g., {@code Actions#hideAnnotationsPressAction}).
599+
*/
600+
protected static final Set<String> NATIVE_NAMES_EXCLUDED_FROM_PALETTE = Set.of(
601+
"abort bookmark", "abort manual transformation",
602+
"snt-pick-sigma-point", "snt-hide-annotations-press", "snt-hide-annotations-release");
603+
604+
/**
605+
* Wires {@code commandFinder}'s own show/toggle shortcut ({@link SNTCommandFinder#getAccelerator()})
606+
* into this viewer's keybindings, so it fires while the Bvv/Bdv window itself has focus. Standard
607+
* Swing accelerators (as installed by {@link SNTCommandFinder#attach(JDialog)}) don't reach here:
608+
* BDV/BVV's behavior-based trigger layer intercepts keystrokes before a component's ordinary
609+
* InputMap/ActionMap ever sees them (see the identical constraint noted where {@code sntIMap}/
610+
* {@code sntAMap} are built in {@code Bvv}/{@code Bdv}), so the shortcut has to be added to that
611+
* same {@code sntIMap}/{@code sntAMap} pair instead, alongside SNT's other viewer-overlay bindings.
612+
*
613+
* @param sntIMap the viewer's own SNT-overlay {@link InputMap}, not yet installed via {@code addInputMap}
614+
* @param sntAMap the matching {@link ActionMap}, not yet installed via {@code addActionMap}
615+
* @param commandFinder the palette whose accelerator should open/focus it; a no-op if null
616+
*/
617+
protected static void registerCommandFinderAccelerator(final InputMap sntIMap, final ActionMap sntAMap,
618+
final SNTCommandFinder commandFinder) {
619+
if (commandFinder == null) return;
620+
final String actionKey = "snt-toggle-command-finder";
621+
sntIMap.put(commandFinder.getAccelerator(), actionKey);
622+
sntAMap.put(actionKey, commandFinder.getToggleVisibilityAction());
623+
}
624+
499625
/**
500626
* Adds a Tree to the viewer overlay, assigning it a unique display label.
501627
*

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
import net.imglib2.type.NativeType;
4747
import net.imglib2.type.numeric.RealType;
4848
import org.scijava.command.CommandService;
49+
import com.formdev.flatlaf.extras.FlatSVGIcon;
4950
import sc.fiji.snt.gui.GuiUtils;
5051
import sc.fiji.snt.gui.IconFactory;
52+
import sc.fiji.snt.gui.SNTCommandFinder;
5153
import sc.fiji.snt.gui.cmds.BdvRenderingOptionsCmd;
5254
import sc.fiji.snt.util.ImpUtils;
5355
import sc.fiji.snt.util.SNTPoint;
@@ -874,8 +876,24 @@ public void actionPerformed(final java.awt.event.ActionEvent e) {
874876
sntIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0), "snt-undo-segment"); // see Tracer#undoLastSegment
875877
sntAMap.put("snt-undo-segment", tracer.getUndoSegmentAction());
876878
}
879+
// Command palette shortcut: wired unconditionally (not just in Stream mode), since the BDV
880+
// window can be opened in classic mode too, and its own keybindings layer needs this regardless
881+
if (snt != null && snt.getUI() != null) {
882+
registerCommandFinderAccelerator(sntIMap, sntAMap, snt.getUI().getCommandFinder());
883+
}
877884
vf.getKeybindings().addInputMap("snt", sntIMap);
878885
vf.getKeybindings().addActionMap("snt", sntAMap);
886+
// Surface BDV's own native commands in the command palette. Stream mode only: in classic
887+
// mode the palette already reaches this viewer through SNTUI's regular scraped menus/toolbars
888+
if (snt != null && snt.isStreamMode() && snt.getUI() != null) {
889+
registerNativeCommands(snt.getUI().getCommandFinder(),
890+
vf.getKeybindings().getConcatenatedActionMap(),
891+
vf.getKeybindings().getConcatenatedInputMap(),
892+
List.of("SNT Stream (BDV)"),
893+
new FlatSVGIcon("gui/bdv-logo-dark.svg", IconFactory.defaultSize(), IconFactory.defaultSize()),
894+
NATIVE_NAMES_EXCLUDED_FROM_PALETTE,
895+
NATIVE_KEYS_EXCLUDED_FROM_PALETTE);
896+
}
879897
}
880898
}
881899

0 commit comments

Comments
 (0)