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:
+ *
+ * - System actions (e.g., F1 for help)
+ * - Java text components
+ * - Java widget key listeners
+ * - Java action map bindings
+ * - {@link ComponentBasedDockingAction}s
+ * - {@link ComponentProvider} local actions
+ * - Tool global actions
+ *
+ * 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
*/