From 860570ceb8f12592d62cf6bf0f443b7f6d097667 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:11:59 -0500 Subject: [PATCH] GP-6336 - Key Bindings - Updated table and tree actions to allow for Escape to close the filter --- .../help/topics/Trees/GhidraTreeFilter.html | 4 +- .../KeyBindingOverrideKeyEventDispatcher.java | 19 ++++++ .../docking/action/MultipleKeyAction.java | 61 +++++++++++++------ .../java/docking/widgets/table/GTable.java | 33 ++++++++-- .../widgets/table/GTableFilterPanel.java | 12 +++- .../tree/DefaultGTreeFilterProvider.java | 11 ++++ .../main/java/docking/widgets/tree/GTree.java | 39 +++++++++--- .../widgets/tree/GTreeFilterProvider.java | 22 +++++++ 8 files changed, 165 insertions(+), 36 deletions(-) diff --git a/Ghidra/Framework/Docking/src/main/help/help/topics/Trees/GhidraTreeFilter.html b/Ghidra/Framework/Docking/src/main/help/help/topics/Trees/GhidraTreeFilter.html index 1f950bc14f..a0175dcc40 100644 --- a/Ghidra/Framework/Docking/src/main/help/help/topics/Trees/GhidraTreeFilter.html +++ b/Ghidra/Framework/Docking/src/main/help/help/topics/Trees/GhidraTreeFilter.html @@ -63,10 +63,10 @@ -

Toggle Filter

+

Hide Filter

- The Toggle Filter action will hide and show the filter field of the tree or table. + The Hide Filter action will hide the filter field of the tree or table. To use this action you must first assign it a key binding from the tool options.

diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java index 83c4f2d614..673ffb9288 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java @@ -24,6 +24,7 @@ import java.awt.event.KeyListener; import javax.swing.*; import javax.swing.text.JTextComponent; +import docking.action.*; import docking.actions.KeyBindingUtils; import docking.menu.keys.MenuKeyProcessor; import ghidra.util.bean.GGlassPane; @@ -36,6 +37,24 @@ import ghidra.util.exception.AssertException; *

* {@link #install()} must be called in order to install this Singleton into Java's * key event processing system. + *

+ * Keybindings are processed here to manage how {@link DockingAction}s will get executed. The basic + * action processing flow is: + *

    + *
  1. System actions (e.g., F1 for help)
  2. + *
  3. Java text components
  4. + *
  5. Java widget key listeners
  6. + *
  7. Java action map bindings
  8. + *
  9. {@link ComponentBasedDockingAction}s
  10. + *
  11. {@link ComponentProvider} local actions
  12. + *
  13. Tool global actions
  14. + *
+ * When a key event is processed, if that event has a binding at one of these levels, then that + * binding will be processed, either by our framework or the default Java processing framework. + * Our framework allows for multiple actions to share a key bindings. When that happens, the + * {@link MultipleKeyAction} class is responsible for determining the correct priority for the + * action to be processed. If there is more than one action that maps to a binding, then the user + * will be shown a dialog to choose which action to execute. */ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java index fab437f3e2..84813716a5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java @@ -29,7 +29,11 @@ import generic.util.WindowUtilities; import ghidra.util.Swing; /** - * Action that manages multiple {@link DockingAction}s mapped to a given key binding + * Action that manages multiple {@link DockingAction}s mapped to a given key binding. + *

+ * Actions are ordered in a way that mimics how the {@link KeyBindingOverrideKeyEventDispatcher} + * orders its event processing, lowest level components get precedence over higher-level actions, + * such as global actions. See the javadoc of the dispatcher for more info. */ public class MultipleKeyAction extends DockingKeyBindingAction { private List actions = new ArrayList<>(); @@ -134,19 +138,24 @@ public class MultipleKeyAction extends DockingKeyBindingAction { MultiExecutableAction multiAction = new MultiExecutableAction(); - // - // 1) Prefer local actions for the active provider - // - getLocalContextActions(localContext, multiAction); - if (multiAction.isValid()) { - // At this point, we have local docking actions that may or may not be enabled. Exit - // so that any component specific actions or global found below will not interfere with - // the provider's local actions - return multiAction; - } + /* + Note on action ordering: order matters, as any action found first may prevent actions + found later from being executed, *even if the lower-level action is not enabled. Having + disabled actions prevent higher-level actions from being executed helps us maintain + consistent action execution behavior. The downside of this is that if the user assigns + the same key binding to actions at 2 different levels, the lower priority action will + never get executed. We currently have no way of supporting both actions in this + scenario. It is currently up to the user to avoid this. The one exception we have to + this rule is for ComponentBasedDockingActions. These actions can optionally mark + themselves as invalid for a given context, which then allows the action processing to + go higher up the chain of actions. This is currently done by hard-coding the action + behavior, with no means for the user to change it. This solution seemed good enough + for now. + */ // - // 2) Check for actions local to the source component (e.g., GTable and GTree) + // 1) Check for actions local to the source component (e.g., GTable and GTree). These are + // considered lower level actions that can be processed before the component provider. // getLocalComponentActions(localContext, multiAction); if (multiAction.isValid()) { @@ -156,6 +165,17 @@ public class MultipleKeyAction extends DockingKeyBindingAction { return multiAction; } + // + // 2) Check for local actions for the active provider + // + getLocalContextActions(localContext, multiAction); + if (multiAction.isValid()) { + // At this point, we have local docking actions that may or may not be enabled. Exit + // so that any component specific actions or global found below will not interfere with + // the provider's local actions + return multiAction; + } + // // 3) Check for global actions using the current context // @@ -332,14 +352,13 @@ public class MultipleKeyAction extends DockingKeyBindingAction { return multiAction; } - // - // 1) Check for local actions - // - // Note: dialog key binding actions are proxy actions that get added to the tool as global - // actions. Thus, there are no 'local' actions for the dialog. + /* + See the note in createNonDialogExecutableAction(). + */ // - // 2) Check for actions local to the source component (e.g., GTable and GTree) + // 1) Check for actions local to the source component (e.g., GTable and GTree). These are + // considered lower level actions that can be processed before the component provider. // getLocalComponentActions(context, multiAction); if (multiAction.isValid()) { @@ -349,6 +368,12 @@ public class MultipleKeyAction extends DockingKeyBindingAction { return multiAction; } + // + // 2) Check for dialog local actions + // + // Note: dialog key binding actions are proxy actions that get added to the tool as global + // actions. Thus, there are no 'local' actions for the dialog. + // // 3) Check for global actions using the current context. As noted above, at the time of // writing, dialog actions are all registered at the global level. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java index feac5590cb..bec36dcdb4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java @@ -1532,7 +1532,7 @@ public class GTable extends JTable { activateFilterAction.setHelpLocation(new HelpLocation("Trees", "Activate_Filter")); //@formatter:on - GTableAction toggleFilterAction = new GTableAction("Table/Tree Toggle Filter", owner) { + GTableAction hideFilterAction = new GTableAction("Table/Tree Hide Filter", owner) { @Override public boolean isEnabledForContext(ActionContext context) { @@ -1546,22 +1546,43 @@ public class GTable extends JTable { @Override public void actionPerformed(ActionContext context) { + GTable gTable = (GTable) context.getSourceComponent(); + GTableFilterPanel filterPanel = gTable.getTableFilterPanel(); + filterPanel.close(); + } + + @Override + public boolean isValidComponentContext(ActionContext context) { + /* + Subtle Code Alert! + We use this method to signal that this action is only to be included in the key + binding processing when the filter is showing. This is different than normal + docking actions in that normal actions are always valid, just enabled/disabled. + Returning false here prevents this action from interfering with key bindings + further up the processing chain when the filter is not showing. + */ + if (!super.isValidComponentContext(context)) { + return false; + } GTable gTable = (GTable) context.getSourceComponent(); GTableFilterPanel filterPanel = gTable.getTableFilterPanel(); - filterPanel.toggleVisibility(); + if (filterPanel == null) { + return false; + } + return filterPanel.isShowing(); } }; //@formatter:off - toggleFilterAction.setPopupMenuData(new MenuData( - new String[] { "Toggle Filter" }, + hideFilterAction.setPopupMenuData(new MenuData( + new String[] { "Hide Filter" }, null /*icon*/, actionMenuGroup, NO_MNEMONIC, Integer.toString(subGroupIndex++) ) ); - toggleFilterAction.setHelpLocation(new HelpLocation("Trees", "Toggle_Filter")); + hideFilterAction.setHelpLocation(new HelpLocation("Trees", "Hide_Filter")); //@formatter:on toolActions.addGlobalAction(copyAction); @@ -1571,7 +1592,7 @@ public class GTable extends JTable { toolActions.addGlobalAction(exportColumnsAction); toolActions.addGlobalAction(selectAllAction); toolActions.addGlobalAction(activateFilterAction); - toolActions.addGlobalAction(toggleFilterAction); + toolActions.addGlobalAction(hideFilterAction); } //================================================================================================== diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java index 8c882cf2b3..e252f179c2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java @@ -402,10 +402,20 @@ public class GTableFilterPanel extends JPanel { setVisible(true); isFilterDisplayed = true; } - requestFocus(); } + /** + * Hides this filter if showing. + */ + public void close() { + if (isFilterDisplayed) { + setVisible(false); + isFilterDisplayed = false; + table.requestFocus(); + } + } + /** * Changes the visibility of this filter panel, make it not visible it if showing, showing it if * not visible. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java index 8e8a19d5e6..266cca7ec3 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/DefaultGTreeFilterProvider.java @@ -198,6 +198,17 @@ public class DefaultGTreeFilterProvider implements GTreeFilterProvider { if (isFilterDisplayed) { filterPanel.requestFocus(); } + else { + gTree.requestFocus(); + } + } + + @Override + public void close() { + if (isFilterDisplayed) { + doToggleVisibility(); + gTree.requestFocus(); + } } private void doToggleVisibility() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java index be7fe09479..739efa20ab 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java @@ -1978,22 +1978,43 @@ public class GTree extends JPanel implements BusyListener { ) ); activateFilterAction.setKeyBindingData(new KeyBindingData("Control F")); - activateFilterAction.setHelpLocation(new HelpLocation("Trees", "Toggle_Filter")); - - GTreeAction toggleFilterAction = new GTreeAction("Table/Tree Toggle Filter", owner) { + activateFilterAction.setHelpLocation(new HelpLocation("Trees", "Activate_Filter")); + //@formatter:on + + GTreeAction hideFilterAction = new GTreeAction("Table/Tree Hide Filter", owner) { @Override public void actionPerformed(ActionContext context) { GTree gTree = getTree(context); - gTree.filterProvider.toggleVisibility(); + gTree.filterProvider.close(); + } + + @Override + public boolean isValidComponentContext(ActionContext context) { + /* + Subtle Code Alert! + We use this method to signal that this action is only to be included in the key + binding processing when the filter is showing. This is different than normal + docking actions in that normal actions are always valid, just enabled/disabled. + Returning false here prevents this action from interfering with key bindings + further up the processing chain when the filter is not showing. + */ + if (!super.isValidComponentContext(context)) { + return false; + } + + GTree gTree = getTree(context); + return gTree.filterProvider.isShowing(); } }; - //@formatter:on - toggleFilterAction.setPopupMenuData(new MenuData( - new String[] { "Toggle Filter" }, + + //@formatter:off + hideFilterAction.setPopupMenuData(new MenuData( + new String[] { "Hide Filter" }, null, actionMenuGroup, NO_MNEMONIC, Integer.toString(subGroupIndex++))); - toggleFilterAction.setHelpLocation(new HelpLocation("Trees", "Toggle_Filter")); + hideFilterAction.setHelpLocation(new HelpLocation("Trees", "Hide_Filter")); + //@formatter:on // these actions are self-explanatory and do need help collapseAction.markHelpUnnecessary(); @@ -2007,7 +2028,7 @@ public class GTree extends JPanel implements BusyListener { toolActions.addGlobalAction(expandTreeAction); toolActions.addGlobalAction(copyFormattedAction); toolActions.addGlobalAction(activateFilterAction); - toolActions.addGlobalAction(toggleFilterAction); + toolActions.addGlobalAction(hideFilterAction); } private static String generateFilterPreferenceKey() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java index 58e302b5a0..82d9780844 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeFilterProvider.java @@ -126,6 +126,28 @@ public interface GTreeFilterProvider { } } + /** + * Hides this filter if showing. + */ + public default void close() { + JComponent c = getFilterComponent(); + if (c.isShowing()) { + c.setVisible(false); + } + } + + /** + * Returns true if the filter is showing. + * @return true if the filter is showing. + * @see #activate() + * @see #toggleVisibility() + * @see #close() + */ + public default boolean isShowing() { + JComponent c = getFilterComponent(); + return c.isShowing(); + } + /** * A method for subclasses to do any optional cleanup */