GT-2869 - Shared Key Bindings - created new shared keybinding concept

that replaces the DummyKeyBindingsOptionsAction
This commit is contained in:
dragonmacher 2019-05-20 12:48:56 -04:00
parent 1fa2bd7979
commit c9bd3a8b2b
9 changed files with 624 additions and 151 deletions

View File

@ -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.
*
* <P>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<DockingActionIf> 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<Program, AutoAnalysisManager> programToManagersMap =
(Map<Program, AutoAnalysisManager>) getInstanceField("managerMap",
AutoAnalysisManager.class);
Collection<AutoAnalysisManager> 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<PluginTool> ref = new AtomicReference<>();

View File

@ -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.
* <p>
* 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 {
/**
* <b>Users creating actions should not call this method, but should instead call
* {@link #setKeyBindingData(KeyBindingData)}.</b>
* @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 <tt>Ctrl</tt>
* key is changed to the <tt>Command</tt> 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)) {

View File

@ -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.
* <p>
* 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);
/**
* <b>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 <tt>Ctrl</tt>
* key is changed to the <tt>Command</tt> 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.
*
* <p>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.
*
* <p>This value is not meant to change over the life of the action. Thus, there is no
* <code>set</code> method to change this value. Rather, you should override this method
* to return <code>true</code> as desired.
*
* @return true to share a shared keybinding
*/
public default boolean usesSharedKeyBinding() {
return false;
}
}

View File

@ -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<String, List<DockingActionIf>> actionMap;
private Options keyBindingOptions;
private Map<String, List<DockingActionIf>> actionMap = new HashMap<>();
private Map<String, SharedStubKeyBindingAction> 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<DockingActionIf> getDockingActionsByFullActionName(String fullActionName) {
List<DockingActionIf> list = actionMap.get(fullActionName);
public List<DockingActionIf> getDockingActionsByFullActionName(String fullName) {
List<DockingActionIf> list = actionMap.get(fullName);
if (list == null) {
return new ArrayList<>();
}

View File

@ -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.
*
* <p>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<DockingActionIf, KeyStroke> 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<Entry<DockingActionIf, KeyStroke>> entries = clientActions.entrySet();
for (Entry<DockingActionIf, KeyStroke> 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);
}
}

View File

@ -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.
*
* <P>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<DockingActionIf> 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) {

View File

@ -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<String> 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<DockingActionIf> 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<DockingActionIf> 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<DockingActionIf> actions = getActions(tool, action.getName());
for (DockingActionIf toolAction : actions) {
assertNotSame(toolAction, action);
}
}
private void assertNoSharedKeyBindingStubInstalled(String sharedName) {
List<DockingActionIf> 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");
}
}
}

View File

@ -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() {

View File

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