Merge remote-tracking branch

'origin/GP-6336-dragonmacher-filters-hide-action--SQUASHED'
(#8771)
This commit is contained in:
Ryan Kurtz
2026-02-23 14:33:26 -05:00
8 changed files with 165 additions and 36 deletions
@@ -63,10 +63,10 @@
</BLOCKQUOTE>
<H3><A name="Toggle_Filter"></A>Toggle Filter</H3>
<H3><A name="Hide_Filter"></A>Hide Filter</H3>
<BLOCKQUOTE>
<P>
The <B>Toggle Filter</B> action will hide and show the filter field of the tree or table.
The <B>Hide Filter</B> 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.
</P>
</BLOCKQUOTE>
@@ -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;
* <p>
* {@link #install()} must be called in order to install this <code>Singleton</code> into Java's
* key event processing system.
* <P>
* Keybindings are processed here to manage how {@link DockingAction}s will get executed. The basic
* action processing flow is:
* <OL>
* <LI>System actions (e.g., F1 for help)</LI>
* <LI>Java text components</LI>
* <LI>Java widget key listeners</LI>
* <LI>Java action map bindings</LI>
* <LI>{@link ComponentBasedDockingAction}s</LI>
* <LI>{@link ComponentProvider} local actions</LI>
* <LI>Tool global actions</LI>
* </OL>
* 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 {
@@ -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.
* <P>
* 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<ActionData> 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.
@@ -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);
}
//==================================================================================================
@@ -402,10 +402,20 @@ public class GTableFilterPanel<ROW_OBJECT> 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.
@@ -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() {
@@ -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() {
@@ -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
*/