GP-4436 - Mouse Bindings

This commit is contained in:
dragonmacher 2024-04-09 17:40:12 -04:00
parent eca5195dea
commit 8aeebf919a
61 changed files with 3136 additions and 919 deletions

View file

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.KeyStroke;
import docking.ActionContext;
import docking.action.*;
@ -39,6 +40,7 @@ import ghidra.app.services.NavigationHistoryService;
import ghidra.app.util.HelpTopics;
import ghidra.app.util.viewer.field.BrowserCodeUnitFormat;
import ghidra.framework.model.DomainFile;
import ghidra.framework.options.ActionTrigger;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address;
@ -46,6 +48,7 @@ import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolTable;
import ghidra.util.HelpLocation;
import gui.event.MouseBinding;
/**
* <CODE>NextPrevAddressPlugin</CODE> allows the user to go back and forth in
@ -83,7 +86,7 @@ public class NextPrevAddressPlugin extends Plugin {
/**
* Creates a new instance of the plugin
*
*
* @param tool the tool
*/
public NextPrevAddressPlugin(PluginTool tool) {
@ -119,7 +122,7 @@ public class NextPrevAddressPlugin extends Plugin {
//==================================================================================================
// Private Methods
//==================================================================================================
//==================================================================================================
private List<DockingActionIf> getPreviousActions(Navigatable navigatable) {
Program lastProgram = null;
@ -297,6 +300,9 @@ public class NextPrevAddressPlugin extends Plugin {
private class NextPreviousAction extends MultiActionDockingAction {
private static final int MOUSE_BUTTON_4 = 4;
private static final int MOUSE_BUTTON_5 = 5;
private final boolean isNext;
NextPreviousAction(String name, String owner, boolean isNext) {
@ -306,8 +312,15 @@ public class NextPrevAddressPlugin extends Plugin {
setToolBarData(new ToolBarData(isNext ? NEXT_ICON : PREVIOUS_ICON,
ToolConstants.TOOLBAR_GROUP_TWO));
setHelpLocation(new HelpLocation(HelpTopics.NAVIGATION, name));
int keycode = isNext ? KeyEvent.VK_RIGHT : KeyEvent.VK_LEFT;
setKeyBindingData(new KeyBindingData(keycode, InputEvent.ALT_DOWN_MASK));
int keyCode = isNext ? KeyEvent.VK_RIGHT : KeyEvent.VK_LEFT;
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, InputEvent.ALT_DOWN_MASK);
int mouseButton = isNext ? MOUSE_BUTTON_5 : MOUSE_BUTTON_4;
MouseBinding mouseBinding = new MouseBinding(mouseButton);
setKeyBindingData(new KeyBindingData(new ActionTrigger(keyStroke, mouseBinding)));
setDescription(isNext ? "Go to next location" : "Go to previous location");
addToWindowWhen(NavigatableActionContext.class);
}

View file

@ -345,7 +345,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
plugin.getTool().setStatusInfo("User cancelled keybinding.");
return;
}
action.setKeyBindingData(new KeyBindingData(dialog.getKeyStroke()));
KeyStroke newKs = dialog.getKeyStroke();
action.setKeyBindingData(newKs == null ? null : new KeyBindingData(newKs));
scriptTable.repaint();
}

View file

@ -90,6 +90,7 @@ class KeyBindingInputDialog extends DialogComponentProvider implements KeyEntryL
}
void setKeyStroke(KeyStroke ks) {
this.ks = ks;
kbField.setKeyStroke(ks);
}
}

View file

@ -85,10 +85,13 @@ class ScriptAction extends DockingAction {
}
private KeyBindingData checkForFallbackKeybindingCondition(KeyBindingData keyBindingData) {
KeyStroke newKeyStroke = keyBindingData.getKeyBinding();
if (newKeyStroke != null) {
// we have a valid value; the current keybinding data is what we want
return keyBindingData;
if (keyBindingData != null) {
KeyStroke newKeyStroke = keyBindingData.getKeyBinding();
if (newKeyStroke != null) {
// we have a valid value; the current keybinding data is what we want
return keyBindingData;
}
}
// check to see if we have a fallback value
@ -106,7 +109,10 @@ class ScriptAction extends DockingAction {
private void updateUserDefinedKeybindingStatus(KeyBindingData keyBindingData) {
// we have a user defined keybinding if the keystroke for the action differs from
// that which is defined in the metadata of the script
KeyStroke actionKeyStroke = keyBindingData.getKeyBinding();
KeyStroke actionKeyStroke = null;
if (keyBindingData != null) {
actionKeyStroke = keyBindingData.getKeyBinding();
}
ScriptInfo info = infoManager.getExistingScriptInfo(script);
KeyStroke metadataKeyBinding = info.getKeyBinding();
isUserDefinedKeyBinding = !SystemUtilities.isEqual(actionKeyStroke, metadataKeyBinding);
@ -128,8 +134,9 @@ class ScriptAction extends DockingAction {
ScriptInfo info = infoManager.getScriptInfo(script);
KeyStroke stroke = info.getKeyBinding();
if (!isUserDefinedKeyBinding) {
setKeyBindingData(new KeyBindingData(stroke));
setKeyBindingData(stroke == null ? null : new KeyBindingData(stroke));
}
Icon icon = info.getToolBarImage(false);
if (icon != null) {
ToolBarData data = getToolBarData();

View file

@ -588,8 +588,8 @@ public abstract class GhidraScript extends FlatProgramAPI {
if (isRunningHeadless()) {
// only change client authenticator in headless mode
try {
HeadlessClientAuthenticator
.installHeadlessClientAuthenticator(ClientUtil.getUserName(), null, false);
HeadlessClientAuthenticator.installHeadlessClientAuthenticator(
ClientUtil.getUserName(), null, false);
}
catch (IOException e) {
throw new RuntimeException("Unexpected Exception", e);
@ -1316,6 +1316,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
case FILE_TYPE:
case FONT_TYPE:
case KEYSTROKE_TYPE:
case ACTION_TRIGGER:
// do nothing; don't allow user to set these options (doesn't make any sense)
break;
@ -2333,22 +2334,22 @@ public abstract class GhidraScript extends FlatProgramAPI {
/**
* Prompts for multiple values at the same time. To use this method, you must first
* create a {@link GhidraValuesMap} and define the values that will be supplied by this method.
* create a {@link GhidraValuesMap} and define the values that will be supplied by this method.
* In the GUI environment, this will result in a single dialog with an entry for each value
* defined in the values map. This method returns a GhidraValuesMap with the values supplied by
* the user in GUI mode or command line arguments in headless mode. If the user cancels the
* dialog, a cancelled exception will be thrown, and unless it is explicity caught by the
* script, will terminate the script. Also, if the values map has a {@link ValuesMapValidator},
* the values will be validated when the user presses the "OK" button and will only exit the
* the user in GUI mode or command line arguments in headless mode. If the user cancels the
* dialog, a cancelled exception will be thrown, and unless it is explicity caught by the
* script, will terminate the script. Also, if the values map has a {@link ValuesMapValidator},
* the values will be validated when the user presses the "OK" button and will only exit the
* dialog if the validate check passes. Otherwise, the validator should have reported an error
* message in the dialog and the dialog will remain visible.
*
*
* <p>
* Regardless of environment -- if script arguments have been set, this method will use the
* next arguments in the array and advance the array index until all values in the values map
* have been satisfied and so the next call to an ask method will get the next argument after
* those consumed by this call.
*
* those consumed by this call.
*
* @param title the title of the dialog if in GUI mode
* @param optionalMessage an optional message that is displayed in the dialog, just above the
* list of name/value pairs
@ -2616,7 +2617,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
* (in headless mode or when using .properties file)
* @param message the message to display next to the input field (in GUI mode) or the
* second part of the variable name (in headless mode or when using .properties file)
* @param defaultValue the optional default address as a String - if null is passed or an invalid
* @param defaultValue the optional default address as a String - if null is passed or an invalid
* address is given no default will be shown in dialog
* @return the user-specified Address value
* @throws CancelledException if the user hit the 'cancel' button in GUI mode
@ -2759,14 +2760,14 @@ public abstract class GhidraScript extends FlatProgramAPI {
*
* @param title the title of the pop-up dialog (in GUI mode) or the variable name (in
* headless mode)
* @return the user-selected Program with this script as the consumer if a program was
* @return the user-selected Program with this script as the consumer if a program was
* selected. Null is returned if a program is not selected. NOTE: It is very important that
* the program instance returned by this method ALWAYS be properly released when no longer
* the program instance returned by this method ALWAYS be properly released when no longer
* needed. The script which invoked this method must be
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
* properly release the program may result in improper project disposal. If the program was
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
* properly release the program may result in improper project disposal. If the program was
* opened by the tool, the tool will be a second consumer responsible for its own release.
* @throws VersionException if the Program is out-of-date from the version of Ghidra and an
* @throws VersionException if the Program is out-of-date from the version of Ghidra and an
* upgrade was not been performed. In non-headless mode, the user will have already been
* notified via a popup dialog.
* @throws IOException if there is an error accessing the Program's DomainObject
@ -2781,7 +2782,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
/**
* Returns a Program, using the title parameter for guidance with the option to upgrade
* if needed. The actual behavior of the method depends on your environment, which can be
* if needed. The actual behavior of the method depends on your environment, which can be
* GUI or headless. You can control whether or not the program is allowed to upgrade via
* the {@code upgradeIfNeeded} parameter.
* <br>
@ -2811,14 +2812,14 @@ public abstract class GhidraScript extends FlatProgramAPI {
* @param upgradeIfNeeded if true, program will be upgraded if needed and possible. If false,
* the program will only be upgraded after first prompting the user. In headless mode, it will
* attempt to upgrade only if the parameter is true.
* @return the user-selected Program with this script as the consumer if a program was
* @return the user-selected Program with this script as the consumer if a program was
* selected. Null is returned if a program is not selected. NOTE: It is very important that
* the program instance returned by this method ALWAYS be properly released when no longer
* the program instance returned by this method ALWAYS be properly released when no longer
* needed. The script which invoked this method must be
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
* properly release the program may result in improper project disposal. If the program was
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
* properly release the program may result in improper project disposal. If the program was
* opened by the tool, the tool will be a second consumer responsible for its own release.
* @throws VersionException if the Program is out-of-date from the version of GHIDRA and an
* @throws VersionException if the Program is out-of-date from the version of GHIDRA and an
* upgrade was not been performed. In non-headless mode, the user will have already been
* notified via a popup dialog.
* @throws IOException if there is an error accessing the Program's DomainObject
@ -3139,13 +3140,13 @@ public abstract class GhidraScript extends FlatProgramAPI {
* only be used in headed mode.
* <p>
* In the GUI environment, this method displays a password popup dialog that prompts the user
* for a password. There is no pre-population of the input. If the user cancels the dialog, it
* is immediately disposed, and any input to that dialog is cleared from memory. If the user
* completes the dialog, then the password is returned in a wrapped buffer. The buffer can be
* cleared by calling {@link Password#close()}; however, it is meant to be used in a
* {@code try-with-resources} block. The pattern does not guarantee protection of the password,
* for a password. There is no pre-population of the input. If the user cancels the dialog, it
* is immediately disposed, and any input to that dialog is cleared from memory. If the user
* completes the dialog, then the password is returned in a wrapped buffer. The buffer can be
* cleared by calling {@link Password#close()}; however, it is meant to be used in a
* {@code try-with-resources} block. The pattern does not guarantee protection of the password,
* but it will help you avoid some typical pitfalls:
*
*
* <pre>
* String user = askString("Login", "Username:");
* Project project;
@ -3153,12 +3154,12 @@ public abstract class GhidraScript extends FlatProgramAPI {
* project = doLoginAndOpenProject(user, password.getPasswordChars());
* }
* </pre>
*
*
* The buffer will be zero-filled upon leaving the {@code try-with-resources} block. If, in the
* sample, the {@code doLoginAndOpenProject} method or any part of its implementation needs to
* retain the password, it must make a copy. It is then the implementation's responsibility to
* protect its copy.
*
*
* @param title the title of the dialog
* @param prompt the prompt to the left of the input field, or null to display "Password:"
* @return the password
@ -3639,10 +3640,10 @@ public abstract class GhidraScript extends FlatProgramAPI {
* null is returned. For more control over the import process, {@link AutoImporter} may be
* directly called.
* <p>
* NOTE: The returned {@link Program} is not automatically saved into the current project.
* NOTE: The returned {@link Program} is not automatically saved into the current project.
* <p>
* NOTE: It is the responsibility of the script that calls this method to release the returned
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
* needed, where <code>consumer</code> is <code>this</code>.
*
* @param file the file to import
@ -3662,11 +3663,11 @@ public abstract class GhidraScript extends FlatProgramAPI {
}
/**
* Imports the specified file as raw binary. For more control over the import process,
* Imports the specified file as raw binary. For more control over the import process,
* {@link AutoImporter} may be directly called.
* <p>
* NOTE: It is the responsibility of the script that calls this method to release the returned
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
* needed, where <code>consumer</code> is <code>this</code>.
*
* @param file the file to import

View file

@ -18,13 +18,13 @@ package ghidra.app.script;
import org.apache.logging.log4j.message.Message;
/**
* A simple {@link Message} implementation that allows us to use the filtering capability
* A simple {@link Message} implementation that allows us to use the filtering capability
* of log4j. This class has a formatted and unformatted message. log4j writes the the formatted
* message out. Our formatted message is the original message given to us. We use the
* unformatted message, in conjunction with a regex filter to allow for filtering such that
* message out. Our formatted message is the original message given to us. We use the
* unformatted message, in conjunction with a regex filter to allow for filtering such that
* the script log file only has script messages.
*
* <P>See logj4-appender-rolling-file-scripts.xml
*
* <P>See log4j-appender-rolling-file-scripts.xml
*/
public class ScriptMessage implements Message {

View file

@ -166,7 +166,7 @@ class PropertiesXmlMgr {
strMap.add(addr, str);
}
else if ("bookmarks".equals(type)) {
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
// were saved as simple properties
BookmarkManager bmMgr = program.getBookmarkManager();
if (!overwrite) {
@ -254,8 +254,7 @@ class PropertiesXmlMgr {
list.setDate(name, new Date(value));
}
else if ("color".equals(type)) {
Color color =
ColorUtils.getColor(XmlUtilities.parseInt(element.getAttribute("VALUE")));
Color color = ColorUtils.getColor(XmlUtilities.parseInt(element.getAttribute("VALUE")));
list.setColor(name, color);
}
else if ("file".equals(type)) {
@ -280,7 +279,19 @@ class PropertiesXmlMgr {
String xmlString = XmlUtilities.unEscapeElementEntities(escapedXML);
KeyStroke keyStroke =
(KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(xmlString);
list.setKeyStroke(name, keyStroke);
ActionTrigger trigger = null;
if (keyStroke != null) {
trigger = new ActionTrigger(keyStroke);
}
list.setActionTrigger(name, trigger);
}
else if ("actionTrigger".equals(type)) {
String escapedXML = element.getAttribute("VALUE");
String xmlString = XmlUtilities.unEscapeElementEntities(escapedXML);
ActionTrigger actionTrigger =
(ActionTrigger) OptionType.ACTION_TRIGGER.convertStringToObject(xmlString);
list.setActionTrigger(name, actionTrigger);
}
else if ("custom".equals(type)) {
String escapedXML = element.getAttribute("VALUE");
@ -401,9 +412,15 @@ class PropertiesXmlMgr {
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
break;
case KEYSTROKE_TYPE:
attrs.addAttribute("TYPE", "keyStroke");
KeyStroke keyStroke = propList.getKeyStroke(name, null);
xmlString = OptionType.KEYSTROKE_TYPE.convertObjectToString(keyStroke);
attrs.addAttribute("TYPE", "actionTrigger");
ActionTrigger trigger = propList.getActionTrigger(name, null);
xmlString = OptionType.ACTION_TRIGGER.convertObjectToString(trigger);
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
break;
case ACTION_TRIGGER:
attrs.addAttribute("TYPE", "actionTrigger");
ActionTrigger actionTrigger = propList.getActionTrigger(name, null);
xmlString = OptionType.ACTION_TRIGGER.convertObjectToString(actionTrigger);
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
break;
case CUSTOM_TYPE:

View file

@ -30,6 +30,7 @@ import docking.actions.KeyEntryDialog;
import docking.actions.ToolActions;
import docking.tool.util.DockingToolConstants;
import generic.theme.GIcon;
import ghidra.framework.options.ActionTrigger;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@ -443,7 +444,8 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio
ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
// shared option name/format: "Provider Name (Shared)" - the shared action's owner is the Tool
runSwing(() -> keyOptions.setKeyStroke(provider.getName() + " (Shared)", newKs));
runSwing(() -> keyOptions.setActionTrigger(provider.getName() + " (Shared)",
new ActionTrigger(newKs)));
waitForSwing();
}
@ -491,7 +493,11 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio
// Option name: the action name with the 'Shared' owner
String fullName = provider.getName() + " (Shared)";
KeyStroke optionsKs = runSwing(() -> options.getKeyStroke(fullName, null));
ActionTrigger actionTrigger = runSwing(() -> options.getActionTrigger(fullName, null));
KeyStroke optionsKs = null;
if (actionTrigger != null) {
optionsKs = actionTrigger.getKeyStroke();
}
assertEquals("Key stroke in options does not match expected key stroke", expectedKs,
optionsKs);
}

View file

@ -807,6 +807,7 @@ public class CommentsPluginTest extends AbstractGhidraHeadedIntegrationTest {
assertNotNull(button);
pressButton(button, false);
waitForSwing();
waitForBusyTool(tool);
}
private CommentsDialog editComment(Address a) {

View file

@ -36,6 +36,7 @@ import docking.tool.util.DockingToolConstants;
import docking.widgets.table.TableSortState;
import ghidra.app.nav.Navigatable;
import ghidra.app.nav.TestDummyNavigatable;
import ghidra.framework.options.ActionTrigger;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.DummyPluginTool;
import ghidra.program.database.ProgramBuilder;
@ -466,7 +467,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
String name = action.getName() + " (" + action.getOwner() + ")";
runSwing(() -> keyOptions.setKeyStroke(name, newKs));
runSwing(() -> keyOptions.setActionTrigger(name, new ActionTrigger(newKs)));
waitForSwing();
KeyStroke actual = action.getKeyBinding();

View file

@ -346,9 +346,13 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
private String clearKeyBinding(Options options) {
String keyBindingName = "Go To Next Function (CodeBrowserPlugin)";
KeyStroke ks = options.getKeyStroke(keyBindingName, null);
ActionTrigger actionTrigger = options.getActionTrigger(keyBindingName, null);
assertNotNull(actionTrigger);
KeyStroke ks = actionTrigger.getKeyStroke();
assertNotNull(ks);
options.setKeyStroke(keyBindingName, null);
options.setActionTrigger(keyBindingName, null);
return keyBindingName;
}
@ -378,7 +382,12 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
}
private void verifyKeyBindingIsStillCleared(Options options, String optionName) {
KeyStroke ksValue = options.getKeyStroke(optionName, null);
ActionTrigger actionTrigger = options.getActionTrigger(optionName, null);
if (actionTrigger == null) {
return;
}
KeyStroke ksValue = actionTrigger.getKeyStroke();
assertNull(ksValue);
}

View file

@ -48,8 +48,7 @@ import ghidra.app.plugin.core.memory.MemoryMapPlugin;
import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin;
import ghidra.app.plugin.core.navigation.NavigationHistoryPlugin;
import ghidra.framework.model.ToolServices;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.mgr.OptionsManager;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@ -194,12 +193,11 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
debug("d");
// now repeat the above test with changing some values before writing out
invokeInstanceMethod("putObject", defaultKeyBindings,
new Class[] { String.class, Object.class },
new Object[] { "TestAction1 (Owner1)", KeyStroke.getKeyStroke(65, 0) });
invokeInstanceMethod("putObject", defaultKeyBindings,
new Class[] { String.class, Object.class },
new Object[] { "TestAction2 (Owner 2)", KeyStroke.getKeyStroke(66, 0) });
defaultKeyBindings.putObject("TestAction1 (Owner1)",
new ActionTrigger(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0)));
defaultKeyBindings.putObject("TestAction2 (Owner 2)",
new ActionTrigger(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0)));
debug("e");
@ -366,8 +364,9 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
setKeyBindingsUpDialog(tool);
ToolOptions options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
KeyStroke optionBinding = options.getKeyStroke(action.getFullName(), null);
assertEquals(appliedBinding, optionBinding);
ActionTrigger actionTrigger = options.getActionTrigger(action.getFullName(), null);
KeyStroke optionKeyStroke = actionTrigger.getKeyStroke();
assertEquals(appliedBinding, optionKeyStroke);
closeAllWindows();
}
@ -429,7 +428,8 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
// setup our test variables
panel = (KeyBindingsPanel) getEditorPanel(keyBindingsNode, optionsDialog);
table = findComponent(panel, JTable.class);
keyField = (JTextField) getInstanceField("ksField", panel);
Object actionBindingPanel = getInstanceField("actionBindingPanel", panel);
keyField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
model = table.getModel();
debug("ff");
@ -518,8 +518,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
String owner = action.getOwnerDescription();
for (int i = 0; i < model.getRowCount(); i++) {
if (actionName.equals(model.getValueAt(i, 0)) &&
owner.equals(model.getValueAt(i, 2))) {
if (actionName.equals(model.getValueAt(i, 0)) && owner.equals(model.getValueAt(i, 2))) {
final int idx = i;
runSwing(() -> {
table.setRowSelectionInterval(idx, idx);
@ -627,7 +626,12 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
for (String name : propertyNames) {
boolean match = panelKeyStrokeMap.containsKey(name);
KeyStroke optionsKs = oldOptions.getKeyStroke(name, null);
ActionTrigger actionTrigger = oldOptions.getActionTrigger(name, null);
KeyStroke optionsKs = null;
if (actionTrigger != null) {
optionsKs = actionTrigger.getKeyStroke();
}
KeyStroke panelKs = panelKeyStrokeMap.get(name);
// if the value is null, then it would not have been placed into the options map

View file

@ -94,10 +94,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
// look for the info panel
MultiLineLabel label = findComponent(panel, MultiLineLabel.class);
String str = "To add or change a key binding, select an action\n" +
"and type any key combination\n" +
" \n" +
"To remove a key binding, select an action and\n" +
"press <Enter> or <Backspace>";
"and type any key combination\n" + " \n" +
"To remove a key binding, select an action and\n" + "press <Enter> or <Backspace>";
assertEquals(str, label.getLabel());
@ -215,9 +213,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
// verify that no action is mapped to the new binding
int keyCode = KeyEvent.VK_0;
int modifiers = InputEvent.ALT_DOWN_MASK | InputEvent.ALT_GRAPH_DOWN_MASK;
KeyEvent keyEvent =
new KeyEvent(dialog, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), modifiers,
keyCode, KeyEvent.CHAR_UNDEFINED);
KeyEvent keyEvent = new KeyEvent(dialog, KeyEvent.KEY_PRESSED, System.currentTimeMillis(),
modifiers, keyCode, KeyEvent.CHAR_UNDEFINED);
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent);
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
Action action =
@ -233,8 +230,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
assertEquals(ks, getKeyStroke(action1));
// verify the additional binding for 'Alt Graph'
action =
(Action) TestUtils.invokeInstanceMethod("getActionForKeyStroke", dwm, keyStroke);
action = (Action) TestUtils.invokeInstanceMethod("getActionForKeyStroke", dwm, keyStroke);
assertNotNull(action);
}
@ -283,8 +279,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
boolean success = msg.contains(action1.getName()) && msg.contains(action2.getName());
assertTrue("In-use action message incorrect.\n\tIt should contain these 2 actions:\n\t\t" +
action1.getName() + "\n\t\t" + action2.getName() + ".\nActual message:\n" +
msg + "\n", success);
action1.getName() + "\n\t\t" + action2.getName() + ".\nActual message:\n" + msg + "\n",
success);
}
@Test
@ -560,8 +556,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
dialog.setVisible(true);
});
table = findComponent(panel, JTable.class);
keyField = findComponent(panel, JTextField.class);
keyField = (JTextField) getInstanceField("ksField", panel);
keyField = (JTextField) findComponentByName(panel, "Key Entry Text Field");
statusPane = findComponent(panel, JTextPane.class);
model = table.getModel();
waitForSwing();

View file

@ -24,6 +24,8 @@ import java.beans.PropertyEditor;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.text.JTextComponent;
@ -60,6 +62,7 @@ import ghidra.framework.preferences.Preferences;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.ColorUtils;
import gui.event.MouseBinding;
/**
* Tests for the options dialog.
@ -420,25 +423,154 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
}
@Test
public void testRestoreDefaultsForKeybindings() throws Exception {
String actionName = "Clear Cut";
String pluginName = "DataTypeManagerPlugin";
KeyStroke defaultKeyStroke = getKeyBinding(actionName);
assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
public void testKeybindings_SetMouseBounding_NoDefaultBindings() throws Exception {
int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
String actionName = "Clear Color";
String actionOwner = "ColorizingPlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertNull(defaultKeyStroke);
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertNull(defaultMouseBinding);
int button = 1;
int modifiers = 0;
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
apply();
assertOptionsKeyStroke(tool, actionName, pluginName, newKeyStroke);
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
restoreDefaults();
KeyStroke currentBinding = getKeyBinding(actionName);
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertEquals("Mouse binding not restored after a call to restore defautls",
defaultMouseBinding, currentMouseBinding);
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
}
@Test
public void testKeybindings_SetMouseBoundingAndKeyBinding_NoDefaultBindings() throws Exception {
String actionName = "Clear Color";
String actionOwner = "ColorizingPlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertNull(defaultKeyStroke);
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertNull(defaultMouseBinding);
int button = 1;
int modifiers = 0;
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
int keyCode = KeyEvent.VK_Q;
modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
apply();
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
restoreDefaults();
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertEquals("Mouse binding not restored after a call to restore defautls",
defaultMouseBinding, currentMouseBinding);
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
}
@Test
public void testKeybindings_SetMouseBoundingAndKeyBinding_ClearKeyBinding() throws Exception {
String actionName = "Clear Color";
String actionOwner = "ColorizingPlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertNull(defaultKeyStroke);
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertNull(defaultMouseBinding);
int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
int button = 1;
modifiers = 0;
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
apply();
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
clearKeyBinding(actionName, actionOwner);
apply();
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding); // unchanged
restoreDefaults();
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertEquals("Mouse binding not restored after a call to restore defautls",
defaultMouseBinding, currentMouseBinding);
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
}
@Test
public void testKeybindings_SetMouseBounding_DefaultKeyBinding() throws Exception {
String actionName = "Clear Cut";
String actionOwner = "DataTypeManagerPlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertNotNull(defaultKeyStroke);
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertNull(defaultMouseBinding);
int button = 1;
int modifiers = 0;
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
apply();
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
restoreDefaults();
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertEquals("Mouse binding not restored after a call to restore defautls",
defaultMouseBinding, currentMouseBinding);
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
}
@Test
public void testRestoreDefaultsForKeybindings() throws Exception {
String actionName = "Clear Cut";
String actionOwner = "DataTypeManagerPlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
apply();
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
restoreDefaults();
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
currentBinding);
assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
}
@Test
@ -449,23 +581,23 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
setUpDialog(frontEndTool);
String actionName = "Archive Project";
String pluginName = "ArchivePlugin";
KeyStroke defaultKeyStroke = getKeyBinding(actionName);
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
String actionOwner = "ArchivePlugin";
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, defaultKeyStroke);
int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
apply();
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, newKeyStroke);
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, newKeyStroke);
restoreDefaults();
KeyStroke currentBinding = getKeyBinding(actionName);
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
currentBinding);
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, defaultKeyStroke);
}
@Test
@ -745,11 +877,13 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
// Inner Classes
//=================================================================================================
private KeyStroke getKeyBinding(String actionName) throws Exception {
private MouseBinding getMouseBindingFromTable(String actionName, String actionOwner)
throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
int row = selectRowForAction(panel, actionName);
int row = selectRowForAction(panel, actionName, actionOwner);
JTable table = (JTable) getInstanceField("actionTable", panel);
@SuppressWarnings("unchecked")
@ -763,36 +897,150 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
if (StringUtils.isBlank(keyBindingColumnValue)) {
return null;
}
String mouseBinding = keyBindingColumnValue;
Pattern p = Pattern.compile(".*\\((.*)\\)");
Matcher matcher = p.matcher(keyBindingColumnValue);
if (matcher.matches()) {
mouseBinding = matcher.group(1);
}
return MouseBinding.getMouseBinding(mouseBinding);
}
private KeyStroke getKeyBindingFromTable(String actionName, String actionOwner)
throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
int row = selectRowForAction(panel, actionName, actionOwner);
JTable table = (JTable) getInstanceField("actionTable", panel);
@SuppressWarnings("unchecked")
RowObjectFilterModel<DockingActionIf> model =
(RowObjectFilterModel<DockingActionIf>) table.getModel();
DockingActionIf rowValue = model.getModelData().get(row);
String keyBindingColumnValue =
(String) model.getColumnValueForRow(rowValue, 1 /* key binding column */);
if (StringUtils.isBlank(keyBindingColumnValue)) {
return null;
}
int index = keyBindingColumnValue.indexOf("(");
if (index != -1) {
int endIndex = keyBindingColumnValue.indexOf(")");
if (endIndex != -1) {
keyBindingColumnValue = keyBindingColumnValue.substring(0, index);
}
}
return KeyBindingUtils.parseKeyStroke(keyBindingColumnValue);
}
private void assertOptionsMouseBinding(PluginTool pluginTool, String actionName,
String pluginName, MouseBinding value) {
Options options = pluginTool.getOptions(DockingToolConstants.KEY_BINDINGS);
ActionTrigger actionTrigger =
options.getActionTrigger(actionName + " (" + pluginName + ")", null);
if (actionTrigger == null) {
assertNull("The options mouse binding does not match the value in the options table",
value);
return;
}
MouseBinding mouseBinding = actionTrigger.getMouseBinding();
assertEquals("The options mouse binding does not match the value in the options table",
value, mouseBinding);
}
private void assertOptionsKeyStroke(PluginTool pluginTool, String actionName, String pluginName,
KeyStroke value) throws Exception {
Options options = pluginTool.getOptions(DockingToolConstants.KEY_BINDINGS);
KeyStroke optionsKeyStroke =
options.getKeyStroke(actionName + " (" + pluginName + ")", null);
assertEquals("The options keystroke does not match the value in keybinding options table",
value, optionsKeyStroke);
ActionTrigger actionTrigger =
options.getActionTrigger(actionName + " (" + pluginName + ")", null);
if (actionTrigger == null) {
assertNull("The options keystroke does not match the value in the options table",
value);
return;
}
KeyStroke keyStroke = actionTrigger.getKeyStroke();
assertEquals("The options keystroke does not match the value in the options table", value,
keyStroke);
}
private KeyStroke setKeyBinding(String actionName, int modifiers, int keyCode, char keyChar)
throws Exception {
private MouseBinding setMouseBinding(String actionName, String actionOwner, int modifiers,
int button) throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
final KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
selectRowForAction(panel, actionName);
selectRowForAction(panel, actionName, actionOwner);
setToggleButtonSelected(panel, "Enter Mouse Binding", true);
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
JTextField textField = (JTextField) getInstanceField("mouseEntryField", actionBindingPanel);
clickMouse(textField, button, 5, 5, 1, modifiers);
waitForSwing();
MouseBinding expectedMouseBinding = new MouseBinding(button, modifiers);
waitForSwing();
waitForSwing();
waitForSwing();
waitForSwing();
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
assertEquals("Did not properly set mouse binding", expectedMouseBinding,
currentMouseBinding);
return currentMouseBinding;
}
private KeyStroke setKeyBinding(String actionName, String actionOwner, int modifiers,
int keyCode, char keyChar) throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
selectRowForAction(panel, actionName, actionOwner);
setToggleButtonSelected(panel, "Enter Mouse Binding", false);
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
JTextField textField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
JTextField textField = (JTextField) getInstanceField("ksField", panel);
triggerKey(textField, modifiers, keyCode, keyChar);
waitForSwing();
KeyStroke expectedKeyStroke = KeyStroke.getKeyStroke(keyCode, modifiers, false);
KeyStroke currentBinding = getKeyBinding(actionName);
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
assertEquals("Did not properly set new keybinding", expectedKeyStroke, currentBinding);
return currentBinding;
}
private int selectRowForAction(KeyBindingsPanel panel, String actionName) {
private void clearKeyBinding(String actionName, String actionOwner) throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
selectRowForAction(panel, actionName, actionOwner);
setToggleButtonSelected(panel, "Enter Mouse Binding", false);
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
JTextField textField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
triggerBackspaceKey(textField);
waitForSwing();
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
assertNull(currentBinding);
}
private int selectRowForAction(KeyBindingsPanel panel, String actionName, String actionOwner) {
final JTable table = (JTable) getInstanceField("actionTable", panel);
@SuppressWarnings("unchecked")
final RowObjectFilterModel<DockingActionIf> model =
@ -806,15 +1054,25 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
String rowActionName =
(String) model.getColumnValueForRow(rowData, 0 /* action name column */);
if (rowActionName.equals(actionName)) {
actionRow = i;
break;
String rowActionOwner =
(String) model.getColumnValueForRow(rowData, 2 /* owner column */);
if (rowActionOwner.equals(actionOwner)) {
actionRow = i;
break;
}
}
}
assertTrue("Could not find row for action: " + actionName, actionRow != -1);
assertTrue("Could not find row for action: " + actionName + " (" + actionOwner + ")",
actionRow != -1);
final int row = actionRow;
runSwing(() -> table.setRowSelectionInterval(row, row));
int row = actionRow;
runSwing(() -> {
table.setRowSelectionInterval(row, row);
Rectangle cellRectangle = table.getCellRect(row, row, true);
table.scrollRectToVisible(cellRectangle);
});
return row;
}
@ -1039,17 +1297,21 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
showOptionsDialog(pluginTool);
}
private void showOptionsDialog(PluginTool pluginTool) throws Exception {
// TODO change to getAction("Edit Options")
private void editOptions(PluginTool pluginTool) {
Set<DockingActionIf> list = pluginTool.getAllActions();
for (DockingActionIf action : list) {
if (action.getName().equals("Edit Options")) {
performAction(action, false);
break;
waitForSwing();
return;
}
}
fail("Unable to find action 'Edit Options'");
}
waitForSwing();
private void showOptionsDialog(PluginTool pluginTool) throws Exception {
editOptions(pluginTool);
dialog = waitForDialogComponent(OptionsDialog.class);
optionsPanel = (OptionsPanel) getInstanceField("panel", dialog);
Container pane = dialog.getComponent();

View file

@ -19,6 +19,8 @@ import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditorSupport;
import java.io.File;
@ -30,7 +32,6 @@ import javax.swing.KeyStroke;
import org.junit.*;
import docking.test.AbstractDockingTest;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.ThemeManager;
import ghidra.framework.options.*;
@ -39,20 +40,19 @@ import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.util.HelpLocation;
import ghidra.util.exception.InvalidInputException;
import gui.event.MouseBinding;
public class OptionsDBTest extends AbstractDockingTest {
private OptionsDB options;
private ProgramBuilder builder;
private int txID;
private GColor testColor;
public enum fruit {
Apple, Pear, Orange
}
public OptionsDBTest() {
super();
}
@Before
@ -62,7 +62,6 @@ public class OptionsDBTest extends AbstractDockingTest {
txID = program.startTransaction("Test");
options = new OptionsDB(program);
ThemeManager.getInstance().setColor("color.test", Palette.MAGENTA);
testColor = new GColor("color.test");
}
private void saveAndRestoreOptions() {
@ -223,10 +222,38 @@ public class OptionsDBTest extends AbstractDockingTest {
@Test
public void testSaveKeyStrokeOption() {
options.setKeyStroke("Foo", KeyStroke.getKeyStroke('a', 0));
options.setKeyStroke("Foo", KeyStroke.getKeyStroke(KeyEvent.VK_A, 0));
saveAndRestoreOptions();
assertEquals(KeyStroke.getKeyStroke('a', 0),
options.getKeyStroke("Foo", KeyStroke.getKeyStroke('b', 0)));
KeyStroke savedKs = options.getKeyStroke("Foo", null);
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), savedKs);
}
@Test
public void testSaveActionTrigger_KeyStroke() {
KeyStroke ks = KeyStroke.getKeyStroke('a', 0);
ActionTrigger trigger = new ActionTrigger(ks);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test
public void testSaveActionTrigger_MouseBinding() {
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
ActionTrigger trigger = new ActionTrigger(mb);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test
public void testSaveActionTrigger_KeyStrokeAndMouseBinding() {
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
ActionTrigger trigger = new ActionTrigger(ks, mb);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test
@ -678,7 +705,7 @@ public class OptionsDBTest extends AbstractDockingTest {
}
public static class MyPropertyEditor extends PropertyEditorSupport {
//
}
}

View file

@ -19,6 +19,8 @@ import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditorSupport;
import java.io.File;
@ -35,6 +37,7 @@ import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.util.HelpLocation;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.exception.InvalidInputException;
import gui.event.MouseBinding;
public class OptionsTest extends AbstractGuiTest {
@ -161,10 +164,37 @@ public class OptionsTest extends AbstractGuiTest {
@Test
public void testSaveKeyStrokeOption() {
options.setKeyStroke("Foo", KeyStroke.getKeyStroke('a', 0));
options.setKeyStroke("Foo", KeyStroke.getKeyStroke(KeyEvent.VK_A, 0));
saveAndRestoreOptions();
assertEquals(KeyStroke.getKeyStroke('a', 0),
options.getKeyStroke("Foo", KeyStroke.getKeyStroke('b', 0)));
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), options.getKeyStroke("Foo", null));
}
@Test
public void testSaveActionTrigger_KeyStroke() {
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
ActionTrigger trigger = new ActionTrigger(ks);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test
public void testSaveActionTrigger_MouseBinding() {
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
ActionTrigger trigger = new ActionTrigger(mb);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test
public void testSaveActionTrigger_KeyStrokeAndMouseBinding() {
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
ActionTrigger trigger = new ActionTrigger(ks, mb);
options.setActionTrigger("Foo", trigger);
saveAndRestoreOptions();
assertEquals(trigger, options.getActionTrigger("Foo", null));
}
@Test

View file

@ -20,11 +20,7 @@ import java.awt.Font;
import java.io.File;
import java.util.Date;
import javax.swing.KeyStroke;
import ghidra.framework.options.CustomOption;
import ghidra.framework.options.OptionType;
import ghidra.framework.options.Options;
import ghidra.framework.options.*;
import ghidra.program.model.data.ISF.IsfObject;
import ghidra.util.exception.AssertException;
@ -39,88 +35,94 @@ public class ExtProperty implements IsfObject {
this.type = type;
this.value = value;
}
public ExtProperty(String name, Options propList) {
this.name = name;
OptionType optionType = propList.getType(name);
switch (optionType) {
case INT_TYPE:
type = "int";
value = Integer.toString(propList.getInt(name, 0));
break;
case LONG_TYPE:
type = "long";
value = Long.toString(propList.getLong(name, 0));
break;
case STRING_TYPE:
type = "string";
value = propList.getString(name, "");
break;
case BOOLEAN_TYPE:
type = "bool";
value = Boolean.toString(propList.getBoolean(name, true));
break;
case DOUBLE_TYPE:
type = "double";
value = Double.toString(propList.getDouble(name, 0));
break;
case FLOAT_TYPE:
type = "float";
value = Float.toString(propList.getFloat(name, 0f));
break;
case DATE_TYPE:
type = "date";
Date date = propList.getDate(name, (Date) null);
long time = date == null ? 0 : date.getTime();
value = Long.toHexString(time);
break;
case COLOR_TYPE:
type = "color";
Color color = propList.getColor(name, null);
int rgb = color.getRGB();
value = Integer.toHexString(rgb);
break;
case ENUM_TYPE:
type = "enum";
@SuppressWarnings({ "unchecked", "rawtypes" })
Enum enuum = propList.getEnum(name, null);
String enumString = OptionType.ENUM_TYPE.convertObjectToString(enuum);
value = escapeElementEntities(enumString);
break;
case FILE_TYPE:
type = "file";
File file = propList.getFile(name, null);
String path = file.getAbsolutePath();
value = path;
break;
case FONT_TYPE:
type = "font";
Font font = propList.getFont(name, null);
enumString = OptionType.FONT_TYPE.convertObjectToString(font);
value = escapeElementEntities(enumString);
break;
case KEYSTROKE_TYPE:
type = "keyStroke";
KeyStroke keyStroke = propList.getKeyStroke(name, null);
enumString = OptionType.KEYSTROKE_TYPE.convertObjectToString(keyStroke);
value = escapeElementEntities(enumString);
break;
case CUSTOM_TYPE:
type = "custom";
CustomOption custom = propList.getCustomOption(name, null);
enumString = OptionType.CUSTOM_TYPE.convertObjectToString(custom);
value = escapeElementEntities(enumString);
break;
case BYTE_ARRAY_TYPE:
type = "bytes";
byte[] bytes = propList.getByteArray(name, null);
enumString = OptionType.BYTE_ARRAY_TYPE.convertObjectToString(bytes);
value = escapeElementEntities(enumString);
break;
case NO_TYPE:
break;
default:
throw new AssertException();
case INT_TYPE:
type = "int";
value = Integer.toString(propList.getInt(name, 0));
break;
case LONG_TYPE:
type = "long";
value = Long.toString(propList.getLong(name, 0));
break;
case STRING_TYPE:
type = "string";
value = propList.getString(name, "");
break;
case BOOLEAN_TYPE:
type = "bool";
value = Boolean.toString(propList.getBoolean(name, true));
break;
case DOUBLE_TYPE:
type = "double";
value = Double.toString(propList.getDouble(name, 0));
break;
case FLOAT_TYPE:
type = "float";
value = Float.toString(propList.getFloat(name, 0f));
break;
case DATE_TYPE:
type = "date";
Date date = propList.getDate(name, (Date) null);
long time = date == null ? 0 : date.getTime();
value = Long.toHexString(time);
break;
case COLOR_TYPE:
type = "color";
Color color = propList.getColor(name, null);
int rgb = color.getRGB();
value = Integer.toHexString(rgb);
break;
case ENUM_TYPE:
type = "enum";
@SuppressWarnings({ "unchecked", "rawtypes" })
Enum enuum = propList.getEnum(name, null);
String enumString = OptionType.ENUM_TYPE.convertObjectToString(enuum);
value = escapeElementEntities(enumString);
break;
case FILE_TYPE:
type = "file";
File file = propList.getFile(name, null);
String path = file.getAbsolutePath();
value = path;
break;
case FONT_TYPE:
type = "font";
Font font = propList.getFont(name, null);
enumString = OptionType.FONT_TYPE.convertObjectToString(font);
value = escapeElementEntities(enumString);
break;
case KEYSTROKE_TYPE:
type = "actionTrigger";
ActionTrigger trigger = propList.getActionTrigger(name, null);
enumString = OptionType.ACTION_TRIGGER.convertObjectToString(trigger);
value = escapeElementEntities(enumString);
break;
case ACTION_TRIGGER:
type = "actionTrigger";
ActionTrigger actionTrigger = propList.getActionTrigger(name, null);
enumString = OptionType.ACTION_TRIGGER.convertObjectToString(actionTrigger);
value = escapeElementEntities(enumString);
break;
case CUSTOM_TYPE:
type = "custom";
CustomOption custom = propList.getCustomOption(name, null);
enumString = OptionType.CUSTOM_TYPE.convertObjectToString(custom);
value = escapeElementEntities(enumString);
break;
case BYTE_ARRAY_TYPE:
type = "bytes";
byte[] bytes = propList.getByteArray(name, null);
enumString = OptionType.BYTE_ARRAY_TYPE.convertObjectToString(bytes);
value = escapeElementEntities(enumString);
break;
case NO_TYPE:
break;
default:
throw new AssertException();
}
}
@ -129,11 +131,11 @@ public class ExtProperty implements IsfObject {
private static final String APOSTROPHE = "&apos;";
private static final String QUOTE = "&quot;";
private static final String AMPERSAND = "&amp;";
/**
* Converts any special or reserved characters in the specified SARIF string
* into the equivalent Unicode encoding.
*
*
* @param sarif the SARIF string
* @return the encoded SARIF string
*/
@ -143,7 +145,8 @@ public class ExtProperty implements IsfObject {
int codePoint = sarif.codePointAt(offset);
offset += Character.charCount(codePoint);
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) && (codePoint != 0x0D)) {
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) &&
(codePoint != 0x0D)) {
continue;
}
if (codePoint >= 0x7F) {

View file

@ -15,42 +15,23 @@
*/
package sarif.managers;
import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.swing.KeyStroke;
import com.google.gson.JsonArray;
import ghidra.app.util.importer.MessageLog;
import ghidra.framework.options.CustomOption;
import ghidra.framework.options.OptionType;
import ghidra.framework.options.Options;
import ghidra.framework.options.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Bookmark;
import ghidra.program.model.listing.BookmarkManager;
import ghidra.program.model.listing.BookmarkType;
import ghidra.program.model.listing.Program;
import ghidra.program.model.util.IntPropertyMap;
import ghidra.program.model.util.LongPropertyMap;
import ghidra.program.model.util.ObjectPropertyMap;
import ghidra.program.model.util.PropertyMap;
import ghidra.program.model.util.PropertyMapManager;
import ghidra.program.model.util.StringPropertyMap;
import ghidra.program.model.util.VoidPropertyMap;
import ghidra.util.ColorUtils;
import ghidra.util.SaveableColor;
import ghidra.util.SaveablePoint;
import ghidra.program.model.listing.*;
import ghidra.program.model.util.*;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.task.TaskLauncher;
@ -79,8 +60,8 @@ public class PropertiesSarifMgr extends SarifMgr {
////////////////////////////
@Override
public boolean read(Map<String, Object> result, SarifProgramOptions options, TaskMonitor monitor)
throws CancelledException {
public boolean read(Map<String, Object> result, SarifProgramOptions options,
TaskMonitor monitor) throws CancelledException {
processProperty(result, options == null || options.isOverwritePropertyConflicts());
return true;
}
@ -92,18 +73,20 @@ public class PropertiesSarifMgr extends SarifMgr {
Address addr = getLocation(result);
if (addr != null) {
processPropertyMapEntry(addr, name, result, overwrite);
} else {
}
else {
processPropertyListEntry(name, result, overwrite);
}
} catch (Exception e) {
}
catch (Exception e) {
log.appendException(e);
}
}
@SuppressWarnings("unchecked")
private void processPropertyMapEntry(Address addr, String name, Map<String, Object> result, boolean overwrite)
throws DuplicateNameException {
private void processPropertyMapEntry(Address addr, String name, Map<String, Object> result,
boolean overwrite) throws DuplicateNameException {
String type = (String) result.get("type");
if (type != null) {
@ -128,44 +111,51 @@ public class PropertiesSarifMgr extends SarifMgr {
voidMap = propMapMgr.createVoidPropertyMap(name);
}
voidMap.add(addr);
} else if ("int".equals(type)) {
}
else if ("int".equals(type)) {
int value = Integer.parseInt(val, 16);
IntPropertyMap intMap = propMapMgr.getIntPropertyMap(name);
if (intMap == null) {
intMap = propMapMgr.createIntPropertyMap(name);
}
intMap.add(addr, value);
} else if ("long".equals(type)) {
}
else if ("long".equals(type)) {
long value = Long.parseLong(val, 16);
LongPropertyMap longMap = propMapMgr.getLongPropertyMap(name);
if (longMap == null) {
longMap = propMapMgr.createLongPropertyMap(name);
}
longMap.add(addr, value);
} else if ("string".equals(type)) {
}
else if ("string".equals(type)) {
String str = val;
StringPropertyMap strMap = propMapMgr.getStringPropertyMap(name);
if (strMap == null) {
strMap = propMapMgr.createStringPropertyMap(name);
}
strMap.add(addr, str);
} else if ("color".equals(type)) {
ObjectPropertyMap<SaveableColor> objMap = (ObjectPropertyMap<SaveableColor>) propMapMgr
.getObjectPropertyMap(name);
}
else if ("color".equals(type)) {
ObjectPropertyMap<SaveableColor> objMap =
(ObjectPropertyMap<SaveableColor>) propMapMgr.getObjectPropertyMap(name);
if (objMap == null) {
objMap = propMapMgr.createObjectPropertyMap(name, SaveableColor.class);
}
objMap.add(addr, new SaveableColor(Color.decode(val)));
} else if ("point".equals(type)) {
}
else if ("point".equals(type)) {
String xstr = val.substring(val.indexOf("[x="), val.indexOf(","));
String ystr = val.substring(val.indexOf("y="), val.indexOf("]"));
ObjectPropertyMap<SaveablePoint> objMap = (ObjectPropertyMap<SaveablePoint>) propMapMgr
.getObjectPropertyMap(name);
ObjectPropertyMap<SaveablePoint> objMap =
(ObjectPropertyMap<SaveablePoint>) propMapMgr.getObjectPropertyMap(name);
if (objMap == null) {
objMap = propMapMgr.createObjectPropertyMap(name, SaveablePoint.class);
}
objMap.add(addr, new SaveablePoint(new Point(Integer.parseInt(xstr), Integer.parseInt(ystr))));
} else if ("bookmarks".equals(type)) {
objMap.add(addr,
new SaveablePoint(new Point(Integer.parseInt(xstr), Integer.parseInt(ystr))));
}
else if ("bookmarks".equals(type)) {
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
// were saved as simple properties
BookmarkManager bmMgr = program.getBookmarkManager();
@ -177,7 +167,8 @@ public class PropertiesSarifMgr extends SarifMgr {
}
}
bmMgr.setBookmark(addr, BookmarkType.NOTE, name, val);
} else {
}
else {
log.appendMsg("Unsupported PROPERTY usage");
}
}
@ -202,13 +193,14 @@ public class PropertiesSarifMgr extends SarifMgr {
}
@SuppressWarnings("unchecked")
private void processPropertyListEntry(String pathname, Map<String, Object> result, boolean overwrite)
throws Exception {
private void processPropertyListEntry(String pathname, Map<String, Object> result,
boolean overwrite) throws Exception {
String listName = getPropertyList(pathname);
String name = getPropertyName(pathname);
if (listName == null || name == null) {
log.appendMsg("Property NAME attribute must contain both category prefix and property name");
log.appendMsg(
"Property NAME attribute must contain both category prefix and property name");
return;
}
Options list = program.getOptions(listName);
@ -223,48 +215,76 @@ public class PropertiesSarifMgr extends SarifMgr {
Object val = result.get("value");
if (type == null || "void".equals(type)) {
log.appendMsg("Unsupported PROPERTY usage");
} else if ("int".equals(type)) {
}
else if ("int".equals(type)) {
list.setInt(name, Integer.parseInt((String) val, 16));
} else if ("long".equals(type)) {
}
else if ("long".equals(type)) {
list.setLong(name, Long.parseLong((String) val, 16));
} else if ("double".equals(type)) {
}
else if ("double".equals(type)) {
list.setDouble(name, Double.parseDouble((String) val));
} else if ("float".equals(type)) {
}
else if ("float".equals(type)) {
list.setFloat(name, Float.parseFloat((String) val));
} else if ("bool".equals(type)) {
}
else if ("bool".equals(type)) {
list.setBoolean(name, Boolean.parseBoolean((String) val));
} else if ("string".equals(type)) {
}
else if ("string".equals(type)) {
list.setString(name, (String) val);
} else if ("date".equals(type)) {
}
else if ("date".equals(type)) {
list.setDate(name, new Date(Long.parseLong((String) val, 16)));
} else if ("color".equals(type)) {
}
else if ("color".equals(type)) {
Color color = ColorUtils.getColor((Integer) val);
list.setColor(name, color);
} else if ("file".equals(type)) {
}
else if ("file".equals(type)) {
File file = new File((String) val);
list.setFile(name, file);
} else if ("enum".equals(type)) {
}
else if ("enum".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
@SuppressWarnings("rawtypes")
Enum enuum = (Enum) OptionType.ENUM_TYPE.convertStringToObject(sarifString);
list.setEnum(name, enuum);
} else if ("font".equals(type)) {
}
else if ("font".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
Font font = (Font) OptionType.FONT_TYPE.convertStringToObject(sarifString);
list.setFont(name, font);
} else if ("keyStroke".equals(type)) {
}
else if ("keyStroke".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
KeyStroke keyStroke = (KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(sarifString);
list.setKeyStroke(name, keyStroke);
} else if ("custom".equals(type)) {
KeyStroke keyStroke =
(KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(sarifString);
ActionTrigger trigger = null;
if (keyStroke != null) {
trigger = new ActionTrigger(keyStroke);
}
list.setActionTrigger(name, trigger);
}
else if ("actionTrigger".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
CustomOption custom = (CustomOption) OptionType.CUSTOM_TYPE.convertStringToObject(sarifString);
ActionTrigger actionTrigger =
(ActionTrigger) OptionType.ACTION_TRIGGER.convertStringToObject(sarifString);
list.setActionTrigger(name, actionTrigger);
}
else if ("custom".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
CustomOption custom =
(CustomOption) OptionType.CUSTOM_TYPE.convertStringToObject(sarifString);
list.setCustomOption(name, custom);
} else if ("bytes".equals(type)) {
}
else if ("bytes".equals(type)) {
String sarifString = unEscapeElementEntities((String) val);
byte[] bytes = (byte[]) OptionType.BYTE_ARRAY_TYPE.convertStringToObject(sarifString);
list.setByteArray(name, bytes);
} else {
}
else {
log.appendMsg("Unsupported PROPERTY usage");
}
}
@ -273,7 +293,8 @@ public class PropertiesSarifMgr extends SarifMgr {
// SARIF WRITE CURRENT DTD //
/////////////////////////////
void write(JsonArray results, AddressSetView set, TaskMonitor monitor) throws IOException, CancelledException {
void write(JsonArray results, AddressSetView set, TaskMonitor monitor)
throws IOException, CancelledException {
monitor.setMessage("Writing PROPERTIES ...");
List<String> request = program.getOptionsNames();
@ -290,13 +311,14 @@ public class PropertiesSarifMgr extends SarifMgr {
writeAsSARIF(program, set, mapRequest, results);
}
public static void writeAsSARIF(Program program, List<String> request, JsonArray results) throws IOException {
public static void writeAsSARIF(Program program, List<String> request, JsonArray results)
throws IOException {
SarifPropertyListWriter writer = new SarifPropertyListWriter(program, request, null);
new TaskLauncher(new SarifWriterTask(SUBKEY, writer, results), null);
}
public static void writeAsSARIF(Program program, AddressSetView set, List<PropertyMap<?>> request,
JsonArray results) throws IOException {
public static void writeAsSARIF(Program program, AddressSetView set,
List<PropertyMap<?>> request, JsonArray results) throws IOException {
SarifPropertyMapWriter writer = new SarifPropertyMapWriter(request, program, set, null);
new TaskLauncher(new SarifWriterTask(SUBKEY, writer, results), null);
}

View file

@ -19,8 +19,6 @@ import java.awt.Component;
import java.io.*;
import java.util.*;
import javax.swing.KeyStroke;
import org.jdom.Document;
import org.jdom.output.XMLOutputter;
@ -51,7 +49,6 @@ import ghidra.framework.options.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginException;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.project.tool.GhidraTool;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.Reference;
@ -144,8 +141,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
toolTemplate = ToolUtils.readToolTemplate(toolFileName);
}
PluginTool newTool =
(GhidraTool) toolTemplate.createTool(controller.getTool().getProject());
PluginTool newTool = toolTemplate.createTool(controller.getTool().getProject());
try {
VersionTrackingSubordinatePluginX pluginX =
new VersionTrackingSubordinatePluginX(newTool, isSourceTool);
@ -190,33 +186,38 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
if (processingOptions) {
return;
}
if (!(newValue instanceof ActionTrigger)) {
return;
}
processingOptions = true;
try {
if (!(newValue instanceof KeyStroke)) {
return;
}
KeyStroke keyStroke = (KeyStroke) newValue;
if (sourceTool != null) {
Options sourceOptions = sourceTool.getOptions(ToolConstants.KEY_BINDINGS);
if (sourceOptions != options) {
sourceOptions.setKeyStroke(optionName, keyStroke);
sourceTool.refreshKeybindings();
return;
}
}
if (destinationTool != null) {
Options destinationOptions = destinationTool.getOptions(ToolConstants.KEY_BINDINGS);
if (destinationOptions != options) {
destinationOptions.setKeyStroke(optionName, keyStroke);
destinationTool.refreshKeybindings();
}
}
updateActionTrigger(options, optionName, (ActionTrigger) newValue);
}
finally {
processingOptions = false;
}
}
private void updateActionTrigger(ToolOptions options, String optionName,
ActionTrigger trigger) {
if (sourceTool != null) {
Options sourceOptions = sourceTool.getOptions(DockingToolConstants.KEY_BINDINGS);
if (sourceOptions != options) {
sourceOptions.setActionTrigger(optionName, trigger);
return;
}
}
if (destinationTool != null) {
Options destinationOptions =
destinationTool.getOptions(DockingToolConstants.KEY_BINDINGS);
if (destinationOptions != options) {
destinationOptions.setActionTrigger(optionName, trigger);
}
}
}
private void createMatchActions(final PluginTool newTool) {
newTool.setMenuGroup(new String[] { VTPlugin.MATCH_POPUP_MENU_NAME }, "1", "1");
newTool.setMenuGroup(new String[] { VTPlugin.MARKUP_POPUP_MENU_NAME }, "1", "2");

View file

@ -0,0 +1,123 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.BorderLayout;
import java.util.Objects;
import javax.swing.*;
import docking.widgets.checkbox.GCheckBox;
import gui.event.MouseBinding;
/**
* A panel that displays inputs for key strokes and mouse bindings.
*/
public class ActionBindingPanel extends JPanel {
private static final String DISABLED_HINT = "Select an action";
private KeyEntryTextField keyEntryField;
private JCheckBox useMouseBindingCheckBox;
private MouseEntryTextField mouseEntryField;
private JPanel textFieldPanel;
private DockingActionInputBindingListener listener;
public ActionBindingPanel(DockingActionInputBindingListener listener) {
this.listener = Objects.requireNonNull(listener);
build();
}
private void build() {
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
textFieldPanel = new JPanel(new BorderLayout());
keyEntryField = new KeyEntryTextField(20, ks -> listener.keyStrokeChanged(ks));
keyEntryField.setDisabledHint(DISABLED_HINT);
keyEntryField.setEnabled(false); // enabled on action selection
mouseEntryField = new MouseEntryTextField(20, mb -> listener.mouseBindingChanged(mb));
mouseEntryField.setDisabledHint(DISABLED_HINT);
mouseEntryField.setEnabled(false); // enabled on action selection
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
String checkBoxText = "Enter Mouse Binding";
useMouseBindingCheckBox = new GCheckBox(checkBoxText);
useMouseBindingCheckBox
.setToolTipText("When checked, the text field accepts mouse buttons");
useMouseBindingCheckBox.setName(checkBoxText);
useMouseBindingCheckBox.addItemListener(e -> updateTextField());
add(textFieldPanel);
add(Box.createHorizontalStrut(5));
add(useMouseBindingCheckBox);
}
private void updateTextField() {
if (useMouseBindingCheckBox.isSelected()) {
textFieldPanel.remove(keyEntryField);
textFieldPanel.add(mouseEntryField, BorderLayout.NORTH);
}
else {
textFieldPanel.remove(mouseEntryField);
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
}
validate();
repaint();
}
public void setKeyBindingData(KeyStroke ks, MouseBinding mb) {
keyEntryField.setKeyStroke(ks);
mouseEntryField.setMouseBinding(mb);
}
@Override
public void setEnabled(boolean enabled) {
keyEntryField.clearField();
mouseEntryField.clearField();
keyEntryField.setEnabled(enabled);
mouseEntryField.setEnabled(enabled);
}
public void clearKeyStroke() {
keyEntryField.clearField();
}
public KeyStroke getKeyStroke() {
return keyEntryField.getKeyStroke();
}
public MouseBinding getMouseBinding() {
return mouseEntryField.getMouseBinding();
}
public void clearMouseBinding() {
mouseEntryField.clearField();
}
public boolean isMouseBinding() {
return useMouseBindingCheckBox.isSelected();
}
}

View file

@ -47,13 +47,14 @@ public class ActionToGuiMapper {
popupActionManager = new PopupActionManager(winMgr, menuGroupMap);
DockingWindowsContextSensitiveHelpListener.install();
MouseBindingMouseEventDispatcher.install();
}
/**
* Register a specific Help content location for a component.
* The DocWinListener will be notified with the help location if the specified
* component 'c' has focus and the help key is pressed.
*
*
* @param c component
* @param helpLocation the help location
*/

View file

@ -230,6 +230,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
if (!isVisible()) {
return;
}
dockingTool.toFront();
if (defaultFocusComponent != null) {
DockingWindowManager.requestFocus(defaultFocusComponent);

View file

@ -0,0 +1,38 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import javax.swing.KeyStroke;
import gui.event.MouseBinding;
/**
* A simple listener interface to notify clients of changes to key strokes and mouse bindings.
*/
public interface DockingActionInputBindingListener {
/**
* Called when the key stroke is changed.
* @param ks the key stroke.
*/
public void keyStrokeChanged(KeyStroke ks);
/**
* Called when the mouse binding is changed.
* @param mb the mouse binding.
*/
public void mouseBindingChanged(MouseBinding mb);
}

View file

@ -0,0 +1,77 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.event.ActionEvent;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingActionIf;
import ghidra.util.Msg;
import ghidra.util.Swing;
/**
* A simple class to handle executing the given action. This class will generate the action context
* as needed and validate the context before executing the action.
*/
public class DockingActionPerformer {
private DockingActionPerformer() {
// static only
}
/**
* Executes the given action later on the Swing thread.
* @param action the action.
* @param event the event that triggered the action.
*/
public static void perform(DockingActionIf action, ActionEvent event) {
perform(action, event, DockingWindowManager.getActiveInstance());
}
/**
* Executes the given action later on the Swing thread.
* @param action the action.
* @param event the event that triggered the action.
* @param windowManager the window manager containing the action being processed.
*/
public static void perform(DockingActionIf action, ActionEvent event,
DockingWindowManager windowManager) {
if (windowManager == null) {
// not sure if this can happen
Msg.error(DockingActionPerformer.class,
"No window manager found; unable to execute action: " + action.getFullName());
}
DockingWindowManager.clearMouseOverHelp();
ActionContext context = windowManager.createActionContext(action);
context.setSourceObject(event.getSource());
context.setEventClickModifiers(event.getModifiers());
// this gives the UI some time to repaint before executing the action
Swing.runLater(() -> {
windowManager.setStatusText("");
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
toggleAction.setSelected(!toggleAction.isSelected());
}
action.actionPerformed(context);
}
});
}
}

View file

@ -30,10 +30,9 @@ import docking.actions.KeyBindingUtils;
*/
public abstract class DockingKeyBindingAction extends AbstractAction {
protected Tool tool;
protected DockingActionIf dockingAction;
protected final KeyStroke keyStroke;
protected final Tool tool;
protected KeyStroke keyStroke;
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
super(KeyBindingUtils.parseKeyStroke(keyStroke));
@ -42,14 +41,9 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
this.keyStroke = keyStroke;
}
KeyStroke getKeyStroke() {
return keyStroke;
}
@Override
public boolean isEnabled() {
// always enable; this is a reserved binding and cannot be disabled
return true;
return true; // always enable; this is a internal action that cannot be disabled
}
public abstract KeyBindingPrecedence getKeyBindingPrecedence();

View file

@ -0,0 +1,58 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.event.ActionEvent;
import java.util.Objects;
import javax.swing.AbstractAction;
import docking.action.DockingActionIf;
import gui.event.MouseBinding;
/**
* A class for using actions associated with mouse bindings. This class is meant to only by used by
* internal Ghidra mouse event processing.
*/
public class DockingMouseBindingAction extends AbstractAction {
private DockingActionIf dockingAction;
private MouseBinding mouseBinding;
public DockingMouseBindingAction(DockingActionIf action, MouseBinding mouseBinding) {
this.dockingAction = Objects.requireNonNull(action);
this.mouseBinding = Objects.requireNonNull(mouseBinding);
}
public String getFullActionName() {
return dockingAction.getFullName();
}
@Override
public boolean isEnabled() {
return true; // always enable; this is a internal action that cannot be disabled
}
@Override
public void actionPerformed(ActionEvent e) {
DockingActionPerformer.perform(dockingAction, e);
}
@Override
public String toString() {
return getFullActionName() + " (" + mouseBinding + ")";
}
}

View file

@ -29,8 +29,7 @@ import javax.swing.*;
import org.apache.commons.collections4.map.LazyMap;
import org.jdom.Element;
import docking.action.ActionContextProvider;
import docking.action.DockingActionIf;
import docking.action.*;
import docking.actions.*;
import docking.widgets.PasswordDialog;
import generic.util.WindowUtilities;
@ -41,6 +40,7 @@ import ghidra.util.*;
import ghidra.util.datastruct.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.SwingUpdateManager;
import gui.event.MouseBinding;
import help.Help;
import help.HelpService;
import util.CollectionUtils;
@ -707,7 +707,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
/**
* Get the local actions installed on the given provider
*
*
* @param provider the provider
* @return an iterator over the actions
*/
@ -754,7 +754,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
* Returns any action that is bound to the given keystroke for the tool associated with this
* DockingWindowManager instance.
*
* @param keyStroke The keystroke to check for key bindings.
* @param keyStroke The keystroke to check for a bound action.
* @return The action that is bound to the keystroke, or null of there is no binding for the
* given keystroke.
*/
@ -768,6 +768,24 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return null;
}
/**
* Returns any action that is bound to the given mouse binding for the tool associated with this
* DockingWindowManager instance.
*
* @param mouseBinding The mouse binding to check for a bound action.
* @return The action associated with the mouse binding , or null of there is no binding for the
* given keystroke.
*/
Action getActionForMouseBinding(MouseBinding mouseBinding) {
DockingToolActions toolActions = tool.getToolActions();
if (toolActions instanceof ToolActions) {
// Using a cast here; it didn't make sense to include this 'getAction' on the
// DockingToolActions
return ((ToolActions) toolActions).getAction(mouseBinding);
}
return null;
}
//==================================================================================================
// End Package-level Methods
//==================================================================================================
@ -1189,8 +1207,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return;
}
tool.getToolActions()
.removeActions(DOCKING_WINDOWS_OWNER);
tool.getToolActions().removeActions(DOCKING_WINDOWS_OWNER);
Map<String, List<ComponentPlaceholder>> permanentMap =
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
@ -1206,12 +1223,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
String subMenuName = provider.getWindowSubMenuName();
if (provider.isTransient() && !provider.isSnapshot()) {
transientMap.get(subMenuName)
.add(placeholder);
transientMap.get(subMenuName).add(placeholder);
}
else {
permanentMap.get(subMenuName)
.add(placeholder);
permanentMap.get(subMenuName).add(placeholder);
}
}
promoteSingleMenuGroups(permanentMap);
@ -1225,8 +1240,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
}
private boolean isWindowMenuShowing() {
MenuElement[] selectedPath = MenuSelectionManager.defaultManager()
.getSelectedPath();
MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath();
if (selectedPath == null || selectedPath.length == 0) {
return false;
}
@ -1282,8 +1296,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
List<ComponentPlaceholder> list = lazyMap.get(key);
if (list.size() == 1) {
lazyMap.get(null /*submenu name*/)
.add(list.get(0));
lazyMap.get(null /*submenu name*/).add(list.get(0));
lazyMap.remove(key);
}
}
@ -1422,10 +1435,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
ComponentProvider provider = entry.getKey();
ComponentPlaceholder placeholder = entry.getValue();
if (provider.getOwner()
.equals(focusOwner) &&
provider.getName()
.equals(focusName)) {
if (provider.getOwner().equals(focusOwner) && provider.getName().equals(focusName)) {
focusReplacement = placeholder;
break; // found one!
}
@ -1502,7 +1512,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
/**
* Clears the docking window manager's notion of the active provider. This is used
* when a component that is not contained within a dockable component gets focus
* when a component that is not contained within a dockable component gets focus
* (e.g., JTabbedPanes for stacked components).
*/
private void deactivateFocusedComponent() {
@ -1552,8 +1562,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
@Override
public void propertyChange(PropertyChangeEvent evt) {
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getActiveWindow();
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
if (!isMyWindow(win)) {
return;
}
@ -1693,8 +1702,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
toolPreferencesElement.getChildren(PreferenceState.PREFERENCE_STATE_NAME);
for (Object name : children) {
Element preferencesElement = (Element) name;
preferenceStateMap.put(preferencesElement.getAttribute("NAME")
.getValue(),
preferenceStateMap.put(preferencesElement.getAttribute("NAME").getValue(),
new PreferenceState(preferencesElement));
}
}
@ -1954,7 +1962,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
/*
Note: Which window should be the parent of the dialog when the user does not specify?
Some use cases; a dialog is shown from:
1) A toolbar action
2) A component provider's code
@ -1962,7 +1970,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
4) A background thread
5) The help window
6) A modal password dialog appears over the splash screen
It seems like the parent should be the active window for 1-2.
Case 3 should probably use the window of the dialog provider.
Case 4 should probably use the main tool frame, since the user may be
@ -1970,12 +1978,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
active window, we can default to the tool's frame.
Case 5 should use the help window.
Case 6 should use the splash screen as the parent.
We have not yet solidified how we should parent. This documentation is meant to
move us towards clarity as we find Use Cases that don't make sense. (Once we
finalize our understanding, we should update the javadoc to list exactly where
the given Dialog Component will be shown.)
Use Case
A -The user presses an action on a toolbar from a window on screen 1, while the
main tool frame is on screen 2. We want the popup window to appear on screen
@ -1994,12 +2002,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
E -A long-running API shows a non-modal progress dialog. This API then shows a
results dialog which is also non-modal. We do not want to parent the new dialog
to the original dialog, since it is a progress dialog that will go away.
For now, the easiest mental model to use is to always prefer the active non-transient
window so that a dialog will appear in the user's view. If we find a case where this is
not desired, then document it here.
*/
DockingWindowManager dwm = getActiveInstance();
@ -2183,8 +2191,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
setStatusText(text);
if (beep) {
Toolkit.getDefaultToolkit()
.beep();
Toolkit.getDefaultToolkit().beep();
}
}
@ -2201,8 +2208,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
* A convenience method to make an attention-grabbing noise to the user
*/
public static void beep() {
Toolkit.getDefaultToolkit()
.beep();
Toolkit.getDefaultToolkit().beep();
}
/*
@ -2274,8 +2280,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
if (includeMain) {
winList.add(root.getMainWindow());
}
Iterator<DetachedWindowNode> it = root.getDetachedWindows()
.iterator();
Iterator<DetachedWindowNode> it = root.getDetachedWindows().iterator();
while (it.hasNext()) {
DetachedWindowNode node = it.next();
Window win = node.getWindow();
@ -2450,8 +2455,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
defaultContextProviderMap.entrySet();
for (Entry<Class<? extends ActionContext>, ActionContextProvider> entry : entrySet) {
contextMap.put(entry.getKey(), entry.getValue()
.getActionContext(null));
contextMap.put(entry.getKey(), entry.getValue().getActionContext(null));
}
return contextMap;
}
@ -2472,7 +2476,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return context;
}
// Some actions work on a non-active, default component provider. See if this action
// Some actions work on a non-active, default component provider. See if this action
// supports that.
if (action.supportsDefaultContext()) {
context = getDefaultContext(action.getContextClass());

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,12 +18,8 @@ package docking;
import javax.swing.KeyStroke;
/**
* Interface used to notify listener when a keystroke was entered in the
* KeyEntryPanel.
*
*
* Interface used to notify listener when a keystroke has changed.
*/
public interface KeyEntryListener {
public void processEntry(KeyStroke keyStroke);
}

View file

@ -17,16 +17,20 @@ package docking;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Objects;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import docking.actions.KeyBindingUtils;
import docking.widgets.textfield.HintTextField;
/**
* Text field captures key strokes and notifies a listener to process the key entry.
*/
public class KeyEntryTextField extends JTextField {
public class KeyEntryTextField extends HintTextField {
private static final String HINT = "Type a key";
private String disabledHint = HINT;
private KeyEntryListener listener;
private String ksName;
@ -38,11 +42,28 @@ public class KeyEntryTextField extends JTextField {
* @param listener listener that is notified when the a key is pressed
*/
public KeyEntryTextField(int columns, KeyEntryListener listener) {
super(columns);
super(HINT);
setName("Key Entry Text Field");
getAccessibleContext().setAccessibleName(getName());
setColumns(columns);
this.listener = listener;
addKeyListener(new MyKeyListener());
}
@Override
public void setEnabled(boolean enabled) {
setHint(enabled ? HINT : disabledHint);
super.setEnabled(enabled);
}
/**
* Sets the hint text that will be displayed when this field is disabled
* @param disabledHint the hint text
*/
public void setDisabledHint(String disabledHint) {
this.disabledHint = Objects.requireNonNull(disabledHint);
}
/**
* Get the current key stroke
* @return the key stroke
@ -56,7 +77,7 @@ public class KeyEntryTextField extends JTextField {
* @param ks the new key stroke
*/
public void setKeyStroke(KeyStroke ks) {
processEntry(ks);
processKeyStroke(ks, false);
setText(KeyBindingUtils.parseKeyStroke(ks));
}
@ -66,7 +87,7 @@ public class KeyEntryTextField extends JTextField {
currentKeyStroke = null;
}
private void processEntry(KeyStroke ks) {
private void processKeyStroke(KeyStroke ks, boolean notify) {
ksName = null;
currentKeyStroke = ks;
@ -79,7 +100,10 @@ public class KeyEntryTextField extends JTextField {
ksName = KeyBindingUtils.parseKeyStroke(ks);
}
}
listener.processEntry(ks);
if (notify) {
listener.processEntry(ks);
}
}
private class MyKeyListener implements KeyListener {
@ -107,7 +131,7 @@ public class KeyEntryTextField extends JTextField {
if (!isClearKey(keyCode) && !isModifiersOnly(e)) {
keyStroke = KeyStroke.getKeyStroke(keyCode, e.getModifiersEx());
}
processEntry(keyStroke);
processKeyStroke(keyStroke, true);
e.consume();
}

View file

@ -18,9 +18,7 @@ package docking;
import java.awt.event.ActionEvent;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingActionIf;
import docking.menu.MenuHandler;
import ghidra.util.Swing;
public class MenuBarMenuHandler extends MenuHandler {
@ -41,24 +39,7 @@ public class MenuBarMenuHandler extends MenuHandler {
}
@Override
public void processMenuAction(final DockingActionIf action, final ActionEvent event) {
DockingWindowManager.clearMouseOverHelp();
ActionContext context = windowManager.createActionContext(action);
context.setSourceObject(event.getSource());
// this gives the UI some time to repaint before executing the action
Swing.runLater(() -> {
windowManager.setStatusText("");
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
toggleAction.setSelected(!toggleAction.isSelected());
}
action.actionPerformed(context);
}
});
public void processMenuAction(DockingActionIf action, ActionEvent event) {
DockingActionPerformer.perform(action, event, windowManager);
}
}

View file

@ -0,0 +1,185 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.*;
import java.awt.event.*;
import org.apache.logging.log4j.message.ParameterizedMessage;
import ghidra.util.Msg;
import gui.event.MouseBinding;
/**
* Allows Ghidra to give preference to its mouse event processing over the default Java mouse event
* processing. This class allows us to assign mouse bindings to actions.
* <p>
* {@link #install()} must be called in order to install this <code>Singleton</code> into Java's
* mouse event processing system.
*
* @see KeyBindingOverrideKeyEventDispatcher
*/
public class MouseBindingMouseEventDispatcher {
private static MouseBindingMouseEventDispatcher instance;
static synchronized void install() {
if (instance == null) {
instance = new MouseBindingMouseEventDispatcher();
}
}
/**
* Provides the current focus owner. This allows for dependency injection.
*/
private FocusOwnerProvider focusProvider = new DefaultFocusOwnerProvider();
/**
* We use this action as a signal that we intend to process a mouse binding and that no other
* Java component should try to handle it.
* <p>
* This action is one that is triggered by a mouse pressed, but will be processed on a
* mouse released. We do this to ensure that we consume all related mouse events (press and
* release) and to be consistent with the {@link KeyBindingOverrideKeyEventDispatcher}.
*/
private PendingActionInfo inProgressAction;
private MouseBindingMouseEventDispatcher() {
// Note: see the documentation on addAWTEventListener() for limitations of using this
// listener mechanism
Toolkit toolkit = Toolkit.getDefaultToolkit();
AWTEventListener listener = new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent event) {
process((MouseEvent) event);
}
};
toolkit.addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK);
}
private void process(MouseEvent e) {
int id = e.getID();
if (id == MouseEvent.MOUSE_ENTERED || id == MouseEvent.MOUSE_EXITED) {
return;
}
// always let the application finish processing key events that it started
if (actionInProgress(e)) {
return;
}
MouseBinding mouseBinding = MouseBinding.getMouseBinding(e);
DockingMouseBindingAction action = getDockingKeyBindingActionForEvent(mouseBinding);
Msg.trace(this,
new ParameterizedMessage("Mouse binding to action: {} to {}", mouseBinding, action));
if (action == null) {
return;
}
inProgressAction = new PendingActionInfo(action, mouseBinding);
e.consume();
}
/**
* Used to clear the flag that signals we are in the middle of processing a Ghidra action.
*/
private boolean actionInProgress(MouseEvent e) {
if (inProgressAction == null) {
Msg.trace(this, "No mouse binding action in progress");
return false;
}
// Note: mouse buttons can be simultaneously clicked. This means that the order of pressed
// and released events may arrive intermixed. To handle this correctly, we allow the
// MouseBinding to check for the matching release event.
MouseBinding mouseBinding = inProgressAction.mouseBinding();
boolean isMatching = mouseBinding.isMatchingRelease(e);
Msg.trace(this,
new ParameterizedMessage(
"Is release event for in-progress mouse binding action? {} for {}", isMatching,
inProgressAction.action()));
if (isMatching) {
DockingMouseBindingAction action = inProgressAction.action();
inProgressAction = null;
String command = null;
Object source = e.getSource();
long when = e.getWhen();
int modifiers = e.getModifiersEx();
action.actionPerformed(
new ActionEvent(source, ActionEvent.ACTION_PERFORMED, command, when, modifiers));
}
e.consume();
return true;
}
private DockingMouseBindingAction getDockingKeyBindingActionForEvent(
MouseBinding mouseBinding) {
DockingWindowManager activeManager = getActiveDockingWindowManager();
if (activeManager == null) {
return null;
}
DockingMouseBindingAction bindingAction =
(DockingMouseBindingAction) activeManager.getActionForMouseBinding(mouseBinding);
return bindingAction;
}
private DockingWindowManager getActiveDockingWindowManager() {
// we need an active window to process events
Window activeWindow = focusProvider.getActiveWindow();
if (activeWindow == null) {
return null;
}
DockingWindowManager activeManager = DockingWindowManager.getActiveInstance();
if (activeManager == null) {
// this can happen if clients use DockingWindows Look and Feel settings or
// DockingWindows widgets without using the DockingWindows system (like in tests or
// in stand-alone, non-Ghidra apps).
return null;
}
DockingWindowManager managingInstance = getDockingWindowManagerForWindow(activeWindow);
if (managingInstance != null) {
return managingInstance;
}
// this is a case where the current window is unaffiliated with a DockingWindowManager,
// like a JavaHelp window
return activeManager;
}
private static DockingWindowManager getDockingWindowManagerForWindow(Window activeWindow) {
DockingWindowManager manager = DockingWindowManager.getInstance(activeWindow);
if (manager != null) {
return manager;
}
if (activeWindow instanceof DockingDialog) {
DockingDialog dockingDialog = (DockingDialog) activeWindow;
return dockingDialog.getOwningWindowManager();
}
return null;
}
private record PendingActionInfo(DockingMouseBindingAction action, MouseBinding mouseBinding) {
//
}
}

View file

@ -0,0 +1,137 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking;
import java.awt.event.*;
import java.util.Objects;
import java.util.function.Consumer;
import docking.widgets.textfield.HintTextField;
import gui.event.MouseBinding;
public class MouseEntryTextField extends HintTextField {
private static final String HINT = "Press a mouse button";
private String disabledHint = HINT;
private MouseBinding mouseBinding;
private Consumer<MouseBinding> listener;
public MouseEntryTextField(int columns, Consumer<MouseBinding> listener) {
super(HINT);
setColumns(columns);
setName("Mouse Entry Text Field");
getAccessibleContext().setAccessibleName(getName());
this.listener = Objects.requireNonNull(listener);
addMouseListener(new MyMouseListener());
addKeyListener(new MyKeyListener());
}
@Override
public void setEnabled(boolean enabled) {
setHint(enabled ? HINT : disabledHint);
super.setEnabled(enabled);
}
/**
* Sets the hint text that will be displayed when this field is disabled
* @param disabledHint the hint text
*/
public void setDisabledHint(String disabledHint) {
this.disabledHint = Objects.requireNonNull(disabledHint);
}
public MouseBinding getMouseBinding() {
return mouseBinding;
}
public void setMouseBinding(MouseBinding mb) {
processMouseBinding(mb, false);
}
public void clearField() {
processMouseBinding(null, false);
}
private void processMouseBinding(MouseBinding mb, boolean notify) {
this.mouseBinding = mb;
if (mouseBinding == null) {
setText("");
}
else {
setText(mouseBinding.getDisplayText());
}
if (notify) {
listener.accept(mb);
}
}
private class MyMouseListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (!MouseEntryTextField.this.isEnabled()) {
return;
}
int modifiersEx = e.getModifiersEx();
int button = e.getButton();
processMouseBinding(new MouseBinding(button, modifiersEx), true);
e.consume();
}
@Override
public void mouseReleased(MouseEvent e) {
e.consume();
}
@Override
public void mouseClicked(MouseEvent e) {
e.consume();
}
}
private class MyKeyListener implements KeyListener {
@Override
public void keyTyped(KeyEvent e) {
e.consume();
}
@Override
public void keyReleased(KeyEvent e) {
e.consume();
}
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (isClearKey(keyCode)) {
processMouseBinding(null, true);
}
e.consume();
}
private boolean isClearKey(int keyCode) {
return keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER;
}
}
}

View file

@ -30,6 +30,7 @@ import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException;
import gui.event.MouseBinding;
import resources.ResourceManager;
import utilities.util.reflection.ReflectionUtilities;
@ -323,6 +324,10 @@ public abstract class DockingAction implements DockingActionIf {
return menuItem;
}
private MouseBinding getMouseBinding() {
return keyBindingData == null ? null : keyBindingData.getMouseBinding();
}
@Override
public KeyBindingType getKeyBindingType() {
return keyBindingType;
@ -383,6 +388,10 @@ public abstract class DockingAction implements DockingActionIf {
@Override
public void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData) {
if (Objects.equals(keyBindingData, newKeyBindingData)) {
return;
}
KeyBindingData oldData = keyBindingData;
keyBindingData = newKeyBindingData;
firePropertyChanged(KEYBINDING_DATA_PROPERTY, oldData, keyBindingData);
@ -492,8 +501,8 @@ public abstract class DockingAction implements DockingActionIf {
// menu path
if (menuBarData != null) {
buffer.append(" MENU PATH: ")
.append(menuBarData.getMenuPathAsString());
buffer.append(" MENU PATH: ").append(
menuBarData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" MENU GROUP: ").append(menuBarData.getMenuGroup());
buffer.append('\n');
@ -519,8 +528,8 @@ public abstract class DockingAction implements DockingActionIf {
// popup menu path
if (popupMenuData != null) {
buffer.append(" POPUP PATH: ")
.append(popupMenuData.getMenuPathAsString());
buffer.append(" POPUP PATH: ").append(
popupMenuData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" POPUP GROUP: ").append(popupMenuData.getMenuGroup());
buffer.append('\n');
@ -569,10 +578,15 @@ public abstract class DockingAction implements DockingActionIf {
KeyStroke keyStroke = getKeyBinding();
if (keyStroke != null) {
buffer.append(" KEYBINDING: ").append(keyStroke.toString());
buffer.append(" KEYBINDING: ").append(keyStroke);
buffer.append('\n');
}
MouseBinding mouseBinding = getMouseBinding();
if (mouseBinding != null) {
buffer.append(" MOUSE BINDING: ").append(mouseBinding);
}
String inception = getInceptionInformation();
if (inception != null) {
buffer.append("\n \n");

View file

@ -15,21 +15,27 @@
*/
package docking.action;
import java.util.Objects;
import javax.swing.KeyStroke;
import docking.KeyBindingPrecedence;
import docking.actions.KeyBindingUtils;
import ghidra.framework.options.ActionTrigger;
import gui.event.MouseBinding;
/**
* An object that contains a key stroke and the precedence for when that key stroke should be used.
*
* <p>Note: this class creates key strokes that work on key {@code pressed}. This effectively
* A class for storing an action's key stroke, mouse binding or both.
* <p>
* Note: this class creates key strokes that work on key {@code pressed}. This effectively
* normalizes all client key bindings to work on the same type of key stroke (pressed, typed or
* released).
*/
public class KeyBindingData {
private KeyStroke keyStroke;
private KeyBindingPrecedence keyBindingPrecedence;
private KeyBindingPrecedence keyBindingPrecedence = KeyBindingPrecedence.DefaultLevel;
private MouseBinding mouseBinding;
public KeyBindingData(KeyStroke keyStroke) {
this(keyStroke, KeyBindingPrecedence.DefaultLevel);
@ -43,17 +49,44 @@ public class KeyBindingData {
this(KeyStroke.getKeyStroke(keyCode, modifiers));
}
/**
* Constructs an instance of this class that uses a mouse binding instead of a key stroke.
* @param mouseBinding the mouse binding.
*/
public KeyBindingData(MouseBinding mouseBinding) {
this.mouseBinding = Objects.requireNonNull(mouseBinding);
}
/**
* Creates a key stroke from the given text. See
* {@link KeyBindingUtils#parseKeyStroke(KeyStroke)}. The key stroke created for this class
* will always be a key {@code pressed} key stroke.
*
*
* @param keyStrokeString the key stroke string to parse
*/
public KeyBindingData(String keyStrokeString) {
this(parseKeyStrokeString(keyStrokeString));
}
/**
* Creates a key binding data with the given action trigger.
* @param actionTrigger the trigger; may not be null
*/
public KeyBindingData(ActionTrigger actionTrigger) {
Objects.requireNonNull(actionTrigger);
this.keyStroke = actionTrigger.getKeyStroke();
this.mouseBinding = actionTrigger.getMouseBinding();
}
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
throw new IllegalArgumentException(
"Can't set precedence to System KeyBindingPrecedence");
}
this.keyStroke = Objects.requireNonNull(keyStroke);
this.keyBindingPrecedence = Objects.requireNonNull(precedence);
}
private static KeyStroke parseKeyStrokeString(String keyStrokeString) {
KeyStroke keyStroke = KeyBindingUtils.parseKeyStroke(keyStrokeString);
if (keyStroke == null) {
@ -62,13 +95,33 @@ public class KeyBindingData {
return keyStroke;
}
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
throw new IllegalArgumentException(
"Can't set precedence to System KeyBindingPrecedence");
/**
* Returns a key binding data object that matches the given trigger. If the existing key
* binding object already matches the new trigger, then the existing key binding data is
* returned. If the new trigger is null, the null will be returned.
*
* @param kbData the existing key binding data; my be null
* @param newTrigger the new action trigger; may be null
* @return a key binding data based on the new action trigger; may be null
*/
public static KeyBindingData update(KeyBindingData kbData, ActionTrigger newTrigger) {
if (kbData == null) {
if (newTrigger == null) {
return null; // no change
}
return new KeyBindingData(newTrigger); // trigger added
}
this.keyStroke = keyStroke;
this.keyBindingPrecedence = precedence;
if (newTrigger == null) {
return null; // trigger has been cleared
}
ActionTrigger existingTrigger = kbData.getActionTrigger();
if (existingTrigger.equals(newTrigger)) {
return kbData;
}
return new KeyBindingData(newTrigger);
}
/**
@ -87,10 +140,56 @@ public class KeyBindingData {
return keyBindingPrecedence;
}
/**
* Returns the mouse binding assigned to this key binding data.
* @return the mouse binding; may be null
*/
public MouseBinding getMouseBinding() {
return mouseBinding;
}
/**
* Creates a new action trigger with the values of this class
* @return the action trigger
*/
public ActionTrigger getActionTrigger() {
return new ActionTrigger(keyStroke, mouseBinding);
}
@Override
public String toString() {
return getClass().getSimpleName() + "[KeyStroke=" + keyStroke + ", precedence=" +
keyBindingPrecedence + "]";
keyBindingPrecedence + ", MouseBinding=" + mouseBinding + "]";
}
@Override
public int hashCode() {
return Objects.hash(keyBindingPrecedence, keyStroke, mouseBinding);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
KeyBindingData other = (KeyBindingData) obj;
if (keyBindingPrecedence != other.keyBindingPrecedence) {
return false;
}
if (!Objects.equals(keyStroke, other.keyStroke)) {
return false;
}
if (!Objects.equals(mouseBinding, other.mouseBinding)) {
return false;
}
return true;
}
static KeyBindingData createSystemKeyBindingData(KeyStroke keyStroke) {
@ -118,8 +217,15 @@ public class KeyBindingData {
KeyBindingPrecedence precedence = newKeyBindingData.getKeyBindingPrecedence();
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
return createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
KeyBindingData kbd =
createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
kbd.mouseBinding = newKeyBindingData.mouseBinding;
return kbd;
}
return new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
KeyBindingData kbd =
new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
kbd.mouseBinding = newKeyBindingData.mouseBinding;
return kbd;
}
}

View file

@ -27,11 +27,12 @@ import docking.*;
import docking.actions.KeyBindingUtils;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;
import gui.event.MouseBinding;
/**
* A class that organizes system key bindings by mapping them to assigned {@link DockingActionIf}s.
*
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
*
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
* class knows how to map a single key binding to multiple actions.
*/
public class KeyBindingsManager implements PropertyChangeListener {
@ -39,8 +40,9 @@ public class KeyBindingsManager implements PropertyChangeListener {
// this map exists to update the MultiKeyBindingAction when the key binding changes
private Map<DockingActionIf, ComponentProvider> actionToProviderMap = new HashMap<>();
private Map<KeyStroke, DockingKeyBindingAction> dockingKeyMap = new HashMap<>();
private Map<DockingActionIf, SystemKeyBindingAction> dockingActionToSystemActionMap =
new HashMap<>();
private Map<MouseBinding, DockingMouseBindingAction> dockingMouseMap = new HashMap<>();
private Map<String, DockingActionIf> systemActionsByFullName = new HashMap<>();
private Tool tool;
public KeyBindingsManager(Tool tool) {
@ -53,11 +55,20 @@ public class KeyBindingsManager implements PropertyChangeListener {
actionToProviderMap.put(action, optionalProvider);
}
KeyStroke keyBinding = action.getKeyBinding();
KeyBindingData kbData = action.getKeyBindingData();
if (kbData == null) {
return;
}
KeyStroke keyBinding = kbData.getKeyBinding();
if (keyBinding != null) {
addKeyBinding(optionalProvider, action, keyBinding);
}
MouseBinding mouseBinding = kbData.getMouseBinding();
if (mouseBinding != null) {
doAddMouseBinding(action, mouseBinding);
}
}
public void addSystemAction(DockingActionIf action) {
@ -90,7 +101,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
return;
}
// map standard keystroke to action
// map standard keystroke to action
doAddKeyBinding(provider, action, keyStroke);
// map workaround keystroke to action
@ -103,7 +114,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
return null; // clearing the key stroke
}
//
//
// 1) Handle case with given key stroke already in use by a system action
//
Action existingAction = dockingKeyMap.get(ks);
@ -118,12 +129,16 @@ public class KeyBindingsManager implements PropertyChangeListener {
return ksString + " in use by System action '" + systemDockingAction.getName() + "'";
}
//
// 2) Handle the case where a system action key stroke is being set to something that is
if (dockingAction == null) {
return null; // the client is only checking the keystroke and not any associated action
}
//
// 2) Handle the case where a system action key stroke is being set to something that is
// already in-use by some other action
//
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(dockingAction);
if (systemAction != null && existingAction != null) {
//
boolean hasSystemAction = systemActionsByFullName.containsKey(dockingAction.getFullName());
if (hasSystemAction && existingAction != null) {
return "System action cannot be set to in-use key stroke";
}
@ -133,12 +148,12 @@ public class KeyBindingsManager implements PropertyChangeListener {
private void fixupAltGraphKeyStrokeMapping(ComponentProvider provider, DockingActionIf action,
KeyStroke keyStroke) {
// special case
// special case
int modifiers = keyStroke.getModifiers();
if ((modifiers & InputEvent.ALT_DOWN_MASK) == InputEvent.ALT_DOWN_MASK) {
//
// Also register the 'Alt' binding with the 'Alt Graph' mask. This fixes the but
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
// that have different key codes for the left and right Alt keys.
//
modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK;
@ -170,8 +185,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
return;
}
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(action);
if (systemAction != null) {
if (systemActionsByFullName.containsKey(action.getFullName())) {
// the user has updated the binding for a System action; re-install it
registerSystemKeyBinding(action, mappingKeyStroke);
return;
@ -182,6 +196,23 @@ public class KeyBindingsManager implements PropertyChangeListener {
new MultipleKeyAction(tool, provider, action, actionKeyStroke));
}
private void doAddMouseBinding(DockingActionIf action, MouseBinding mouseBinding) {
DockingMouseBindingAction mouseBindingAction = dockingMouseMap.get(mouseBinding);
if (mouseBindingAction != null) {
String existingName = mouseBindingAction.getFullActionName();
String message = """
Attempted to use the same mouse binding for multiple actions. \
Multiple mouse bindings are not supported. Binding: %s \
New action: %s; existing action: %s
""".formatted(mouseBinding, action.getFullName(), existingName);
Msg.error(this, message);
return;
}
dockingMouseMap.put(mouseBinding, new DockingMouseBindingAction(action, mouseBinding));
}
private void addSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
KeyBindingData binding = KeyBindingData.createSystemKeyBindingData(keyStroke);
action.setKeyBindingData(binding);
@ -191,7 +222,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
private void registerSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
SystemKeyBindingAction systemAction = new SystemKeyBindingAction(tool, action, keyStroke);
dockingKeyMap.put(keyStroke, systemAction);
dockingActionToSystemActionMap.put(action, systemAction);
systemActionsByFullName.put(action.getFullName(), action);
}
private void removeKeyBinding(KeyStroke keyStroke, DockingActionIf action) {
@ -242,17 +273,30 @@ public class KeyBindingsManager implements PropertyChangeListener {
}
}
public Action getDockingKeyAction(KeyStroke keyStroke) {
public Action getDockingAction(KeyStroke keyStroke) {
return dockingKeyMap.get(keyStroke);
}
public Action getDockingAction(MouseBinding mouseBinding) {
return dockingMouseMap.get(mouseBinding);
}
public boolean isSystemAction(DockingActionIf action) {
return systemActionsByFullName.containsKey(action.getFullName());
}
public DockingActionIf getSystemAction(String fullName) {
return systemActionsByFullName.get(fullName);
}
public Set<DockingActionIf> getSystemActions() {
return new HashSet<>(dockingActionToSystemActionMap.keySet());
return new HashSet<>(systemActionsByFullName.values());
}
public void dispose() {
dockingKeyMap.clear();
dockingMouseMap.clear();
actionToProviderMap.clear();
dockingActionToSystemActionMap.clear();
systemActionsByFullName.clear();
}
}

View file

@ -46,13 +46,12 @@ import ghidra.util.filechooser.GhidraFileFilter;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
import util.CollectionUtils;
import utilities.util.reflection.ReflectionUtilities;
/**
* A class to provide utilities for system key bindings, such as importing and
* exporting key binding configurations.
*
*
*
*
* @since Tracker Id 329
*/
public class KeyBindingUtils {
@ -100,7 +99,7 @@ public class KeyBindingUtils {
* <p>
* If there is a problem reading the data then the user will be shown an
* error dialog.
*
*
* @param inputStream the input stream from which to read options
* @return An options object that is composed of key binding names and their
* associated keystrokes.
@ -141,7 +140,7 @@ public class KeyBindingUtils {
* <p>
* If there is a problem writing the data then the user will be shown an
* error dialog.
*
*
* @param keyBindingOptions The options that contains key binding data.
*/
public static void exportKeyBindings(ToolOptions keyBindingOptions) {
@ -177,14 +176,14 @@ public class KeyBindingUtils {
* Changes the given key event to the new source component and then dispatches that event.
* This method is intended for clients that wish to effectively take a key event given to
* one component and give it to another component.
*
*
* <p>This method exists to deal with the complicated nature of key event processing and
* how our (not Java's) framework processes key event bindings to trigger actions. If not
* for our special processing of action key bindings, then this method would not be
* necessary.
*
*
* <p><b>This is seldom-used code; if you don't know when to use this code, then don't.</b>
*
*
* @param newSource the new target of the event
* @param e the existing event
*/
@ -199,7 +198,7 @@ public class KeyBindingUtils {
/*
Unusual Code Alert!
The KeyboardFocusManager is a complicated beast. Here we use knowledge of one such
complication to correctly route key events. If the client of this method passes
a component whose 'isShowing()' returns false, then the manager will not send the
@ -208,13 +207,13 @@ public class KeyBindingUtils {
attached; for example, when we are using said components with a renderer to perform
our own painting. In the case of non-attached components, we must call the
redispatchEvent() method ourselves.
Why don't we just always call redispatchEvent()? Well, that
method will not pass the new cloned event we just created back through the full
key event pipeline. This means that tool-level (our Tool API, not Java)
actions will not work, as tool-level actions are handled at the beginning of the
key event pipeline, not by the components themselves.
Also, we have here guilty knowledge that the aforementioned tool-level key processing
will check to see if the event was consumed. If consumed, then no further processing
will happen; if not consumed, then the framework will continue to process the event
@ -245,7 +244,7 @@ public class KeyBindingUtils {
* <p>
* The given action must have a keystroke assigned, or this method will do
* nothing.
*
*
* @param component the component to which the given action will be bound
* @param action the action to bind
*/
@ -263,12 +262,12 @@ public class KeyBindingUtils {
* <p>
* The given action must have a keystroke assigned, or this method will do
* nothing.
*
*
* <p>
* A typical use-case is to register an existing docking action with a text
* component, which is needed because the docking key event processing will
* not execute docking- registered actions if a text component has focus.
*
*
* @param component the component to which the given action will be bound
* @param action the action to bind
* @param contextProvider the provider of the context
@ -289,12 +288,12 @@ public class KeyBindingUtils {
* <p>
* The given action must have a keystroke assigned, or this method will do
* nothing.
*
*
* <p>
* A typical use-case is to register an existing docking action with a text
* component, which is needed because the docking key event processing will
* not execute docking- registered actions if a text component has focus.
*
*
* @param component the component to which the given action will be bound
* @param action the action to bind
* @param contextProvider the provider of the context
@ -311,7 +310,7 @@ public class KeyBindingUtils {
/**
* Registers the given action with the given key binding on the given
* component.
*
*
* @param component the component to which the action will be registered
* @param keyStroke the keystroke for to which the action will be bound
* @param action the action to execute when the given keystroke is triggered
@ -353,7 +352,7 @@ public class KeyBindingUtils {
* action with the same key binding from firing. This is useful when your
* application is using tool-level key bindings that share the same
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
*
*
* @param component the component for which to clear the key binding
* @param action the action from which to get the key binding
*/
@ -373,7 +372,7 @@ public class KeyBindingUtils {
* Note: this method clears the key binding for the
* {@link JComponent#WHEN_FOCUSED} and
* {@link JComponent#WHEN_ANCESTOR_OF_FOCUSED_COMPONENT} focus conditions.
*
*
* @param component the component for which to clear the key binding
* @param keyStroke the keystroke of the binding to be cleared
* @see #clearKeyBinding(JComponent, KeyStroke, int)
@ -387,7 +386,7 @@ public class KeyBindingUtils {
* Allows clients to clear Java key bindings. This is useful when your
* application is using tool-level key bindings that share the same
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
*
*
* @param component the component for which to clear the key binding
* @param keyStroke the keystroke of the binding to be cleared
* @param focusCondition the particular focus condition under which the
@ -405,7 +404,7 @@ public class KeyBindingUtils {
/**
* Clears the currently assigned Java key binding for the action by the given name. This
* method will find the currently assigned key binding, if any, and then remove it.
*
*
* @param component the component for which to clear the key binding
* @param actionName the name of the action that should not have a key binding
* @see LookAndFeel
@ -420,9 +419,8 @@ public class KeyBindingUtils {
KeyStroke keyStroke = null;
KeyStroke[] keys = inputMap.allKeys();
if (keys == null) {
Msg.debug(KeyBindingUtils.class,
"Cannot remove action by name; does not exist: '" + actionName + "' " +
"on component: " + component.getClass().getSimpleName());
Msg.debug(KeyBindingUtils.class, "Cannot remove action by name; does not exist: '" +
actionName + "' " + "on component: " + component.getClass().getSimpleName());
return;
}
@ -442,7 +440,7 @@ public class KeyBindingUtils {
/**
* Returns the registered action for the given keystroke, or null of no
* action is bound to that keystroke.
*
*
* @param component the component for which to check the binding
* @param keyStroke the keystroke for which to find a bound action
* @param focusCondition the focus condition under which to check for the
@ -464,12 +462,12 @@ public class KeyBindingUtils {
/**
* A utility method to get all key binding actions. This method will
* only return actions that support {@link KeyBindingType key bindings}.
*
*
* <p>The mapping returned provides a list of items because it is possible for there to
* exists multiple actions with the same name and owner. (This can happen when multiple copies
* of a component provider are shown, each with their own set of actions that share the
* same name.)
*
*
* @param tool the tool containing the actions
* @return the actions mapped by their full name (e.g., 'Name (OwnerName)')
*/
@ -496,13 +494,12 @@ public class KeyBindingUtils {
* A utility method to get all key binding actions that have the given owner.
* This method will remove duplicate actions and will only return actions
* that support {@link KeyBindingType key bindings}.
*
*
* @param tool the tool containing the actions
* @param owner the action owner name
* @return the actions
*/
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool,
String owner) {
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool, String owner) {
Map<String, DockingActionIf> deduper = new HashMap<>();
Set<DockingActionIf> actions = tool.getDockingActionsByOwnerName(owner);
@ -522,7 +519,7 @@ public class KeyBindingUtils {
/**
* Returns all actions that match the given owner and name
*
*
* @param allActions the universe of actions
* @param owner the owner
* @param name the name
@ -539,13 +536,13 @@ public class KeyBindingUtils {
/**
* Takes the existing docking action and allows it to be registered with
* Swing components
*
*
* <p>
* The new action will not be correctly wired into the Docking Action
* Context system. This means that the given docking action should not rely
* on {@link DockingAction#isEnabledForContext(docking.ActionContext)} to
* work when called from the Swing widget.
*
*
* @param action the docking action to adapt to a Swing {@link Action}
* @return the new action
*/
@ -553,66 +550,10 @@ public class KeyBindingUtils {
return new ActionAdapter(action);
}
/**
* Checks each action in the given collection against the given new action to make sure that
* they share the same default key binding.
*
* @param newAction the action to check
* @param existingActions the actions that have already been checked
*/
public static void assertSameDefaultKeyBindings(DockingActionIf newAction,
Collection<DockingActionIf> existingActions) {
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
return;
}
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
KeyStroke defaultKs = getKeyStroke(newDefaultBinding);
for (DockingActionIf action : existingActions) {
if (!action.getKeyBindingType().supportsKeyBindings()) {
continue;
}
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
KeyStroke existingKs = getKeyStroke(existingDefaultBinding);
if (!Objects.equals(defaultKs, existingKs)) {
logDifferentKeyBindingsWarnigMessage(newAction, action, existingKs);
break; // one warning seems like enough
}
}
}
/**
* Logs a warning message for the two given actions to signal that they do not share the
* same default key binding
*
* @param newAction the new action
* @param existingAction the action that has already been validated
* @param existingDefaultKs the current validated key stroke
*/
public static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
DockingActionIf existingAction, KeyStroke existingDefaultKs) {
//@formatter:off
String s = "Shared Key Binding Actions have different default values. These " +
"must be the same." +
"\n\tAction name: '"+existingAction.getName()+"'" +
"\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(KeyBindingUtils.class, s, ReflectionUtilities.createJavaFilteredThrowable());
}
/**
* Updates the given data with system-independent versions of key modifiers. For example,
* the <code>control</code> key will be converted to the <code>command</code> key on the Mac.
*
*
* @param keyStroke the keystroke to validate
* @return the potentially changed keystroke
*/
@ -676,7 +617,7 @@ public class KeyBindingUtils {
* and we want it to look like: "Ctrl-M".
* <br>In Java 11 we have seen toString() values get printed with repeated text, such
* as: "shift ctrl pressed SHIFT". We want to trim off the repeated modifiers.
*
*
* @param keyStroke the key stroke
* @return the string value; the empty string if the key stroke is null
*/
@ -792,14 +733,18 @@ public class KeyBindingUtils {
* Ctrl-Alt-Z
* ctrl Z
* </pre>
*
*
* <p><b>Note:</b> The returned keystroke will always correspond to a {@code pressed} event,
* regardless of the value passed in (pressed, typed or released).
*
*
* @param keyStroke the key stroke
* @return the new key stroke (as returned by {@link KeyStroke#getKeyStroke(String)}
*/
public static KeyStroke parseKeyStroke(String keyStroke) {
if (StringUtils.isBlank(keyStroke)) {
return null;
}
List<String> pieces = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(keyStroke, "- ");
while (tokenizer.hasMoreTokens()) {
@ -873,13 +818,6 @@ public class KeyBindingUtils {
return !action.getKeyBindingType().isManaged();
}
private static KeyStroke getKeyStroke(KeyBindingData data) {
if (data == null) {
return null;
}
return data.getKeyBinding();
}
// prompts the user for a file location from which to read key binding data
private static InputStream getInputStreamForFile(File startingDir) {
File selectedFile = getFileFromUser(startingDir);

View file

@ -24,35 +24,40 @@ import docking.Tool;
import docking.action.DockingActionIf;
import docking.action.KeyBindingData;
import docking.tool.util.DockingToolConstants;
import ghidra.framework.options.ActionTrigger;
import ghidra.framework.options.ToolOptions;
import gui.event.MouseBinding;
import util.CollectionUtils;
/**
* An object that maps actions to key strokes.
* An object that maps actions to key strokes and mouse bindings.
* <p>
* This class knows how to load all system actions and how to load any key bindings for those
* actions from the tool's options. Clients can make changes to the state of this class that can
* then be applied to the system by calling {@link #applyChanges()}.
* This class knows how to load all system actions and how to load any key and mouse bindings for
* those actions from the tool's options. Clients can make changes to the state of this class that
* can then be applied to the system by calling {@link #applyChanges()}.
*/
public class KeyBindings {
private Tool tool;
private ToolOptions keyBindingOptions;
private Map<String, List<DockingActionIf>> actionsByFullName;
private Map<String, List<String>> actionNamesByKeyStroke = new HashMap<>();
private Map<String, KeyStroke> keyStrokesByFullName = new HashMap<>();
// allows clients to populate a table of all actions
private List<DockingActionIf> uniqueActions = new ArrayList<>();
// to know what has been changed
private Map<String, KeyStroke> originalKeyStrokesByFullName = new HashMap<>();
private String longestActionName = "";
// allows clients to know if a given key stroke or mouse binding is in use
private Map<KeyStroke, List<String>> actionNamesByKeyStroke = new HashMap<>();
private Map<MouseBinding, String> actionNameByMouseBinding = new HashMap<>();
private ToolOptions options;
// tracks all changes to an action's key stroke and mouse bindings, which allows us to apply
// and restore options values
private Map<String, ActionKeyBindingState> actionInfoByFullName = new HashMap<>();
private String longestActionName = "";
public KeyBindings(Tool tool) {
this.tool = tool;
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
init();
}
@ -61,22 +66,40 @@ public class KeyBindings {
return Collections.unmodifiableList(uniqueActions);
}
/* used for testing */
public Map<String, KeyStroke> getKeyStrokesByFullActionName() {
return Collections.unmodifiableMap(keyStrokesByFullName);
Map<String, KeyStroke> result = new HashMap<>();
Set<Entry<String, ActionKeyBindingState>> entries = actionInfoByFullName.entrySet();
for (Entry<String, ActionKeyBindingState> entry : entries) {
String key = entry.getKey();
KeyStroke value = entry.getValue().getCurrentKeyStroke();
result.put(key, value);
}
return result;
}
public boolean containsAction(String fullName) {
return actionsByFullName.containsKey(fullName);
return actionInfoByFullName.containsKey(fullName);
}
public KeyStroke getKeyStroke(String fullName) {
return keyStrokesByFullName.get(fullName);
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
return info.getCurrentKeyStroke();
}
public String getActionsForKeyStrokeText(String keyStrokeText) {
public MouseBinding getMouseBinding(String fullName) {
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
return info.getCurrentMouseBinding();
}
public String getActionForMouseBinding(MouseBinding mouseBinding) {
return actionNameByMouseBinding.get(mouseBinding);
}
public String getActionsForKeyStrokeText(KeyStroke keyStroke) {
StringBuffer sb = new StringBuffer();
List<String> names = actionNamesByKeyStroke.get(keyStrokeText);
List<String> names = actionNamesByKeyStroke.get(keyStroke);
if (CollectionUtils.isBlank(names)) {
return sb.toString();
}
@ -85,14 +108,16 @@ public class KeyBindings {
return n1.compareToIgnoreCase(n2);
});
sb.append("Actions mapped to key " + keyStrokeText + ":\n");
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
sb.append("Actions mapped to key " + ksName + ":\n");
for (int i = 0; i < names.size(); i++) {
sb.append(" ");
String name = names.get(i);
List<DockingActionIf> actions = actionsByFullName.get(name);
DockingActionIf action = actions.get(0);
sb.append(action.getName());
ActionKeyBindingState info = actionInfoByFullName.get(name);
DockingActionIf action = info.getRepresentativeAction();
String shortName = action.getName();
sb.append(shortName);
sb.append(" (").append(action.getOwnerDescription()).append(')');
if (i < names.size() - 1) {
sb.append("\n");
@ -105,58 +130,79 @@ public class KeyBindings {
return longestActionName;
}
public boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
public boolean isMouseBindingInUse(String fullName, MouseBinding newBinding) {
// remove old keystroke for action name
KeyStroke oldKs = keyStrokesByFullName.get(actionName);
if (oldKs != null) {
String oldName = KeyBindingUtils.parseKeyStroke(oldKs);
if (oldName.equals(ksName)) {
String existingName = actionNameByMouseBinding.get(newBinding);
if (existingName == null || newBinding == null) {
return false; // no new binding, or not in use
}
return !Objects.equals(existingName, fullName);
}
public boolean setActionMouseBinding(String fullName, MouseBinding newBinding) {
MouseBinding currentBinding = getMouseBinding(fullName);
if (currentBinding != null) {
if (currentBinding.equals(newBinding)) {
return false;
}
removeFromKeyMap(oldKs, actionName);
}
addActionKeyStroke(keyStroke, actionName);
keyStrokesByFullName.put(actionName, keyStroke);
actionNameByMouseBinding.remove(currentBinding);
}
if (newBinding != null) {
actionNameByMouseBinding.put(newBinding, fullName);
}
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
info.setCurrentMouseBinding(newBinding);
return true;
}
public boolean removeKeyStroke(String actionName) {
if (keyStrokesByFullName.containsKey(actionName)) {
KeyStroke stroke = keyStrokesByFullName.get(actionName);
if (stroke == null) {
// nothing to remove; nothing has changed
public boolean setActionKeyStroke(String fullName, KeyStroke newKs) {
String newKsName = KeyBindingUtils.parseKeyStroke(newKs);
// remove old keystroke for action name
KeyStroke currentKs = getKeyStroke(fullName);
if (currentKs != null) {
String currentName = KeyBindingUtils.parseKeyStroke(currentKs);
if (currentName.equals(newKsName)) {
return false;
}
removeFromKeyMap(stroke, actionName);
keyStrokesByFullName.put(actionName, null);
return true;
removeFromKeyMap(fullName, currentKs);
}
return false;
addActionKeyStroke(fullName, newKs);
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
info.setCurrentKeyStroke(newKs);
return true;
}
public boolean removeKeyStroke(String fullName) {
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
if (info == null) {
return false; // not sure if this can happen
}
KeyStroke currentKeyStroke = info.getCurrentKeyStroke();
if (currentKeyStroke == null) {
return false; // nothing to remove; nothing has changed
}
removeFromKeyMap(fullName, currentKeyStroke);
info.setCurrentKeyStroke(null);
return true;
}
/**
* Restores the tool options key bindings to the default values originally loaded when the
* Restores the tool options key bindings to the default values originally loaded when the
* system started.
*/
public void restoreOptions() {
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
for (Entry<String, List<DockingActionIf>> entry : entries) {
List<DockingActionIf> actions = entry.getValue();
// pick one action, they are all conceptually the same
DockingActionIf action = actions.get(0);
String actionName = entry.getKey();
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
KeyStroke newKeyStroke =
(defaultBinding == null) ? null : defaultBinding.getKeyBinding();
updateOptions(actionName, currentKeyStroke, newKeyStroke);
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
info.restore(keyBindingOptions);
}
}
@ -164,14 +210,8 @@ public class KeyBindings {
* Cancels any pending changes that have not yet been applied.
*/
public void cancelChanges() {
Iterator<String> iter = originalKeyStrokesByFullName.keySet().iterator();
while (iter.hasNext()) {
String actionName = iter.next();
KeyStroke originalKS = originalKeyStrokesByFullName.get(actionName);
KeyStroke modifiedKS = keyStrokesByFullName.get(actionName);
if (modifiedKS != null && !modifiedKS.equals(originalKS)) {
keyStrokesByFullName.put(actionName, originalKS);
}
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
info.cancelChanges();
}
}
@ -179,84 +219,203 @@ public class KeyBindings {
* Applies any pending changes.
*/
public void applyChanges() {
Iterator<String> iter = keyStrokesByFullName.keySet().iterator();
while (iter.hasNext()) {
String actionName = iter.next();
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
KeyStroke originalKeyStroke = originalKeyStrokesByFullName.get(actionName);
updateOptions(actionName, originalKeyStroke, currentKeyStroke);
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
info.apply(keyBindingOptions);
}
}
private void removeFromKeyMap(KeyStroke ks, String actionName) {
private void removeFromKeyMap(String actionName, KeyStroke ks) {
if (ks == null) {
return;
}
String ksName = KeyBindingUtils.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
List<String> list = actionNamesByKeyStroke.get(ks);
if (list != null) {
list.remove(actionName);
if (list.isEmpty()) {
actionNamesByKeyStroke.remove(ksName);
actionNamesByKeyStroke.remove(ks);
}
}
}
private void updateOptions(String fullActionName, KeyStroke currentKeyStroke,
KeyStroke newKeyStroke) {
if (Objects.equals(currentKeyStroke, newKeyStroke)) {
return;
}
options.setKeyStroke(fullActionName, newKeyStroke);
originalKeyStrokesByFullName.put(fullActionName, newKeyStroke);
keyStrokesByFullName.put(fullActionName, newKeyStroke);
List<DockingActionIf> actions = actionsByFullName.get(fullActionName);
for (DockingActionIf action : actions) {
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKeyStroke));
}
}
private void init() {
actionsByFullName = KeyBindingUtils.getAllActionsByFullName(tool);
actionInfoByFullName = new HashMap<>();
Map<String, List<DockingActionIf>> actionsByFullName =
KeyBindingUtils.getAllActionsByFullName(tool);
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
for (Entry<String, List<DockingActionIf>> entry : entries) {
// pick one action, they are all conceptually the same
List<DockingActionIf> actions = entry.getValue();
DockingActionIf action = actions.get(0);
uniqueActions.add(action);
String actionName = entry.getKey();
KeyStroke ks = options.getKeyStroke(actionName, null);
keyStrokesByFullName.put(actionName, ks);
addActionKeyStroke(ks, actionName);
originalKeyStrokesByFullName.put(actionName, ks);
String fullName = entry.getKey();
ActionTrigger trigger = keyBindingOptions.getActionTrigger(fullName, null);
String shortName = action.getName();
KeyStroke ks = null;
MouseBinding mb = null;
if (trigger != null) {
ks = trigger.getKeyStroke();
mb = trigger.getMouseBinding();
}
ActionKeyBindingState info = new ActionKeyBindingState(actions, ks, mb);
actionInfoByFullName.put(fullName, info);
uniqueActions.add(info.getRepresentativeAction());
addActionKeyStroke(fullName, ks);
String shortName = info.getShortName();
if (shortName.length() > longestActionName.length()) {
longestActionName = shortName;
}
}
}
private void addActionKeyStroke(KeyStroke ks, String actionName) {
private void addActionKeyStroke(String actionName, KeyStroke ks) {
if (ks == null) {
return;
}
String ksName = KeyBindingUtils.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
List<String> list = actionNamesByKeyStroke.get(ks);
if (list == null) {
list = new ArrayList<>();
actionNamesByKeyStroke.put(ksName, list);
actionNamesByKeyStroke.put(ks, list);
}
if (!list.contains(actionName)) {
list.add(actionName);
}
}
/**
* A class to store current and original values for key strokes and mouse bindings. This is
* used to apply changes and restore default values.
*/
private class ActionKeyBindingState {
private List<DockingActionIf> actions = new ArrayList<>();
private KeyStroke originalKeyStroke;
private KeyStroke currentKeyStroke;
private MouseBinding originalMouseBinding;
private MouseBinding currentMouseBinding;
ActionKeyBindingState(List<DockingActionIf> actions, KeyStroke ks, MouseBinding mb) {
this.actions.addAll(actions);
this.originalKeyStroke = ks;
this.currentKeyStroke = ks;
this.originalMouseBinding = mb;
this.currentMouseBinding = mb;
}
public DockingActionIf getRepresentativeAction() {
// pick one action, they are all conceptually the same
return actions.get(0);
}
String getShortName() {
// pick one action, they are all conceptually the same
return actions.get(0).getName();
}
String getFullName() {
return getRepresentativeAction().getFullName();
}
public MouseBinding getCurrentMouseBinding() {
return currentMouseBinding;
}
public void setCurrentMouseBinding(MouseBinding newMouseBinding) {
this.currentMouseBinding = newMouseBinding;
}
public KeyStroke getCurrentKeyStroke() {
return currentKeyStroke;
}
public void setCurrentKeyStroke(KeyStroke newKeyStroke) {
this.currentKeyStroke = newKeyStroke;
}
public void cancelChanges() {
currentKeyStroke = originalKeyStroke;
currentMouseBinding = originalMouseBinding;
}
public void apply(ToolOptions keyStrokeOptions) {
if (!hasChanged()) {
return;
}
KeyBindingData kbd = getCurrentKeyBindingData();
apply(keyStrokeOptions, kbd);
}
private void apply(ToolOptions keyStrokeOptions, KeyBindingData keyBinding) {
if (keyBinding == null) {
// no bindings; bindings have been cleared
for (DockingActionIf action : actions) {
action.setUnvalidatedKeyBindingData(null);
}
return;
}
ActionTrigger newTrigger = keyBinding.getActionTrigger();
String fullName = getFullName();
keyStrokeOptions.setActionTrigger(fullName, newTrigger);
}
private boolean hasChanged() {
return !Objects.equals(originalKeyStroke, currentKeyStroke) ||
!Objects.equals(originalMouseBinding, currentMouseBinding);
}
private boolean matches(KeyBindingData kbData) {
if (CollectionUtils.isAllNull(kbData, currentKeyStroke, currentMouseBinding)) {
return true;
}
if (kbData == null) {
return false;
}
KeyStroke otherKs = kbData.getKeyBinding();
if (!Objects.equals(otherKs, currentKeyStroke)) {
return false;
}
MouseBinding otherMb = kbData.getMouseBinding();
return Objects.equals(otherMb, currentMouseBinding);
}
private KeyBindingData getCurrentKeyBindingData() {
if (currentKeyStroke == null && currentMouseBinding == null) {
return null; // the key binding data does not exist or has been cleared
}
DockingActionIf action = getRepresentativeAction();
KeyBindingData kbData = action.getKeyBindingData();
ActionTrigger trigger = new ActionTrigger(currentKeyStroke, currentMouseBinding);
return KeyBindingData.update(kbData, trigger);
}
// restores the options to their default values
public void restore(ToolOptions options) {
DockingActionIf action = getRepresentativeAction();
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
if (!matches(defaultBinding)) {
apply(options, defaultBinding);
}
cancelChanges();
}
}
}

View file

@ -145,6 +145,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
*/
public void setKeyStroke(KeyStroke ks) {
keyEntryField.setKeyStroke(ks);
updateCollisionPane(ks);
}
@Override
@ -169,7 +170,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
return;
}
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
action.setUnvalidatedKeyBindingData(newKs == null ? null : new KeyBindingData(newKs));
close();
}
@ -192,8 +193,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
return;
}
String ksName = KeyBindingUtils.parseKeyStroke(ks);
String text = keyBindings.getActionsForKeyStrokeText(ksName);
String text = keyBindings.getActionsForKeyStrokeText(ks);
try {
doc.insertString(0, text, textAttrs);
collisionPane.setCaretPosition(0);

View file

@ -18,8 +18,6 @@ package docking.actions;
import java.util.*;
import java.util.Map.Entry;
import javax.swing.KeyStroke;
import org.apache.commons.collections4.Bag;
import org.apache.commons.collections4.bag.HashBag;
import org.apache.commons.lang3.StringUtils;
@ -28,8 +26,9 @@ import docking.ActionContext;
import docking.DockingWindowManager;
import docking.action.*;
import docking.tool.ToolConstants;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.options.*;
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
@ -63,7 +62,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
* 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 WeakHashMap<DockingActionIf, ActionTrigger> clientActions = new WeakHashMap<>();
private ToolOptions keyBindingOptions;
private Bag<String> actionOwners = new HashBag<>();
@ -73,11 +72,13 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
*
* @param name The name of the action--this will be displayed in the options as the name of
* key binding's action
* @param defaultKs the default key stroke for this stub. The key stroke will be validated
* each time an action is added to this stub to ensure that the defaults are in sync.
* @param defaultActionTrigger the default action trigger for this stub. The action trigger
* will be validated each time an action is added to this stub to ensure that the
* defaults are in sync.
* @param options the tool's key binding options
*/
SharedStubKeyBindingAction(String name, KeyStroke defaultKs, ToolOptions options) {
SharedStubKeyBindingAction(String name, ActionTrigger defaultActionTrigger,
ToolOptions options) {
// Note: we need to have this stub registered to use key bindings so that the options will
// restore the saved key binding to this class, which will then notify any of the
// shared actions using this stub.
@ -87,7 +88,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
// Dummy keybinding actions don't have help--the real action does
DockingWindowManager.getHelpService().excludeFromHelp(this);
setUnvalidatedKeyBindingData(new KeyBindingData(defaultKs));
setKeyBindingData(this, defaultActionTrigger);
// A listener to keep the shared, stub keybindings in sync with their clients
options.addOptionsChangeListener(this);
@ -119,7 +120,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
void addClientAction(DockingActionIf action) {
// 1) Validate new action keystroke against existing actions
KeyStroke defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
ActionTrigger defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
// 2) Add the action and the validated keystroke, as this is the default keystroke
clientActions.put(action, defaultKs);
@ -159,61 +160,69 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
return super.getDescription();
}
private KeyStroke validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
private ActionTrigger validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
// this value may be null
KeyBindingData defaultBinding = newAction.getDefaultKeyBindingData();
KeyStroke newDefaultKs = getKeyStroke(defaultBinding);
ActionTrigger newDefaulTrigger = getActionTrigger(defaultBinding);
Set<Entry<DockingActionIf, KeyStroke>> entries = clientActions.entrySet();
for (Entry<DockingActionIf, KeyStroke> entry : entries) {
Set<Entry<DockingActionIf, ActionTrigger>> entries = clientActions.entrySet();
for (Entry<DockingActionIf, ActionTrigger> entry : entries) {
DockingActionIf existingAction = entry.getKey();
KeyStroke existingDefaultKs = entry.getValue();
if (Objects.equals(existingDefaultKs, newDefaultKs)) {
ActionTrigger existingDefaultTrigger = entry.getValue();
if (Objects.equals(existingDefaultTrigger, newDefaulTrigger)) {
continue;
}
KeyBindingUtils.logDifferentKeyBindingsWarnigMessage(newAction, existingAction,
existingDefaultKs);
logDifferentKeyBindingsWarnigMessage(newAction, existingAction, existingDefaultTrigger);
//
// 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));
// set the existing action's keystroke to be the winner
newAction.setKeyBindingData(existingAction.getKeyBindingData());
// one message is probably enough;
return existingDefaultKs;
return existingDefaultTrigger;
}
return newDefaultKs;
return newDefaulTrigger;
}
private void updateActionKeyStrokeFromOptions(DockingActionIf action, KeyStroke defaultKs) {
private void updateActionKeyStrokeFromOptions(DockingActionIf action,
ActionTrigger defaultTrigger) {
KeyStroke stubKs = defaultKs;
KeyStroke optionsKs = getKeyStrokeFromOptions(defaultKs);
if (!Objects.equals(defaultKs, optionsKs)) {
// we use the 'unvalidated' call since this value is provided by the user--we assume
// that user input is correct; we only validate programmer input
action.setUnvalidatedKeyBindingData(new KeyBindingData(optionsKs));
stubKs = optionsKs;
ActionTrigger stubTrigger = defaultTrigger;
ActionTrigger optionsTrigger = getActionTriggerFromOptions(defaultTrigger);
if (!Objects.equals(defaultTrigger, optionsTrigger)) {
setKeyBindingData(action, optionsTrigger);
stubTrigger = optionsTrigger;
}
setUnvalidatedKeyBindingData(new KeyBindingData(stubKs));
setKeyBindingData(this, stubTrigger);
}
private KeyStroke getKeyStrokeFromOptions(KeyStroke validatedKeyStroke) {
KeyStroke ks = keyBindingOptions.getKeyStroke(getFullName(), validatedKeyStroke);
return ks;
private void setKeyBindingData(DockingActionIf action, ActionTrigger actionTrigger) {
KeyBindingData kbData = null;
if (actionTrigger != null) {
kbData = new KeyBindingData(actionTrigger);
}
// we use the 'unvalidated' call since this value is provided by the user--we assume
// that user input is correct; we only validate programmer input
action.setUnvalidatedKeyBindingData(kbData);
}
private KeyStroke getKeyStroke(KeyBindingData data) {
private ActionTrigger getActionTriggerFromOptions(ActionTrigger validatedTrigger) {
return keyBindingOptions.getActionTrigger(getFullName(), validatedTrigger);
}
private ActionTrigger getActionTrigger(KeyBindingData data) {
if (data == null) {
return null;
}
return data.getKeyBinding();
return data.getActionTrigger();
}
@Override
@ -224,11 +233,11 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
return; // not my binding
}
KeyStroke newKs = (KeyStroke) newValue;
ActionTrigger newTrigger = (ActionTrigger) newValue;
setKeyBindingData(this, newTrigger);
for (DockingActionIf action : clientActions.keySet()) {
// we use the 'unvalidated' call since this value is provided by the user--we assume
// that user input is correct; we only validate programmer input
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
setKeyBindingData(action, newTrigger);
}
}
@ -253,4 +262,23 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
clientActions.clear();
keyBindingOptions.removeOptionsChangeListener(this);
}
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
//@formatter:off
String s = "Shared Key Binding Actions have different default values. These " +
"must be the same." +
"\n\tAction name: '"+existingAction.getName()+ "'" +
"\n\tAction 1: " + existingAction.getInceptionInformation() +
"\n\t\tAction Trigger: " + existingDefaultTrigger +
"\n\tAction 2: " + newAction.getInceptionInformation() +
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
"\nUsing the " +
"first value set - " + existingDefaultTrigger;
//@formatter:on
Msg.warn(SharedStubKeyBindingAction.class, s,
ReflectionUtilities.createJavaFilteredThrowable());
}
}

View file

@ -37,7 +37,9 @@ import docking.tool.util.DockingToolConstants;
import ghidra.framework.options.*;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;
import gui.event.MouseBinding;
import util.CollectionUtils;
import utilities.util.reflection.ReflectionUtilities;
/**
* An class to manage actions registered with the tool
@ -51,7 +53,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
/*
Map of Maps of Sets
Owner Name ->
Action Name -> Set of Actions
*/
@ -60,15 +62,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private Map<String, SharedStubKeyBindingAction> sharedActionMap = new HashMap<>();
private ToolOptions keyBindingOptions;
private ToolOptions options;
private Tool tool;
private KeyBindingsManager keyBindingsManager;
private OptionsChangeListener optionChangeListener = (options, optionName, oldValue,
newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue);
private OptionsChangeListener optionChangeListener = (toolOptions, optionName, oldValue,
newValue) -> updateKeyBindingsFromOptions(optionName, (ActionTrigger) newValue);
/**
* Construct an ActionManager
*
*
* @param tool tool using this ActionManager
* @param actionToGuiHelper the class that takes actions and maps them to GUI widgets
*/
@ -76,8 +78,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
this.tool = tool;
this.actionGuiHelper = actionToGuiHelper;
this.keyBindingsManager = new KeyBindingsManager(tool);
this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
this.keyBindingOptions.addOptionsChangeListener(optionChangeListener);
this.options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
this.options.addOptionsChangeListener(optionChangeListener);
createSystemActions();
SharedActionRegistry.installSharedActions(tool, this);
@ -112,12 +114,12 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
// Some System actions support changing the keybinding. In the future, all System actions
// may support this.
if (action.getKeyBindingType().isManaged()) {
KeyBindingData kbd = action.getKeyBindingData();
KeyStroke ks = kbd.getKeyBinding();
loadKeyBindingFromOptions(action, ks);
ActionTrigger actionTrigger = getActionTrigger(action);
loadKeyBindingFromOptions(action, actionTrigger);
}
keyBindingsManager.addSystemAction(action);
addActionToMap(action);
}
public void dispose() {
@ -127,12 +129,64 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
}
private void addActionToMap(DockingActionIf action) {
Set<DockingActionIf> actions = getActionStorage(action);
KeyBindingUtils.assertSameDefaultKeyBindings(action, actions);
assertSameDefaultActionTrigger(action, actions);
actions.add(action);
}
private static void assertSameDefaultActionTrigger(DockingActionIf newAction,
Collection<DockingActionIf> existingActions) {
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
return;
}
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
ActionTrigger defaultTrigger = getActionTrigger(newDefaultBinding);
for (DockingActionIf action : existingActions) {
if (!action.getKeyBindingType().supportsKeyBindings()) {
continue;
}
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
ActionTrigger existingTrigger = getActionTrigger(existingDefaultBinding);
if (!Objects.equals(defaultTrigger, existingTrigger)) {
logDifferentKeyBindingsWarnigMessage(newAction, action, existingTrigger);
break; // one warning seems like enough
}
}
}
/*
* Verifies that two equivalent actions (same name and owner) share the same default action
* trigger. It is considered a programming mistake for two equivalent actions to have different
* triggers.
*/
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
//@formatter:off
String s = "Shared Key Binding Actions have different default values. These " +
"must be the same." +
"\n\tAction name: '"+existingAction.getName()+ "'" +
"\n\tAction 1: " + existingAction.getInceptionInformation() +
"\n\t\tAction Trigger: " + existingDefaultTrigger +
"\n\tAction 2: " + newAction.getInceptionInformation() +
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
"\nUsing the " +
"first value set - " + existingDefaultTrigger;
//@formatter:on
Msg.warn(ToolActions.class, s, ReflectionUtilities.createJavaFilteredThrowable());
}
private static ActionTrigger getActionTrigger(KeyBindingData data) {
if (data == null) {
return null;
}
return data.getActionTrigger();
}
/**
* Add an action that works specifically with a component provider.
* @param provider provider associated with the action
@ -170,32 +224,45 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return;
}
KeyStroke ks = action.getKeyBinding();
loadKeyBindingFromOptions(action, ks);
ActionTrigger actionTrigger = getActionTrigger(action);
loadKeyBindingFromOptions(action, actionTrigger);
keyBindingsManager.addAction(provider, action);
}
private void loadKeyBindingFromOptions(DockingActionIf action, KeyStroke ks) {
String description = "Keybinding for " + action.getFullName();
keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null,
description);
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
if (!Objects.equals(ks, newKs)) {
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
private ActionTrigger getActionTrigger(DockingActionIf action) {
KeyBindingData kbData = action.getKeyBindingData();
if (kbData != null) {
return kbData.getActionTrigger();
}
return null;
}
private void loadKeyBindingFromOptions(DockingActionIf action, ActionTrigger actionTrigger) {
String fullName = action.getFullName();
String description = "Keybinding for " + fullName;
options.registerOption(fullName, OptionType.ACTION_TRIGGER, actionTrigger, null,
description);
KeyBindingData existingKbData = action.getKeyBindingData();
ActionTrigger newTrigger = options.getActionTrigger(fullName, actionTrigger);
KeyBindingData newKbData = KeyBindingData.update(existingKbData, newTrigger);
action.setUnvalidatedKeyBindingData(newKbData);
}
private void installSharedKeyBinding(ComponentProvider provider, 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 -> {
ActionTrigger actionTrigger = getActionTrigger(action);
SharedStubKeyBindingAction newStub =
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
registerStub(newStub, defaultKeyStroke);
new SharedStubKeyBindingAction(name, actionTrigger, options);
registerStub(newStub, actionTrigger);
return newStub;
});
@ -209,10 +276,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
}
}
private void registerStub(SharedStubKeyBindingAction stub, KeyStroke defaultKeyStroke) {
private void registerStub(SharedStubKeyBindingAction stub, ActionTrigger defaultActionTrigger) {
stub.addPropertyChangeListener(this);
loadKeyBindingFromOptions(stub, defaultKeyStroke);
loadKeyBindingFromOptions(stub, defaultActionTrigger);
keyBindingsManager.addAction(null, stub);
}
@ -243,10 +310,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return; // no actions registered for this owner
}
// Note: this method is called when plugins are removed. 'owner' is the name of the plugin.
// This method will also get called while passing the system owner. In that case, we do
// not want to remove system actions in this method. We check below for system actions.
//@formatter:off
toCleanup.values()
.stream()
.flatMap(set -> set.stream())
.filter(action -> !keyBindingsManager.isSystemAction(action)) // (see note above)
.forEach(action -> removeGlobalAction(action))
;
//@formatter:on
@ -312,32 +384,33 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private Iterator<DockingActionIf> getAllActionsIterator() {
// chain all items together, rather than copy the data
// Note: do not use Apache's IteratorUtils.chainedIterator. It degrades exponentially
return Stream
.concat(
actionsByNameByOwner.values()
.stream()
.flatMap(actionsByName -> actionsByName.values()
.stream())
.flatMap(actions -> actions.stream()),
sharedActionMap.values()
.stream())
.iterator();
//@formatter:off
return Stream.concat(
actionsByNameByOwner.values().stream()
.flatMap(actionsByName -> actionsByName.values().stream())
.flatMap(actions -> actions.stream()),
sharedActionMap.values().stream()).iterator();
//@formatter:on
}
/**
* Get the keybindings for each action so that they are still registered as being used;
* otherwise the options will be removed because they are noted as not being used.
/*
* An odd method that really shoulnd't be on the interface. This is a call that allows the
* framework to signal that the ToolOptions have been rebuilt, such as when restoring from xml.
* During a rebuild, ToolOptions does not send out events, so this class does not get any of the
* values from the new options. This method tells us to get the new version of the options from
* the tool.
*/
public synchronized void restoreKeyBindings() {
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
public synchronized void optionsRebuilt() {
// grab the new, rebuilt options
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
Iterator<DockingActionIf> it = getKeyBindingActionsIterator();
for (DockingActionIf action : CollectionUtils.asIterable(it)) {
KeyStroke ks = action.getKeyBinding();
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
if (!Objects.equals(ks, newKs)) {
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
}
KeyBindingData currentKbData = action.getKeyBindingData();
ActionTrigger optionsTrigger = options.getActionTrigger(action.getFullName(), null);
KeyBindingData newKbData = KeyBindingData.update(currentKbData, optionsTrigger);
action.setUnvalidatedKeyBindingData(newKbData);
}
}
@ -377,8 +450,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
keyBindingsManager.removeAction(action);
getActionStorage(action).remove(action);
if (!action.getKeyBindingType()
.isShared()) {
if (!action.getKeyBindingType().isShared()) {
return;
}
@ -391,12 +463,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private Set<DockingActionIf> getActionStorage(DockingActionIf action) {
String owner = action.getOwner();
String name = action.getName();
return actionsByNameByOwner.get(owner)
.get(name);
return actionsByNameByOwner.get(owner).get(name);
}
private void updateKeyBindingsFromOptions(ToolOptions options, String optionName,
KeyStroke newKs) {
private void updateKeyBindingsFromOptions(String optionName, ActionTrigger newTrigger) {
// note: the 'shared actions' update themselves, so we only need to handle standard actions
@ -405,21 +475,30 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
String name = matcher.group(1);
String owner = matcher.group(2);
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner)
.get(name);
for (DockingActionIf action : actions) {
KeyStroke oldKs = action.getKeyBinding();
if (Objects.equals(oldKs, newKs)) {
continue; // prevent bouncing
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner).get(name);
if (actions.isEmpty()) {
// An empty actions list implies that the action changed in the options is a shared
// action or a system action. Shared actions will update themselves. Here we will
// handle system actions.
DockingActionIf systemAction = keyBindingsManager.getSystemAction(optionName);
if (systemAction != null) {
KeyBindingData oldKbData = systemAction.getKeyBindingData();
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
systemAction.setUnvalidatedKeyBindingData(newKbData);
}
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
return;
}
for (DockingActionIf action : actions) {
KeyBindingData oldKbData = action.getKeyBindingData();
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
action.setUnvalidatedKeyBindingData(newKbData);
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!evt.getPropertyName()
.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
return;
}
@ -431,15 +510,19 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return;
}
//
// Check to see if we need to update the options to reflect the change to the action's key
// binding data.
//
KeyBindingData newKeyBindingData = (KeyBindingData) evt.getNewValue();
KeyStroke newKs = null;
ActionTrigger newTrigger = null;
if (newKeyBindingData != null) {
newKs = newKeyBindingData.getKeyBinding();
newTrigger = newKeyBindingData.getActionTrigger();
}
KeyStroke currentKs = keyBindingOptions.getKeyStroke(action.getFullName(), null);
if (!Objects.equals(currentKs, newKs)) {
keyBindingOptions.setKeyStroke(action.getFullName(), newKs);
ActionTrigger currentTrigger = options.getActionTrigger(action.getFullName(), null);
if (!Objects.equals(currentTrigger, newTrigger)) {
options.setActionTrigger(action.getFullName(), newTrigger);
keyBindingsChanged();
}
}
@ -456,8 +539,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
Iterator<DockingActionIf> it = actionGuiHelper.getComponentActions(provider);
while (it.hasNext()) {
DockingActionIf action = it.next();
if (action.getName()
.equals(actionName)) {
if (action.getName().equals(actionName)) {
return action;
}
}
@ -476,7 +558,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
}
public Action getAction(KeyStroke ks) {
return keyBindingsManager.getDockingKeyAction(ks);
return keyBindingsManager.getDockingAction(ks);
}
public Action getAction(MouseBinding mb) {
return keyBindingsManager.getDockingAction(mb);
}
DockingActionIf getSharedStubKeyBindingAction(String name) {
@ -487,23 +573,22 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
* Allows clients to register an action by using a placeholder. This is useful when
* an API wishes to have a central object (like a plugin) register actions for transient
* providers, that may not be loaded until needed.
*
*
* <p>This method may be called multiple times with the same conceptual placeholder--the
* placeholder will only be added once.
*
*
* @param placeholder the placeholder containing information related to the action it represents
*/
@Override
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) {
String name = placeholder.getName();
KeyStroke defaultKeyStroke = placeholder.getKeyBinding();
SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> {
ActionTrigger actionTrigger = getActionTrigger(placeholder);
SharedStubKeyBindingAction newStub =
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
registerStub(newStub, defaultKeyStroke);
new SharedStubKeyBindingAction(name, actionTrigger, options);
registerStub(newStub, actionTrigger);
return newStub;
});
@ -511,4 +596,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
stub.addActionOwner(owner);
}
private ActionTrigger getActionTrigger(SharedDockingActionPlaceholder placeholder) {
KeyStroke defaultKs = placeholder.getKeyBinding();
if (defaultKs != null) {
return new ActionTrigger(defaultKs);
}
return null;
}
}

View file

@ -21,10 +21,9 @@ import java.beans.PropertyChangeListener;
import javax.swing.JButton;
import docking.ActionContext;
import docking.DockingActionPerformer;
import docking.DockingWindowManager;
import docking.action.*;
import ghidra.util.Swing;
/**
* Class to manager toolbar buttons.
@ -113,23 +112,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
@Override
public void actionPerformed(ActionEvent event) {
DockingWindowManager.clearMouseOverHelp();
ActionContext context = getWindowManager().createActionContext(toolBarAction);
context.setSourceObject(event.getSource());
context.setEventClickModifiers(event.getModifiers());
// this gives the UI some time to repaint before executing the action
Swing.runLater(() -> {
if (toolBarAction.isValidContext(context) &&
toolBarAction.isEnabledForContext(context)) {
if (toolBarAction instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) toolBarAction;
toggleAction.setSelected(!toggleAction.isSelected());
}
toolBarAction.actionPerformed(context);
}
});
DockingActionPerformer.perform(toolBarAction, event, getWindowManager());
}
private DockingWindowManager getWindowManager() {

View file

@ -149,9 +149,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
public static Window getWindowByTitleContaining(Window parentWindow, String text) {
Set<Window> winList = getWindows(parentWindow);
Iterator<Window> iter = winList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : winList) {
if (!w.isShowing()) {
continue;
}
@ -169,9 +167,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
protected static Window getWindowByTitle(Window parentWindow, String title) {
Set<Window> winList = getWindows(parentWindow);
Iterator<Window> iter = winList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : winList) {
if (!w.isShowing()) {
continue;
}
@ -212,9 +208,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
while (totalTime <= timeout) {
Set<Window> winList = getAllWindows();
Iterator<Window> it = winList.iterator();
while (it.hasNext()) {
Window w = it.next();
for (Window w : winList) {
if (windowClass.isAssignableFrom(w.getClass()) && w.isShowing()) {
return w;
}
@ -499,9 +493,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
while (totalTime <= DEFAULT_WINDOW_TIMEOUT) {
Set<Window> winList = getAllWindows();
Iterator<Window> iter = winList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : winList) {
if ((w instanceof JDialog) && w.isShowing()) {
String windowTitle = getTitleForWindow(w);
if (title.equals(windowTitle)) {
@ -534,9 +526,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
while (totalTime <= DEFAULT_WAIT_TIMEOUT) {
Set<Window> winList = getWindows(window);
Iterator<Window> iter = winList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : winList) {
if ((w instanceof JDialog) && w.isShowing()) {
String windowTitle = getTitleForWindow(w);
if (title.equals(windowTitle)) {
@ -637,9 +627,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
private static <T extends DialogComponentProvider> T getDialogComponent(Window parentWindow,
Class<T> ghidraClass) {
Set<Window> winList = getWindows(parentWindow);
Iterator<Window> iter = winList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : winList) {
DialogComponentProvider dialogComponentProvider =
getDialogComponentProvider(w, ghidraClass);
if (dialogComponentProvider != null) {
@ -953,9 +941,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
// So, just ignore the exception. Client code that *really* wants all windows,
// like that which waits for windows, should be calling this method repeatedly anyway.
}
Iterator<Window> iter = dockableWinList.iterator();
while (iter.hasNext()) {
Window w = iter.next();
for (Window w : dockableWinList) {
windowSet.add(w);
findOwnedWindows(w, windowSet);
}
@ -1123,9 +1109,8 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
public static Set<DockingActionIf> getActionsByOwnerAndName(Tool tool, String owner,
String name) {
Set<DockingActionIf> ownerActions = tool.getDockingActionsByOwnerName(owner);
return ownerActions.stream()
.filter(action -> action.getName().equals(name))
.collect(Collectors.toSet());
return ownerActions.stream().filter(action -> action.getName().equals(name)).collect(
Collectors.toSet());
}
/**

View file

@ -23,8 +23,7 @@ import docking.Tool;
public interface DockingToolConstants {
/**
* Name of options for key bindings that map action name to a
* key stroke object.
*/
* Name of options for key bindings that map action name to a key stroke or mouse binding.
*/
public final static String KEY_BINDINGS = "Key Bindings";
}

View file

@ -81,6 +81,14 @@ public class HintTextField extends JTextField {
validateField();
}
/**
* Sets the hint for this text field
* @param hint the hint text
*/
public void setHint(String hint) {
this.hint = hint;
}
/**
* Key listener allows us to check field validity on every key typed
*/

View file

@ -32,10 +32,12 @@ import docking.*;
import docking.action.*;
import docking.test.AbstractDockingTest;
import docking.tool.util.DockingToolConstants;
import ghidra.framework.options.ActionTrigger;
import ghidra.framework.options.ToolOptions;
import ghidra.util.Msg;
import ghidra.util.SpyErrorLogger;
import ghidra.util.exception.AssertException;
import gui.event.MouseBinding;
public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
@ -468,7 +470,15 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
private void setSharedKeyBinding(KeyStroke newKs) {
ToolOptions options = getKeyBindingOptions();
runSwing(() -> options.setKeyStroke(SHARED_FULL_NAME, newKs));
runSwing(() -> {
ActionTrigger actionTrigger = options.getActionTrigger(SHARED_FULL_NAME, null);
MouseBinding existingMouseBinding = null;
if (actionTrigger != null) {
existingMouseBinding = actionTrigger.getMouseBinding();
}
ActionTrigger newTrigger = new ActionTrigger(newKs, existingMouseBinding);
options.setActionTrigger(SHARED_FULL_NAME, newTrigger);
});
waitForSwing();
}
@ -496,7 +506,10 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
public SharedNameAction(String owner, KeyStroke ks) {
super(SHARED_NAME, owner, KeyBindingType.SHARED);
setKeyBindingData(new KeyBindingData(ks));
if (ks != null) {
setKeyBindingData(new KeyBindingData(ks));
}
}
@Override

View file

@ -32,13 +32,10 @@
<logger name="org.jdom" level="WARN"/>
<logger name="generic.help" level="DEBUG"/>
<logger name="generic.random" level="WARN"/>
<logger name="generic.watchdog" level="DEBUG" />
<logger name="docking.help" level="DEBUG"/>
<logger name="docking.event.mouse" level="DEBUG" />
<logger name="docking.framework" level="DEBUG" />
<logger name="docking.widgets.table" level="DEBUG" />
<logger name="docking.widgets.filechooser" level="DEBUG" />
<logger name="docking" level="DEBUG"/>
<logger name="ghidra.feature.fid" level="INFO" />
<logger name="ghidra.framework" level="DEBUG"/>
@ -57,8 +54,6 @@
<!-- Ignore warnings about missing content classes in test env -->
<logger name="ghidra.framework.project.tool.GhidraToolTemplate" level="ERROR"/>
<logger name="functioncalls" level="DEBUG" />
<logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
<logger name="ghidra.net" level="WARN"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
@ -77,8 +72,10 @@
<logger name="ghidra.app.script" level="INFO" />
<logger name="ghidra.app.util.importer" level="DEBUG" />
<logger name="ghidra.app.util.opinion" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.extensions" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" />
<logger name="functioncalls" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />

View file

@ -30,15 +30,11 @@
<logger name="org.jdom" level="WARN"/>
<logger name="generic.help" level="DEBUG"/>
<logger name="generic.random" level="WARN"/>
<logger name="generic.watchdog" level="DEBUG" />
<logger name="docking.help" level="DEBUG"/>
<logger name="docking.event.mouse" level="DEBUG" />
<logger name="docking.framework" level="DEBUG" />
<logger name="docking.framework.SplashScreen" level="TRACE" />
<logger name="docking.widgets.table" level="DEBUG" />
<logger name="docking.widgets.filechooser" level="DEBUG" />
<logger name="docking" level="DEBUG"/>
<logger name="ghidra.feature.fid" level="INFO" />
<logger name="ghidra.framework" level="DEBUG"/>
<logger name="ghidra.graph" level="DEBUG" />
@ -55,9 +51,7 @@
<!-- Ignore warnings about missing content classes in test env -->
<logger name="ghidra.framework.project.tool.GhidraToolTemplate" level="ERROR"/>
<logger name="functioncalls" level="DEBUG" />
<logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
<logger name="ghidra.net" level="WARN"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
@ -78,6 +72,7 @@
<logger name="ghidra.app.util.opinion" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" />
<logger name="functioncalls" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />

View file

@ -49,6 +49,7 @@ public abstract class AbstractOptions implements Options {
set.add(Color.class);
set.add(Font.class);
set.add(KeyStroke.class);
set.add(ActionTrigger.class);
set.add(File.class);
set.add(Date.class);
return set;
@ -154,6 +155,17 @@ public abstract class AbstractOptions implements Options {
if (type == OptionType.FONT_TYPE) {
warnShouldUseTheme("font");
}
if (type == OptionType.KEYSTROKE_TYPE) {
type = OptionType.ACTION_TRIGGER;
if (defaultValue instanceof KeyStroke) {
defaultValue = new ActionTrigger((KeyStroke) defaultValue);
}
if (editorSupplier != null) {
Msg.error(this, "Custom KeyStroke property editors are no longer supported. " +
"Use ActionTrigger instead");
editorSupplier = null;
}
}
if (!type.isCompatible(defaultValue)) {
throw new IllegalStateException(
@ -187,8 +199,7 @@ public abstract class AbstractOptions implements Options {
}
Option option =
createRegisteredOption(optionName, type, description, help, defaultValue,
editor);
createRegisteredOption(optionName, type, description, help, defaultValue, editor);
valueMap.put(optionName, option);
}
@ -235,7 +246,7 @@ public abstract class AbstractOptions implements Options {
// There are several cases where an existing option may exist when registering an option
// 1) the option was accessed before it was registered
// 2) the option was loaded from a store (database or toolstate)
// 2) the option was loaded from a store (database or tool state)
// 3) the option was registered more than once.
//
// The only time this is a problem is if the exiting option type is not compatible with
@ -288,13 +299,21 @@ public abstract class AbstractOptions implements Options {
valueMap.put(optionName, option);
}
}
else if (type != OptionType.NO_TYPE && type != option.getOptionType()) {
throw new IllegalStateException(
"Expected option type: " + type + ", but was type: " + option.getOptionType());
}
validateOptionType(option, type);
return option;
}
private void validateOptionType(Option option, OptionType type) {
if (type == option.getOptionType() || type == OptionType.NO_TYPE) {
return;
}
throw new IllegalStateException(
"Expected option type: " + type + ", but was type: " + option.getOptionType());
}
@Override
public void putObject(String optionName, Object newValue) {
if (newValue == null) {
@ -503,9 +522,30 @@ public abstract class AbstractOptions implements Options {
@Override
public KeyStroke getKeyStroke(String optionName, KeyStroke defaultValue) {
Option option = getOption(optionName, OptionType.KEYSTROKE_TYPE, defaultValue);
ActionTrigger defaultTrigger = null;
if (defaultValue != null) {
defaultTrigger = new ActionTrigger(defaultValue);
}
Option option = getOption(optionName, OptionType.ACTION_TRIGGER, defaultTrigger);
try {
return (KeyStroke) option.getValue(defaultValue);
ActionTrigger actionTrigger = (ActionTrigger) option.getValue(defaultTrigger);
if (actionTrigger != null) {
return actionTrigger.getKeyStroke();
}
return null;
}
catch (ClassCastException e) {
return defaultValue;
}
}
@Override
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue) {
Option option = getOption(optionName, OptionType.ACTION_TRIGGER, defaultValue);
try {
return (ActionTrigger) option.getValue(defaultValue);
}
catch (ClassCastException e) {
return defaultValue;
@ -592,7 +632,16 @@ public abstract class AbstractOptions implements Options {
@Override
public void setKeyStroke(String optionName, KeyStroke value) {
putObject(optionName, value, OptionType.KEYSTROKE_TYPE);
ActionTrigger actionTrigger = null;
if (value != null) {
actionTrigger = new ActionTrigger(value);
}
setActionTrigger(optionName, actionTrigger);
}
@Override
public void setActionTrigger(String optionName, ActionTrigger value) {
putObject(optionName, value, OptionType.ACTION_TRIGGER);
}
@Override

View file

@ -0,0 +1,208 @@
/* ###
* 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 ghidra.framework.options;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.KeyStroke;
import org.apache.commons.lang3.StringUtils;
import gui.event.MouseBinding;
import util.CollectionUtils;
/**
* Represents a way to trigger an action in the system. A trigger is based on a key stroke, a mouse
* binding or both.
*/
public class ActionTrigger {
private final static Pattern TO_STRING_PATTERN =
Pattern.compile(".*Key Stroke\\[(.*)\\].*Mouse Binding\\[(.*)\\]");
private final static String KEY_STROKE = "KeyStroke";
private final static String MOUSE_BINDING = "MouseBinding";
private KeyStroke keyStroke;
private MouseBinding mouseBinding;
/**
* Creates an action trigger with the given key stroke.
* @param keyStroke the key stroke
*/
public ActionTrigger(KeyStroke keyStroke) {
this(keyStroke, null);
}
/**
* Creates an action trigger with the given mouse binding.
* @param mouseBinding the mouse binding
*/
public ActionTrigger(MouseBinding mouseBinding) {
this(null, mouseBinding);
}
/**
* A convenience constructor for creating an action trigger with either or both values set. At
* least one of the values must be non-null.
*
* @param keyStroke the key stroke; may be null
* @param mouseBinding the mouse binding; may be null
*/
public ActionTrigger(KeyStroke keyStroke, MouseBinding mouseBinding) {
if (CollectionUtils.isAllNull(keyStroke, mouseBinding)) {
throw new NullPointerException("Both the key stroke and mouse bindng cannot be null");
}
this.keyStroke = keyStroke;
this.mouseBinding = mouseBinding;
}
public KeyStroke getKeyStroke() {
return keyStroke;
}
public MouseBinding getMouseBinding() {
return mouseBinding;
}
@Override
public String toString() {
StringBuilder buffy = new StringBuilder("ActionTrigger: ");
buffy.append("Key Stroke[");
if (keyStroke != null) {
buffy.append(keyStroke.toString());
}
buffy.append("], Mouse Binding[");
if (mouseBinding != null) {
buffy.append(mouseBinding.toString());
}
buffy.append(']');
return buffy.toString();
}
/**
* Creates a new action trigger from the given string. The string is expected to be the result
* of calling {@link #toString()} on an instance of this class.
*
* @param string the string to parse.
* @return the new instance or null of the string is invalid.
*/
public static ActionTrigger getActionTrigger(String string) {
Matcher matcher = TO_STRING_PATTERN.matcher(string);
if (!matcher.matches()) {
return null;
}
String ksString = matcher.group(1);
String mbString = matcher.group(2);
KeyStroke ks = null;
if (!StringUtils.isBlank(ksString)) {
ks = KeyStroke.getKeyStroke(ksString);
}
MouseBinding mb = null;
if (!StringUtils.isBlank(mbString)) {
mb = MouseBinding.getMouseBinding(mbString);
}
return create(ks, mb);
}
/**
* Writes this action trigger's data into the given save state.
* @param saveState the save state
*/
public void writeState(SaveState saveState) {
String ksString = "";
if (keyStroke != null) {
ksString = keyStroke.toString();
}
saveState.putString(KEY_STROKE, ksString);
String mbString = "";
if (mouseBinding != null) {
mbString = mouseBinding.toString();
}
saveState.putString(MOUSE_BINDING, mbString);
}
/**
* Creates a new action trigger by reading data from the given save state.
* @param saveState the save state
* @return the new action trigger
*/
public static ActionTrigger create(SaveState saveState) {
KeyStroke ks = null;
String value = saveState.getString(KEY_STROKE, null);
if (!StringUtils.isBlank(value)) {
ks = KeyStroke.getKeyStroke(value);
}
MouseBinding mb = null;
value = saveState.getString(MOUSE_BINDING, null);
if (value != null) {
mb = MouseBinding.getMouseBinding(value);
}
return create(ks, mb);
}
private static ActionTrigger create(KeyStroke ks, MouseBinding mb) {
if (ks == null && mb == null) {
return null;
}
return new ActionTrigger(ks, mb);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((keyStroke == null) ? 0 : keyStroke.hashCode());
result = prime * result + ((mouseBinding == null) ? 0 : mouseBinding.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ActionTrigger other = (ActionTrigger) obj;
if (!Objects.equals(keyStroke, other.keyStroke)) {
return false;
}
return Objects.equals(mouseBinding, other.mouseBinding);
}
}

View file

@ -19,6 +19,8 @@ import java.beans.PropertyEditor;
import java.io.File;
import java.io.IOException;
import javax.swing.KeyStroke;
import org.apache.commons.io.FilenameUtils;
import ghidra.util.HelpLocation;
@ -104,6 +106,15 @@ public class FileOptions extends AbstractOptions {
@Override
protected Option createUnregisteredOption(String optionName, OptionType type,
Object defaultValue) {
if (type == OptionType.KEYSTROKE_TYPE) {
// convert key strokes to action triggers
type = OptionType.ACTION_TRIGGER;
if (defaultValue instanceof KeyStroke keyStroke) {
defaultValue = new ActionTrigger(keyStroke);
}
}
return new FileOption(optionName, type, null, null, defaultValue, false, null);
}

View file

@ -45,7 +45,8 @@ public enum OptionType {
FILE_TYPE(File.class, new FileStringAdapter()),
COLOR_TYPE(Color.class, new ColorStringAdapter()),
FONT_TYPE(Font.class, new FontStringAdapter()),
KEYSTROKE_TYPE(KeyStroke.class, new KeyStrokeStringAdapter());
KEYSTROKE_TYPE(KeyStroke.class, new KeyStrokeStringAdapter()),
ACTION_TRIGGER(ActionTrigger.class, new ActionTriggerStringAdapter());
private Class<?> clazz;
private StringAdapter stringAdapter;
@ -241,8 +242,7 @@ public enum OptionType {
}
catch (Exception e) {
Msg.error(this,
"Can't create customOption instance for: " + customOptionClassName +
e);
"Can't create customOption instance for: " + customOptionClassName + e);
}
return null;
}
@ -331,4 +331,11 @@ public enum OptionType {
}
}
static class ActionTriggerStringAdapter extends StringAdapter {
@Override
Object stringToObject(String string) {
return ActionTrigger.getActionTrigger(string);
}
}
}

View file

@ -116,7 +116,7 @@ public interface Options {
* Note, this method should not be used for
* colors and font as doing so will result in those colors and fonts becoming disconnected
* to the current theme. Instead use
*
*
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
* @param optionName the name of the option being registered.
@ -139,7 +139,7 @@ public interface Options {
* to the current theme. Instead use
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
*
*
* @param optionName the name of the option being registered.
* @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default
@ -166,7 +166,7 @@ public interface Options {
* may be thrown. This API will not use the supplier when in headless mode, this avoiding the
* creation of GUI components. For this to work correctly, clients using custom property
* editors must defer construction of the editor until the supplier is called.
*
*
* @param optionName the name of the option being registered.
* @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default
@ -392,16 +392,27 @@ public interface Options {
public Font getFont(String optionName, Font defaultValue);
/**
* Get the KeyStrokg for the given action name.
* Get the KeyStroke for the given action name.
* @param optionName the option name
* @param defaultValue value that is stored and returned if there is no
* option with the given name
* @return KeyStroke option
* @throws IllegalArgumentException is a option exists with the given
* name but it is not a KeyStroke
* @deprecated use {@link #getActionTrigger(String, ActionTrigger)} instead
*/
@Deprecated(since = "11.1", forRemoval = true)
public KeyStroke getKeyStroke(String optionName, KeyStroke defaultValue);
/**
* Get the {@link ActionTrigger} for the given full action name.
* @param optionName the action name
* @param defaultValue value that is stored and returned if there is no
* option with the given name
* @return the action trigger
*/
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue);
/**
* Get the string value for the given option name.
* @param optionName option name
@ -507,9 +518,20 @@ public interface Options {
* @param value KeyStroke to set
* @throws IllegalArgumentException if a option with the given
* name already exists, but it is not a KeyStroke
* @deprecated use {@link #setActionTrigger(String, ActionTrigger)} instead
*/
@Deprecated(since = "11.1", forRemoval = true)
public void setKeyStroke(String optionName, KeyStroke value);
/**
* Sets the action trigger value for the option
* @param optionName name of the option
* @param value action trigger to set
* @throws IllegalArgumentException if a option with the given
* name already exists, but it is not an action trigger
*/
public void setActionTrigger(String optionName, ActionTrigger value);
/**
* Set the String value for the option.
* @param optionName name of the option
@ -570,14 +592,14 @@ public interface Options {
/**
* Restores <b>all</b> options contained herein to their default values.
*
*
* @see #restoreDefaultValue(String)
*/
public void restoreDefaultValues();
/**
* Restores the option denoted by the given name to its default value.
*
*
* @param optionName The name of the option to restore
* @see #restoreDefaultValues()
*/
@ -585,11 +607,11 @@ public interface Options {
/**
* Returns a Options object that is a sub-options of this options.
*
*
* <p>Note: the option path can have {@link Options#DELIMITER} characters which will be
* used to create a hierarchy with each element in the path resulting in sub-option of the
* previous path element.
*
*
* @param path the path for the sub-options object
* @return an Options object that is a sub-options of this options
*/

View file

@ -64,8 +64,8 @@ public class SubOptions implements Options {
Set<String> childCategories = AbstractOptions.getChildCategories(optionPaths);
List<Options> childOptions = new ArrayList<>(childCategories.size());
for (String categoryName : childCategories) {
childOptions.add(new SubOptions(options, categoryName, prefix + categoryName +
DELIMITER));
childOptions.add(
new SubOptions(options, categoryName, prefix + categoryName + DELIMITER));
}
return childOptions;
}
@ -170,6 +170,11 @@ public class SubOptions implements Options {
return options.getKeyStroke(prefix + optionName, defaultValue);
}
@Override
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue) {
return options.getActionTrigger(prefix + optionName, defaultValue);
}
@Override
public String getString(String optionName, String defaultValue) {
return options.getString(prefix + optionName, defaultValue);
@ -235,6 +240,11 @@ public class SubOptions implements Options {
options.setKeyStroke(prefix + optionName, value);
}
@Override
public void setActionTrigger(String optionName, ActionTrigger value) {
options.setActionTrigger(prefix + optionName, value);
}
@Override
public void setString(String optionName, String value) {
options.setString(prefix + optionName, value);

View file

@ -128,6 +128,12 @@ public class ToolOptions extends AbstractOptions {
Class<?> c = Class.forName(element.getAttributeValue(CLASS_ATTRIBUTE));
Constructor<?> constructor = c.getDeclaredConstructor();
WrappedOption wo = (WrappedOption) constructor.newInstance();
wo.readState(new SaveState(element));
if (wo instanceof WrappedKeyStroke wrappedKs) {
wo = wrappedKs.toWrappedActionTrigger();
}
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
valueMap.put(optionName, option);
@ -138,7 +144,6 @@ public class ToolOptions extends AbstractOptions {
option.doSetCurrentValue(null); // use doSet so that it is not registered
}
else {
wo.readState(new SaveState(element));
option.doSetCurrentValue(wo.getObject()); // use doSet so that it is not registered
}
}
@ -256,6 +261,9 @@ public class ToolOptions extends AbstractOptions {
if (value instanceof KeyStroke) {
return new WrappedKeyStroke((KeyStroke) value);
}
if (value instanceof ActionTrigger) {
return new WrappedActionTrigger((ActionTrigger) value);
}
if (value instanceof File) {
return new WrappedFile((File) value);
}
@ -415,6 +423,15 @@ public class ToolOptions extends AbstractOptions {
@Override
protected Option createUnregisteredOption(String optionName, OptionType type,
Object defaultValue) {
if (type == OptionType.KEYSTROKE_TYPE) {
// convert key strokes to action triggers
type = OptionType.ACTION_TRIGGER;
if (defaultValue instanceof KeyStroke keyStroke) {
defaultValue = new ActionTrigger(keyStroke);
}
}
return new ToolOption(optionName, type, null, null, defaultValue, false, null);
}

View file

@ -0,0 +1,67 @@
/* ###
* 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 ghidra.framework.options;
import java.util.Objects;
public class WrappedActionTrigger implements WrappedOption {
private ActionTrigger actionTrigger;
/**
* Default constructor
*/
WrappedActionTrigger() {
// for reflection
}
/**
* Construct a wrapper object using the given ActionTrigger.
* @param actionTrigger the action trigger
*/
WrappedActionTrigger(ActionTrigger actionTrigger) {
this.actionTrigger = actionTrigger;
}
@Override
public Object getObject() {
return actionTrigger;
}
@Override
public void readState(SaveState saveState) {
actionTrigger = ActionTrigger.create(saveState);
}
@Override
public void writeState(SaveState saveState) {
if (actionTrigger == null) {
return;
}
actionTrigger.writeState(saveState);
}
@Override
public OptionType getOptionType() {
return OptionType.ACTION_TRIGGER;
}
@Override
public String toString() {
return Objects.toString(actionTrigger);
}
}

View file

@ -38,6 +38,7 @@ class WrappedKeyStroke implements WrappedOption {
/**
* Construct a wrapper object using the given KeyStroke.
* @param ks the keystroke
*/
WrappedKeyStroke(KeyStroke ks) {
this.keyStroke = ks;
@ -48,31 +49,15 @@ class WrappedKeyStroke implements WrappedOption {
return keyStroke;
}
/**
* Read the components for a Key Stroke from the given
* SaveState object to restore this WrappedKeyStroke.
*/
@Override
public void readState(SaveState saveState) {
if (saveState.hasValue(KEY_CODE)) {
int keyCode = saveState.getInt(KEY_CODE, 0);
int modifiers = saveState.getInt(MODIFIERS, 0);
String version = System.getProperty("java.version");
if (version.startsWith("1.4")) {
modifiers &= 0x0f;
modifiers |= modifiers << 6;
}
else if (version.startsWith("1.3")) {
modifiers &= 0x0f;
}
keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
}
}
/**
* Write the components for the wrapped Key Stroke to the given
* SaveState object.
*/
@Override
public void writeState(SaveState saveState) {
if (keyStroke == null) {
@ -91,4 +76,17 @@ class WrappedKeyStroke implements WrappedOption {
public String toString() {
return Objects.toString(keyStroke);
}
/**
* A method to allow for converting the deprecated options key stroke usage to the new action
* trigger usage
* @return a WrappedActionTrigger
*/
public WrappedActionTrigger toWrappedActionTrigger() {
ActionTrigger trigger = null;
if (keyStroke != null) {
trigger = new ActionTrigger(keyStroke);
}
return new WrappedActionTrigger(trigger);
}
}

View file

@ -0,0 +1,238 @@
/* ###
* 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 gui.event;
import static org.apache.commons.lang3.StringUtils.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import ghidra.util.Msg;
/**
* A simple class that represents a mouse button and any modifiers needed to bind an action to a
* mouse input event.
* <P>
* The modifiers used by this class will include the button down mask for the given button. This
* is done to match how {@link MouseEvent} uses its modifiers.
*/
public class MouseBinding {
private static final Pattern BUTTON_PATTERN =
Pattern.compile("button(\\d+)", Pattern.CASE_INSENSITIVE);
private static final String SHIFT = "Shift";
private static final String CTRL = "Ctrl";
private static final String ALT = "Alt";
private static final String META = "Meta";
private int modifiers = -1;
private int button = -1;
/**
* Construct a binding with the given button number of the desired mouse button (e.g., 1, 2,...)
* @param button the button number
*/
public MouseBinding(int button) {
this(button, -1);
}
/**
* Construct a binding with the given button number of the desired mouse button (e.g., 1, 2,...)
* as well as any desired modifiers (e.g., {@link InputEvent#SHIFT_DOWN_MASK}).
* @param button the button number
* @param modifiers the event modifiers
*/
public MouseBinding(int button, int modifiers) {
this.button = button;
// The button down mask is applied to the mouse event modifiers by Java. Thus, for us to
// match the mouse event modifiers, we need to add the button down mask here.
this.modifiers = InputEvent.getMaskForButton(button);
if (modifiers > 0) {
this.modifiers |= modifiers;
}
}
/**
* The button used by this class
* @return the button used by this class
*/
public int getButton() {
return button;
}
/**
* The modifiers used by this class
* @return the modifiers used by this class
*/
public int getModifiers() {
return modifiers;
}
/**
* A user-friendly display string for this class
* @return a user-friendly display string for this class
*/
public String getDisplayText() {
String modifiersText = InputEvent.getModifiersExText(modifiers);
if (StringUtils.isBlank(modifiersText)) {
// not sure if this can happen, since we add the button number to the modifiers
return "Button" + button;
}
return modifiersText;
}
/**
* Create a mouse binding for the given event
* @param e the event
* @return the mouse binding
*/
public static MouseBinding getMouseBinding(MouseEvent e) {
return new MouseBinding(e.getButton(), e.getModifiersEx());
}
/**
* Creates a mouse binding from the given string. The string is expected to be of the form:
* {@code Ctrl+Button1}, which is the form of the text generated by {@link #getDisplayText()}.
*
* @param mouseString the mouse string
* @return the mouse binding or null if an invalid string was given
*/
public static MouseBinding getMouseBinding(String mouseString) {
int button = getButton(mouseString);
if (button == -1) {
return null;
}
// be flexible on the tokens for splitting, even though '+' seems to be the standard
StringTokenizer tokenizer = new StringTokenizer(mouseString, "- +");
List<String> pieces = new ArrayList<>();
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (!pieces.contains(token)) {
pieces.add(token);
}
}
int modifiers = 0;
for (Iterator<String> iterator = pieces.iterator(); iterator.hasNext();) {
String piece = iterator.next();
if (indexOfIgnoreCase(piece, SHIFT) != -1) {
modifiers |= InputEvent.SHIFT_DOWN_MASK;
iterator.remove();
}
else if (indexOfIgnoreCase(piece, CTRL) != -1) {
modifiers |= InputEvent.CTRL_DOWN_MASK;
iterator.remove();
}
else if (indexOfIgnoreCase(piece, ALT) != -1) {
modifiers |= InputEvent.ALT_DOWN_MASK;
iterator.remove();
}
else if (indexOfIgnoreCase(piece, META) != -1) {
modifiers |= InputEvent.META_DOWN_MASK;
iterator.remove();
}
}
return new MouseBinding(button, modifiers);
}
private static int getButton(String mouseString) {
Matcher buttonMatcher = BUTTON_PATTERN.matcher(mouseString);
if (buttonMatcher.find()) {
String numberString = buttonMatcher.group(1);
try {
int intValue = Integer.parseInt(numberString);
if (intValue > 0) {
return intValue;
}
}
catch (NumberFormatException e) {
Msg.error(MouseBinding.class, "Unable to parse button number %s in text %s"
.formatted(numberString, mouseString));
}
}
return -1;
}
/**
* Returns true if the given mouse event is the mouse released event for the mouse button used
* by this class. This method will ignore modifier text, since modifiers can be pressed and
* released independent of the mouse button's release.
*
* @param e the event
* @return true if the given mouse event is the mouse released event for the mouse button used
* by this class
*/
public boolean isMatchingRelease(MouseEvent e) {
int otherButton = e.getButton();
if (button != otherButton) {
return false;
}
int id = e.getID();
if (id == MouseEvent.MOUSE_RELEASED || id == MouseEvent.MOUSE_CLICKED) {
// not sure if released and clicked are sent for every OS / mouse combo
return true;
}
return false;
}
@Override
public String toString() {
return getDisplayText();
}
@Override
public int hashCode() {
return Objects.hash(button, modifiers);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MouseBinding other = (MouseBinding) obj;
if (button != other.button) {
return false;
}
if (modifiers != other.modifiers) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,152 @@
/* ###
* 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 gui.event;
import static org.junit.Assert.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import org.junit.Test;
public class MouseBindingTest {
private static final int CTRL = InputEvent.CTRL_DOWN_MASK;
private static final int SHIFT = InputEvent.SHIFT_DOWN_MASK;
@Test
public void testConstructor_InvalidButton() {
try {
new MouseBinding(0);
fail();
}
catch (IllegalArgumentException e) {
// expected
}
try {
new MouseBinding(-1);
fail();
}
catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void testGetMouseBinding_BadButton() {
assertNull(MouseBinding.getMouseBinding("Button"));
assertNull(MouseBinding.getMouseBinding("Button0"));
assertNull(MouseBinding.getMouseBinding("Cats"));
assertNull(MouseBinding.getMouseBinding("Buttons12"));
}
@Test
public void testGetMouseBindingFromText() {
MouseBinding mb = MouseBinding.getMouseBinding("Button1");
int button = 1;
assertEquals(button, mb.getButton());
assertModifiers(mb, buttonMask(button));
mb = MouseBinding.getMouseBinding("Button2");
button = 2;
assertEquals(button, mb.getButton());
assertModifiers(mb, buttonMask(button));
mb = MouseBinding.getMouseBinding("Ctrl+Button1");
button = 1;
assertEquals(button, mb.getButton());
assertModifiers(mb, CTRL, buttonMask(button));
mb = MouseBinding.getMouseBinding("Ctrl+Shift+Button2");
button = 2;
assertEquals(button, mb.getButton());
assertModifiers(mb, CTRL, SHIFT, buttonMask(button));
}
@Test
public void testGetMouseBindingFromEvent() {
int button = 1;
int modifiers = buttonMask(1);
JPanel source = new JPanel();
MouseEvent event = new MouseEvent(source, MouseEvent.MOUSE_PRESSED,
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
MouseBinding mb = MouseBinding.getMouseBinding(event);
assertEquals(button, mb.getButton());
assertModifiers(mb, buttonMask(button));
}
@Test
public void testIsMatchingRelease() {
int button = 1;
MouseBinding mb = new MouseBinding(button);
int modifiers = buttonMask(1);
JPanel source = new JPanel();
MouseEvent pressed = new MouseEvent(source, MouseEvent.MOUSE_PRESSED,
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
assertFalse(mb.isMatchingRelease(pressed));
MouseEvent released = new MouseEvent(source, MouseEvent.MOUSE_RELEASED,
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
assertTrue(mb.isMatchingRelease(released));
MouseEvent clicked = new MouseEvent(source, MouseEvent.MOUSE_RELEASED,
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
assertTrue(mb.isMatchingRelease(clicked));
// test that modifiers are ignored when determining what is a matching release
modifiers = InputEvent.SHIFT_DOWN_MASK ^ buttonMask(button);
released = new MouseEvent(source, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
modifiers, 0, 0, 1, false, button);
assertTrue(mb.isMatchingRelease(released));
}
@Test
public void testGetDisplayString() {
int button = 1;
MouseBinding mb = new MouseBinding(button);
assertEquals("Button1", mb.getDisplayText());
mb = MouseBinding.getMouseBinding("Button1");
assertEquals("Button1", mb.getDisplayText());
mb = MouseBinding.getMouseBinding("Button1 pressed");
assertEquals("Button1", mb.getDisplayText());
mb = MouseBinding.getMouseBinding("Shift+Button2");
assertEquals("Shift+Button2", mb.getDisplayText());
}
private void assertModifiers(MouseBinding mb, int... expected) {
int actual = mb.getModifiers();
int allMods = 0;
for (int mod : expected) {
allMods ^= mod;
}
assertEquals(allMods, actual);
}
private int buttonMask(int buttonNumber) {
return InputEvent.getMaskForButton(buttonNumber);
}
}

View file

@ -20,6 +20,8 @@ import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import javax.swing.KeyStroke;
import db.*;
import ghidra.framework.options.*;
import ghidra.util.*;
@ -362,6 +364,15 @@ class OptionsDB extends AbstractOptions {
type = OptionType.values()[record.getByteValue(TYPE_COL)];
}
}
else if (type == OptionType.KEYSTROKE_TYPE) {
// convert key strokes to action triggers
type = OptionType.ACTION_TRIGGER;
if (defaultValue instanceof KeyStroke keyStroke) {
defaultValue = new ActionTrigger(keyStroke);
}
}
return new DBOption(optionName, type, null, null, defaultValue, false, null);
}

View file

@ -1402,7 +1402,7 @@ public abstract class PluginTool extends AbstractDockingTool {
protected void restoreOptionsFromXml(Element root) {
optionsMgr.setConfigState(root.getChild("OPTIONS"));
toolActions.restoreKeyBindings();
toolActions.optionsRebuilt();
setToolOptionsHelpLocation();
}
@ -1418,7 +1418,6 @@ public abstract class PluginTool extends AbstractDockingTool {
protected void restorePluginsFromXml(Element elem) throws PluginException {
pluginMgr.restorePluginsFromXml(elem);
}
PluginEvent[] getLastEvents() {
@ -1553,10 +1552,6 @@ public abstract class PluginTool extends AbstractDockingTool {
return winMgr.getActiveComponentProvider();
}
public void refreshKeybindings() {
toolActions.restoreKeyBindings();
}
public void setUnconfigurable() {
isConfigurable = false;
}

View file

@ -28,8 +28,7 @@ import javax.swing.table.TableColumn;
import org.apache.commons.lang3.StringUtils;
import docking.DockingUtils;
import docking.KeyEntryTextField;
import docking.*;
import docking.action.DockingActionIf;
import docking.actions.*;
import docking.tool.util.DockingToolConstants;
@ -37,13 +36,12 @@ import docking.widgets.*;
import docking.widgets.label.GIconLabel;
import docking.widgets.table.*;
import generic.theme.Gui;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HTMLUtilities;
import ghidra.util.Swing;
import ghidra.util.*;
import ghidra.util.layout.PairLayout;
import ghidra.util.layout.VerticalLayout;
import gui.event.MouseBinding;
import help.Help;
import help.HelpService;
import resources.Icons;
@ -66,7 +64,8 @@ public class KeyBindingsPanel extends JPanel {
private JPanel infoPanel;
private MultiLineLabel collisionLabel;
private KeyBindingsTableModel tableModel;
private KeyEntryTextField ksField;
private ActionBindingListener actionBindingListener = new ActionBindingListener();
private ActionBindingPanel actionBindingPanel;
private GTableFilterPanel<DockingActionIf> tableFilterPanel;
private EmptyBorderButton helpButton;
@ -207,11 +206,11 @@ public class KeyBindingsPanel extends JPanel {
}
private JPanel createKeyEntryPanel() {
ksField = new KeyEntryTextField(20, keyStroke -> keyStrokeChanged(keyStroke));
actionBindingPanel = new ActionBindingPanel(actionBindingListener);
// this is the lower panel that holds the key entry text field
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
p.add(ksField);
p.add(actionBindingPanel);
JPanel keyPanel = new JPanel(new BorderLayout());
@ -221,8 +220,7 @@ public class KeyBindingsPanel extends JPanel {
MultiLineLabel mlabel =
new MultiLineLabel("To add or change a key binding, select an action\n" +
"and type any key combination\n \n" +
"To remove a key binding, select an action and\n" +
"press <Enter> or <Backspace>");
"To remove a key binding, select an action and\n" + "press <Enter> or <Backspace>");
JPanel labelPanel = new JPanel();
labelPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0));
BoxLayout bl = new BoxLayout(labelPanel, BoxLayout.X_AXIS);
@ -334,8 +332,12 @@ public class KeyBindingsPanel extends JPanel {
Map<String, KeyStroke> localActionMap = new HashMap<>();
List<String> optionNames = keyBindingOptions.getOptionNames();
for (String name : optionNames) {
KeyStroke newKeyStroke = keyBindingOptions.getKeyStroke(name, null);
localActionMap.put(name, newKeyStroke);
ActionTrigger actionTrigger = keyBindingOptions.getActionTrigger(name, null);
KeyStroke optionsKs = null;
if (actionTrigger != null) {
optionsKs = actionTrigger.getKeyStroke();
}
localActionMap.put(name, optionsKs);
}
return localActionMap;
}
@ -383,9 +385,9 @@ public class KeyBindingsPanel extends JPanel {
return action.getFullName();
}
private void showActionsMappedToKeyStroke(String ksName) {
private void showActionsMappedToKeyStroke(KeyStroke ks) {
String text = keyBindings.getActionsForKeyStrokeText(ksName);
String text = keyBindings.getActionsForKeyStrokeText(ks);
if (StringUtils.isBlank(text)) {
text = " ";
}
@ -413,9 +415,6 @@ public class KeyBindingsPanel extends JPanel {
Map<String, KeyStroke> keyStrokesByActionName =
createActionNameToKeyStrokeMap(keyBindingOptions);
if (keyStrokesByActionName == null) {
return;
}
boolean changes = false;
@ -445,7 +444,7 @@ public class KeyBindingsPanel extends JPanel {
/**
* Processes KeyStroke entry from the text field.
*/
private void keyStrokeChanged(KeyStroke ks) {
private void updateKeyStroke(KeyStroke ks) {
clearInfoPanel();
DockingActionIf action = getSelectedAction();
@ -458,25 +457,56 @@ public class KeyBindingsPanel extends JPanel {
String errorMessage = toolActions.validateActionKeyBinding(action, ks);
if (errorMessage != null) {
statusLabel.setText(errorMessage);
ksField.clearField();
actionBindingPanel.clearKeyStroke();
return;
}
String selectedActionName = getSelectedActionName();
if (selectedActionName != null) {
if (setActionKeyStroke(selectedActionName, ks)) {
String keyStrokeText = KeyBindingUtils.parseKeyStroke(ks);
showActionsMappedToKeyStroke(keyStrokeText);
tableModel.fireTableDataChanged();
changesMade(true);
}
String selectedActionName = action.getFullName();
if (setActionKeyStroke(selectedActionName, ks)) {
showActionsMappedToKeyStroke(ks);
tableModel.fireTableDataChanged();
changesMade(true);
}
}
private void updateMouseBinding(MouseBinding mb) {
clearInfoPanel();
DockingActionIf action = getSelectedAction();
if (action == null) {
statusLabel.setText("No action is selected.");
return;
}
String selectedActionName = action.getFullName();
if (setMouseBinding(selectedActionName, mb)) {
tableModel.fireTableDataChanged();
changesMade(true);
}
}
private boolean setMouseBinding(String actionName, MouseBinding mouseBinding) {
if (keyBindings.isMouseBindingInUse(actionName, mouseBinding)) {
String existingName = keyBindings.getActionForMouseBinding(mouseBinding);
String message = """
Mouse binding '%s' already in use by '%s'.
The existing binding must be cleared before it can be used again.
""".formatted(mouseBinding, existingName);
Msg.showInfo(this, actionBindingPanel, "Mouse Binding In Use", message);
actionBindingPanel.clearMouseBinding();
return false;
}
return keyBindings.setActionMouseBinding(actionName, mouseBinding);
}
// returns true if the key stroke is a new value
private boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
if (!isValidKeyStroke(keyStroke)) {
ksField.setText("");
actionBindingPanel.clearKeyStroke();
return keyBindings.removeKeyStroke(actionName);
}
@ -513,20 +543,22 @@ public class KeyBindingsPanel extends JPanel {
String fullActionName = getSelectedActionName();
if (fullActionName == null) {
statusLabel.setText("");
actionBindingPanel.setEnabled(false);
return;
}
actionBindingPanel.setEnabled(true);
helpButton.setEnabled(true);
KeyStroke ks = keyBindings.getKeyStroke(fullActionName);
String ksName = "";
clearInfoPanel();
KeyStroke ks = keyBindings.getKeyStroke(fullActionName);
if (ks != null) {
ksName = KeyBindingUtils.parseKeyStroke(ks);
showActionsMappedToKeyStroke(ksName);
showActionsMappedToKeyStroke(ks);
}
ksField.setText(ksName);
MouseBinding mb = keyBindings.getMouseBinding(fullActionName);
actionBindingPanel.setKeyBindingData(ks, mb);
// make sure the label gets enough space
statusLabel.setPreferredSize(
@ -543,8 +575,7 @@ public class KeyBindingsPanel extends JPanel {
}
private class KeyBindingsTableModel extends AbstractSortedTableModel<DockingActionIf> {
private final String[] columnNames =
{ "Action Name", "KeyBinding", "Plugin Name" };
private final String[] columnNames = { "Action Name", "KeyBinding", "Plugin Name" };
private List<DockingActionIf> actions;
@ -561,15 +592,23 @@ public class KeyBindingsPanel extends JPanel {
@Override
public Object getColumnValueForRow(DockingActionIf action, int columnIndex) {
String fullName = action.getFullName();
switch (columnIndex) {
case ACTION_NAME:
return action.getName();
case KEY_BINDING:
KeyStroke ks = keyBindings.getKeyStroke(action.getFullName());
String text = "";
KeyStroke ks = keyBindings.getKeyStroke(fullName);
if (ks != null) {
return KeyBindingUtils.parseKeyStroke(ks);
text += KeyBindingUtils.parseKeyStroke(ks);
}
return "";
MouseBinding mb = keyBindings.getMouseBinding(fullName);
if (mb != null) {
text += " (" + mb.getDisplayText() + ")";
}
return text.trim();
case PLUGIN_NAME:
return action.getOwnerDescription();
}
@ -606,4 +645,17 @@ public class KeyBindingsPanel extends JPanel {
return String.class;
}
}
private class ActionBindingListener implements DockingActionInputBindingListener {
@Override
public void keyStrokeChanged(KeyStroke ks) {
updateKeyStroke(ks);
}
@Override
public void mouseBindingChanged(MouseBinding mb) {
updateMouseBinding(mb);
}
}
}