diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadedIntegrationTest.java b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadedIntegrationTest.java index e5b56ea77d..ad55183d67 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadedIntegrationTest.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadedIntegrationTest.java @@ -15,21 +15,20 @@ */ package ghidra.test; -import java.awt.Window; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.Iterator; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import docking.*; +import docking.DialogComponentProvider; import docking.action.DockingActionIf; import docking.widgets.fieldpanel.FieldPanel; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.listener.FieldMouseListener; import docking.widgets.fieldpanel.support.FieldLocation; import ghidra.GhidraTestApplicationLayout; -import ghidra.app.plugin.core.analysis.AutoAnalysisManager; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.framework.ApplicationConfiguration; import ghidra.framework.GhidraApplicationConfiguration; @@ -41,10 +40,10 @@ import ghidra.program.model.listing.Program; import ghidra.util.TaskUtilities; import ghidra.util.exception.AssertException; import junit.framework.AssertionFailedError; -import util.CollectionUtils; import utility.application.ApplicationLayout; -public abstract class AbstractGhidraHeadedIntegrationTest extends AbstractGhidraHeadlessIntegrationTest { +public abstract class AbstractGhidraHeadedIntegrationTest + extends AbstractGhidraHeadlessIntegrationTest { public AbstractGhidraHeadedIntegrationTest() { super(); @@ -113,42 +112,6 @@ public abstract class AbstractGhidraHeadedIntegrationTest extends AbstractGhidra return null; } - /** - * Finds the action by the given owner name and action name. - * If you do not know the owner name, then use - * the call {@link #getActions(DockingTool, String)} instead. - * - *

Note: more specific test case subclasses provide other methods for finding actions - * when you have an owner name (which is usually the plugin name). - * - * @param tool the tool containing all system actions - * @param name the name to match - * @return the matching action; null if no matching action can be found - */ - public static DockingActionIf getAction(DockingTool tool, String owner, String name) { - String fullName = name + " (" + owner + ")"; - List actions = tool.getDockingActionsByFullActionName(fullName); - if (actions.isEmpty()) { - return null; - } - - if (actions.size() > 1) { - // This shouldn't happen - throw new AssertionFailedError( - "Found more than one action for name '" + fullName + "'"); - } - - return CollectionUtils.any(actions); - } - - public static DockingActionIf getAction(Plugin plugin, String actionName) { - return getAction(plugin.getTool(), plugin.getName(), actionName); - } - - public static DockingActionIf getLocalAction(ComponentProvider provider, String actionName) { - return getAction(provider.getTool(), provider.getName(), actionName); - } - public static PluginTool showTool(final PluginTool tool) { runSwing(() -> { boolean wasErrorGUIEnabled = isUseErrorGUI(); @@ -162,9 +125,7 @@ public abstract class AbstractGhidraHeadedIntegrationTest extends AbstractGhidra /** * Shows the given DialogComponentProvider using the given tool's - * {@link PluginTool#showDialog(DialogComponentProvider)} method. After calling show on a - * new thread the method will then wait for the dialog to be shown by calling - * {@link TestEnv#waitForDialogComponent(Window, Class, int)}. + * {@link PluginTool#showDialog(DialogComponentProvider)} method. * * @param tool The tool used to show the given provider. * @param provider The DialogComponentProvider to show. @@ -206,22 +167,8 @@ public abstract class AbstractGhidraHeadedIntegrationTest extends AbstractGhidra waitForSwing(); } - /** - * @deprecated use {@link #waitForBusyTool(PluginTool)} instead - */ - @Deprecated - public static void waitForAnalysis() { - @SuppressWarnings("unchecked") - Map programToManagersMap = - (Map) getInstanceField("managerMap", - AutoAnalysisManager.class); - - Collection managers = programToManagersMap.values(); - for (AutoAnalysisManager manager : managers) { - while (manager.isAnalyzing()) { - sleep(DEFAULT_WAIT_DELAY); - } - } + public static DockingActionIf getAction(Plugin plugin, String actionName) { + return getAction(plugin.getTool(), plugin.getName(), actionName); } /** @@ -230,6 +177,7 @@ public abstract class AbstractGhidraHeadedIntegrationTest extends AbstractGhidra * * @param project The project which with the tool is associated. * @param tool The tool to be saved + * @return the new tool */ public static PluginTool saveTool(final Project project, final PluginTool tool) { AtomicReference ref = new AtomicReference<>(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java index c91695342e..bf60aab0df 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingAction.java @@ -51,11 +51,6 @@ import utilities.util.reflection.ReflectionUtilities; * method allows actions to manage their own enablement. Otherwise, the default behavior for this * method is to return the current enabled property of the action. This allows for the possibility * for plugins to manage the enablement of its actions. - *

- * By default, actions that are not enabledForContext do not appear in the popup menu. To change - * that behavior, implementors can also override {@link #deleteThisContextMethod(ActionContext)}. - * This method is used to determine if the action should appear on the popup menu based on the given - * context. */ public abstract class DockingAction implements DockingActionIf { @@ -283,8 +278,8 @@ public abstract class DockingAction implements DockingActionIf { //================================================================================================== /** - * Sets the {@link #MenuData} to be used to put this action on the tool's menu bar. - * @param newMenuData the MenuData to be used to put this action on the tool's menu bar. + * Sets the {@link MenuData} to be used to put this action on the tool's menu bar + * @param newMenuData the MenuData to be used to put this action on the tool's menu bar */ public void setMenuBarData(MenuData newMenuData) { MenuBarData oldData = menuBarData; @@ -295,8 +290,8 @@ public abstract class DockingAction implements DockingActionIf { } /** - * Sets the {@link #MenuData} to be used to put this action in the tool's popup menu. - * @param newMenuData the MenuData to be used to put this action on the tool's popup menu. + * Sets the {@link MenuData} to be used to put this action in the tool's popup menu + * @param newMenuData the MenuData to be used to put this action on the tool's popup menu */ public void setPopupMenuData(MenuData newMenuData) { PopupMenuData oldData = popupMenuData; @@ -307,8 +302,8 @@ public abstract class DockingAction implements DockingActionIf { } /** - * Sets the {@link #ToolBarData} to be used to put this action on the tool's toolbar. - * @param newToolBarData the ToolBarData to be used to put this action on the tool's toolbar. + * Sets the {@link ToolBarData} to be used to put this action on the tool's toolbar + * @param newToolBarData the ToolBarData to be used to put this action on the tool's toolbar */ public void setToolBarData(ToolBarData newToolBarData) { @@ -321,7 +316,7 @@ public abstract class DockingAction implements DockingActionIf { } /** - * Sets the {@link #KeyBindingData} to be used to assign this action to a keybinding. + * Sets the {@link KeyBindingData} to be used to assign this action to a keybinding. * @param newKeyBindingData the KeyBindingData to be used to assign this action to a keybinding. */ @Override @@ -339,10 +334,7 @@ public abstract class DockingAction implements DockingActionIf { /** * Users creating actions should not call this method, but should instead call * {@link #setKeyBindingData(KeyBindingData)}. - * @param newKeyBindingData the KeyBindingData to be used to assign this action to a keybinding. - * @param validate true signals that this method should convert keybindings to their - * OS-dependent form (for example, on Mac a Ctrl - * key is changed to the Command key). + * @param newKeyBindingData the KeyBindingData to be used to assign this action to a keybinding */ @Override public void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData) { @@ -364,7 +356,7 @@ public abstract class DockingAction implements DockingActionIf { /** * Sets the description to be used in the tooltip. - * @param description the description to be set. + * @param newDescription the description to be set. */ public void setDescription(String newDescription) { if (SystemUtilities.isEqual(newDescription, description)) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingActionIf.java b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingActionIf.java index dd14547d31..38f2634904 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/DockingActionIf.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/DockingActionIf.java @@ -33,27 +33,30 @@ public interface DockingActionIf extends HelpDescriptor { public static final String TOOLBAR_DATA_PROPERTY = "ToolBar"; /** - * Returns the name of the action. + * Returns the name of the action + * @return the name */ - public abstract String getName(); + public String getName(); /** - * Returns the owner of this action. + * Returns the owner of this action + * @return the owner */ - public abstract String getOwner(); + public String getOwner(); /** - * Returns a short description of this action. Generally used for a tooltip. + * Returns a short description of this action. Generally used for a tooltip + * @return the description */ - public abstract String getDescription(); + public String getDescription(); /** - * Adds a listener to be notified if any property changes. + * Adds a listener to be notified if any property changes * @param listener The property change listener that will be notified of * property change events. - * @see AbstractAction#addPropertyChangeListener(java.beans.PropertyChangeListener) + * @see Action#addPropertyChangeListener(java.beans.PropertyChangeListener) */ - public abstract void addPropertyChangeListener(PropertyChangeListener listener); + public void addPropertyChangeListener(PropertyChangeListener listener); /** * Removes a listener to be notified of property changes. @@ -61,15 +64,15 @@ public interface DockingActionIf extends HelpDescriptor { * @param listener The property change listener that will be notified of * property change events. * @see #addPropertyChangeListener(PropertyChangeListener) - * @see AbstractAction#addPropertyChangeListener(java.beans.PropertyChangeListener) + * @see Action#addPropertyChangeListener(java.beans.PropertyChangeListener) */ - public abstract void removePropertyChangeListener(PropertyChangeListener listener); + public void removePropertyChangeListener(PropertyChangeListener listener); /** - * Enables or disables the action. + * Enables or disables the action * - * @param newValue true to enable the action, false to - * disable it + * @param newValue true to enable the action, false to disable it + * @return the enabled value of the action after this call */ public boolean setEnabled(boolean newValue); @@ -124,32 +127,34 @@ public interface DockingActionIf extends HelpDescriptor { /** * Returns the full name (the action name combined with the owner name) + * @return the full name */ - public abstract String getFullName(); + public String getFullName(); /** * method to actually perform the action logic for this action. * @param context the {@link ActionContext} object that provides information about where and how * this action was invoked. */ - public abstract void actionPerformed(ActionContext context); + public void actionPerformed(ActionContext context); /** * method is used to determine if this action should be displayed on the current popup. This * method will only be called if the action has popup {@link PopupMenuData} set. *

* Generally, actions don't need to override this method as the default implementation will - * defer to the {@link #isEnabledForContext()}, which will have the effect of adding the - * action to the popup only if it is enabled for a given context. By overriding this method, + * defer to the {@link #isEnabledForContext(ActionContext)}, which will have the effect + * of adding the action to the popup only if it is enabled for a given context. + * By overriding this method, * you can change this behavior so that the action will be added to the popup, even if it is * disabled for the context, by having this method return true even if the - * {@link #isEnabledForContext()} method will return false, resulting in the action appearing - * in the popup menu, but begin disabled. + * {@link #isEnabledForContext(ActionContext)} method will return false, resulting in the + * action appearing in the popup menu, but begin disabled. * * @param context the {@link ActionContext} from the active provider. * @return true if this action is appropriate for the given context. */ - public abstract boolean isAddToPopup(ActionContext context); + public boolean isAddToPopup(ActionContext context); /** * Method that actions implement to indicate if this action is valid (knows how to work with, is @@ -162,7 +167,7 @@ public interface DockingActionIf extends HelpDescriptor { * @param context the {@link ActionContext} from the active provider. * @return true if this action is appropriate for the given context. */ - public abstract boolean isValidContext(ActionContext context); + public boolean isValidContext(ActionContext context); /** * Method that actions implement to indicate if this action is valid (knows how to work with, is @@ -172,10 +177,10 @@ public interface DockingActionIf extends HelpDescriptor { * If you want a global action to only work on the global context, then override this method * and return false. * - * @param context the global {@link ActionContext} from the active provider. + * @param globalContext the global {@link ActionContext} from the active provider. * @return true if this action is appropriate for the given context. */ - public abstract boolean isValidGlobalContext(ActionContext globalContext); + public boolean isValidGlobalContext(ActionContext globalContext); /** * Method used to determine if this action should be enabled for the given context. @@ -202,13 +207,14 @@ public interface DockingActionIf extends HelpDescriptor { * @param context the current {@link ActionContext} for the window. * @return true if the action should be enabled for the context or false otherwise. */ - public abstract boolean isEnabledForContext(ActionContext context); + public boolean isEnabledForContext(ActionContext context); /** - * Returns a string that includes source file and line number information of where this action was - * created. + * Returns a string that includes source file and line number information of where + * this action was created + * @return the inception information */ - public abstract String getInceptionInformation(); + public String getInceptionInformation(); /** * Returns a JButton that is suitable for this action. For example, It creates a ToggleButton @@ -250,7 +256,7 @@ public interface DockingActionIf extends HelpDescriptor { * @param keyBindingData if non-null, assigns a keybinding to the action. Otherwise, removes * any keybinding from the action. */ - public abstract void setKeyBindingData(KeyBindingData keyBindingData); + public void setKeyBindingData(KeyBindingData keyBindingData); /** * Users creating actions should not call this method, but should instead call @@ -260,11 +266,25 @@ public interface DockingActionIf extends HelpDescriptor { * {@link #setKeyBindingData(KeyBindingData)} so that keybindings are set exactly as they * are given. * - * @param newKeyBindingData the KeyBindingData to be used to assign this action to a keybinding. - * @param validate true signals that this method should convert keybindings to their - * OS-dependent form (for example, on Mac a Ctrl - * key is changed to the Command key). + * @param newKeyBindingData the KeyBindingData to be used to assign this action to a keybinding */ - public abstract void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData); + public void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData); + /** + * Returns true if this action shares a keybinding with other actions. If this returns true, + * then this action, and any action that shares a name with this action, will be updated + * to the same key binding value whenever the key binding options change. + * + *

This will be false for the vast majority of actions. If you are unsure if your action + * should use a shared keybinding, then do not set this value to true. + * + *

This value is not meant to change over the life of the action. Thus, there is no + * set method to change this value. Rather, you should override this method + * to return true as desired. + * + * @return true to share a shared keybinding + */ + public default boolean usesSharedKeyBinding() { + return false; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/DockingToolActionManager.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/DockingToolActionManager.java index 66030f504b..eea428b53a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/DockingToolActionManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/DockingToolActionManager.java @@ -24,8 +24,7 @@ import javax.swing.KeyStroke; import docking.*; import docking.action.*; import docking.tool.util.DockingToolConstants; -import ghidra.framework.options.OptionType; -import ghidra.framework.options.Options; +import ghidra.framework.options.*; import ghidra.util.exception.AssertException; /** @@ -34,8 +33,9 @@ import ghidra.util.exception.AssertException; public class DockingToolActionManager implements PropertyChangeListener { private DockingWindowManager winMgr; - private Map> actionMap; - private Options keyBindingOptions; + private Map> actionMap = new HashMap<>(); + private Map sharedActionMap = new HashMap<>(); + private ToolOptions keyBindingOptions; private DockingTool dockingTool; /** @@ -48,7 +48,6 @@ public class DockingToolActionManager implements PropertyChangeListener { public DockingToolActionManager(DockingTool tool, DockingWindowManager windowManager) { this.dockingTool = tool; this.winMgr = windowManager; - actionMap = new HashMap<>(); keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); } @@ -88,18 +87,47 @@ public class DockingToolActionManager implements PropertyChangeListener { public synchronized void addToolAction(DockingActionIf action) { action.addPropertyChangeListener(this); addActionToMap(action); - if (action.isKeyBindingManaged()) { - KeyStroke ks = action.getKeyBinding(); - keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, - null, null); - KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks); - if (ks != newKs) { - action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs)); - } - } + setKeyBindingOption(action); winMgr.addToolAction(action); } + private void setKeyBindingOption(DockingActionIf action) { + + if (!action.isKeyBindingManaged()) { + return; + } + + if (action.usesSharedKeyBinding()) { + installSharedKeyBinding(action); + return; + } + + KeyStroke ks = action.getKeyBinding(); + keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null, + null); + KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks); + if (!Objects.equals(ks, newKs)) { + action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs)); + } + } + + private void installSharedKeyBinding(DockingActionIf action) { + String name = action.getName(); + KeyStroke defaultKeyStroke = action.getKeyBinding(); + + // get or create the stub to which we will add the action + SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> { + + SharedStubKeyBindingAction newStub = + new SharedStubKeyBindingAction(name, keyBindingOptions); + keyBindingOptions.registerOption(newStub.getFullName(), OptionType.KEYSTROKE_TYPE, + defaultKeyStroke, null, null); + return newStub; + }); + + stub.addClientAction(action); + } + /** * Removes the given action from the tool * @param action the action to be removed. @@ -173,8 +201,8 @@ public class DockingToolActionManager implements PropertyChangeListener { * @param fullActionName full name for the action, e.g., "My Action (My Plugin)" * @return list of actions; empty if no action exists with the given name */ - public List getDockingActionsByFullActionName(String fullActionName) { - List list = actionMap.get(fullActionName); + public List getDockingActionsByFullActionName(String fullName) { + List list = actionMap.get(fullName); if (list == null) { return new ArrayList<>(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java new file mode 100644 index 0000000000..e7d3442a2f --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/SharedStubKeyBindingAction.java @@ -0,0 +1,180 @@ +/* ### + * 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.actions; + +import java.util.*; +import java.util.Map.Entry; + +import javax.swing.KeyStroke; + +import docking.ActionContext; +import docking.DockingWindowManager; +import docking.action.*; +import ghidra.framework.options.OptionsChangeListener; +import ghidra.framework.options.ToolOptions; +import ghidra.util.Msg; +import utilities.util.reflection.ReflectionUtilities; + +/** + * A stub action that allows key bindings to be edited through the key bindings options. This + * allows plugins to create actions that share keybindings without having to manage those + * keybindings themselves. + * + *

Clients should not be using this class directly. + */ +class SharedStubKeyBindingAction extends DockingAction implements OptionsChangeListener { + + static final String SHARED_OWNER = "Tool"; + + /* + * We save the client actions for later validate and options updating. We also need the + * default key binding data, which is stored in the value of this map. + * + * Note: This collection is weak; the actions will stay as long as they are + * registered in the tool. + */ + private WeakHashMap clientActions = new WeakHashMap<>(); + + private ToolOptions keyBindingOptions; + + /** + * Creates a new dummy action by the given name and default keystroke value + * + * @param name The name of the action--this will be displayed in the options as the name of + * key binding's action + * @param options the tool's key binding options + */ + public SharedStubKeyBindingAction(String name, ToolOptions options) { + super(name, SHARED_OWNER); + this.keyBindingOptions = options; + + // Dummy keybinding actions don't have help--the real action does + DockingWindowManager.getHelpService().excludeFromHelp(this); + + // A listener to keep the shared, stub keybindings in sync with their clients + options.addOptionsChangeListener(this); + } + + void addClientAction(DockingActionIf action) { + + // 1) Validate new action keystroke against existing actions + KeyStroke validatedKeyStroke = validateActionsHaveTheSameDefaultKeyStroke(action); + + // 2) Update the given action with the current option value. This allows clients to + // add and remove actions after the tool has been initialized. + validatedKeyStroke = updateKeyStrokeFromOptions(validatedKeyStroke); + + clientActions.put(action, validatedKeyStroke); + } + + private KeyStroke validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) { + + // this value may be null + KeyBindingData defaultBinding = newAction.getDefaultKeyBindingData(); + KeyStroke newDefaultKs = getKeyStroke(defaultBinding); + + Set> entries = clientActions.entrySet(); + for (Entry entry : entries) { + DockingActionIf existingAction = entry.getKey(); + KeyStroke existingDefaultKs = entry.getValue(); + if (Objects.equals(existingDefaultKs, newDefaultKs)) { + continue; + } + + logDifferentKeyBindingsWarnigMessage(newAction, existingAction, existingDefaultKs); + + // + // Not sure which keystroke to prefer here--keep the first one that was set + // + + // set the new action's keystroke to be the winner + newAction.setKeyBindingData(new KeyBindingData(existingDefaultKs)); + + // one message is probably enough; + return existingDefaultKs; + } + + return newDefaultKs; + } + + private void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction, + DockingActionIf existingAction, KeyStroke existingDefaultKs) { + + //@formatter:off + String s = "Shared Key Binding Actions have different deafult values. These " + + "must be the same." + + "\n\tAction 1: " + existingAction.getInceptionInformation() + + "\n\t\tKey Binding: " + existingDefaultKs + + "\n\tAction 2: " + newAction.getInceptionInformation() + + "\n\t\tKey Binding: " + newAction.getKeyBinding() + + "\nUsing the " + + "first value set - " + existingDefaultKs + ; + //@formatter:on + + Msg.warn(this, s, ReflectionUtilities.createJavaFilteredThrowable()); + } + + private KeyStroke updateKeyStrokeFromOptions(KeyStroke validatedKeyStroke) { + return keyBindingOptions.getKeyStroke(getFullName(), validatedKeyStroke); + } + + private KeyStroke getKeyStroke(KeyBindingData data) { + if (data == null) { + return null; + } + return data.getKeyBinding(); + } + + @Override + public void optionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + + if (!optionName.startsWith(getName())) { + return; // not my binding + } + + KeyStroke newKs = (KeyStroke) newValue; + for (DockingActionIf action : clientActions.keySet()) { + + // Note: update this to say why we are using the 'unvalidated' call instead of the + // setKeyBindingData() call + action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs)); + } + } + + @Override + public void actionPerformed(ActionContext context) { + // no-op; this is a dummy! + } + + @Override + public boolean isAddToPopup(ActionContext context) { + return false; + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + return false; + } + + @Override + public void dispose() { + super.dispose(); + clientActions.clear(); + keyBindingOptions.removeOptionsChangeListener(this); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java b/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java index fa9ab2bcbe..47d06cae3e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/test/AbstractDockingTest.java @@ -483,6 +483,7 @@ public abstract class AbstractDockingTest extends AbstractGenericTest { /** * A convenience method to close all of the windows and frames that the current Java * windowing environment knows about + * * @deprecated instead call the new {@link #closeAllWindows()} */ @Deprecated @@ -1136,6 +1137,39 @@ public abstract class AbstractDockingTest extends AbstractGenericTest { return CollectionUtils.any(actions); } + /** + * Finds the action by the given owner name and action name. + * If you do not know the owner name, then use + * the call {@link #getActions(DockingTool, String)} instead. + * + *

Note: more specific test case subclasses provide other methods for finding actions + * when you have an owner name (which is usually the plugin name). + * + * @param tool the tool containing all system actions + * @param owner the owner of the action + * @param name the name to match + * @return the matching action; null if no matching action can be found + */ + public static DockingActionIf getAction(DockingTool tool, String owner, String name) { + String fullName = name + " (" + owner + ")"; + List actions = tool.getDockingActionsByFullActionName(fullName); + if (actions.isEmpty()) { + return null; + } + + if (actions.size() > 1) { + // This shouldn't happen + throw new AssertionFailedError( + "Found more than one action for name '" + fullName + "'"); + } + + return CollectionUtils.any(actions); + } + + public static DockingActionIf getLocalAction(ComponentProvider provider, String actionName) { + return getAction(provider.getTool(), provider.getName(), actionName); + } + /** * Returns the given dialog's action that has the given name * @@ -1417,8 +1451,8 @@ public abstract class AbstractDockingTest extends AbstractGenericTest { /** * Simulates a user initiated keystroke using the keybinding of the given action * - * @param destination the action's destination component - * @param action The action to simulate pressing. + * @param destination the component for the action being executed + * @param action The action to simulate pressing */ public static void triggerActionKey(Component destination, DockingActionIf action) { diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/actions/SharedKeybindingDockingActionTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/actions/SharedKeybindingDockingActionTest.java new file mode 100644 index 0000000000..cc7b984ba1 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/actions/SharedKeybindingDockingActionTest.java @@ -0,0 +1,289 @@ +/* ### + * 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.actions; + +import static org.junit.Assert.*; + +import java.awt.event.KeyEvent; +import java.util.List; +import java.util.Set; + +import javax.swing.KeyStroke; + +import org.apache.commons.collections4.IterableUtils; +import org.junit.Before; +import org.junit.Test; + +import docking.*; +import docking.action.*; +import docking.test.AbstractDockingTest; +import docking.tool.util.DockingToolConstants; +import ghidra.framework.options.ToolOptions; +import ghidra.util.Msg; +import ghidra.util.SpyErrorLogger; + +public class SharedKeybindingDockingActionTest extends AbstractDockingTest { + + private static final String SHARED_NAME = "Shared Action Name"; + private static final String SHARED_OWNER = SharedStubKeyBindingAction.SHARED_OWNER; + + // format: getName() + " (" + getOwner() + ")"; + private static final String SHARED_FULL_NAME = SHARED_NAME + " (" + SHARED_OWNER + ")"; + + private static final KeyStroke DEFAULT_KS_1 = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0); + private static final KeyStroke DEFAULT_KS_DIFFERENT_THAN_1 = + KeyStroke.getKeyStroke(KeyEvent.VK_B, 0); + private static final String OWNER_1 = "Owner1"; + private static final String OWNER_2 = "Owner2"; + + private SpyErrorLogger spyLogger = new SpyErrorLogger(); + + private DockingTool tool; + + @Before + public void setUp() { + tool = new FakeDockingTool(); + + Msg.setErrorLogger(spyLogger); + } + + @Test + public void testSharedKeyBinding_SameDefaultKeyBindings() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_1); + + tool.addAction(action1); + tool.addAction(action2); + + assertNoLoggedMessages(); + assertKeyBinding(action1, DEFAULT_KS_1); + assertKeyBinding(action2, DEFAULT_KS_1); + } + + @Test + public void testSharedKeyBinding_OptionsChange() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_1); + + tool.addAction(action1); + tool.addAction(action2); + + KeyStroke newKs = KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0); + setSharedKeyBinding(newKs); + + assertNoLoggedMessages(); + assertKeyBinding(action1, newKs); + assertKeyBinding(action2, newKs); + } + + @Test + public void testSharedKeyBinding_DifferentDefaultKeyBindings() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_DIFFERENT_THAN_1); + + tool.addAction(action1); + tool.addAction(action2); + + // both bindings should keep the first one that was set when they are different + assertImproperDefaultBindingMessage(); + assertKeyBinding(action1, DEFAULT_KS_1); + assertKeyBinding(action2, DEFAULT_KS_1); + } + + @Test + public void testSharedKeyBinding_NoDefaultKeyBindings() { + + TestAction action1 = new TestAction(OWNER_1, null); + TestAction action2 = new TestAction(OWNER_2, null); + + tool.addAction(action1); + tool.addAction(action2); + + // both bindings are null; this is allowed + assertNoLoggedMessages(); + assertKeyBinding(action1, null); + assertKeyBinding(action2, null); + } + + @Test + public void testSharedKeyBinding_OneDefaultOneUndefinedDefaultKeyBinding() { + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, null); + + tool.addAction(action1); + tool.addAction(action2); + + // both bindings should keep the first one that was set when they are different + assertImproperDefaultBindingMessage(); + assertKeyBinding(action1, DEFAULT_KS_1); + assertKeyBinding(action2, DEFAULT_KS_1); + } + + @Test + public void testSharedKeyBinding_RemoveAction() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_1); + + tool.addAction(action1); + tool.addAction(action2); + + tool.removeAction(action1); + + assertActionNotInTool(action1); + assertActionInTool(action2); + + tool.removeAction(action2); + assertActionNotInTool(action2); + + String sharedName = action1.getFullName(); + assertNoSharedKeyBindingStubInstalled(sharedName); + } + + @Test + public void testSharedKeyBinding_AddSameActionTwice() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + + tool.addAction(action1); + tool.addAction(action1); + + assertOnlyOneVersionOfActionInTool(action1); + + assertNoLoggedMessages(); + assertKeyBinding(action1, DEFAULT_KS_1); + } + + @Test + public void testSharedKeyBinding_OnlyOneEntryInOptions() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_1); + + tool.addAction(action1); + tool.addAction(action2); + + // verify that the actions are not in the options, but that the shared action is + ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); + List names = keyOptions.getOptionNames(); + assertTrue(names.contains(SHARED_FULL_NAME)); + assertFalse(names.contains(action1.getFullName())); + assertFalse(names.contains(action2.getFullName())); + } + + @Test + public void testSharedKeyBinding_AddActionAfterOptionHasChanged() { + + TestAction action1 = new TestAction(OWNER_1, DEFAULT_KS_1); + TestAction action2 = new TestAction(OWNER_2, DEFAULT_KS_1); + + tool.addAction(action1); + KeyStroke newKs = KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0); + setSharedKeyBinding(newKs); + + assertKeyBinding(action1, newKs); + + // verify the newly added keybinding gets the newly changed option + tool.addAction(action2); + assertKeyBinding(action1, newKs); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private void assertOnlyOneVersionOfActionInTool(TestAction action) { + Set actions = getActions(tool, action.getName()); + assertEquals("There should be only one instance of this action in the tool: " + action, 1, + actions.size()); + } + + private void assertActionInTool(TestAction action) { + + Set actions = getActions(tool, action.getName()); + for (DockingActionIf toolAction : actions) { + if (toolAction == action) { + return; + } + } + + fail("Action is not in the tool: " + action); + } + + private void assertActionNotInTool(TestAction action) { + Set actions = getActions(tool, action.getName()); + for (DockingActionIf toolAction : actions) { + assertNotSame(toolAction, action); + } + } + + private void assertNoSharedKeyBindingStubInstalled(String sharedName) { + + List actions = tool.getDockingActionsByFullActionName(sharedName); + assertTrue("There should be no actions registered for '" + sharedName + "'", + actions.isEmpty()); + } + + private void setSharedKeyBinding(KeyStroke newKs) { + ToolOptions options = getKeyBindingOptions(); + runSwing(() -> options.setKeyStroke(SHARED_FULL_NAME, newKs)); + waitForSwing(); + } + + private ToolOptions getKeyBindingOptions() { + return tool.getOptions(DockingToolConstants.KEY_BINDINGS); + } + + private void assertNoLoggedMessages() { + assertTrue("Spy logger not empty: " + spyLogger, IterableUtils.isEmpty(spyLogger)); + } + + private void assertImproperDefaultBindingMessage() { + spyLogger.assertLogMessage("shared", "key", "binding", "action", "different", "default"); + } + + private void assertKeyBinding(TestAction action, KeyStroke expectedKs) { + assertEquals(expectedKs, action.getKeyBinding()); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private class TestAction extends DockingAction { + + public TestAction(String owner, KeyStroke ks) { + super(SHARED_NAME, owner); + + if (ks != null) { + setKeyBindingData(new KeyBindingData(ks)); + } + } + + @Override + public boolean usesSharedKeyBinding() { + return true; + } + + @Override + public void actionPerformed(ActionContext context) { + fail("Action performed should not have been called"); + } + } +} diff --git a/Ghidra/Framework/Docking/src/test/java/docking/DockingKeybindingActionTest.java b/Ghidra/Framework/Docking/src/test/java/docking/DockingActionKeybindingTest.java similarity index 96% rename from Ghidra/Framework/Docking/src/test/java/docking/DockingKeybindingActionTest.java rename to Ghidra/Framework/Docking/src/test/java/docking/DockingActionKeybindingTest.java index 1cd46caef0..e9b2436ccb 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/DockingKeybindingActionTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/DockingActionKeybindingTest.java @@ -26,11 +26,7 @@ import org.junit.Test; import generic.test.AbstractGenericTest; -public class DockingKeybindingActionTest extends AbstractGenericTest { - - public DockingKeybindingActionTest() { - super(); - } +public class DockingActionKeybindingTest extends AbstractGenericTest { @Test public void testKeybinding_Unmodified() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java index 972488ab9d..9babb9be57 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java @@ -30,6 +30,7 @@ import docking.DockingUtils; import docking.KeyEntryTextField; import docking.action.DockingActionIf; import docking.action.KeyBindingData; +import docking.tool.util.DockingToolConstants; import docking.util.KeyBindingUtils; import docking.widgets.MultiLineLabel; import docking.widgets.OptionDialog; @@ -38,7 +39,6 @@ import docking.widgets.table.*; import ghidra.framework.options.Options; import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.PluginTool; -import ghidra.framework.plugintool.util.ToolConstants; import ghidra.util.HTMLUtilities; import ghidra.util.ReservedKeyBindings; import ghidra.util.exception.AssertException; @@ -78,10 +78,6 @@ public class KeyBindingsPanel extends JPanel { private PropertyChangeListener propertyChangeListener; private GTableFilterPanel tableFilterPanel; - /** - * Constructor - * @param options options that have the key binding mappings. - */ public KeyBindingsPanel(PluginTool tool, Options options) { this.tool = tool; this.options = options; @@ -350,7 +346,7 @@ public class KeyBindingsPanel extends JPanel { // run this after the current pending events in the swing // thread so that the screen will repaint itself SwingUtilities.invokeLater(() -> { - ToolOptions keyBindingOptions = tool.getOptions(ToolConstants.KEY_BINDINGS); + ToolOptions keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); KeyBindingUtils.exportKeyBindings(keyBindingOptions); }); }); @@ -447,16 +443,6 @@ public class KeyBindingsPanel extends JPanel { selectionModel.addListSelectionListener(new TableSelectionListener()); } - /** - * Update the keyMap and the actionMap and enable the apply button on - * the dialog. - * @param action plugin action could be null if ksName is not associated - * with a plugin action - * @param defaultActionName name of the action - * @param ksName keystroke name - * @return true if the old keystroke is different from the current - * keystroke - */ private boolean checkAction(String actionName, KeyStroke keyStroke) { String ksName = KeyEntryTextField.parseKeyStroke(keyStroke);