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);