diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesActionContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesActionContext.java index bb14af45ef..27868baaab 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesActionContext.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesActionContext.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,13 +15,15 @@ */ package ghidra.app.plugin.core.datamgr; -import java.util.ArrayList; -import java.util.List; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.Transferable; +import java.util.*; import javax.swing.tree.TreePath; import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; +import docking.widgets.tree.support.GTreeNodeTransferable; import ghidra.app.context.ProgramActionContext; import ghidra.app.plugin.core.datamgr.archive.BuiltInSourceArchive; import ghidra.app.plugin.core.datamgr.archive.ProjectArchive; @@ -37,6 +39,8 @@ public class DataTypesActionContext extends ProgramActionContext implements Doma private DataTypeArchiveGTree archiveGTree; private List domainFiles; + private List clipboardNodes; + public DataTypesActionContext(DataTypesProvider provider, Program program, DataTypeArchiveGTree archiveGTree, GTreeNode clickedNode) { this(provider, program, archiveGTree, clickedNode, false); @@ -44,13 +48,33 @@ public class DataTypesActionContext extends ProgramActionContext implements Doma public DataTypesActionContext(DataTypesProvider provider, Program program, DataTypeArchiveGTree archiveGTree, GTreeNode clickedNode, boolean isToolbarAction) { - super(provider, program, archiveGTree); this.archiveGTree = archiveGTree; this.clickedNode = clickedNode; this.isToolbarAction = isToolbarAction; } + public List getClipboardNodes() { + if (clipboardNodes != null) { + return clipboardNodes; + } + + // cache, since this could be slow + DataTypesProvider dtProvider = (DataTypesProvider) getComponentProvider(); + DataTypeManagerPlugin plugin = dtProvider.getPlugin(); + Clipboard clipboard = plugin.getClipboard(); + Transferable transferable = clipboard.getContents(this); + if (transferable instanceof GTreeNodeTransferable) { + GTreeNodeTransferable gtTransferable = (GTreeNodeTransferable) transferable; + clipboardNodes = gtTransferable.getAllData(); + } + + if (clipboardNodes == null) { + clipboardNodes = Collections.emptyList(); + } + return clipboardNodes; + } + public boolean isToolbarAction() { return isToolbarAction; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ClearCutAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ClearCutAction.java index 3d2e6cdfef..3998081af7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ClearCutAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ClearCutAction.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,11 +15,7 @@ */ package ghidra.app.plugin.core.datamgr.actions; -import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; -import ghidra.app.plugin.core.datamgr.tree.DataTypeTreeNode; - import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.Transferable; import java.awt.event.KeyEvent; import java.util.List; @@ -28,7 +23,9 @@ import docking.ActionContext; import docking.action.DockingAction; import docking.action.KeyBindingData; import docking.widgets.tree.GTreeNode; -import docking.widgets.tree.support.GTreeNodeTransferable; +import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; +import ghidra.app.plugin.core.datamgr.DataTypesActionContext; +import ghidra.app.plugin.core.datamgr.tree.DataTypeTreeNode; public class ClearCutAction extends DockingAction { private Clipboard clipboard; @@ -42,25 +39,35 @@ public class ClearCutAction extends DockingAction { setEnabled(true); } + @Override + public boolean isValidContext(ActionContext context) { + + // + // This action is particular about when it is valid. This is so that it does not interfere + // with Escape key presses for the parent window, except when this action has work to do. + // + if (!(context instanceof DataTypesActionContext dtc)) { + return false; + } + + return !dtc.getClipboardNodes().isEmpty(); + } + @Override public boolean isEnabledForContext(ActionContext context) { + // If we are valid, then we are enabled (see isValidContext()). Most actions are always + // valid, but only sometimes enabled. We use the valid check to remove ourselves completely + // from the workflow. But, if we are valid, then we are also enabled. return true; } @Override public void actionPerformed(ActionContext context) { - Transferable transferable = clipboard.getContents(this); - if (transferable instanceof GTreeNodeTransferable) { - GTreeNodeTransferable gtTransferable = (GTreeNodeTransferable) transferable; - List nodeList = gtTransferable.getAllData(); - if (nodeList.isEmpty()) { - return; - } - DataTypeTreeNode node = (DataTypeTreeNode) nodeList.get(0); - if (node.isCut()) { - clipboard.setContents(null, null); - } - + DataTypesActionContext dtc = (DataTypesActionContext) context; + List nodeList = dtc.getClipboardNodes(); + DataTypeTreeNode node = (DataTypeTreeNode) nodeList.get(0); + if (node.isCut()) { + clipboard.setContents(null, null); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index f736a043b2..9580d0a34f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -1327,7 +1327,7 @@ public class DialogComponentProvider private void addKeyBindingAction(DockingActionIf action) { - DialogActionProxy proxy = new DialogActionProxy(action); + DialogActionProxy proxy = new DialogActionProxy(this, action); keyBindingProxyActions.add(proxy); // The tool will be null when clients add actions to this dialog before it has been shown. @@ -1481,8 +1481,11 @@ public class DialogComponentProvider */ private class DialogActionProxy extends DockingActionProxy { - public DialogActionProxy(DockingActionIf dockingAction) { + private DialogComponentProvider provider; + + public DialogActionProxy(DialogComponentProvider provider, DockingActionIf dockingAction) { super(dockingAction); + this.provider = provider; } @Override @@ -1494,5 +1497,18 @@ public class DialogComponentProvider public ToolBarData getToolBarData() { return null; } + + @Override + public boolean isEnabledForContext(ActionContext context) { + if (context instanceof DialogActionContext dialogContext) { + DialogComponentProvider contextProvider = + dialogContext.getDialogComponentProvider(); + if (provider != contextProvider) { + return false; + } + return dockingAction.isEnabledForContext(context); + } + return false; + } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingActionProxy.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingActionProxy.java index 8b2fd3e9dd..de74bbb809 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingActionProxy.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingActionProxy.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,7 +31,7 @@ public class DockingActionProxy implements ToggleDockingActionIf, MultiActionDockingActionIf, PropertyChangeListener { private WeakSet propertyListeners = WeakDataStructureFactory.createSingleThreadAccessWeakSet(); - private final DockingActionIf dockingAction; + protected final DockingActionIf dockingAction; public DockingActionProxy(DockingActionIf dockingAction) { this.dockingAction = dockingAction; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java index bdc600b9ad..3b6f4edfc2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java @@ -15,8 +15,7 @@ */ package docking; -import java.awt.Toolkit; -import java.awt.event.ActionEvent; +import java.awt.Component; import java.util.List; import javax.swing.AbstractAction; @@ -47,33 +46,12 @@ public abstract class DockingKeyBindingAction extends AbstractAction { return true; // always enable; this is a internal action that cannot be disabled } - public void reportNotEnabled() { - String name = dockingAction.getName(); - String ksText = KeyBindingUtils.parseKeyStroke(keyStroke); - String message = "Action '%s' (%s) not currently enabled".formatted(name, ksText); - tool.setStatusInfo(message, true); - Toolkit.getDefaultToolkit().beep(); - } - - public abstract KeyBindingPrecedence getKeyBindingPrecedence(); + public abstract ExecutableAction getExecutableAction(Component focusOwner); public boolean isSystemKeybindingPrecedence() { return false; } - @Override - public void actionPerformed(final ActionEvent e) { - tool.setStatusInfo(""); - ComponentProvider provider = tool.getActiveComponentProvider(); - ActionContext context = getLocalContext(provider); - context.setSourceObject(e.getSource()); - dockingAction.actionPerformed(context); - } - - public List getValidActions(Object source) { - return getActions(); // the action for this class is always enabled and valid - } - protected ActionContext getLocalContext(ComponentProvider localProvider) { if (localProvider == null) { return new DefaultActionContext(); @@ -90,4 +68,5 @@ public abstract class DockingKeyBindingAction extends AbstractAction { public List getActions() { return List.of(dockingAction); } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ExecutableAction.java b/Ghidra/Framework/Docking/src/main/java/docking/ExecutableAction.java index 6a758c11ba..47a698d994 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ExecutableAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ExecutableAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,37 +15,24 @@ */ package docking; -import docking.action.DockingActionIf; -import docking.action.ToggleDockingActionIf; -import generic.json.Json; +import java.awt.Component; -public class ExecutableAction { +/** + * A class used by the {@link KeyBindingOverrideKeyEventDispatcher}. It represents an action and + * the context in which that action should operate if {@link #execute()} is called. This class is + * created for each keystroke that maps to a tool action. + *

+ * This is not meant to be used outside of this API. + */ +public interface ExecutableAction { - DockingActionIf action; - ActionContext context; + public boolean isValid(); - public ExecutableAction(DockingActionIf action, ActionContext context) { - this.action = action; - this.context = context; - } + public boolean isEnabled(); - public void execute() { - // Toggle actions do not toggle its state directly therefor we have to do it for - // them before we execute the action. - if (action instanceof ToggleDockingActionIf) { - ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action; - toggleAction.setSelected(!toggleAction.isSelected()); - } + public void reportNotEnabled(Component focusOwner); - action.actionPerformed(context); - } + public KeyBindingPrecedence getKeyBindingPrecedence(); - public DockingActionIf getAction() { - return action; - } - - @Override - public String toString() { - return Json.toString(action); - } + public void execute(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KbEnabledState.java b/Ghidra/Framework/Docking/src/main/java/docking/KbEnabledState.java new file mode 100644 index 0000000000..047b33e1b3 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/KbEnabledState.java @@ -0,0 +1,32 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking; + +/** + * A class to track an action's precedence and enablement + * @param precedence the precedence + * @param isValid true if valid + * @param isEnabled true if enabled + */ +public record KbEnabledState(KeyBindingPrecedence precedence, boolean isValid, + boolean isEnabled) { + + public KbEnabledState { + if (!isValid && isEnabled) { + throw new IllegalArgumentException("Cannot be enable if not also valid"); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java index ad4e5556fd..664086dcbd 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java @@ -20,12 +20,10 @@ import static docking.KeyBindingPrecedence.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; -import java.util.List; 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; @@ -57,7 +55,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher * Posterity Note: While debugging we will not get a KeyEvent.KEY_RELEASED event if * the focus changes from the application to the debugger tool. */ - private DockingKeyBindingAction inProgressAction; + private ExecutableAction inProgressAction; /** * Provides the current focus owner. This allows for dependency injection. @@ -119,75 +117,56 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher return true; } + // Special case for when we use one of our built-in menu navigation actions. We ignore + // docking actions if a menu is open (this is done in action.getEnabledState()). if (MenuKeyProcessor.processMenuKeyEvent(event)) { return true; } - DockingKeyBindingAction action = getDockingKeyBindingActionForEvent(event); + DockingKeyBindingAction action = getActionForEvent(event); if (action == null) { return false; // let the normal event flow continue } // *Special*, System key bindings--these can always be processed and are a higher priority - if (processSystemActionPrecedence(action, event)) { + Component focusOwner = focusProvider.getFocusOwner(); + ExecutableAction executableAction = action.getExecutableAction(focusOwner); + if (processSystemActionPrecedence(executableAction, event)) { return true; } - // some known special cases that we don't wish to process - if (!isValidContextForAction(event, action)) { - return false; - } - if (willBeHandledByTextComponent(event)) { return false; } - // no actions valid at all - // return - - // actions that are valid, but not enabled - - // also, is applicable: - // isValidContext(); - // is action for active local for provider; - // is focused - - // actions that are enabled - - KeyBindingPrecedence keyBindingPrecedence = getValidKeyBindingPrecedence(action); - if (keyBindingPrecedence == null) { - // Note: we used to return false here. Returning false allows Java to handle a given - // key stroke when our actions are disabled. We have decided it is simpler to - // always consume a given key stroke when we have actions registered. This - // prevents inconsistent action firing between Ghidra and Java, depending upon - // Ghidra's action enablement. If we find a case that is broken by this change, - // then we will need a more robust solution here. -// action.reportNotEnabled(); -// return true; + if (!executableAction.isValid()) { + // The action is not currently valid for the given focus owner. Let all key strokes go + // to Java when we have no valid context. This allows keys like Escape to work on Java + // widgets. return false; } + if (!executableAction.isEnabled()) { + // The action is valid, but is not enabled. In this case, we do not want Java to get + // the event. Instead, let the user know that they cannot perform the requested action. + // We keep the action from Java in this case to keep the event processing consistent + // whether or not the action is enabled. + executableAction.reportNotEnabled(focusOwner); + return true; + } + // Process the key event in precedence order. // If it processes the event at any given level, the short-circuit operator will kick out. // Finally, if the exception statement is reached, then someone has added a new level // of precedence that this algorithm has not taken into account! // @formatter:off - return processKeyListenerPrecedence(action, keyBindingPrecedence, event) || - processComponentActionMapPrecedence(action, keyBindingPrecedence, event) || - processActionAtPrecedence(DefaultLevel, keyBindingPrecedence, action, event) || + return processKeyListenerPrecedence(executableAction, event) || + processComponentActionMapPrecedence(executableAction, event) || + processActionAtPrecedence(DefaultLevel, executableAction, event) || throwAssertException(); // @formatter:on } - private KeyBindingPrecedence getValidKeyBindingPrecedence(DockingKeyBindingAction action) { - - if (action instanceof MultipleKeyAction) { - MultipleKeyAction multiAction = (MultipleKeyAction) action; - return multiAction.geValidKeyBindingPrecedence(focusProvider.getFocusOwner()); - } - return action.getKeyBindingPrecedence(); - } - /** * Returns true if the given key event should be blocked (i.e., not processed by us or Java). */ @@ -226,56 +205,13 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher private boolean actionInProgress(KeyEvent event) { boolean wasInProgress = inProgressAction != null; if (event.getID() == KeyEvent.KEY_RELEASED) { - DockingKeyBindingAction action = inProgressAction; + ExecutableAction action = inProgressAction; inProgressAction = null; - KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(event); - - // note: this call has no effect if 'action' is null - Object source = event.getSource(); - int modifiersEx = event.getModifiersEx(); - SwingUtilities.notifyAction(action, keyStroke, event, source, modifiersEx); - - } - return wasInProgress; - } - - private boolean isValidContextForAction(KeyEvent event, DockingKeyBindingAction kbAction) { - Window activeWindow = focusProvider.getActiveWindow(); - if (!(activeWindow instanceof DockingDialog dialog)) { - return true; // allow all non-dialog windows to process events - } - - // The choice to ignore modal dialogs was made long ago. We cannot remember why the - // choice was made, but speculate that odd things can happen when keybindings are - // processed with modal dialogs open. For now, do not let key bindings get processed - // for modal dialogs. This can be changed in the future if needed. - if (!dialog.isModal()) { - return true; - } - - // Allow modal dialogs to process their own actions - DialogComponentProvider provider = dialog.getComponent(); - List actions = kbAction.getValidActions(event.getSource()); - if (actions.isEmpty()) { - return false; // no actions; not a valid key stroke for this dialog - } - for (DockingActionIf action : actions) { - if (!isAllowedDialogAction(provider, action)) { - return false; + if (action != null) { + action.execute(); } } - - return true; // all actions belong to the active dialog; this is a valid action - } - - private boolean isAllowedDialogAction(DialogComponentProvider provider, - DockingActionIf action) { - - if (action instanceof ComponentBasedDockingAction) { - return true; // these actions work on low-level components, which may live in dialogs - } - - return provider.isDialogKeyBindingAction(action); + return wasInProgress; } private boolean isSettingKeyBindings(KeyEvent event) { @@ -370,7 +306,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher throw new AssertException("New precedence added to KeyBindingPrecedence?"); } - private boolean processSystemActionPrecedence(DockingKeyBindingAction action, + private boolean processSystemActionPrecedence(ExecutableAction executableAction, KeyEvent event) { if (isSettingKeyBindings(event)) { @@ -379,21 +315,17 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher return false; } - if (!action.isSystemKeybindingPrecedence()) { + KeyBindingPrecedence precedence = executableAction.getKeyBindingPrecedence(); + if (precedence != SystemActionsLevel) { return false; } - if (inProgressAction != null) { - return true; - } - - inProgressAction = action; // this will be handled on the release + inProgressAction = executableAction; // this will be handled on the release return true; } - private boolean processKeyListenerPrecedence(DockingKeyBindingAction action, - KeyBindingPrecedence keyBindingPrecedence, KeyEvent e) { - if (processActionAtPrecedence(KeyBindingPrecedence.KeyListenerLevel, keyBindingPrecedence, + private boolean processKeyListenerPrecedence(ExecutableAction action, KeyEvent e) { + if (processActionAtPrecedence(KeyBindingPrecedence.KeyListenerLevel, action, e)) { return true; } @@ -407,10 +339,8 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher return false; } - private boolean processComponentActionMapPrecedence(DockingKeyBindingAction action, - KeyBindingPrecedence keyBindingPrecedence, KeyEvent event) { - - if (processActionAtPrecedence(ActionMapLevel, keyBindingPrecedence, action, event)) { + private boolean processComponentActionMapPrecedence(ExecutableAction action, KeyEvent event) { + if (processActionAtPrecedence(ActionMapLevel, action, event)) { return true; } @@ -422,11 +352,11 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher return false; } - private boolean processActionAtPrecedence(KeyBindingPrecedence precedence, - KeyBindingPrecedence keyBindingPrecedence, DockingKeyBindingAction action, - KeyEvent event) { + private boolean processActionAtPrecedence(KeyBindingPrecedence keyBindingPrecedence, + ExecutableAction action, KeyEvent event) { - if (keyBindingPrecedence != precedence) { + KeyBindingPrecedence actionPrecedence = action.getKeyBindingPrecedence(); + if (keyBindingPrecedence != actionPrecedence) { return false; } @@ -506,7 +436,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher * @param event The key event to check. * @return An action, if one is available for the given key event, in the current context. */ - private DockingKeyBindingAction getDockingKeyBindingActionForEvent(KeyEvent event) { + private DockingKeyBindingAction getActionForEvent(KeyEvent event) { DockingWindowManager activeManager = getActiveDockingWindowManager(); if (activeManager == null) { return null; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/MultiActionDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/MultiActionDialog.java index 0565bc9981..fb96d851a1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/MultiActionDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/MultiActionDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import java.util.List; import javax.swing.*; import docking.action.DockingActionIf; +import docking.action.ToggleDockingActionIf; import docking.event.mouse.GMouseListenerAdapter; import docking.widgets.label.GIconLabel; import docking.widgets.label.GLabel; @@ -33,25 +34,27 @@ import docking.widgets.label.GLabel; public class MultiActionDialog extends DialogComponentProvider { private String keystrokeName; - private List list; private JList actionList; private DefaultListModel listModel; + private List actions; + private ActionContext context; + /** * Constructor * @param keystrokeName keystroke name - * @param list list of actions + * @param actions list of actions + * @param context the context */ - public MultiActionDialog(String keystrokeName, List list) { + public MultiActionDialog(String keystrokeName, List actions, + ActionContext context) { super("Select Action", true); this.keystrokeName = keystrokeName; + this.context = context; init(); - setActionList(list); + setActionList(actions); } - /** - * The callback method for when the "OK" button is pressed. - */ @Override protected void okCallback() { maybeDoAction(); @@ -65,21 +68,23 @@ public class MultiActionDialog extends DialogComponentProvider { close(); - ExecutableAction actionProxy = list.get(index); - actionProxy.execute(); + DockingActionIf action = actions.get(index); + + // Toggle actions do not toggle its state directly therefor we have to do it for + // them before we execute the action. + if (action instanceof ToggleDockingActionIf) { + ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action; + toggleAction.setSelected(!toggleAction.isSelected()); + } + + action.actionPerformed(context); } - /** - * Set the list of actions that are enabled - * @param list list of actions selected - */ - public void setActionList(List list) { + private void setActionList(List actions) { okButton.setEnabled(false); - this.list = list; + this.actions = actions; listModel.clear(); - for (int i = 0; i < list.size(); i++) { - ExecutableAction actionProxy = list.get(i); - DockingActionIf action = actionProxy.getAction(); + for (DockingActionIf action : actions) { listModel.addElement(action.getName() + " (" + action.getOwnerDescription() + ")"); } actionList.setSelectedIndex(0); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/SystemExecutableAction.java b/Ghidra/Framework/Docking/src/main/java/docking/SystemExecutableAction.java new file mode 100644 index 0000000000..e7b096109d --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/SystemExecutableAction.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking; + +import java.awt.Component; + +import javax.help.UnsupportedOperationException; + +import docking.action.DockingActionIf; +import docking.action.ToggleDockingActionIf; + +public class SystemExecutableAction implements ExecutableAction { + private DockingActionIf action; + private ActionContext context; + + public SystemExecutableAction(DockingActionIf action, ActionContext context) { + this.action = action; + this.context = context; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void reportNotEnabled(Component focusOwner) { + // we are always enabled + throw new UnsupportedOperationException(); + } + + @Override + public KeyBindingPrecedence getKeyBindingPrecedence() { + return action.getKeyBindingData().getKeyBindingPrecedence(); + } + + @Override + public void execute() { + // Toggle actions do not toggle its state directly therefor we have to do it for + // them before we execute the action. + if (action instanceof ToggleDockingActionIf) { + ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action; + toggleAction.setSelected(!toggleAction.isSelected()); + } + + action.actionPerformed(context); + } + + @Override + public String toString() { + return action.getFullName(); + } +} 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 9e0ea4121c..a8f3050f79 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java @@ -20,6 +20,7 @@ import java.awt.event.ActionEvent; import java.util.*; import java.util.List; +import javax.help.UnsupportedOperationException; import javax.swing.*; import docking.*; @@ -110,112 +111,115 @@ public class MultipleKeyAction extends DockingKeyBindingAction { } @Override - public List getValidActions(Object source) { - - if (ignoreActionWhileMenuShowing()) { - return List.of(); - } - - List validActions = new ArrayList<>(); - List proxyActions = getActionsForCurrentOrDefaultContext(source); - for (ExecutableAction proxy : proxyActions) { - DockingActionIf action = proxy.getAction(); - validActions.add(action); - } - return validActions; + public void actionPerformed(ActionEvent event) { + // A vestige from when we used to send this class through the Swing API. Execution is now + // done on the ExecutableAction this class creates. + throw new UnsupportedOperationException(); } - @Override - public void actionPerformed(final ActionEvent event) { - // Build list of actions which are valid in current context - List list = getActionsForCurrentOrDefaultContext(event.getSource()); + private boolean ignoreActionWhileMenuShowing(ExecutableAction action) { - // If menu active, disable all key bindings - if (ignoreActionWhileMenuShowing()) { - return; - } - - // If more than one action, prompt user for selection - if (list.size() > 1) { - // popup dialog to show multiple actions - MultiActionDialog dialog = - new MultiActionDialog(KeyBindingUtils.parseKeyStroke(keyStroke), list); - - // doing the show in an invoke later seems to fix a strange swing bug that lock up - // the program if you tried to invoke a new action too quickly after invoking - // it the first time - Swing.runLater(() -> DockingWindowManager.showDialog(dialog)); - } - else if (list.size() == 1) { - ExecutableAction actionProxy = list.get(0); - tool.setStatusInfo(""); - actionProxy.execute(); - } - else { - String name = (String) getValue(Action.NAME); - tool.setStatusInfo("Action (" + name + ") not valid in this context!", true); - Toolkit.getDefaultToolkit().beep(); - } - } - - private boolean ignoreActionWhileMenuShowing() { - if (getKeyBindingPrecedence() == KeyBindingPrecedence.SystemActionsLevel) { - return false; // allow system bindings through "no matter what!" + KeyBindingPrecedence precedence = action.getKeyBindingPrecedence(); + if (precedence == KeyBindingPrecedence.SystemActionsLevel) { + // Allow system bindings through. This allows actions like Help to work for menus. + return false; } MenuSelectionManager menuManager = MenuSelectionManager.defaultManager(); return menuManager.getSelectedPath().length != 0; } - private List getValidContextActions(ActionContext localContext, + private ExecutableAction createNonDialogExecutableAction(ActionContext localContext, Map, ActionContext> contextMap) { - List list = new ArrayList<>(); - boolean hasLocalActionsForKeyBinding = false; + + 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; + } + + // + // 2) Check for actions local to the source component (e.g., GTable and GTree) + // + getLocalComponentActions(localContext, multiAction); + if (multiAction.isValid()) { + // At this point, we have local component actions that may or may not be enabled. Exit + // so that any global actions found below will not interfere with these component + // actions. + return multiAction; + } + + // + // 3) Check for global actions using the current context + // + getGlobalActions(localContext, multiAction); + if (multiAction.isValid()) { + // We have found global actions that are valid for the current local context. Do not + // also look for global actions that work for the default context. + return multiAction; + } + + // + // 4) Check for global actions using the default context. This is a final fallback to allow + // global actions to work that are unrelated to the current active component's context. + // + getGlobalDefaultContextActions(contextMap, multiAction); + return multiAction; + } + + private void getLocalContextActions(ActionContext localContext, + MultiExecutableAction multiAction) { + for (ActionData actionData : actions) { - if (actionData.isMyProvider(localContext)) { - hasLocalActionsForKeyBinding = true; - if (isValidAndEnabled(actionData, localContext)) { - list.add(new ExecutableAction(actionData.action, localContext)); - } + if (!actionData.isMyProvider(localContext)) { + continue; + } + + if (!isValid(actionData, localContext)) { + continue; + } + + multiAction.setLocal(true); + multiAction.setContext(localContext); + multiAction.addValidAction(actionData.action); + + if (isEnabled(actionData, localContext)) { + multiAction.addEnabledAction(actionData.action); } } + } - if (hasLocalActionsForKeyBinding) { - // At this point, we have local actions that may or may not be enabled. Return here - // so that any component specific actions found below will not interfere with the - // provider's local actions - return list; - } + private void getLocalComponentActions(ActionContext localContext, + MultiExecutableAction multiAction) { - // - // 2) Check for actions local to the source component - // for (ActionData actionData : actions) { if (!(actionData.action instanceof ComponentBasedDockingAction componentAction)) { continue; } - if (componentAction.isValidComponentContext(localContext)) { - hasLocalActionsForKeyBinding = true; - if (isValidAndEnabled(actionData, localContext)) { - list.add(new ExecutableAction(actionData.action, localContext)); - } + if (!componentAction.isValidComponentContext(localContext)) { + continue; + } + + multiAction.setContext(localContext); + multiAction.addValidAction(actionData.action); + + if (isEnabled(actionData, localContext)) { + multiAction.addEnabledAction(actionData.action); } } + } - if (hasLocalActionsForKeyBinding) { - // We have locals, ignore the globals. This prevents global actions from processing - // the given keybinding when a local action exits, regardless of enablement. - return list; - } + private void getGlobalActions(ActionContext localContext, + MultiExecutableAction multiAction) { - // - // 3) Check for default context actions - // for (ActionData actionData : actions) { if (!actionData.isGlobalAction()) { continue; @@ -223,13 +227,25 @@ public class MultipleKeyAction extends DockingKeyBindingAction { // When looking for context matches, we prefer local context, even though this // is a 'global' action. This allows more specific context to be used when available - if (isValidAndEnabled(actionData, localContext)) { - list.add(new ExecutableAction(actionData.action, localContext)); + if (!isValid(actionData, localContext)) { continue; } - // this happens if we are in a dialog, default context is not used - if (contextMap == null) { + multiAction.setContext(localContext); + multiAction.addValidAction(actionData.action); + + if (isEnabled(actionData, localContext)) { + multiAction.addEnabledAction(actionData.action); + } + } + } + + private void getGlobalDefaultContextActions( + Map, ActionContext> contextMap, + MultiExecutableAction multiAction) { + + for (ActionData actionData : actions) { + if (!actionData.isGlobalAction()) { continue; } @@ -238,19 +254,33 @@ public class MultipleKeyAction extends DockingKeyBindingAction { } ActionContext defaultContext = contextMap.get(actionData.getContextType()); - if (isValidAndEnabled(actionData, defaultContext)) { - list.add(new ExecutableAction(actionData.action, defaultContext)); + if (!isValid(actionData, defaultContext)) { + continue; + } + + multiAction.setContext(defaultContext); + multiAction.addValidAction(actionData.action); + + if (isEnabled(actionData, defaultContext)) { + multiAction.addEnabledAction(actionData.action); } } - return list; } - private boolean isValidAndEnabled(ActionData actionData, ActionContext context) { + private boolean isValid(ActionData actionData, ActionContext context) { if (context == null) { return false; } DockingActionIf a = actionData.action; - return a.isValidContext(context) && a.isEnabledForContext(context); + return a.isValidContext(context); + } + + private boolean isEnabled(ActionData actionData, ActionContext context) { + if (context == null) { + return false; + } + DockingActionIf a = actionData.action; + return a.isEnabledForContext(context); } @Override @@ -259,65 +289,87 @@ public class MultipleKeyAction extends DockingKeyBindingAction { } @Override - public KeyBindingPrecedence getKeyBindingPrecedence() { - return geValidKeyBindingPrecedence(null); - } + public ExecutableAction getExecutableAction(Component source) { + ExecutableAction action = createExecutableAction(source); - /** - * This is a special version of {@link #getKeyBindingPrecedence()} that allows the internal - * key event processing to specify the source component when determining how precedence should - * be established for the actions contained herein. - * @param source the component; may be null - * @return the precedence; may be null - */ - public KeyBindingPrecedence geValidKeyBindingPrecedence(Component source) { - - List validActions = getActionsForCurrentOrDefaultContext(source); - if (validActions.isEmpty()) { - return null; // a signal that no actions are valid for the current context + // If menu active, disable all default key bindings + if (ignoreActionWhileMenuShowing(action)) { + return new MultiExecutableAction(); } - if (validActions.size() != 1) { - return KeyBindingPrecedence.DefaultLevel; - } - - ExecutableAction actionProxy = validActions.get(0); - DockingActionIf action = actionProxy.getAction(); - return action.getKeyBindingData().getKeyBindingPrecedence(); + return action; } - private List getActionsForCurrentOrDefaultContext(Object eventSource) { + private ExecutableAction createExecutableAction(Object eventSource) { DockingWindowManager dwm = tool.getWindowManager(); Window window = getWindow(dwm, eventSource); if (window instanceof DockingDialog) { - return getDialogActions(window); + return createDialogActions(eventSource, window); } ComponentProvider localProvider = getProvider(dwm, eventSource); ActionContext localContext = getLocalContext(localProvider); localContext.setSourceObject(eventSource); Map, ActionContext> contextMap = - tool.getWindowManager().getDefaultActionContextMap(); - List validActions = getValidContextActions(localContext, contextMap); - return validActions; + dwm.getDefaultActionContextMap(); + return createNonDialogExecutableAction(localContext, contextMap); } - private List getDialogActions(Window window) { + private ExecutableAction createDialogActions(Object eventSource, Window window) { + + MultiExecutableAction multiAction = new MultiExecutableAction(); + DockingDialog dockingDialog = (DockingDialog) window; DialogComponentProvider provider = dockingDialog.getDialogComponent(); if (provider == null) { // this can happen if the dialog is closed during key event processing - return Collections.emptyList(); + return multiAction; } ActionContext context = provider.getActionContext(null); if (context == null) { - return Collections.emptyList(); + return multiAction; } - List validActions = getValidContextActions(context, null); - return validActions; + // + // 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. + + // + // 2) Check for actions local to the source component (e.g., GTable and GTree) + // + getLocalComponentActions(context, multiAction); + if (multiAction.isValid()) { + // At this point, we have local component actions that may or may not be enabled. Exit + // so that any global actions found below will not interfere with these component + // actions. + return multiAction; + } + + // + // 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. + // + getGlobalActions(context, multiAction); + + // The choice to ignore global actions for modal dialogs was made long ago. We cannot + // remember why the choice was made, but speculate that odd things can happen when + // keybindings are processed with modal dialogs open. For now, do not let non-dialog + // actions get processed for modal dialogs. This can be changed in the future if needed. + if (provider.isModal()) { + multiAction.filterAndKeepOnlyDialogActions(provider); + } + + // Note: we currently do not use *default* global actions in dialogs. It is not clear if + // this decision was intentional. + // if (!provider.isModal()) { + // getGlobalDefaultContextActions(...); + // } + + return multiAction; } private ComponentProvider getProvider(DockingWindowManager dwm, Object eventSource) { @@ -383,6 +435,9 @@ public class MultipleKeyAction extends DockingKeyBindingAction { } boolean isMyProvider(ActionContext localContext) { + if (provider == null) { + return false; + } ComponentProvider otherProvider = localContext.getComponentProvider(); return provider == otherProvider; } @@ -392,6 +447,142 @@ public class MultipleKeyAction extends DockingKeyBindingAction { String providerString = provider == null ? "" : provider.toString() + " - "; return providerString + action; } + } + /** + * An extension of {@link ExecutableAction} that itself contains 0 or more + * {@link ExecutableAction}s. This class is used to create a snapshot of all actions valid and + * enabled for a given keystroke. + */ + private class MultiExecutableAction implements ExecutableAction { + + private List validActions = new ArrayList<>(); + private List enabledActions = new ArrayList<>(); + + private ActionContext context; + private boolean isLocalAction; + + @Override + public void execute() { + + if (enabledActions.size() == 1) { + DockingActionIf action = enabledActions.get(0); + tool.setStatusInfo(""); + + // Toggle actions do not toggle its state directly therefor we have to do it for + // them before we execute the action. + if (action instanceof ToggleDockingActionIf) { + ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action; + toggleAction.setSelected(!toggleAction.isSelected()); + } + + action.actionPerformed(context); + + return; + } + + // If more than one action, prompt user to choose from multiple actions + MultiActionDialog dialog = + new MultiActionDialog(KeyBindingUtils.parseKeyStroke(keyStroke), enabledActions, + context); + + // doing the show in an invoke later seems to fix a strange swing bug that lock up + // the program if you tried to invoke a new action too quickly after invoking + // it the first time + Swing.runLater(() -> DockingWindowManager.showDialog(dialog)); + } + + @Override + public KeyBindingPrecedence getKeyBindingPrecedence() { + KeyBindingPrecedence precedence = KeyBindingPrecedence.DefaultLevel; + if (enabledActions.size() == 1) { + DockingActionIf action = enabledActions.get(0); + precedence = action.getKeyBindingData().getKeyBindingPrecedence(); + } + return precedence; + } + + @Override + public boolean isValid() { + return !validActions.isEmpty(); + } + + @Override + public boolean isEnabled() { + return !enabledActions.isEmpty(); + } + + void setLocal(boolean isLocal) { + this.isLocalAction = isLocal; + } + + void setContext(ActionContext context) { + if (this.context != null && this.context != context) { + throw new IllegalArgumentException("Context cannot be changed once set"); + } + this.context = context; + } + + void addValidAction(DockingActionIf a) { + validActions.add(a); + } + + void addEnabledAction(DockingActionIf a) { + enabledActions.add(a); + } + + /** + * Keeps only those actions in the list that are owned by the given dialog provider + * @param provider the provider + */ + void filterAndKeepOnlyDialogActions(DialogComponentProvider provider) { + + Iterator it = validActions.iterator(); + while (it.hasNext()) { + DockingActionIf action = it.next(); + if (!provider.isDialogKeyBindingAction(action)) { + it.remove(); + enabledActions.remove(action); + } + } + } + + private String getContextText(Component focusOwner) { + DockingWindowManager dwm = tool.getWindowManager(); + Window window = getWindow(dwm, focusOwner); + if (window instanceof DockingDialog) { + return "in this dialog"; + } + + if (!isLocalAction) { + // no need to warn about global/default actions, as that may be annoying when the + // keystrokes bubble up to the global level + return null; + } + + ComponentProvider provider = context.getComponentProvider(); + if (provider != null) { + return "in " + provider.getName(); + } + + return "for context"; + } + + @Override + public void reportNotEnabled(Component focusOwner) { + + String contextText = getContextText(focusOwner); + if (contextText == null) { + return; + } + + DockingActionIf action = validActions.get(0); + String actionName = action.getName(); + String ksText = KeyBindingUtils.parseKeyStroke(keyStroke); + String message = + "'%s' (%s) not currently enabled %s".formatted(actionName, ksText, contextText); + tool.setStatusInfo(message, true); + Toolkit.getDefaultToolkit().beep(); + } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/SystemKeyBindingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/SystemKeyBindingAction.java index c5bf2391c4..cb1d08ef77 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/SystemKeyBindingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/SystemKeyBindingAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,6 +15,10 @@ */ package docking.action; +import java.awt.Component; +import java.awt.event.ActionEvent; + +import javax.help.UnsupportedOperationException; import javax.swing.KeyStroke; import docking.*; @@ -33,13 +37,29 @@ public class SystemKeyBindingAction extends DockingKeyBindingAction { return dockingAction; } + private ActionContext getContext(Component focusOwner) { + ComponentProvider provider = tool.getActiveComponentProvider(); + ActionContext context = getLocalContext(provider); + context.setSourceObject(focusOwner); + return context; + } + @Override public boolean isSystemKeybindingPrecedence() { return true; } @Override - public KeyBindingPrecedence getKeyBindingPrecedence() { - return KeyBindingPrecedence.SystemActionsLevel; + public ExecutableAction getExecutableAction(Component focusOwner) { + ActionContext context = getContext(focusOwner); + return new SystemExecutableAction(dockingAction, context); } + + @Override + public void actionPerformed(ActionEvent e) { + // A vestige from when we used to send this class through the Swing API. Execution is now + // done on the ExecutableAction this class creates. + throw new UnsupportedOperationException(); + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java index aca1b5434f..21c3454feb 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java @@ -258,6 +258,11 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options return false; } + @Override + public boolean isValidContext(ActionContext context) { + return false; + } + @Override public boolean isEnabledForContext(ActionContext context) { return false; 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 bc25c1e11f..ab05d8a86c 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 @@ -1923,7 +1923,7 @@ public class GTree extends JPanel implements BusyListener { @Override public void actionPerformed(ActionContext context) { - GTree gTree = (GTree) context.getSourceComponent(); + GTree gTree = getTree(context); gTree.tree.isCopyFormatted = true; try { Action builtinCopyAction = TransferHandler.getCopyAction(); @@ -1948,7 +1948,7 @@ public class GTree extends JPanel implements BusyListener { GTreeAction activateFilterAction = new GTreeAction("Table/Tree Activate Filter", owner) { @Override public void actionPerformed(ActionContext context) { - GTree gTree = (GTree) context.getSourceComponent(); + GTree gTree = getTree(context); gTree.filterProvider.activate(); } }; @@ -1966,7 +1966,7 @@ public class GTree extends JPanel implements BusyListener { GTreeAction toggleFilterAction = new GTreeAction("Table/Tree Toggle Filter", owner) { @Override public void actionPerformed(ActionContext context) { - GTree gTree = (GTree) context.getSourceComponent(); + GTree gTree = getTree(context); gTree.filterProvider.toggleVisibility(); } };