|
30 | 30 | import sc.fiji.snt.*; |
31 | 31 | import sc.fiji.snt.gui.GuiUtils; |
32 | 32 | import sc.fiji.snt.gui.IconFactory; |
| 33 | +import sc.fiji.snt.gui.SNTCommandFinder; |
33 | 34 | import sc.fiji.snt.tracing.SearchInterface; |
34 | 35 | import sc.fiji.snt.util.BoundingBox; |
35 | 36 | import sc.fiji.snt.util.PointInImage; |
|
40 | 41 | import javax.swing.*; |
41 | 42 | import java.awt.*; |
42 | 43 | import java.awt.event.ActionEvent; |
| 44 | +import java.awt.event.InputEvent; |
| 45 | +import java.awt.event.KeyEvent; |
43 | 46 | import java.awt.event.MouseAdapter; |
44 | 47 | import java.awt.event.MouseEvent; |
45 | 48 | import java.io.File; |
@@ -496,6 +499,129 @@ public boolean flyTo(final BoundingBox box) { |
496 | 499 | */ |
497 | 500 | protected abstract Action getViewerAction(String name); |
498 | 501 |
|
| 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 | + |
499 | 625 | /** |
500 | 626 | * Adds a Tree to the viewer overlay, assigning it a unique display label. |
501 | 627 | * |
|
0 commit comments