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

View file

@ -24,7 +24,7 @@ import org.apache.logging.log4j.message.Message;
* 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

@ -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;
@ -44,83 +40,89 @@ public class ExtProperty implements IsfObject {
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();
}
}
@ -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,6 +47,7 @@ public class ActionToGuiMapper {
popupActionManager = new PopupActionManager(winMgr, menuGroupMap);
DockingWindowsContextSensitiveHelpListener.install();
MouseBindingMouseEventDispatcher.install();
}
/**

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;
@ -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!
}
@ -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));
}
}
@ -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;
}

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,6 +49,14 @@ 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
@ -54,6 +68,25 @@ public class KeyBindingData {
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,6 +27,7 @@ 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.
@ -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) {
@ -118,12 +129,16 @@ public class KeyBindingsManager implements PropertyChangeListener {
return ksString + " in use by System action '" + systemDockingAction.getName() + "'";
}
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";
}
@ -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,7 +46,6 @@ 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
@ -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;
}
@ -501,8 +499,7 @@ public class KeyBindingUtils {
* @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);
@ -553,62 +550,6 @@ 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.
@ -800,6 +741,10 @@ public class KeyBindingUtils {
* @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,37 +130,70 @@ 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;
}
/**
@ -143,20 +201,8 @@ public class KeyBindings {
* 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
@ -60,11 +62,11 @@ 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
@ -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) {
@ -497,13 +583,12 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
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"/>
@ -78,7 +73,9 @@
<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.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,14 +30,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.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"/>
@ -56,8 +52,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"/>
@ -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

@ -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

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