GP-4317 - Removing the 'reserved' concept

This commit is contained in:
dragonmacher 2024-02-17 11:21:31 -05:00
parent 52e6360d96
commit e44daf55aa
40 changed files with 1143 additions and 834 deletions

View file

@ -17,7 +17,8 @@
//@category Examples.Bundle //@category Examples.Bundle
import docking.ActionContext; import docking.ActionContext;
import docking.action.*; import docking.action.DockingAction;
import docking.action.ToolBarData;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.app.services.CodeViewerService; import ghidra.app.services.CodeViewerService;
import ghidra.app.services.ConsoleService; import ghidra.app.services.ConsoleService;
@ -40,7 +41,7 @@ public class ActivatorExampleScript extends GhidraScript {
println("The activator will remove the action if this bundle is deactivated,"); println("The activator will remove the action if this bundle is deactivated,");
println(" e.g. if this script is modified and the bundle needs to be reloaded."); println(" e.g. if this script is modified and the bundle needs to be reloaded.");
DockingAction action = new DockingAction("Added by script!!", null, false) { DockingAction action = new DockingAction("Added by script!!", null) {
@Override @Override
public void actionPerformed(ActionContext context) { public void actionPerformed(ActionContext context) {
ConsoleService console = tool.getService(ConsoleService.class); ConsoleService console = tool.getService(ConsoleService.class);

View file

@ -59,9 +59,12 @@
the tool options. For example, the following keys cannot be changed:</P> the tool options. For example, the following keys cannot be changed:</P>
<UL> <UL>
<LI><TT><B>F1, Help, Ctrl-F1, F4</B></TT> (this bindings are reserved and cannot be used <LI>System Action Keybindings - System action default key bindings can be changed, with
when assigning key bindings to actions)</LI> added restrictions: 1) The binding for a System action cannot be used by any other action,
2) A key stroke bound to a System action cannot be used by another action until that
key stroke is cleared from the System action. The UI will show a message when these
restrictions are triggered.
</LI>
<LI>Menu Navigation Actions: <TT><B>1-9, Page Up/Down, End, Home</B></TT> (these key <LI>Menu Navigation Actions: <TT><B>1-9, Page Up/Down, End, Home</B></TT> (these key
bindings are usable with a menu or popup menu open and are otherwise available for bindings are usable with a menu or popup menu open and are otherwise available for
assignment to key bindings).</LI> assignment to key bindings).</LI>

View file

@ -21,24 +21,28 @@ import java.awt.Component;
import javax.swing.*; import javax.swing.*;
import docking.*; import docking.*;
import docking.actions.KeyBindingUtils;
import docking.actions.ToolActions;
import docking.widgets.label.GLabel; import docking.widgets.label.GLabel;
import generic.util.action.ReservedKeyBindings;
import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
class KeyBindingInputDialog extends DialogComponentProvider implements KeyEntryListener { class KeyBindingInputDialog extends DialogComponentProvider implements KeyEntryListener {
private KeyEntryTextField kbField; private KeyEntryTextField kbField;
private KeyStroke ks; private KeyStroke ks;
private boolean isCancelled; private boolean isCancelled;
private Plugin plugin;
KeyBindingInputDialog(Component parent, String scriptName, KeyStroke currentKeyStroke, KeyBindingInputDialog(Component parent, String scriptName, KeyStroke currentKeyStroke,
Plugin plugin, HelpLocation help) { Plugin plugin, HelpLocation help) {
super("Assign Script Key Binding", true, true, true, false); super("Assign Script Key Binding", true, true, true, false);
this.plugin = plugin;
kbField = new KeyEntryTextField(20, this); kbField = new KeyEntryTextField(20, this);
kbField.setName("KEY_BINDING"); kbField.setName("KEY_BINDING");
kbField.setText( kbField.setText(
currentKeyStroke == null ? "" : KeyEntryTextField.parseKeyStroke(currentKeyStroke)); currentKeyStroke == null ? "" : KeyBindingUtils.parseKeyStroke(currentKeyStroke));
JPanel panel = new JPanel(new BorderLayout(10, 10)); JPanel panel = new JPanel(new BorderLayout(10, 10));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
@ -55,8 +59,11 @@ class KeyBindingInputDialog extends DialogComponentProvider implements KeyEntryL
@Override @Override
protected void okCallback() { protected void okCallback() {
if (ks != null && ReservedKeyBindings.isReservedKeystroke(ks)) { PluginTool tool = plugin.getTool();
setStatusText(kbField.getText() + " is a reserved keystroke"); ToolActions toolActions = (ToolActions) tool.getToolActions();
String errorMessage = toolActions.validateActionKeyBinding(null, ks);
if (errorMessage != null) {
setStatusText(errorMessage);
return; return;
} }

View file

@ -47,7 +47,7 @@ public class ComponentInfoPlugin extends Plugin {
//@formatter:off //@formatter:off
DockingAction action = DockingAction action =
new ActionBuilder("Component Display", getName()) new ActionBuilder("Component Display", getName())
.menuPath("Help", "Show Diagnostic Component Information") .menuPath("Help", "Component Diagnostics")
.onAction(e -> showComponentDialog()) .onAction(e -> showComponentDialog())
.buildAndInstall(tool); .buildAndInstall(tool);
//@formatter:on //@formatter:on

View file

@ -235,7 +235,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
debug("a"); debug("a");
// get current options // get current options
ToolOptions toolKeyBindingOptions = (ToolOptions) getInstanceField("options", panel); ToolOptions toolKeyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
debug("b"); debug("b");
@ -289,7 +289,8 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
debug("i"); debug("i");
ToolOptions newlyLoadedDefaultOptions = (ToolOptions) getInstanceField("options", panel); ToolOptions newlyLoadedDefaultOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
assertOptionsMatch( assertOptionsMatch(
"The options from the first tool instance have changed " + "The options from the first tool instance have changed " +
"in the second tool instance even though the testing changes were not applied.", "in the second tool instance even though the testing changes were not applied.",
@ -328,7 +329,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
setKeyBindingsUpDialog(tool); setKeyBindingsUpDialog(tool);
newlyLoadedDefaultOptions = (ToolOptions) getInstanceField("options", panel); newlyLoadedDefaultOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
assertOptionsDontMatch( assertOptionsDontMatch(
"The options are the same after making changes, applying, closing and reloading.", "The options are the same after making changes, applying, closing and reloading.",
originalOptions, newlyLoadedDefaultOptions); originalOptions, newlyLoadedDefaultOptions);
@ -364,7 +365,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
assertEquals(newBinding, restoredBinding); assertEquals(newBinding, restoredBinding);
setKeyBindingsUpDialog(tool); setKeyBindingsUpDialog(tool);
ToolOptions options = (ToolOptions) getInstanceField("options", panel); ToolOptions options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
KeyStroke optionBinding = options.getKeyStroke(action.getFullName(), null); KeyStroke optionBinding = options.getKeyStroke(action.getFullName(), null);
assertEquals(appliedBinding, optionBinding); assertEquals(appliedBinding, optionBinding);
@ -457,7 +458,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
} }
private void setSelectionPath(final GTree tree, final TreePath path) throws Exception { private void setSelectionPath(final GTree tree, final TreePath path) throws Exception {
SwingUtilities.invokeAndWait(() -> tree.setSelectionPath(path)); runSwing(() -> tree.setSelectionPath(path));
} }
private GTreeNode getGTreeNode(GTreeNode parent, String nodeName) throws Exception { private GTreeNode getGTreeNode(GTreeNode parent, String nodeName) throws Exception {
@ -520,7 +521,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
if (actionName.equals(model.getValueAt(i, 0)) && if (actionName.equals(model.getValueAt(i, 0)) &&
owner.equals(model.getValueAt(i, 2))) { owner.equals(model.getValueAt(i, 2))) {
final int idx = i; final int idx = i;
SwingUtilities.invokeAndWait(() -> { runSwing(() -> {
table.setRowSelectionInterval(idx, idx); table.setRowSelectionInterval(idx, idx);
Rectangle rect = table.getCellRect(idx, idx, true); Rectangle rect = table.getCellRect(idx, idx, true);
table.scrollRectToVisible(rect); table.scrollRectToVisible(rect);

View file

@ -20,6 +20,7 @@ import static org.junit.Assert.*;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.event.InputEvent; import java.awt.event.InputEvent;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Set; import java.util.Set;
import javax.swing.*; import javax.swing.*;
@ -28,16 +29,17 @@ import javax.swing.table.*;
import org.junit.*; import org.junit.*;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import docking.KeyEntryTextField; import docking.action.*;
import docking.action.DockingActionIf; import docking.actions.KeyBindingUtils;
import docking.tool.util.DockingToolConstants; import docking.actions.ToolActions;
import docking.widgets.MultiLineLabel; import docking.widgets.MultiLineLabel;
import generic.test.TestUtils; import generic.test.TestUtils;
import generic.util.action.SystemKeyBindings;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv; import ghidra.test.TestEnv;
import ghidra.util.Msg;
/** /**
* Tests for key bindings option panel * Tests for key bindings option panel
@ -62,6 +64,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
tool = env.getTool(); tool = env.getTool();
tool.addPlugin(CodeBrowserPlugin.class.getName()); tool.addPlugin(CodeBrowserPlugin.class.getName());
tool.addPlugin(GoToAddressLabelPlugin.class.getName());
env.showTool(); env.showTool();
@ -123,14 +126,6 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
} }
} }
private boolean ignoreAction(DockingActionIf action) {
if (!action.getKeyBindingType().isManaged()) {
return true;
}
return action.getFullName().contains("Table Data");
}
@Test @Test
public void testEditKeyBinding() throws Exception { public void testEditKeyBinding() throws Exception {
// find action that has a keystroke assigned // find action that has a keystroke assigned
@ -139,7 +134,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
selectRowForAction(action); selectRowForAction(action);
triggerText(keyField, "z"); triggerText(keyField, "z");
assertEquals("Z", keyField.getText()); assertKeyFieldText("Z");
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0), getKeyStroke(action)); assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0), getKeyStroke(action));
@ -157,24 +152,19 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
} }
triggerText(keyField, "z"); triggerText(keyField, "z");
assertTrue(statusPane.getText().indexOf("No action is selected.") != -1); assertMessage("No action is selected.");
} }
@Test @Test
public void testSetKeyBinding() throws Exception { public void testSetKeyBinding() throws Exception {
// set a key binding on an action that does not have a key binding // set a key binding on an action that does not have a key binding
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_X); triggerActionKey(keyField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_X);
assertEquals( KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK);
KeyEntryTextField.parseKeyStroke( assertKeyFieldText(KeyBindingUtils.parseKeyStroke(ks));
KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK)),
keyField.getText());
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), assertEquals(ks, getKeyStroke(action1));
getKeyStroke(action1));
} }
@Test @Test
@ -182,57 +172,34 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
selectRowForAction(action1); selectRowForAction(action1);
triggerText(keyField, "x"); triggerText(keyField, "x");
assertEquals("X", keyField.getText()); assertKeyFieldText("X");
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), getKeyStroke(action1)); assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), getKeyStroke(action1));
}
@Test
public void testSetKeyBindingNotAllowed() throws Exception {
selectRowForAction(action1);
triggerActionKey(keyField, 0, KeyEvent.VK_F1);
// F1 is the help key and cannot be used
assertEquals("", keyField.getText());
triggerActionKey(keyField, 0, KeyEvent.VK_HELP);
assertEquals("", keyField.getText());
triggerActionKey(keyField, 0, KeyEvent.VK_SHIFT);
assertEquals("", keyField.getText());
triggerActionKey(keyField, 0, KeyEvent.VK_ENTER);
assertEquals("", keyField.getText());
} }
@Test @Test
public void testSetKeyBinding3() throws Exception { public void testSetKeyBinding3() throws Exception {
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_HOME); typeKeyStroke(InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_HOME);
assertEquals( KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_DOWN_MASK);
KeyEntryTextField.parseKeyStroke( assertKeyFieldText(ks);
KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_DOWN_MASK)),
keyField.getText());
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_DOWN_MASK), assertEquals(ks, getKeyStroke(action1));
getKeyStroke(action1));
} }
@Test @Test
public void testSetKeyBinding4() throws Exception { public void testSetKeyBinding4() throws Exception {
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, 0, KeyEvent.VK_PAGE_UP); typeKeyStroke(KeyEvent.VK_PAGE_UP);
assertEquals( KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0);
KeyEntryTextField.parseKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0)), assertKeyFieldText(ks);
keyField.getText());
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), getKeyStroke(action1)); assertEquals(ks, getKeyStroke(action1));
} }
@Test @Test
@ -259,13 +226,11 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
// set the new binding that uses the 'Alt' key // set the new binding that uses the 'Alt' key
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, InputEvent.ALT_DOWN_MASK, keyCode); typeKeyStroke(InputEvent.ALT_DOWN_MASK, keyCode);
String keyStrokeString = KeyEntryTextField.parseKeyStroke( KeyStroke ks = KeyStroke.getKeyStroke(keyCode, InputEvent.ALT_DOWN_MASK);
KeyStroke.getKeyStroke(keyCode, InputEvent.ALT_DOWN_MASK)); assertKeyFieldText(ks);
assertEquals(keyStrokeString, keyField.getText());
apply(); apply();
assertEquals(KeyStroke.getKeyStroke(keyCode, InputEvent.ALT_DOWN_MASK), assertEquals(ks, getKeyStroke(action1));
getKeyStroke(action1));
// verify the additional binding for 'Alt Graph' // verify the additional binding for 'Alt Graph'
action = action =
@ -278,8 +243,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, 0, KeyEvent.VK_ENTER); typeKeyStroke(KeyEvent.VK_ENTER);
assertEquals("", keyField.getText()); assertNoKeyStrokeText();
apply(); apply();
assertNull(getKeyStroke(action1)); assertNull(getKeyStroke(action1));
@ -289,8 +254,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
public void testClearKeyBinding2() throws Exception { public void testClearKeyBinding2() throws Exception {
selectRowForAction(action1); selectRowForAction(action1);
triggerText(keyField, "\b"); typeBackspace();
assertEquals("", keyField.getText()); assertNoKeyStrokeText();
apply(); apply();
assertNull(getKeyStroke(action1)); assertNull(getKeyStroke(action1));
@ -302,31 +267,36 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
// verify that a list of collisions show up // verify that a list of collisions show up
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, 0, KeyEvent.VK_OPEN_BRACKET); typeKeyStroke(KeyEvent.VK_OPEN_BRACKET);
apply(); apply();
// set same binding on a different action, which will trigger the collisions list // set same binding on a different action, which will trigger the collisions list
selectRowForAction(action2); selectRowForAction(action2);
triggerActionKey(keyField, 0, KeyEvent.VK_OPEN_BRACKET); typeKeyStroke(KeyEvent.VK_OPEN_BRACKET);
MultiLineLabel label = (MultiLineLabel) findComponentByName(panel, "CollisionLabel"); MultiLineLabel label = (MultiLineLabel) findComponentByName(panel, "CollisionLabel");
String msg = label.getLabel(); String msg = label.getLabel();
String[] lines = msg.split("\n"); String[] lines = msg.split("\n");
assertEquals(3, lines.length); assertEquals(3, lines.length);
assertTrue(lines[1].contains(action1.getName()));
assertTrue(lines[2].contains(action2.getName())); 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);
} }
@Test @Test
public void testSetReservedKeybinding() throws Exception { public void testSetReservedKeybinding() throws Exception {
// try to set a reserved keybinding // try to set a reserved keybinding
KeyStroke reservedKeystroke = KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0); KeyStroke reservedKeystroke = SystemKeyBindings.UPDATE_KEY_BINDINGS_KEY; // F4
selectRowForAction(action1); selectRowForAction(action1);
triggerActionKey(keyField, 0, reservedKeystroke.getKeyCode()); typeKeyStroke(reservedKeystroke);
assertEquals("", keyField.getText()); assertNoKeyStrokeText();
assertMessage("F4 in use by System action 'Set KeyBinding'");
apply(); apply();
assertEquals(null, getKeyStroke(action1)); assertEquals(null, getKeyStroke(action1));
@ -336,8 +306,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
selectRowForAction(action1); selectRowForAction(action1);
KeyStroke validKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK); KeyStroke validKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK);
triggerActionKey(keyField, InputEvent.CTRL_DOWN_MASK, validKeyStroke.getKeyCode()); typeKeyStroke(validKeyStroke);
assertEquals(KeyEntryTextField.parseKeyStroke(validKeyStroke), keyField.getText()); assertKeyFieldText(validKeyStroke);
apply(); apply();
assertEquals(validKeyStroke, getKeyStroke(action1)); assertEquals(validKeyStroke, getKeyStroke(action1));
@ -345,22 +315,172 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
// try again to set a reserved binding // try again to set a reserved binding
setUpDialog(); setUpDialog();
selectRowForAction(action1); selectRowForAction(action1);
assertEquals(validKeyStroke, getKeyStroke(action1)); assertEquals(validKeyStroke, getKeyStroke(action1));
String originalText = keyField.getText(); typeKeyStroke(reservedKeystroke.getKeyCode());
triggerActionKey(keyField, 0, reservedKeystroke.getKeyCode()); assertNoKeyStrokeText();
assertMessage("F4 in use by System action 'Set KeyBinding'");
assertEquals(originalText, keyField.getText());
apply(); apply();
assertEquals(validKeyStroke, getKeyStroke(action1)); assertEquals(validKeyStroke, getKeyStroke(action1));
} }
@Test
public void testSetKeyBindingOnSystemAction() throws Exception {
//
// Test that users can change the keybinding for a System action. The new binding cannot
// be in use by any other action.
//
DockingActionIf goToAction = getAction(tool, "Go To Address/Label"); // arbitrary plugin action
KeyStroke goToKs = goToAction.getKeyBinding();
assertNotNull(goToKs);
DockingActionIf systemAction = getAction("Show Context Menu"); // arbitrary system action
KeyStroke systemKs = systemAction.getKeyBinding();
assertNotNull(systemKs);
selectRowForAction(systemAction);
typeKeyStroke(goToKs);
assertNoKeyStrokeText();
assertMessage("System action cannot be set to in-use key stroke");
apply();
assertEquals(systemKs, getKeyStroke(systemAction)); // unchanged
// clear the in-use binding and then try again
clearKeyBinding(goToKs);
setUpDialog();
selectRowForAction(systemAction);
typeKeyStroke(goToKs);
assertKeyFieldText("G");
apply();
assertEquals(goToKs, getKeyStroke(systemAction));
}
@Test
public void testSetKeybindingUsingSystemDefaultBinding_InUse() throws Exception {
//
// Test that users can change the keybinding for a non-System action to use a pre-defined
// System key stroke only if the binding is not in-use by a System action.
//
// This test will clear the system key binding in the UI by using the backspace key.
// Note: The 'Apply' button must be pressed before the system key stroke can be reused.
//
DockingActionIf systemAction = getAction("Show Context Menu"); // arbitrary system action
KeyStroke systemKs = systemAction.getKeyBinding();
String systemKsText = KeyBindingUtils.parseKeyStroke(systemKs);
assertEquals(SystemKeyBindings.CONTEXT_MENU_KEY1, systemKs);
assertNotNull(systemKs);
DockingActionIf goToAction = getAction(tool, "Go To Address/Label"); // arbitrary plugin action
KeyStroke goToKs = goToAction.getKeyBinding();
assertNotNull(goToKs);
setUpDialog();
selectRowForAction(action1);
typeKeyStroke(systemKs);
assertNoKeyStrokeText();
assertMessage(systemKsText + " in use by System action 'Show Context Menu'");
selectRowForAction(systemAction);
typeBackspace();
apply();
assertEquals(null, getKeyStroke(systemAction));
selectRowForAction(action1);
typeKeyStroke(systemKs);
assertKeyFieldText(systemKsText);
assertNoErrorMessage();
}
//================================================================================================== //==================================================================================================
// Private Methods // Private Methods
//================================================================================================== //==================================================================================================
private boolean ignoreAction(DockingActionIf action) {
if (!action.getKeyBindingType().isManaged()) {
return true;
}
return action.getFullName().contains("Table Data");
}
private void assertNoKeyStrokeText() {
assertEquals("", keyField.getText());
}
private void assertKeyFieldText(KeyStroke ks) {
assertKeyFieldText(KeyBindingUtils.parseKeyStroke(ks));
}
private void assertKeyFieldText(String s) {
assertEquals(s, runSwing(() -> keyField.getText()));
}
private void assertNoErrorMessage() {
assertMessage("");
}
private void typeBackspace() {
triggerBackspaceKey(keyField);
waitForSwing();
}
private void typeKeyStroke(KeyStroke ks) {
triggerKey(keyField, ks);
waitForSwing();
}
private void typeKeyStroke(int keyCode) {
typeKeyStroke(0, keyCode);
}
private void typeKeyStroke(int modifiers, int keyCode) {
triggerKey(keyField, modifiers, keyCode, KeyEvent.CHAR_UNDEFINED);
waitForSwing();
}
private void clearKeyBinding(KeyStroke ks) {
ToolActions toolActions = (ToolActions) tool.getToolActions();
Action action = toolActions.getAction(ks);
if (action instanceof MultipleKeyAction multiAction) {
List<DockingActionIf> actions = multiAction.getActions();
for (DockingActionIf dockingAction : actions) {
runSwing(() -> dockingAction.setKeyBindingData(null));
}
}
else if (action instanceof SystemKeyBindingAction systemAction) {
DockingActionIf dockingAction = systemAction.getAction();
runSwing(() -> dockingAction.setKeyBindingData(null));
}
}
private DockingActionIf getAction(String name) {
Set<DockingActionIf> actions = tool.getAllActions();
for (DockingActionIf action : actions) {
if (action.getName().equals(name)) {
return action;
}
}
fail("Unable to find System action '%s'".formatted(name));
return null;
}
private void assertMessage(String text) {
String kbStatusMessage = runSwing(panel::getStatusText);
if (!kbStatusMessage.contains(text)) {
fail("Expected message: " + text + ". Found message: " + kbStatusMessage);
}
}
private void apply() { private void apply() {
runSwing(() -> panel.apply()); runSwing(() -> panel.apply());
waitForSwing(); waitForSwing();
@ -374,7 +494,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
Set<DockingActionIf> list = tool.getAllActions(); Set<DockingActionIf> list = tool.getAllActions();
for (DockingActionIf action : list) { for (DockingActionIf action : list) {
KeyStroke ks = action.getKeyBinding(); KeyStroke ks = action.getKeyBinding();
if (ignoreAction(action) && ks != null && if (!ignoreAction(action) && ks != null &&
ks != KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0)) { ks != KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0)) {
return action; return action;
} }
@ -390,7 +510,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
if (actionName.equals(model.getValueAt(i, 0))) { if (actionName.equals(model.getValueAt(i, 0))) {
if (ks != null) { if (ks != null) {
String ksStr = (String) model.getValueAt(i, 1); String ksStr = (String) model.getValueAt(i, 1);
return ksStr.equals(KeyEntryTextField.parseKeyStroke(ks)); return ksStr.equals(KeyBindingUtils.parseKeyStroke(ks));
} }
return true; return true;
} }
@ -398,24 +518,22 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
return false; return false;
} }
private void selectRowForAction(DockingActionIf action) throws Exception { private void selectRowForAction(DockingActionIf action) {
String actionName = action.getName(); String actionName = action.getName();
Msg.debug(this, "Keybinding Action: " + action.getFullName());
for (int i = 0; i < model.getRowCount(); i++) { for (int i = 0; i < model.getRowCount(); i++) {
if (actionName.equals(model.getValueAt(i, 0))) { if (actionName.equals(model.getValueAt(i, 0))) {
final int idx = i; int idx = i;
Msg.debug(this, "\tselection row for action: " + i);
runSwing(() -> { runSwing(() -> {
table.setRowSelectionInterval(idx, idx); table.setRowSelectionInterval(idx, idx);
Rectangle rect = table.getCellRect(idx, idx, true); Rectangle rect = table.getCellRect(idx, idx, true);
table.scrollRectToVisible(rect); table.scrollRectToVisible(rect);
}); });
waitForSwing();
return; return;
} }
} }
waitForSwing(); fail("Could not find action to select: " + action);
} }
private KeyStroke getKeyStroke(DockingActionIf action) { private KeyStroke getKeyStroke(DockingActionIf action) {
@ -423,8 +541,15 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
} }
private void setUpDialog() throws Exception { private void setUpDialog() throws Exception {
if (panel != null) {
runSwing(() -> { runSwing(() -> {
panel = new KeyBindingsPanel(tool, tool.getOptions(DockingToolConstants.KEY_BINDINGS)); dialog.setVisible(false);
});
}
runSwing(() -> {
panel = new KeyBindingsPanel(tool);
panel.setOptionsPropertyChangeListener(evt -> { panel.setOptionsPropertyChangeListener(evt -> {
// stub // stub
}); });
@ -439,6 +564,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
keyField = (JTextField) getInstanceField("ksField", panel); keyField = (JTextField) getInstanceField("ksField", panel);
statusPane = findComponent(panel, JTextPane.class); statusPane = findComponent(panel, JTextPane.class);
model = table.getModel(); model = table.getModel();
waitForSwing();
} }
// find 2 actions that do not have key bindings so that we can add and change the values // find 2 actions that do not have key bindings so that we can add and change the values

View file

@ -38,6 +38,7 @@ import docking.action.DockingActionIf;
import docking.actions.KeyBindingUtils; import docking.actions.KeyBindingUtils;
import docking.options.editor.*; import docking.options.editor.*;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import docking.tool.util.DockingToolConstants;
import docking.widgets.MultiLineLabel; import docking.widgets.MultiLineLabel;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
@ -51,6 +52,7 @@ import ghidra.app.plugin.core.console.ConsolePlugin;
import ghidra.app.util.viewer.options.OptionsGui; import ghidra.app.util.viewer.options.OptionsGui;
import ghidra.app.util.viewer.options.ScreenElement; import ghidra.app.util.viewer.options.ScreenElement;
import ghidra.framework.main.ConsoleTextPane; import ghidra.framework.main.ConsoleTextPane;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.options.*; import ghidra.framework.options.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.dialog.KeyBindingsPanel; import ghidra.framework.plugintool.dialog.KeyBindingsPanel;
@ -422,47 +424,48 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
String actionName = "Clear Cut"; String actionName = "Clear Cut";
String pluginName = "DataTypeManagerPlugin"; String pluginName = "DataTypeManagerPlugin";
KeyStroke defaultKeyStroke = getKeyBinding(actionName); KeyStroke defaultKeyStroke = getKeyBinding(actionName);
assertOptionsKeyStroke(actionName, pluginName, defaultKeyStroke); assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
int keyCode = KeyEvent.VK_Q; int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK; int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q'); KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
apply(); apply();
assertOptionsKeyStroke(actionName, pluginName, newKeyStroke); assertOptionsKeyStroke(tool, actionName, pluginName, newKeyStroke);
restoreDefaults(); restoreDefaults();
KeyStroke currentBinding = getKeyBinding(actionName); KeyStroke currentBinding = getKeyBinding(actionName);
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke, assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
currentBinding); currentBinding);
assertOptionsKeyStroke(actionName, pluginName, defaultKeyStroke); assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
} }
@Test @Test
public void testRestoreDefaultsForFrontEndKeybindings() throws Exception { public void testRestoreDefaultsForFrontEndKeybindings() throws Exception {
runSwing(() -> dialog.close()); runSwing(() -> dialog.close());
setUpDialog(env.getFrontEndTool()); FrontEndTool frontEndTool = env.getFrontEndTool();
setUpDialog(frontEndTool);
String actionName = "Archive Project"; String actionName = "Archive Project";
String pluginName = "ArchivePlugin"; String pluginName = "ArchivePlugin";
KeyStroke defaultKeyStroke = getKeyBinding(actionName); KeyStroke defaultKeyStroke = getKeyBinding(actionName);
assertOptionsKeyStroke(actionName, pluginName, defaultKeyStroke); assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
int keyCode = KeyEvent.VK_Q; int keyCode = KeyEvent.VK_Q;
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK; int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q'); KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
apply(); apply();
assertOptionsKeyStroke(actionName, pluginName, newKeyStroke); assertOptionsKeyStroke(frontEndTool, actionName, pluginName, newKeyStroke);
restoreDefaults(); restoreDefaults();
KeyStroke currentBinding = getKeyBinding(actionName); KeyStroke currentBinding = getKeyBinding(actionName);
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke, assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
currentBinding); currentBinding);
assertOptionsKeyStroke(actionName, pluginName, defaultKeyStroke); assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
} }
@Test @Test
@ -763,12 +766,9 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
return KeyBindingUtils.parseKeyStroke(keyBindingColumnValue); return KeyBindingUtils.parseKeyStroke(keyBindingColumnValue);
} }
private void assertOptionsKeyStroke(String actionName, String pluginName, KeyStroke value) private void assertOptionsKeyStroke(PluginTool pluginTool, String actionName, String pluginName,
throws Exception { KeyStroke value) throws Exception {
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings"); Options options = pluginTool.getOptions(DockingToolConstants.KEY_BINDINGS);
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
Options options = (Options) getInstanceField("options", panel);
KeyStroke optionsKeyStroke = KeyStroke optionsKeyStroke =
options.getKeyStroke(actionName + " (" + pluginName + ")", null); options.getKeyStroke(actionName + " (" + pluginName + ")", null);
assertEquals("The options keystroke does not match the value in keybinding options table", assertEquals("The options keystroke does not match the value in keybinding options table",
@ -784,6 +784,7 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
JTextField textField = (JTextField) getInstanceField("ksField", panel); JTextField textField = (JTextField) getInstanceField("ksField", panel);
triggerKey(textField, modifiers, keyCode, keyChar); triggerKey(textField, modifiers, keyCode, keyChar);
waitForSwing();
KeyStroke expectedKeyStroke = KeyStroke.getKeyStroke(keyCode, modifiers, false); KeyStroke expectedKeyStroke = KeyStroke.getKeyStroke(keyCode, modifiers, false);
KeyStroke currentBinding = getKeyBinding(actionName); KeyStroke currentBinding = getKeyBinding(actionName);

View file

@ -30,7 +30,7 @@ import docking.actions.KeyBindingUtils;
*/ */
public abstract class DockingKeyBindingAction extends AbstractAction { public abstract class DockingKeyBindingAction extends AbstractAction {
private DockingActionIf dockingAction; protected DockingActionIf dockingAction;
protected final KeyStroke keyStroke; protected final KeyStroke keyStroke;
protected final Tool tool; protected final Tool tool;
@ -54,7 +54,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
public abstract KeyBindingPrecedence getKeyBindingPrecedence(); public abstract KeyBindingPrecedence getKeyBindingPrecedence();
public boolean isReservedKeybindingPrecedence() { public boolean isSystemKeybindingPrecedence() {
return false; return false;
} }

View file

@ -127,8 +127,8 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return false; // let the normal event flow continue return false; // let the normal event flow continue
} }
// *Special*, reserved key bindings--these can always be processed // *Special*, System key bindings--these can always be processed and are a higher priority
if (processReservedKeyActionsPrecedence(action, event)) { if (processSystemActionPrecedence(action, event)) {
return true; return true;
} }
@ -245,6 +245,16 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return true; // default case; allow it through return true; // default case; allow it through
} }
private boolean isSettingKeyBindings(KeyEvent event) {
Component destination = event.getComponent();
if (destination == null) {
Component focusOwner = focusProvider.getFocusOwner();
destination = focusOwner;
}
return destination instanceof KeyEntryTextField;
}
private boolean willBeHandledByTextComponent(KeyEvent event) { private boolean willBeHandledByTextComponent(KeyEvent event) {
Component destination = event.getComponent(); Component destination = event.getComponent();
@ -301,10 +311,16 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
throw new AssertException("New precedence added to KeyBindingPrecedence?"); throw new AssertException("New precedence added to KeyBindingPrecedence?");
} }
private boolean processReservedKeyActionsPrecedence(DockingKeyBindingAction action, private boolean processSystemActionPrecedence(DockingKeyBindingAction action,
KeyEvent event) { KeyEvent event) {
if (!action.isReservedKeybindingPrecedence()) { if (isSettingKeyBindings(event)) {
// This means the user is setting keybindings. Do not process System actions during
// this operation so that the user can assign those keybindings.
return false;
}
if (!action.isSystemKeybindingPrecedence()) {
return false; return false;
} }

View file

@ -25,7 +25,7 @@ import java.awt.event.KeyEvent;
public enum KeyBindingPrecedence { public enum KeyBindingPrecedence {
/** Actions at this level will be processed before all others, including Java components'. */ /** Actions at this level will be processed before all others, including Java components'. */
ReservedActionsLevel, SystemActionsLevel,
/** Actions with this precedence will be processed before key listener on Java components. */ /** Actions with this precedence will be processed before key listener on Java components. */
KeyListenerLevel, KeyListenerLevel,

View file

@ -57,18 +57,7 @@ public class KeyEntryTextField extends JTextField {
*/ */
public void setKeyStroke(KeyStroke ks) { public void setKeyStroke(KeyStroke ks) {
processEntry(ks); processEntry(ks);
setText(parseKeyStroke(ks)); setText(KeyBindingUtils.parseKeyStroke(ks));
}
/**
* Converts the toString() form of the keyStroke, e.g., Ctrl-M is returned as
* "keyCode CtrlM-P" and we want it to look like: "Ctrl-M"
*
* @param ks the keystroke to parse
* @return the parse string for the keystroke
*/
public static String parseKeyStroke(KeyStroke ks) {
return KeyBindingUtils.parseKeyStroke(ks);
} }
public void clearField() { public void clearField() {
@ -114,23 +103,14 @@ public class KeyEntryTextField extends JTextField {
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode(); int keyCode = e.getKeyCode();
if (isHelpKey(keyCode)) {
return;
}
KeyStroke keyStroke = null; KeyStroke keyStroke = null;
if (!isClearKey(keyCode) && !isModifiersOnly(e)) { if (!isClearKey(keyCode) && !isModifiersOnly(e)) {
keyStroke = KeyStroke.getKeyStroke(keyCode, e.getModifiersEx()); keyStroke = KeyStroke.getKeyStroke(keyCode, e.getModifiersEx());
} }
processEntry(keyStroke); processEntry(keyStroke);
e.consume(); e.consume();
} }
private boolean isHelpKey(int keyCode) {
return keyCode == KeyEvent.VK_F1 || keyCode == KeyEvent.VK_HELP;
}
private boolean isClearKey(int keyCode) { private boolean isClearKey(int keyCode) {
return keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER; return keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER;
} }

View file

@ -0,0 +1,156 @@
/* ###
* 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.action;
import java.awt.*;
import javax.swing.KeyStroke;
import docking.*;
import ghidra.util.HelpLocation;
import ghidra.util.SystemUtilities;
import help.HelpService;
/**
* A base system action used for actions that show help information.
*/
public abstract class AbstractHelpAction extends DockingAction {
public AbstractHelpAction(String name, KeyStroke keyStroke, boolean isPrimary) {
super(name, DockingWindowManager.DOCKING_WINDOWS_OWNER, isPrimary);
// Only the primary action will appear in the tool' key binding settings UI. The primary
// action can be managed by the users. The secondary action is not managed at this time.
if (isPrimary) {
setKeyBindingData(new KeyBindingData(keyStroke));
}
else {
createSystemKeyBinding(keyStroke);
}
setEnabled(true);
// Help actions don't have help
DockingWindowManager.getHelpService().excludeFromHelp(this);
}
protected abstract boolean isInfo();
@Override
public boolean isEnabledForContext(ActionContext context) {
return true;
}
@Override
public void actionPerformed(ActionContext context) {
DockingActionIf mouseOverAction = DockingWindowManager.getMouseOverAction();
if (mouseOverAction != null) {
showHelp(mouseOverAction);
return;
}
Object mouseOverObject = DockingWindowManager.getMouseOverObject();
Object helpObject = getFirstAvailableObjectThatHasHelp(mouseOverObject);
if (helpObject != null) {
showHelp(helpObject);
return;
}
// some components are special in that they have help registered just for them
Object eventSource = context.getSourceObject();
helpObject = getFirstAvailableObjectThatHasHelp(eventSource);
if (helpObject != null) {
showHelp(helpObject);
return;
}
// dialogs help is handled differently than core Ghidra components
DialogComponentProvider dialogProvider = findDialogComponentProvider();
if (dialogProvider != null) {
showHelp(dialogProvider.getComponent());
return;
}
// handle our 'normal' CompentProviders...just use the focused provider
DockingWindowManager windowManager = DockingWindowManager.getActiveInstance();
ComponentPlaceholder info = windowManager.getFocusedComponent();
if (info != null) {
ComponentProvider componentProvider = info.getProvider();
showHelp(componentProvider);
return;
}
}
private void showHelp(Object helpObject) {
SystemUtilities.runSwingLater(() -> {
DockingWindowManager windowManager = DockingWindowManager.getActiveInstance();
Component component = windowManager.getActiveComponent();
DockingWindowManager.getHelpService().showHelp(helpObject, isInfo(), component);
});
}
private DialogComponentProvider findDialogComponentProvider() {
KeyboardFocusManager keyboardFocusManager =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window activeWindow = keyboardFocusManager.getActiveWindow();
if (activeWindow instanceof DockingDialog) {
DockingDialog dockingDialog = (DockingDialog) activeWindow;
return dockingDialog.getDialogComponent();
}
return null;
}
private Object getFirstAvailableObjectThatHasHelp(Object startingHelpObject) {
if (startingHelpObject == null) {
return null;
}
// First see if help exists for the given component directly...
HelpService helpService = DockingWindowManager.getHelpService();
HelpLocation helpLocation = helpService.getHelpLocation(startingHelpObject);
if (helpLocation != null) {
return startingHelpObject;
}
// Second, with no help registered for the starting component, start looking for a suitable
// help proxy.
//
// For Components, we can walk their containment hierarchy to find a potential help object
if (!(startingHelpObject instanceof Component)) {
// not a Component; don't know how to find a better help object
return null;
}
return getFirstAvailableComponentThatHasHelp((Component) startingHelpObject);
}
private Component getFirstAvailableComponentThatHasHelp(Component component) {
HelpService helpService = DockingWindowManager.getHelpService();
HelpLocation helpLocation = helpService.getHelpLocation(component);
if (helpLocation != null) {
return component;
}
Container parent = component.getParent();
if (parent == null) {
// nothing else to check
return null;
}
return getFirstAvailableComponentThatHasHelp(parent);
}
}

View file

@ -29,14 +29,14 @@ import org.apache.commons.lang3.StringUtils;
import docking.ActionContext; import docking.ActionContext;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import generic.theme.GColor; import generic.theme.GColor;
import generic.util.action.ReservedKeyBindings; import generic.util.action.SystemKeyBindings;
import ghidra.util.Msg; import ghidra.util.Msg;
public class ComponentThemeInspectorAction extends DockingAction { public class ComponentThemeInspectorAction extends DockingAction {
public ComponentThemeInspectorAction() { public ComponentThemeInspectorAction() {
super("Component Theme Inspector", DockingWindowManager.DOCKING_WINDOWS_OWNER, false); super("Component Theme Inspector", DockingWindowManager.DOCKING_WINDOWS_OWNER, true);
createReservedKeyBinding(ReservedKeyBindings.COMPONENT_THEME_INFO_KEY); createSystemKeyBinding(SystemKeyBindings.COMPONENT_THEME_INFO_KEY);
// System action; no help needed // System action; no help needed
DockingWindowManager.getHelpService().excludeFromHelp(this); DockingWindowManager.getHelpService().excludeFromHelp(this);
@ -99,9 +99,6 @@ public class ComponentThemeInspectorAction extends DockingAction {
next = new Entry(component); next = new Entry(component);
} }
if (entry != null) {
entry.parent = next;
}
entry = next; entry = next;
tree.add(entry); tree.add(entry);
@ -199,7 +196,6 @@ public class ComponentThemeInspectorAction extends DockingAction {
private class Entry { private class Entry {
private Entry parent;
protected Component component; protected Component component;
protected Point mouseLocation; protected Point mouseLocation;

View file

@ -372,8 +372,8 @@ public abstract class DockingAction implements DockingActionIf {
precedence = kbData.getKeyBindingPrecedence(); precedence = kbData.getKeyBindingPrecedence();
} }
if (precedence == KeyBindingPrecedence.ReservedActionsLevel) { if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
return true; // reserved actions are special return true; // system actions are special
} }
// log a trace message instead of throwing an exception, as to not break any legacy code // log a trace message instead of throwing an exception, as to not break any legacy code
@ -453,8 +453,8 @@ public abstract class DockingAction implements DockingActionIf {
* other actions are prevented from using the same KeyStroke as a reserved keybinding. * other actions are prevented from using the same KeyStroke as a reserved keybinding.
* @param keyStroke the keystroke to be used for the keybinding * @param keyStroke the keystroke to be used for the keybinding
*/ */
void createReservedKeyBinding(KeyStroke keyStroke) { void createSystemKeyBinding(KeyStroke keyStroke) {
KeyBindingData data = KeyBindingData.createReservedKeyBindingData(keyStroke); KeyBindingData data = KeyBindingData.createSystemKeyBindingData(keyStroke);
setKeyBindingData(data); setKeyBindingData(data);
} }

View file

@ -53,7 +53,7 @@ public class GlobalFocusTraversalAction extends DockingAction {
DockingWindowManager.DOCKING_WINDOWS_OWNER); DockingWindowManager.DOCKING_WINDOWS_OWNER);
this.forward = forward; this.forward = forward;
createReservedKeyBinding(keybinding); createSystemKeyBinding(keybinding);
setEnabled(isGlobalFocusTraversalEnabled()); setEnabled(isGlobalFocusTraversalEnabled());
} }

View file

@ -15,131 +15,16 @@
*/ */
package docking.action; package docking.action;
import java.awt.*;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import docking.*; public class HelpAction extends AbstractHelpAction {
import ghidra.util.HelpLocation;
import ghidra.util.SystemUtilities;
import help.HelpService;
public class HelpAction extends DockingAction { public HelpAction(KeyStroke keyStroke, boolean isPrimary) {
super("Context Help", keyStroke, isPrimary);
private boolean infoOnly;
public HelpAction(boolean infoOnly, KeyStroke keybinding) {
super(infoOnly ? "HelpInfo" : "Help", DockingWindowManager.DOCKING_WINDOWS_OWNER, false);
createReservedKeyBinding(keybinding);
setEnabled(true);
this.infoOnly = infoOnly;
// Help actions don't have help
DockingWindowManager.getHelpService().excludeFromHelp(this);
} }
@Override @Override
public boolean isEnabledForContext(ActionContext context) { public boolean isInfo() {
return true; return false;
}
@Override
public void actionPerformed(ActionContext context) {
DockingActionIf mouseOverAction = DockingWindowManager.getMouseOverAction();
if (mouseOverAction != null) {
showHelp(mouseOverAction);
return;
}
Object mouseOverObject = DockingWindowManager.getMouseOverObject();
Object helpObject = getFirstAvailableObjectThatHasHelp(mouseOverObject);
if (helpObject != null) {
showHelp(helpObject);
return;
}
// some components are special in that they have help registered just for them
Object eventSource = context.getSourceObject();
helpObject = getFirstAvailableObjectThatHasHelp(eventSource);
if (helpObject != null) {
showHelp(helpObject);
return;
}
// dialogs help is handled differently than core Ghidra components
DialogComponentProvider dialogProvider = findDialogComponentProvider();
if (dialogProvider != null) {
showHelp(dialogProvider.getComponent());
return;
}
// handle our 'normal' CompentProviders...just use the focused provider
DockingWindowManager windowManager = DockingWindowManager.getActiveInstance();
ComponentPlaceholder info = windowManager.getFocusedComponent();
if (info != null) {
ComponentProvider componentProvider = info.getProvider();
showHelp(componentProvider);
return;
}
}
private void showHelp(final Object helpObject) {
SystemUtilities.runSwingLater(() -> {
DockingWindowManager windowManager = DockingWindowManager.getActiveInstance();
Component component = windowManager.getActiveComponent();
DockingWindowManager.getHelpService().showHelp(helpObject, infoOnly, component);
});
}
private DialogComponentProvider findDialogComponentProvider() {
KeyboardFocusManager keyboardFocusManager =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window activeWindow = keyboardFocusManager.getActiveWindow();
if (activeWindow instanceof DockingDialog) {
DockingDialog dockingDialog = (DockingDialog) activeWindow;
return dockingDialog.getDialogComponent();
}
return null;
}
private Object getFirstAvailableObjectThatHasHelp(Object startingHelpObject) {
if (startingHelpObject == null) {
return null;
}
// First see if help exists for the given component directly...
HelpService helpService = DockingWindowManager.getHelpService();
HelpLocation helpLocation = helpService.getHelpLocation(startingHelpObject);
if (helpLocation != null) {
return startingHelpObject;
}
// Second, with no help registered for the starting component, start looking for a suitable
// help proxy.
//
// For Components, we can walk their containment hierarchy to find a potential help object
if (!(startingHelpObject instanceof Component)) {
// not a Component; don't know how to find a better help object
return null;
}
return getFirstAvailableComponentThatHasHelp((Component) startingHelpObject);
}
private Component getFirstAvailableComponentThatHasHelp(Component component) {
HelpService helpService = DockingWindowManager.getHelpService();
HelpLocation helpLocation = helpService.getHelpLocation(component);
if (helpLocation != null) {
return component;
}
Container parent = component.getParent();
if (parent == null) {
// nothing else to check
return null;
}
return getFirstAvailableComponentThatHasHelp(parent);
} }
} }

View file

@ -17,21 +17,14 @@ package docking.action;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import docking.*; public class HelpInfoAction extends AbstractHelpAction {
class ReservedKeyBindingAction extends DockingKeyBindingAction { public HelpInfoAction(KeyStroke keybinding) {
super("Help Info", keybinding, true);
ReservedKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
super(tool, action, keyStroke);
} }
@Override @Override
public boolean isReservedKeybindingPrecedence() { public boolean isInfo() {
return true; return true;
} }
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
return KeyBindingPrecedence.ReservedActionsLevel;
}
} }

View file

@ -63,9 +63,9 @@ public class KeyBindingData {
} }
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) { public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
if (precedence == KeyBindingPrecedence.ReservedActionsLevel) { if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Can't set precedence to Reserved KeyBindingPrecedence"); "Can't set precedence to System KeyBindingPrecedence");
} }
this.keyStroke = keyStroke; this.keyStroke = keyStroke;
this.keyBindingPrecedence = precedence; this.keyBindingPrecedence = precedence;
@ -93,9 +93,9 @@ public class KeyBindingData {
keyBindingPrecedence + "]"; keyBindingPrecedence + "]";
} }
static KeyBindingData createReservedKeyBindingData(KeyStroke keyStroke) { static KeyBindingData createSystemKeyBindingData(KeyStroke keyStroke) {
KeyBindingData keyBindingData = new KeyBindingData(keyStroke); KeyBindingData keyBindingData = new KeyBindingData(keyStroke);
keyBindingData.keyBindingPrecedence = KeyBindingPrecedence.ReservedActionsLevel; keyBindingData.keyBindingPrecedence = KeyBindingPrecedence.SystemActionsLevel;
return keyBindingData; return keyBindingData;
} }
@ -105,7 +105,7 @@ public class KeyBindingData {
* @param newKeyBindingData the data to validate * @param newKeyBindingData the data to validate
* @return the potentially changed data * @return the potentially changed data
*/ */
public static KeyBindingData validateKeyBindingData(KeyBindingData newKeyBindingData) { static KeyBindingData validateKeyBindingData(KeyBindingData newKeyBindingData) {
if (newKeyBindingData == null) { if (newKeyBindingData == null) {
return null; return null;
} }
@ -117,8 +117,8 @@ public class KeyBindingData {
} }
KeyBindingPrecedence precedence = newKeyBindingData.getKeyBindingPrecedence(); KeyBindingPrecedence precedence = newKeyBindingData.getKeyBindingPrecedence();
if (precedence == KeyBindingPrecedence.ReservedActionsLevel) { if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
return createReservedKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding)); return createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
} }
return new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence); return new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
} }

View file

@ -24,7 +24,8 @@ import javax.swing.Action;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import docking.*; import docking.*;
import generic.util.action.ReservedKeyBindings; import docking.actions.KeyBindingUtils;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
/** /**
@ -36,14 +37,14 @@ import ghidra.util.exception.AssertException;
public class KeyBindingsManager implements PropertyChangeListener { public class KeyBindingsManager implements PropertyChangeListener {
// this map exists to update the MultiKeyBindingAction when the key binding changes // this map exists to update the MultiKeyBindingAction when the key binding changes
private Map<DockingActionIf, ComponentProvider> actionToProviderMap; private Map<DockingActionIf, ComponentProvider> actionToProviderMap = new HashMap<>();
private Map<KeyStroke, DockingKeyBindingAction> dockingKeyMap; private Map<KeyStroke, DockingKeyBindingAction> dockingKeyMap = new HashMap<>();
private Map<DockingActionIf, SystemKeyBindingAction> dockingActionToSystemActionMap =
new HashMap<>();
private Tool tool; private Tool tool;
public KeyBindingsManager(Tool tool) { public KeyBindingsManager(Tool tool) {
this.tool = tool; this.tool = tool;
dockingKeyMap = new HashMap<>();
actionToProviderMap = new HashMap<>();
} }
public void addAction(ComponentProvider optionalProvider, DockingActionIf action) { public void addAction(ComponentProvider optionalProvider, DockingActionIf action) {
@ -59,14 +60,17 @@ public class KeyBindingsManager implements PropertyChangeListener {
} }
} }
public void addReservedAction(DockingActionIf action) { public void addSystemAction(DockingActionIf action) {
KeyStroke keyBinding = action.getKeyBinding(); KeyStroke keyStroke = action.getKeyBinding();
Objects.requireNonNull(keyBinding); Objects.requireNonNull(keyStroke);
addReservedKeyBinding(action, keyBinding); DockingKeyBindingAction existingAction = dockingKeyMap.get(keyStroke);
if (existingAction != null) {
throw new AssertException("Attempting to add more than one reserved " +
"action to a given keystroke: " + keyStroke);
} }
public void addReservedAction(DockingActionIf action, KeyStroke ks) { addSystemKeyBinding(action, keyStroke);
addReservedKeyBinding(action, ks); action.addPropertyChangeListener(this);
} }
public void removeAction(DockingActionIf action) { public void removeAction(DockingActionIf action) {
@ -78,17 +82,54 @@ public class KeyBindingsManager implements PropertyChangeListener {
private void addKeyBinding(ComponentProvider provider, DockingActionIf action, private void addKeyBinding(ComponentProvider provider, DockingActionIf action,
KeyStroke keyStroke) { KeyStroke keyStroke) {
if (ReservedKeyBindings.isReservedKeystroke(keyStroke)) { String errorMessage = validateActionKeyBinding(action, keyStroke);
throw new AssertException("Cannot assign action to a reserved keystroke. " + if (errorMessage != null) {
"Action: " + action.getName() + " - Keystroke: " + keyStroke); // Getting here should not be possible from the UI, but may happen if a developer sets
// an incorrect keybinding
Msg.error(this, errorMessage);
return;
} }
// map standard keybinding to action // map standard keystroke to action
doAddKeyBinding(provider, action, keyStroke); doAddKeyBinding(provider, action, keyStroke);
// map workaround keystroke to action
fixupAltGraphKeyStrokeMapping(provider, action, keyStroke); fixupAltGraphKeyStrokeMapping(provider, action, keyStroke);
} }
public String validateActionKeyBinding(DockingActionIf dockingAction, KeyStroke ks) {
if (ks == null) {
return null; // clearing the key stroke
}
//
// 1) Handle case with given key stroke already in use by a system action
//
Action existingAction = dockingKeyMap.get(ks);
if (existingAction instanceof SystemKeyBindingAction systemAction) {
DockingActionIf systemDockingAction = systemAction.getAction();
if (dockingAction == systemDockingAction) {
return null; // same key stroke; not sure if this can happen
}
String ksString = KeyBindingUtils.parseKeyStroke(ks);
return ksString + " in use by System action '" + systemDockingAction.getName() + "'";
}
//
// 2) Handle the case where a system action key stroke is being set to something that is
// already in-use by some other action
//
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(dockingAction);
if (systemAction != null && existingAction != null) {
return "System action cannot be set to in-use key stroke";
}
return null;
}
private void fixupAltGraphKeyStrokeMapping(ComponentProvider provider, DockingActionIf action, private void fixupAltGraphKeyStrokeMapping(ComponentProvider provider, DockingActionIf action,
KeyStroke keyStroke) { KeyStroke keyStroke) {
@ -116,56 +157,65 @@ public class KeyBindingsManager implements PropertyChangeListener {
KeyStroke mappingKeyStroke, KeyStroke actionKeyStroke) { KeyStroke mappingKeyStroke, KeyStroke actionKeyStroke) {
DockingKeyBindingAction existingAction = dockingKeyMap.get(mappingKeyStroke); DockingKeyBindingAction existingAction = dockingKeyMap.get(mappingKeyStroke);
if (existingAction == null) { if (existingAction instanceof MultipleKeyAction) {
dockingKeyMap.put(mappingKeyStroke, MultipleKeyAction multipleKeyction = (MultipleKeyAction) existingAction;
new MultipleKeyAction(tool, provider, action, actionKeyStroke)); multipleKeyction.addAction(provider, action);
return; return;
} }
if (!(existingAction instanceof MultipleKeyAction)) { if (existingAction instanceof SystemKeyBindingAction) {
return; // reserved binding; nothing to do // This should not happen due to protections in the UI
Msg.error(this, "Attempted to use the same keybinding for an existing System action: " +
existingAction + ". Keystroke: " + mappingKeyStroke);
return;
} }
MultipleKeyAction multipleKeyction = (MultipleKeyAction) existingAction; SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(action);
multipleKeyction.addAction(provider, action); if (systemAction != null) {
// the user has updated the binding for a System action; re-install it
registerSystemKeyBinding(action, mappingKeyStroke);
return;
} }
private void addReservedKeyBinding(DockingActionIf action, KeyStroke keyStroke) { // assume existingAction == null
DockingKeyBindingAction existingAction = dockingKeyMap.get(keyStroke); dockingKeyMap.put(mappingKeyStroke,
if (existingAction != null) { new MultipleKeyAction(tool, provider, action, actionKeyStroke));
throw new AssertException("Attempting to add more than one reserved " +
"action to a given keystroke: " + keyStroke);
} }
KeyBindingData binding = KeyBindingData.createReservedKeyBindingData(keyStroke); private void addSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
KeyBindingData binding = KeyBindingData.createSystemKeyBindingData(keyStroke);
action.setKeyBindingData(binding); action.setKeyBindingData(binding);
dockingKeyMap.put(keyStroke, new ReservedKeyBindingAction(tool, action, keyStroke)); registerSystemKeyBinding(action, keyStroke);
}
private void registerSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
SystemKeyBindingAction systemAction = new SystemKeyBindingAction(tool, action, keyStroke);
dockingKeyMap.put(keyStroke, systemAction);
dockingActionToSystemActionMap.put(action, systemAction);
} }
/**
* Remove the keystroke binding from the root pane's input map
* using keystroke specified instead of that specified by the action
*/
private void removeKeyBinding(KeyStroke keyStroke, DockingActionIf action) { private void removeKeyBinding(KeyStroke keyStroke, DockingActionIf action) {
if (keyStroke == null) { if (keyStroke == null) {
return; return;
} }
if (ReservedKeyBindings.isReservedKeystroke(keyStroke)) {
return;
}
DockingKeyBindingAction existingAction = dockingKeyMap.get(keyStroke); DockingKeyBindingAction existingAction = dockingKeyMap.get(keyStroke);
if (existingAction == null) { if (existingAction == null) {
return; return;
} }
if (existingAction instanceof SystemKeyBindingAction) {
dockingKeyMap.remove(keyStroke);
}
else if (existingAction instanceof MultipleKeyAction) {
MultipleKeyAction mkAction = (MultipleKeyAction) existingAction; MultipleKeyAction mkAction = (MultipleKeyAction) existingAction;
mkAction.removeAction(action); mkAction.removeAction(action);
if (mkAction.isEmpty()) { if (mkAction.isEmpty()) {
dockingKeyMap.remove(keyStroke); dockingKeyMap.remove(keyStroke);
} }
} }
}
@Override @Override
public void propertyChange(PropertyChangeEvent evt) { public void propertyChange(PropertyChangeEvent evt) {
@ -196,8 +246,13 @@ public class KeyBindingsManager implements PropertyChangeListener {
return dockingKeyMap.get(keyStroke); return dockingKeyMap.get(keyStroke);
} }
public Set<DockingActionIf> getSystemActions() {
return new HashSet<>(dockingActionToSystemActionMap.keySet());
}
public void dispose() { public void dispose() {
dockingKeyMap.clear(); dockingKeyMap.clear();
actionToProviderMap.clear(); actionToProviderMap.clear();
dockingActionToSystemActionMap.clear();
} }
} }

View file

@ -142,8 +142,8 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
} }
private boolean ignoreActionWhileMenuShowing() { private boolean ignoreActionWhileMenuShowing() {
if (getKeyBindingPrecedence() == KeyBindingPrecedence.ReservedActionsLevel) { if (getKeyBindingPrecedence() == KeyBindingPrecedence.SystemActionsLevel) {
return false; // allow reserved bindings through "no matter what!" return false; // allow system bindings through "no matter what!"
} }
MenuSelectionManager menuManager = MenuSelectionManager.defaultManager(); MenuSelectionManager menuManager = MenuSelectionManager.defaultManager();
@ -238,8 +238,8 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
} }
@Override @Override
public boolean isReservedKeybindingPrecedence() { public boolean isSystemKeybindingPrecedence() {
return false; // MultipleKeyActions can never be reserved return false; // MultipleKeyActions can never be 'system'
} }
@Override @Override

View file

@ -35,7 +35,7 @@ public class NextPreviousWindowAction extends DockingAction {
DockingWindowManager.DOCKING_WINDOWS_OWNER); DockingWindowManager.DOCKING_WINDOWS_OWNER);
this.forward = forward; this.forward = forward;
createReservedKeyBinding(keybinding); createSystemKeyBinding(keybinding);
setEnabled(true); setEnabled(true);
} }

View file

@ -29,10 +29,19 @@ import ghidra.util.Swing;
*/ */
public class ShowContextMenuAction extends DockingAction { public class ShowContextMenuAction extends DockingAction {
public ShowContextMenuAction(KeyStroke keyStroke) { public ShowContextMenuAction(KeyStroke keyStroke, boolean isPrimary) {
super("Show Context Menu", DockingWindowManager.DOCKING_WINDOWS_OWNER); super(isPrimary ? "Show Context Menu" : "Show Context Menu Alternate",
DockingWindowManager.DOCKING_WINDOWS_OWNER, isPrimary);
// Only the primary action will appear in the tool' key binding settings UI. The primary
// action can be managed by the users. The secondary action is not managed at this time.
if (isPrimary) {
setKeyBindingData(new KeyBindingData(keyStroke)); setKeyBindingData(new KeyBindingData(keyStroke));
} }
else {
createSystemKeyBinding(keyStroke);
}
}
@Override @Override
public void actionPerformed(ActionContext context) { public void actionPerformed(ActionContext context) {

View file

@ -24,14 +24,14 @@ import org.apache.logging.log4j.Logger;
import docking.ActionContext; import docking.ActionContext;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import generic.util.action.ReservedKeyBindings; import generic.util.action.SystemKeyBindings;
public class ShowFocusCycleAction extends DockingAction { public class ShowFocusCycleAction extends DockingAction {
static final Logger log = LogManager.getLogger(ShowFocusCycleAction.class); static final Logger log = LogManager.getLogger(ShowFocusCycleAction.class);
public ShowFocusCycleAction() { public ShowFocusCycleAction() {
super("Show Focus Cycle", DockingWindowManager.DOCKING_WINDOWS_OWNER, false); super("Show Focus Cycle", DockingWindowManager.DOCKING_WINDOWS_OWNER);
createReservedKeyBinding(ReservedKeyBindings.FOCUS_CYCLE_INFO_KEY); createSystemKeyBinding(SystemKeyBindings.FOCUS_CYCLE_INFO_KEY);
setEnabled(true); setEnabled(true);
// System action; no help needed // System action; no help needed

View file

@ -25,14 +25,14 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import docking.*; import docking.*;
import generic.util.action.ReservedKeyBindings; import generic.util.action.SystemKeyBindings;
public class ShowFocusInfoAction extends DockingAction { public class ShowFocusInfoAction extends DockingAction {
static final Logger log = LogManager.getLogger(ShowFocusInfoAction.class); static final Logger log = LogManager.getLogger(ShowFocusInfoAction.class);
public ShowFocusInfoAction() { public ShowFocusInfoAction() {
super("Show Focus Info", DockingWindowManager.DOCKING_WINDOWS_OWNER, false); super("Show Focus Info", DockingWindowManager.DOCKING_WINDOWS_OWNER);
createReservedKeyBinding(ReservedKeyBindings.FOCUS_INFO_KEY); createSystemKeyBinding(SystemKeyBindings.FOCUS_INFO_KEY);
setEnabled(true); setEnabled(true);
// System action; no help needed // System action; no help needed

View file

@ -0,0 +1,45 @@
/* ###
* 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.action;
import javax.swing.KeyStroke;
import docking.*;
/**
* An {@link DockingKeyBindingAction} to signal that the given {@link DockingAction} gets priority
* over all other non-system actions in the system.
*/
public class SystemKeyBindingAction extends DockingKeyBindingAction {
SystemKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
super(tool, action, keyStroke);
}
public DockingActionIf getAction() {
return dockingAction;
}
@Override
public boolean isSystemKeybindingPrecedence() {
return true;
}
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
return KeyBindingPrecedence.SystemActionsLevel;
}
}

View file

@ -87,7 +87,7 @@ public interface DockingToolActions {
public Set<DockingActionIf> getActions(String owner); public Set<DockingActionIf> getActions(String owner);
/** /**
* Returns all actions known to the tool * Returns all actions known to the tool.
* @return the actions * @return the actions
*/ */
public Set<DockingActionIf> getAllActions(); public Set<DockingActionIf> getAllActions();
@ -103,4 +103,5 @@ public interface DockingToolActions {
* @param placeholder the placeholder containing information related to the action it represents * @param placeholder the placeholder containing information related to the action it represents
*/ */
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder); public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder);
} }

View file

@ -0,0 +1,262 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.actions;
import java.util.*;
import java.util.Map.Entry;
import javax.swing.KeyStroke;
import docking.Tool;
import docking.action.DockingActionIf;
import docking.action.KeyBindingData;
import docking.tool.util.DockingToolConstants;
import ghidra.framework.options.ToolOptions;
import util.CollectionUtils;
/**
* An object that maps actions to key strokes.
* <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()}.
*/
public class KeyBindings {
private Tool tool;
private Map<String, List<DockingActionIf>> actionsByFullName;
private Map<String, List<String>> actionNamesByKeyStroke = new HashMap<>();
private Map<String, KeyStroke> keyStrokesByFullName = new HashMap<>();
private List<DockingActionIf> uniqueActions = new ArrayList<>();
// to know what has been changed
private Map<String, KeyStroke> originalKeyStrokesByFullName = new HashMap<>();
private String longestActionName = "";
private ToolOptions options;
public KeyBindings(Tool tool) {
this.tool = tool;
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
init();
}
public List<DockingActionIf> getUniqueActions() {
return Collections.unmodifiableList(uniqueActions);
}
public Map<String, KeyStroke> getKeyStrokesByFullActionName() {
return Collections.unmodifiableMap(keyStrokesByFullName);
}
public boolean containsAction(String fullName) {
return actionsByFullName.containsKey(fullName);
}
public KeyStroke getKeyStroke(String fullName) {
return keyStrokesByFullName.get(fullName);
}
public String getActionsForKeyStrokeText(String keyStrokeText) {
StringBuffer sb = new StringBuffer();
List<String> names = actionNamesByKeyStroke.get(keyStrokeText);
if (CollectionUtils.isBlank(names)) {
return sb.toString();
}
names.sort((n1, n2) -> {
return n1.compareToIgnoreCase(n2);
});
sb.append("Actions mapped to key " + keyStrokeText + ":\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());
sb.append(" (").append(action.getOwnerDescription()).append(')');
if (i < names.size() - 1) {
sb.append("\n");
}
}
return sb.toString();
}
public String getLongestActionName() {
return longestActionName;
}
public boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
// remove old keystroke for action name
KeyStroke oldKs = keyStrokesByFullName.get(actionName);
if (oldKs != null) {
String oldName = KeyBindingUtils.parseKeyStroke(oldKs);
if (oldName.equals(ksName)) {
return false;
}
removeFromKeyMap(oldKs, actionName);
}
addActionKeyStroke(keyStroke, actionName);
keyStrokesByFullName.put(actionName, keyStroke);
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
return false;
}
removeFromKeyMap(stroke, actionName);
keyStrokesByFullName.put(actionName, null);
return true;
}
return false;
}
/**
* Restores the tool options key bindings to the default values originally loaded when the
* system started.
*/
public void restoreOptions() {
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
for (Entry<String, List<DockingActionIf>> entry : entries) {
List<DockingActionIf> actions = entry.getValue();
// pick one action, they are all conceptually the same
DockingActionIf action = actions.get(0);
String actionName = entry.getKey();
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
KeyStroke newKeyStroke =
(defaultBinding == null) ? null : defaultBinding.getKeyBinding();
updateOptions(actionName, currentKeyStroke, newKeyStroke);
}
}
/**
* 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);
}
}
}
/**
* 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);
}
}
private void removeFromKeyMap(KeyStroke ks, String actionName) {
if (ks == null) {
return;
}
String ksName = KeyBindingUtils.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
if (list != null) {
list.remove(actionName);
if (list.isEmpty()) {
actionNamesByKeyStroke.remove(ksName);
}
}
}
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);
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 shortName = action.getName();
if (shortName.length() > longestActionName.length()) {
longestActionName = shortName;
}
}
}
private void addActionKeyStroke(KeyStroke ks, String actionName) {
if (ks == null) {
return;
}
String ksName = KeyBindingUtils.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
if (list == null) {
list = new ArrayList<>();
actionNamesByKeyStroke.put(ksName, list);
}
if (!list.contains(actionName)) {
list.add(actionName);
}
}
}

View file

@ -16,42 +16,43 @@
package docking.actions; package docking.actions;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.Objects;
import java.util.List;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.*; import javax.swing.text.*;
import docking.DialogComponentProvider; import docking.*;
import docking.KeyEntryTextField; import docking.action.DockingActionIf;
import docking.action.*; import docking.action.KeyBindingData;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import docking.widgets.label.GIconLabel; import docking.widgets.label.GIconLabel;
import generic.theme.GThemeDefaults.Colors.Messages; import generic.theme.GThemeDefaults.Colors.Messages;
import generic.util.action.ReservedKeyBindings;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import resources.Icons; import resources.Icons;
/** /**
* Dialog to set the key binding on an action; it is popped up when the F4 key * Dialog to set the key binding on an action. It is triggered by the F4 key.
* is hit.
*/ */
public class KeyEntryDialog extends DialogComponentProvider { public class KeyEntryDialog extends DialogComponentProvider {
private KeyBindings keyBindings;
private ToolActions toolActions; private ToolActions toolActions;
private DockingActionIf action; private DockingActionIf action;
private JPanel defaultPanel; private JPanel defaultPanel;
private KeyEntryTextField keyEntryField; private KeyEntryTextField keyEntryField;
private JTextPane collisionPane; private JTextPane collisionPane;
private StyledDocument doc; private StyledDocument doc;
private SimpleAttributeSet tabAttrSet;
private SimpleAttributeSet textAttrSet; private SimpleAttributeSet textAttrSet;
private Color bgColor; private Color bgColor;
public KeyEntryDialog(DockingActionIf action, ToolActions actions) { public KeyEntryDialog(Tool tool, DockingActionIf action) {
super("Set Key Binding for " + action.getName(), true); super("Set Key Binding for " + action.getName(), true);
this.action = action; this.action = action;
this.toolActions = actions; this.toolActions = (ToolActions) tool.getToolActions();
this.keyBindings = new KeyBindings(tool);
setUpAttributes(); setUpAttributes();
createPanel(); createPanel();
KeyStroke keyBinding = action.getKeyBinding(); KeyStroke keyBinding = action.getKeyBinding();
@ -150,21 +151,22 @@ public class KeyEntryDialog extends DialogComponentProvider {
@Override @Override
protected void okCallback() { protected void okCallback() {
KeyStroke newKeyStroke = keyEntryField.getKeyStroke(); KeyStroke newKs = keyEntryField.getKeyStroke();
if (newKeyStroke != null && ReservedKeyBindings.isReservedKeystroke(newKeyStroke)) { String errorMessage = toolActions.validateActionKeyBinding(action, newKs);
setStatusText(keyEntryField.getText() + " is a reserved keystroke"); if (errorMessage != null) {
setStatusText(errorMessage);
return; return;
} }
clearStatusText(); clearStatusText();
KeyStroke existingKeyStroke = action.getKeyBinding(); KeyStroke existingKeyStroke = action.getKeyBinding();
if (Objects.equals(existingKeyStroke, newKeyStroke)) { if (Objects.equals(existingKeyStroke, newKs)) {
setStatusText("Key binding unchanged"); setStatusText("Key binding unchanged");
return; return;
} }
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKeyStroke)); action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
close(); close();
} }
@ -174,10 +176,6 @@ public class KeyEntryDialog extends DialogComponentProvider {
textAttrSet.addAttribute(StyleConstants.FontFamily, "Tahoma"); textAttrSet.addAttribute(StyleConstants.FontFamily, "Tahoma");
textAttrSet.addAttribute(StyleConstants.FontSize, Integer.valueOf(11)); textAttrSet.addAttribute(StyleConstants.FontSize, Integer.valueOf(11));
textAttrSet.addAttribute(StyleConstants.Foreground, Messages.NORMAL); textAttrSet.addAttribute(StyleConstants.Foreground, Messages.NORMAL);
tabAttrSet = new SimpleAttributeSet();
TabStop tabs = new TabStop(20, StyleConstants.ALIGN_LEFT, TabStop.LEAD_NONE);
StyleConstants.setTabSet(tabAttrSet, new TabSet(new TabStop[] { tabs }));
} }
private void updateCollisionPane(KeyStroke ks) { private void updateCollisionPane(KeyStroke ks) {
@ -193,26 +191,10 @@ public class KeyEntryDialog extends DialogComponentProvider {
return; return;
} }
List<DockingActionIf> list = getManagedActionsForKeyStroke(ks);
if (list.size() == 0) {
return;
}
list.sort((a1, a2) -> {
String s1 = a1.getName() + a1.getOwnerDescription();
String s2 = a2.getName() + a2.getOwnerDescription();
return s1.compareToIgnoreCase(s2);
});
String ksName = KeyBindingUtils.parseKeyStroke(ks); String ksName = KeyBindingUtils.parseKeyStroke(ks);
String text = keyBindings.getActionsForKeyStrokeText(ksName);
try { try {
doc.insertString(0, "Actions mapped to " + ksName + "\n\n", textAttrSet); doc.insertString(0, text, textAttrSet);
for (DockingActionIf a : list) {
String collisionStr = "\t" + a.getName() + " (" + a.getOwnerDescription() + ")\n";
int offset = doc.getLength();
doc.insertString(offset, collisionStr, textAttrSet);
doc.setParagraphAttributes(offset, 1, tabAttrSet, false);
}
collisionPane.setCaretPosition(0); collisionPane.setCaretPosition(0);
} }
catch (BadLocationException e) { catch (BadLocationException e) {
@ -220,37 +202,4 @@ public class KeyEntryDialog extends DialogComponentProvider {
} }
} }
private List<DockingActionIf> getManagedActionsForKeyStroke(KeyStroke keyStroke) {
MultipleKeyAction multiAction = getMultipleKeyAction(keyStroke);
if (multiAction == null) {
return Collections.emptyList();
}
List<DockingActionIf> list = multiAction.getActions();
Map<String, DockingActionIf> nameMap = new HashMap<>(list.size());
// the list may have multiple matches for a single owner, which we do not want (see
// SharedStubKeyBindingAction)
for (DockingActionIf dockableAction : list) {
if (shouldAddAction(dockableAction)) {
// this overwrites same named actions
nameMap.put(dockableAction.getName() + dockableAction.getOwner(), dockableAction);
}
}
return new ArrayList<>(nameMap.values());
}
private MultipleKeyAction getMultipleKeyAction(KeyStroke ks) {
Action keyAction = toolActions.getAction(ks);
if (keyAction instanceof MultipleKeyAction) {
return (MultipleKeyAction) keyAction;
}
return null;
}
private boolean shouldAddAction(DockingActionIf dockableAction) {
return dockableAction.getKeyBindingType().isManaged();
}
} }

View file

@ -17,19 +17,22 @@ package docking.actions;
import java.awt.Component; import java.awt.Component;
import docking.ActionContext; import javax.swing.KeyStroke;
import docking.DockingWindowManager;
import docking.*;
import docking.action.*; import docking.action.*;
import ghidra.util.Msg; import ghidra.util.Msg;
public class KeyBindingAction extends DockingAction { public class SetKeyBindingAction extends DockingAction {
public static String NAME = "Set KeyBinding"; public static String NAME = "Set KeyBinding";
private ToolActions toolActions; private Tool tool;
public KeyBindingAction(ToolActions toolActions) { public SetKeyBindingAction(Tool tool, KeyStroke keyStroke) {
super(NAME, DockingWindowManager.DOCKING_WINDOWS_OWNER); super(NAME, DockingWindowManager.DOCKING_WINDOWS_OWNER);
this.toolActions = toolActions; this.tool = tool;
setKeyBindingData(new KeyBindingData(keyStroke));
// Help actions don't have help // Help actions don't have help
DockingWindowManager.getHelpService().excludeFromHelp(this); DockingWindowManager.getHelpService().excludeFromHelp(this);
@ -56,7 +59,7 @@ public class KeyBindingAction extends DockingAction {
return; return;
} }
KeyEntryDialog d = new KeyEntryDialog(action, toolActions); KeyEntryDialog d = new KeyEntryDialog(tool, action);
DockingWindowManager.showDialog(d); DockingWindowManager.showDialog(d);
} }
@ -72,6 +75,7 @@ public class KeyBindingAction extends DockingAction {
// It is not key binding managed, which means that it may be a shared key binding // It is not key binding managed, which means that it may be a shared key binding
String actionName = dockingAction.getName(); String actionName = dockingAction.getName();
ToolActions toolActions = (ToolActions) tool.getToolActions();
DockingActionIf sharedAction = toolActions.getSharedStubKeyBindingAction(actionName); DockingActionIf sharedAction = toolActions.getSharedStubKeyBindingAction(actionName);
if (sharedAction != null) { if (sharedAction != null) {
return sharedAction; return sharedAction;

View file

@ -15,6 +15,8 @@
*/ */
package docking.actions; package docking.actions;
import static generic.util.action.SystemKeyBindings.*;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.*; import java.util.*;
@ -32,7 +34,6 @@ import org.apache.commons.collections4.map.LazyMap;
import docking.*; import docking.*;
import docking.action.*; import docking.action.*;
import docking.tool.util.DockingToolConstants; import docking.tool.util.DockingToolConstants;
import generic.util.action.ReservedKeyBindings;
import ghidra.framework.options.*; import ghidra.framework.options.*;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
@ -60,7 +61,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private Map<String, SharedStubKeyBindingAction> sharedActionMap = new HashMap<>(); private Map<String, SharedStubKeyBindingAction> sharedActionMap = new HashMap<>();
private ToolOptions keyBindingOptions; private ToolOptions keyBindingOptions;
private Tool dockingTool; private Tool tool;
private KeyBindingsManager keyBindingsManager; private KeyBindingsManager keyBindingsManager;
private OptionsChangeListener optionChangeListener = (options, optionName, oldValue, private OptionsChangeListener optionChangeListener = (options, optionName, oldValue,
newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue); newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue);
@ -72,45 +73,49 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
* @param actionToGuiHelper the class that takes actions and maps them to GUI widgets * @param actionToGuiHelper the class that takes actions and maps them to GUI widgets
*/ */
public ToolActions(Tool tool, ActionToGuiHelper actionToGuiHelper) { public ToolActions(Tool tool, ActionToGuiHelper actionToGuiHelper) {
this.dockingTool = tool; this.tool = tool;
this.actionGuiHelper = actionToGuiHelper; this.actionGuiHelper = actionToGuiHelper;
this.keyBindingsManager = new KeyBindingsManager(tool); this.keyBindingsManager = new KeyBindingsManager(tool);
this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
this.keyBindingOptions.addOptionsChangeListener(optionChangeListener); this.keyBindingOptions.addOptionsChangeListener(optionChangeListener);
createReservedKeyBindings(); createSystemActions();
SharedActionRegistry.installSharedActions(tool, this); SharedActionRegistry.installSharedActions(tool, this);
} }
private void createReservedKeyBindings() { private void createSystemActions() {
KeyBindingAction keyBindingAction = new KeyBindingAction(this);
keyBindingsManager.addReservedAction(keyBindingAction,
ReservedKeyBindings.UPDATE_KEY_BINDINGS_KEY);
keyBindingsManager.addReservedAction(new HelpAction(false, ReservedKeyBindings.HELP_KEY1)); addSystemAction(new SetKeyBindingAction(tool, UPDATE_KEY_BINDINGS_KEY));
keyBindingsManager.addReservedAction(new HelpAction(false, ReservedKeyBindings.HELP_KEY2));
keyBindingsManager
.addReservedAction(new HelpAction(true, ReservedKeyBindings.HELP_INFO_KEY));
keyBindingsManager.addReservedAction(
new ShowContextMenuAction(ReservedKeyBindings.CONTEXT_MENU_KEY1));
keyBindingsManager.addReservedAction(
new ShowContextMenuAction(ReservedKeyBindings.CONTEXT_MENU_KEY2));
keyBindingsManager.addReservedAction( addSystemAction(new HelpAction(HELP_KEY1, false));
new NextPreviousWindowAction(ReservedKeyBindings.FOCUS_NEXT_WINDOW_KEY, true)); addSystemAction(new HelpAction(HELP_KEY2, true));
keyBindingsManager.addReservedAction( addSystemAction(new HelpInfoAction(HELP_INFO_KEY));
new NextPreviousWindowAction(ReservedKeyBindings.FOCUS_PREVIOUS_WINDOW_KEY, false)); addSystemAction(new ShowContextMenuAction(CONTEXT_MENU_KEY1, true));
addSystemAction(new ShowContextMenuAction(CONTEXT_MENU_KEY2, false));
keyBindingsManager.addReservedAction( addSystemAction(new NextPreviousWindowAction(FOCUS_NEXT_WINDOW_KEY, true));
new GlobalFocusTraversalAction(ReservedKeyBindings.FOCUS_NEXT_COMPONENT_KEY, true)); addSystemAction(new NextPreviousWindowAction(FOCUS_PREVIOUS_WINDOW_KEY, false));
keyBindingsManager.addReservedAction(
new GlobalFocusTraversalAction(ReservedKeyBindings.FOCUS_PREVIOUS_COMPONENT_KEY, addSystemAction(new GlobalFocusTraversalAction(FOCUS_NEXT_COMPONENT_KEY, true));
false)); addSystemAction(new GlobalFocusTraversalAction(FOCUS_PREVIOUS_COMPONENT_KEY, false));
// helpful debugging actions // helpful debugging actions
keyBindingsManager.addReservedAction(new ShowFocusInfoAction()); addSystemAction(new ShowFocusInfoAction());
keyBindingsManager.addReservedAction(new ShowFocusCycleAction()); addSystemAction(new ShowFocusCycleAction());
keyBindingsManager.addReservedAction(new ComponentThemeInspectorAction()); addSystemAction(new ComponentThemeInspectorAction());
}
private void addSystemAction(DockingAction action) {
// 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);
}
keyBindingsManager.addSystemAction(action);
} }
public void dispose() { public void dispose() {
@ -170,7 +175,6 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
} }
private void loadKeyBindingFromOptions(DockingActionIf action, KeyStroke ks) { private void loadKeyBindingFromOptions(DockingActionIf action, KeyStroke ks) {
String description = "Keybinding for " + action.getFullName(); String description = "Keybinding for " + action.getFullName();
keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null, keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null,
description); description);
@ -288,6 +292,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
result.addAll(sharedActionMap.values()); result.addAll(sharedActionMap.values());
result.addAll(keyBindingsManager.getSystemActions());
return result; return result;
} }
@ -311,7 +317,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
* otherwise the options will be removed because they are noted as not being used. * otherwise the options will be removed because they are noted as not being used.
*/ */
public synchronized void restoreKeyBindings() { public synchronized void restoreKeyBindings() {
keyBindingOptions = dockingTool.getOptions(DockingToolConstants.KEY_BINDINGS); keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
Iterator<DockingActionIf> it = getKeyBindingActionsIterator(); Iterator<DockingActionIf> it = getKeyBindingActionsIterator();
for (DockingActionIf action : CollectionUtils.asIterable(it)) { for (DockingActionIf action : CollectionUtils.asIterable(it)) {
@ -406,8 +412,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
} }
DockingActionIf action = (DockingActionIf) evt.getSource(); DockingActionIf action = (DockingActionIf) evt.getSource();
if (!action.getKeyBindingType() if (!action.getKeyBindingType().isManaged()) {
.isManaged()) {
// this reads unusually, but we need to notify the tool to rebuild its 'Window' menu // this reads unusually, but we need to notify the tool to rebuild its 'Window' menu
// in the case that this action is one of the tool's special actions // in the case that this action is one of the tool's special actions
keyBindingsChanged(); keyBindingsChanged();
@ -429,7 +434,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
// triggered by a user-initiated action; called by propertyChange() // triggered by a user-initiated action; called by propertyChange()
private void keyBindingsChanged() { private void keyBindingsChanged() {
dockingTool.setConfigChanged(true); tool.setConfigChanged(true);
actionGuiHelper.keyBindingsChanged(); actionGuiHelper.keyBindingsChanged();
} }
@ -447,6 +452,17 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
return null; return null;
} }
/**
* Checks whether the given key stroke can be used for the given action for restrictions such as
* those for System level actions.
* @param action the action; may be null
* @param ks the key stroke
* @return A null value if valid; a non-null error message if invalid
*/
public String validateActionKeyBinding(DockingActionIf action, KeyStroke ks) {
return keyBindingsManager.validateActionKeyBinding(action, ks);
}
public Action getAction(KeyStroke ks) { public Action getAction(KeyStroke ks) {
return keyBindingsManager.getDockingKeyAction(ks); return keyBindingsManager.getDockingKeyAction(ks);
} }

View file

@ -1,94 +0,0 @@
/* ###
* 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 generic.util.action;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;
public class ReservedKeyBindings {
private static final int CONTROL_KEY_MODIFIER_MASK =
Toolkit.getDefaultToolkit()
.getMenuShortcutKeyMaskEx();
private ReservedKeyBindings() {
// utils class
}
public static final KeyStroke HELP_KEY1 = KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0);
public static final KeyStroke HELP_KEY2 = KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
public static final KeyStroke HELP_INFO_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_F1, CONTROL_KEY_MODIFIER_MASK);
public static final KeyStroke CONTEXT_MENU_KEY1 =
KeyStroke.getKeyStroke(KeyEvent.VK_F10, InputEvent.SHIFT_DOWN_MASK);
public static final KeyStroke CONTEXT_MENU_KEY2 =
KeyStroke.getKeyStroke(KeyEvent.VK_CONTEXT_MENU, 0);
public static final KeyStroke FOCUS_NEXT_WINDOW_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.CTRL_DOWN_MASK);
public static final KeyStroke FOCUS_PREVIOUS_WINDOW_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_F3,
InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK);
public static final KeyStroke FOCUS_NEXT_COMPONENT_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK);
public static final KeyStroke FOCUS_PREVIOUS_COMPONENT_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
public static final KeyStroke FOCUS_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F2,
CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
public static final KeyStroke FOCUS_CYCLE_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F3,
CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
public static final KeyStroke UPDATE_KEY_BINDINGS_KEY =
KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0);
public static final KeyStroke COMPONENT_THEME_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F9,
CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
// @formatter:off
public static boolean isReservedKeystroke(KeyStroke keyStroke) {
int code = keyStroke.getKeyCode();
if (code == KeyEvent.VK_SHIFT ||
code == KeyEvent.VK_ALT ||
code == KeyEvent.VK_CONTROL ||
code == KeyEvent.VK_CAPS_LOCK ||
code == KeyEvent.VK_TAB ||
HELP_KEY1.equals(keyStroke) ||
HELP_KEY2.equals(keyStroke) ||
HELP_INFO_KEY.equals(keyStroke) ||
UPDATE_KEY_BINDINGS_KEY.equals(keyStroke) ||
FOCUS_INFO_KEY.equals(keyStroke) ||
FOCUS_CYCLE_INFO_KEY.equals(keyStroke) ||
COMPONENT_THEME_INFO_KEY.equals(keyStroke) ||
CONTEXT_MENU_KEY1.equals(keyStroke) ||
CONTEXT_MENU_KEY2.equals(keyStroke) ||
FOCUS_NEXT_WINDOW_KEY.equals(keyStroke) ||
FOCUS_PREVIOUS_WINDOW_KEY.equals(keyStroke) ||
FOCUS_NEXT_COMPONENT_KEY.equals(keyStroke) ||
FOCUS_PREVIOUS_COMPONENT_KEY.equals(keyStroke)) {
return true;
}
// @formatter:on
return false;
}
}

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 generic.util.action;
import static java.awt.event.InputEvent.*;
import static java.awt.event.KeyEvent.*;
import static javax.swing.KeyStroke.*;
import java.awt.Toolkit;
import javax.swing.KeyStroke;
/**
* Default key strokes for System actions.
*/
public class SystemKeyBindings {
private static final int CTRL = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
private static final int CTRL_SHIFT = CTRL | SHIFT_DOWN_MASK;
private static final int CTRL_ALT_SHIFT = CTRL_SHIFT | ALT_DOWN_MASK;
public static final KeyStroke HELP_KEY1 = KeyStroke.getKeyStroke(VK_HELP, 0);
public static final KeyStroke HELP_KEY2 = KeyStroke.getKeyStroke(VK_F1, 0);
public static final KeyStroke HELP_INFO_KEY = getKeyStroke(VK_F1, CTRL);
public static final KeyStroke CONTEXT_MENU_KEY1 = getKeyStroke(VK_F10, SHIFT_DOWN_MASK);
public static final KeyStroke CONTEXT_MENU_KEY2 = getKeyStroke(VK_CONTEXT_MENU, 0);
public static final KeyStroke FOCUS_NEXT_WINDOW_KEY = getKeyStroke(VK_F3, CTRL);
public static final KeyStroke FOCUS_PREVIOUS_WINDOW_KEY = getKeyStroke(VK_F3, CTRL_SHIFT);
public static final KeyStroke FOCUS_NEXT_COMPONENT_KEY = getKeyStroke(VK_TAB, CTRL);
public static final KeyStroke FOCUS_PREVIOUS_COMPONENT_KEY = getKeyStroke(VK_TAB, CTRL_SHIFT);
public static final KeyStroke FOCUS_INFO_KEY = getKeyStroke(VK_F2, CTRL_ALT_SHIFT);
public static final KeyStroke FOCUS_CYCLE_INFO_KEY = getKeyStroke(VK_F3, CTRL_ALT_SHIFT);
public static final KeyStroke UPDATE_KEY_BINDINGS_KEY = getKeyStroke(VK_F4, 0);
public static final KeyStroke COMPONENT_THEME_INFO_KEY = getKeyStroke(VK_F9, CTRL_ALT_SHIFT);
private SystemKeyBindings() {
// utils class
}
}

View file

@ -54,6 +54,9 @@
<logger name="ghidra.framework.project.DefaultProject" level="WARN"/> <logger name="ghidra.framework.project.DefaultProject" level="WARN"/>
<logger name="ghidra.framework.project.DefaultProjectManager" level="INFO"/> <logger name="ghidra.framework.project.DefaultProjectManager" level="INFO"/>
<!-- 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="functioncalls" level="DEBUG" />
<logger name="generic.random" level="WARN"/> <logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/> <logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>

View file

@ -53,6 +53,9 @@
<logger name="ghidra.framework.project.DefaultProject" level="WARN"/> <logger name="ghidra.framework.project.DefaultProject" level="WARN"/>
<logger name="ghidra.framework.project.DefaultProjectManager" level="INFO"/> <logger name="ghidra.framework.project.DefaultProjectManager" level="INFO"/>
<!-- 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="functioncalls" level="DEBUG" />
<logger name="generic.random" level="WARN"/> <logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/> <logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>

View file

@ -660,12 +660,26 @@ public class AbstractGuiTest extends AbstractGenericTest {
* @param s the supplier * @param s the supplier
* @return the value returned by the supplier * @return the value returned by the supplier
*/ */
public static <T> T runSwing(Supplier<T> s) { public static <T> T getSwing(Supplier<T> s) {
AtomicReference<T> ref = new AtomicReference<>(); AtomicReference<T> ref = new AtomicReference<>();
runSwing(() -> ref.set(s.get())); runSwing(() -> ref.set(s.get()));
return ref.get(); return ref.get();
} }
/**
* Returns the value from the given {@link Supplier}, invoking the call in
* the Swing thread. This is useful when you may have values that are being
* changed on the Swing thread and you need the test thread to see the
* changes.
*
* @param s the supplier
* @return the value returned by the supplier
* @see #getSwing(Supplier)
*/
public static <T> T runSwing(Supplier<T> s) {
return getSwing(s);
}
/** /**
* Run the given code snippet on the Swing thread and wait for it to finish * Run the given code snippet on the Swing thread and wait for it to finish
* @param r the runnable code snippet * @param r the runnable code snippet

View file

@ -1260,7 +1260,6 @@ public class DefaultProjectData implements ProjectData {
public void close() { public void close() {
synchronized (this) { synchronized (this) {
if (!closed) { if (!closed) {
Msg.debug(this, "Closing ProjectData: " + projectDir);
closed = true; closed = true;
} }
if (inUseCount != 0) { if (inUseCount != 0) {
@ -1301,8 +1300,6 @@ public class DefaultProjectData implements ProjectData {
return; return;
} }
Msg.debug(this, "Disposing ProjectData: " + projectDir);
closed = true; closed = true;
disposed = true; disposed = true;

View file

@ -20,30 +20,28 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.Map.Entry;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumn; import javax.swing.table.TableColumn;
import org.apache.commons.lang3.StringUtils;
import docking.DockingUtils; import docking.DockingUtils;
import docking.KeyEntryTextField; import docking.KeyEntryTextField;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import docking.action.KeyBindingData; import docking.actions.*;
import docking.actions.KeyBindingUtils;
import docking.tool.util.DockingToolConstants; import docking.tool.util.DockingToolConstants;
import docking.widgets.*; import docking.widgets.*;
import docking.widgets.label.GIconLabel; import docking.widgets.label.GIconLabel;
import docking.widgets.table.*; import docking.widgets.table.*;
import generic.theme.Gui; import generic.theme.Gui;
import generic.util.action.ReservedKeyBindings;
import ghidra.framework.options.Options; import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions; import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HTMLUtilities; import ghidra.util.HTMLUtilities;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.exception.AssertException;
import ghidra.util.layout.PairLayout; import ghidra.util.layout.PairLayout;
import ghidra.util.layout.VerticalLayout; import ghidra.util.layout.VerticalLayout;
import help.Help; import help.Help;
@ -68,31 +66,25 @@ public class KeyBindingsPanel extends JPanel {
private JPanel infoPanel; private JPanel infoPanel;
private MultiLineLabel collisionLabel; private MultiLineLabel collisionLabel;
private KeyBindingsTableModel tableModel; private KeyBindingsTableModel tableModel;
private ListSelectionModel selectionModel;
private Options options;
private Map<String, List<DockingActionIf>> actionsByFullName;
private Map<String, List<String>> actionNamesByKeyStroke = new HashMap<>();
private Map<String, KeyStroke> keyStrokesByFullName = new HashMap<>();
private Map<String, KeyStroke> originalValues = new HashMap<>(); // to know what has been changed
private List<DockingActionIf> tableActions = new ArrayList<>();
private KeyEntryTextField ksField; private KeyEntryTextField ksField;
private GTableFilterPanel<DockingActionIf> tableFilterPanel;
private EmptyBorderButton helpButton;
private KeyBindings keyBindings;
private boolean unappliedChanges; private boolean unappliedChanges;
private PluginTool tool; private PluginTool tool;
private boolean firingTableDataChanged; private boolean firingTableDataChanged;
private PropertyChangeListener propertyChangeListener; private PropertyChangeListener propertyChangeListener;
private GTableFilterPanel<DockingActionIf> tableFilterPanel;
private EmptyBorderButton helpButton;
public KeyBindingsPanel(PluginTool tool, Options options) { public KeyBindingsPanel(PluginTool tool) {
this.tool = tool; this.tool = tool;
this.options = options;
this.keyBindings = new KeyBindings(tool);
createPanelComponents(); createPanelComponents();
createActionMap();
addListeners(); initializeTableWidth();
} }
public void setOptionsPropertyChangeListener(PropertyChangeListener listener) { public void setOptionsPropertyChangeListener(PropertyChangeListener listener) {
@ -105,83 +97,32 @@ public class KeyBindingsPanel extends JPanel {
} }
public void apply() { public void apply() {
Iterator<String> iter = keyStrokesByFullName.keySet().iterator(); keyBindings.applyChanges();
while (iter.hasNext()) {
String actionName = iter.next();
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
KeyStroke originalKeyStroke = originalValues.get(actionName);
updateOptions(actionName, originalKeyStroke, currentKeyStroke);
}
changesMade(false); changesMade(false);
} }
private void updateOptions(String fullActionName, KeyStroke currentKeyStroke,
KeyStroke newKeyStroke) {
if (Objects.equals(currentKeyStroke, newKeyStroke)) {
return;
}
options.setKeyStroke(fullActionName, newKeyStroke);
originalValues.put(fullActionName, newKeyStroke);
keyStrokesByFullName.put(fullActionName, newKeyStroke);
List<DockingActionIf> actions = actionsByFullName.get(fullActionName);
for (DockingActionIf action : actions) {
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKeyStroke));
}
}
public void cancel() { public void cancel() {
Iterator<String> iter = originalValues.keySet().iterator(); keyBindings.cancelChanges();
while (iter.hasNext()) {
String actionName = iter.next();
KeyStroke originalKS = originalValues.get(actionName);
KeyStroke modifiedKS = keyStrokesByFullName.get(actionName);
if (modifiedKS != null && !modifiedKS.equals(originalKS)) {
keyStrokesByFullName.put(actionName, originalKS);
}
}
tableModel.fireTableDataChanged(); tableModel.fireTableDataChanged();
} }
public void reload() { public void reload() {
Swing.runLater(() -> { Swing.runLater(() -> {
// clear the current user key stroke so that it does not appear as though the // clear the action to avoid the appearance of editing while restoring
// user is editing while restoring
actionTable.clearSelection(); actionTable.clearSelection();
restoreDefaultKeybindings(); restoreDefaultKeybindings();
}); });
} }
private void createActionMap() { public String getStatusText() {
return statusLabel.getText();
String longestName = "";
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);
tableActions.add(action);
String actionName = entry.getKey();
KeyStroke ks = options.getKeyStroke(actionName, null);
keyStrokesByFullName.put(actionName, ks);
addToKeyMap(ks, actionName);
originalValues.put(actionName, ks);
String shortName = action.getName();
if (shortName.length() > longestName.length()) {
longestName = shortName;
}
} }
private void initializeTableWidth() {
String longestName = keyBindings.getLongestActionName();
Font f = actionTable.getFont(); Font f = actionTable.getFont();
FontMetrics fm = actionTable.getFontMetrics(f); FontMetrics fm = actionTable.getFontMetrics(f);
int maxWidth = 0; int maxWidth = 0;
@ -197,7 +138,7 @@ public class KeyBindingsPanel extends JPanel {
private void createPanelComponents() { private void createPanelComponents() {
setLayout(new BorderLayout(10, 10)); setLayout(new BorderLayout(10, 10));
tableModel = new KeyBindingsTableModel(); tableModel = new KeyBindingsTableModel(new ArrayList<>(keyBindings.getUniqueActions()));
actionTable = new GTable(tableModel); actionTable = new GTable(tableModel);
JScrollPane sp = new JScrollPane(actionTable); JScrollPane sp = new JScrollPane(actionTable);
@ -225,6 +166,8 @@ public class KeyBindingsPanel extends JPanel {
add(centerPanel, BorderLayout.CENTER); add(centerPanel, BorderLayout.CENTER);
add(statusPanel, BorderLayout.SOUTH); add(statusPanel, BorderLayout.SOUTH);
actionTable.getSelectionModel().addListSelectionListener(new TableSelectionListener());
} }
private JPanel createStatusPanel(JPanel keyPanel) { private JPanel createStatusPanel(JPanel keyPanel) {
@ -264,7 +207,7 @@ public class KeyBindingsPanel extends JPanel {
} }
private JPanel createKeyEntryPanel() { private JPanel createKeyEntryPanel() {
ksField = new KeyEntryTextField(20, keyStroke -> processKeyStrokeEntry(keyStroke)); ksField = new KeyEntryTextField(20, keyStroke -> keyStrokeChanged(keyStroke));
// this is the lower panel that holds the key entry text field // this is the lower panel that holds the key entry text field
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT)); JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
@ -326,10 +269,9 @@ public class KeyBindingsPanel extends JPanel {
// give Swing a chance to repaint // give Swing a chance to repaint
Swing.runLater(() -> { Swing.runLater(() -> {
// clear the current user key stroke so that it does not appear as though the // clear the action to avoid the appearance of editing while restoring
// user is editing while importing
actionTable.clearSelection(); actionTable.clearSelection();
processKeyBindingsFromOptions(KeyBindingUtils.importKeyBindings()); loadKeyBindingsFromImportedOptions(KeyBindingUtils.importKeyBindings());
}); });
}); });
@ -338,7 +280,7 @@ public class KeyBindingsPanel extends JPanel {
exportButton.addActionListener(event -> { exportButton.addActionListener(event -> {
// prompt user to apply changes before exporting // prompt user to apply changes before exporting
boolean continueExport = showExportPrompt(); boolean continueExport = showApplyPrompt();
if (!continueExport) { if (!continueExport) {
return; return;
@ -358,7 +300,7 @@ public class KeyBindingsPanel extends JPanel {
return containerPanel; return containerPanel;
} }
private boolean showExportPrompt() { private boolean showApplyPrompt() {
boolean continueOperation = true; boolean continueOperation = true;
if (unappliedChanges) { if (unappliedChanges) {
int userChoice = OptionDialog.showYesNoCancelDialog(KeyBindingsPanel.this, int userChoice = OptionDialog.showYesNoCancelDialog(KeyBindingsPanel.this,
@ -390,14 +332,11 @@ public class KeyBindingsPanel extends JPanel {
private Map<String, KeyStroke> createActionNameToKeyStrokeMap(Options keyBindingOptions) { private Map<String, KeyStroke> createActionNameToKeyStrokeMap(Options keyBindingOptions) {
Map<String, KeyStroke> localActionMap = new HashMap<>(); Map<String, KeyStroke> localActionMap = new HashMap<>();
List<String> optionNames = keyBindingOptions.getOptionNames(); List<String> optionNames = keyBindingOptions.getOptionNames();
for (String name : optionNames) {
for (String element : optionNames) { KeyStroke newKeyStroke = keyBindingOptions.getKeyStroke(name, null);
KeyStroke newKeyStroke = keyBindingOptions.getKeyStroke(element, null); localActionMap.put(name, newKeyStroke);
localActionMap.put(element, newKeyStroke);
} }
return localActionMap; return localActionMap;
} }
@ -415,52 +354,12 @@ public class KeyBindingsPanel extends JPanel {
} }
private void restoreDefaultKeybindings() { private void restoreDefaultKeybindings() {
Iterator<String> iter = keyStrokesByFullName.keySet().iterator(); keyBindings.restoreOptions();
while (iter.hasNext()) {
String actionName = iter.next();
List<DockingActionIf> actions = actionsByFullName.get(actionName);
if (actions.isEmpty()) {
throw new AssertException("No actions defined for " + actionName);
}
// pick one action, they are all conceptually the same
DockingActionIf action = actions.get(0);
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
KeyStroke newKeyStroke =
(defaultBinding == null) ? null : defaultBinding.getKeyBinding();
updateOptions(actionName, currentKeyStroke, newKeyStroke);
}
// let the table know that changes may have been made // let the table know that changes may have been made
tableModel.fireTableDataChanged(); tableModel.fireTableDataChanged();
} }
private void addListeners() {
selectionModel = actionTable.getSelectionModel();
selectionModel.addListSelectionListener(new TableSelectionListener());
}
private boolean checkAction(String actionName, KeyStroke keyStroke) {
String ksName = KeyEntryTextField.parseKeyStroke(keyStroke);
// remove old keystroke for action name
KeyStroke oldKs = keyStrokesByFullName.get(actionName);
if (oldKs != null) {
String oldName = KeyEntryTextField.parseKeyStroke(oldKs);
if (oldName.equals(ksName)) {
return false;
}
removeFromKeyMap(oldKs, actionName);
}
addToKeyMap(keyStroke, actionName);
keyStrokesByFullName.put(actionName, keyStroke);
changesMade(true);
return true;
}
// signals that there are unapplied changes // signals that there are unapplied changes
private void changesMade(boolean changes) { private void changesMade(boolean changes) {
propertyChangeListener.propertyChange( propertyChangeListener.propertyChange(
@ -469,12 +368,11 @@ public class KeyBindingsPanel extends JPanel {
} }
private DockingActionIf getSelectedAction() { private DockingActionIf getSelectedAction() {
if (selectionModel.isSelectionEmpty()) { if (actionTable.getSelectedRowCount() == 0) {
return null; return null;
} }
int selectedRow = actionTable.getSelectedRow(); int selectedRow = actionTable.getSelectedRow();
int modelRow = tableFilterPanel.getModelRow(selectedRow); return tableFilterPanel.getRowObject(selectedRow);
return tableActions.get(modelRow);
} }
private String getSelectedActionName() { private String getSelectedActionName() {
@ -485,62 +383,20 @@ public class KeyBindingsPanel extends JPanel {
return action.getFullName(); return action.getFullName();
} }
private void addToKeyMap(KeyStroke ks, String actionName) {
if (ks == null) {
return;
}
String ksName = KeyEntryTextField.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
if (list == null) {
list = new ArrayList<>();
actionNamesByKeyStroke.put(ksName, list);
}
if (!list.contains(actionName)) {
list.add(actionName);
}
}
private void removeFromKeyMap(KeyStroke ks, String actionName) {
if (ks == null) {
return;
}
String ksName = KeyEntryTextField.parseKeyStroke(ks);
List<String> list = actionNamesByKeyStroke.get(ksName);
if (list != null) {
list.remove(actionName);
if (list.isEmpty()) {
actionNamesByKeyStroke.remove(ksName);
}
}
}
private void showActionsMappedToKeyStroke(String ksName) { private void showActionsMappedToKeyStroke(String ksName) {
List<String> list = actionNamesByKeyStroke.get(ksName);
if (list == null) { String text = keyBindings.getActionsForKeyStrokeText(ksName);
return; if (StringUtils.isBlank(text)) {
} text = " ";
if (list.size() > 0) {
StringBuffer sb = new StringBuffer();
sb.append("Actions mapped to key " + ksName + ":\n");
for (int i = 0; i < list.size(); i++) {
sb.append(" ");
sb.append(list.get(i));
if (i < list.size() - 1) {
sb.append("\n");
}
}
updateInfoPanel(sb.toString());
}
else {
clearInfoPanel();
} }
updateCollisionPanel(text);
} }
private void clearInfoPanel() { private void clearInfoPanel() {
updateInfoPanel(" "); updateCollisionPanel(" ");
} }
private void updateInfoPanel(String text) { private void updateCollisionPanel(String text) {
infoPanel.removeAll(); infoPanel.removeAll();
infoPanel.repaint(); infoPanel.repaint();
collisionLabel = new MultiLineLabel(text); collisionLabel = new MultiLineLabel(text);
@ -550,111 +406,94 @@ public class KeyBindingsPanel extends JPanel {
validate(); validate();
} }
private void processKeyBindingsFromOptions(Options keyBindingOptions) { private void loadKeyBindingsFromImportedOptions(Options keyBindingOptions) {
if (keyBindingOptions == null) { if (keyBindingOptions == null) {
return; return;
} }
Map<String, KeyStroke> keyBindingsMap = createActionNameToKeyStrokeMap(keyBindingOptions); Map<String, KeyStroke> keyStrokesByActionName =
if (keyBindingsMap == null) { createActionNameToKeyStrokeMap(keyBindingOptions);
if (keyStrokesByActionName == null) {
return; return;
} }
boolean changes = false; boolean changes = false;
// add each new key stroke mapping // add each new key stroke mapping
Iterator<String> iterator = keyBindingsMap.keySet().iterator(); for (String name : keyStrokesByActionName.keySet()) {
while (iterator.hasNext()) {
String name = iterator.next(); KeyStroke keyStroke = keyStrokesByActionName.get(name);
KeyStroke keyStroke = keyBindingsMap.get(name);
keyStroke = KeyBindingUtils.validateKeyStroke(keyStroke); keyStroke = KeyBindingUtils.validateKeyStroke(keyStroke);
// prevent non-existing keybindings from being added to Ghidra (this can happen // prevent non-existing keybindings from being added (this can happen when actions exist
// when actions exist in the imported bindings, but have been removed from // in the imported bindings, but have been removed from the tool
// Ghidra if (!keyBindings.containsAction(name)) {
if (!keyStrokesByFullName.containsKey(name)) {
continue; continue;
} }
// check to see if the key stroke results in a change and // check to see if the key stroke results in a change and
// record that value // record that value
changes |= processKeyStroke(name, keyStroke); changes |= setActionKeyStroke(name, keyStroke);
} }
if (changes) { if (changes) {
changesMade(true); changesMade(true);
tableModel.fireTableDataChanged();
} }
} }
/** /**
* Processes KeyStroke entry from the text field. * Processes KeyStroke entry from the text field.
*/ */
private void processKeyStrokeEntry(KeyStroke ks) { private void keyStrokeChanged(KeyStroke ks) {
clearInfoPanel(); clearInfoPanel();
// An action must be selected DockingActionIf action = getSelectedAction();
if (selectionModel.isSelectionEmpty()) { if (action == null) {
statusLabel.setText("No action is selected."); statusLabel.setText("No action is selected.");
return; return;
} }
if (ks != null && ReservedKeyBindings.isReservedKeystroke(ks)) { ToolActions toolActions = (ToolActions) tool.getToolActions();
statusLabel.setText(KeyEntryTextField.parseKeyStroke(ks) + " is a reserved keystroke"); String errorMessage = toolActions.validateActionKeyBinding(action, ks);
if (errorMessage != null) {
statusLabel.setText(errorMessage);
ksField.clearField(); ksField.clearField();
return; return;
} }
String selectedActionName = getSelectedActionName(); String selectedActionName = getSelectedActionName();
if (selectedActionName != null) { if (selectedActionName != null) {
if (processKeyStroke(selectedActionName, ks)) { if (setActionKeyStroke(selectedActionName, ks)) {
String keyStrokeText = KeyEntryTextField.parseKeyStroke(ks); String keyStrokeText = KeyBindingUtils.parseKeyStroke(ks);
showActionsMappedToKeyStroke(keyStrokeText); showActionsMappedToKeyStroke(keyStrokeText);
tableModel.fireTableDataChanged(); tableModel.fireTableDataChanged();
changesMade(true);
} }
} }
} }
// returns true if the key stroke is a new value // returns true if the key stroke is a new value
private boolean processKeyStroke(String actionName, KeyStroke keyStroke) { private boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
// Clear entry if enter or backspace if (!isValidKeyStroke(keyStroke)) {
if (keyStroke == null) { ksField.setText("");
removeKeystroke(actionName); return keyBindings.removeKeyStroke(actionName);
}
else {
char keyChar = keyStroke.getKeyChar();
if (Character.isWhitespace(keyChar) ||
Character.getType(keyChar) == Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE) {
removeKeystroke(actionName);
}
else {
// check the action to see if is different than the current value
return checkAction(actionName, keyStroke);
}
} }
return keyBindings.setActionKeyStroke(actionName, keyStroke);
}
private boolean isValidKeyStroke(KeyStroke ks) {
if (ks == null) {
return false; return false;
} }
char keyChar = ks.getKeyChar();
private void removeKeystroke(String selectedActionName) { return !Character.isWhitespace(keyChar) &&
ksField.setText(""); Character.getType(keyChar) != Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE;
if (keyStrokesByFullName.containsKey(selectedActionName)) {
KeyStroke stroke = keyStrokesByFullName.get(selectedActionName);
if (stroke == null) {
// nothing to remove; nothing has changed
return;
}
removeFromKeyMap(stroke, selectedActionName);
keyStrokesByFullName.put(selectedActionName, null);
tableModel.fireTableDataChanged();
changesMade(true);
}
} }
Map<String, KeyStroke> getKeyStrokeMap() { Map<String, KeyStroke> getKeyStrokeMap() {
return keyStrokesByFullName; return keyBindings.getKeyStrokesByFullActionName();
} }
//================================================================================================== //==================================================================================================
@ -678,12 +517,12 @@ public class KeyBindingsPanel extends JPanel {
} }
helpButton.setEnabled(true); helpButton.setEnabled(true);
KeyStroke ks = keyStrokesByFullName.get(fullActionName); KeyStroke ks = keyBindings.getKeyStroke(fullActionName);
String ksName = ""; String ksName = "";
clearInfoPanel(); clearInfoPanel();
if (ks != null) { if (ks != null) {
ksName = KeyEntryTextField.parseKeyStroke(ks); ksName = KeyBindingUtils.parseKeyStroke(ks);
showActionsMappedToKeyStroke(ksName); showActionsMappedToKeyStroke(ksName);
} }
@ -693,9 +532,7 @@ public class KeyBindingsPanel extends JPanel {
statusLabel.setPreferredSize( statusLabel.setPreferredSize(
new Dimension(statusLabel.getPreferredSize().width, STATUS_LABEL_HEIGHT)); new Dimension(statusLabel.getPreferredSize().width, STATUS_LABEL_HEIGHT));
// pick one action, they are all conceptually the same DockingActionIf action = getSelectedAction();
List<DockingActionIf> actions = actionsByFullName.get(fullActionName);
DockingActionIf action = actions.get(0);
String description = action.getDescription(); String description = action.getDescription();
if (description == null || description.trim().isEmpty()) { if (description == null || description.trim().isEmpty()) {
description = action.getName(); description = action.getName();
@ -709,8 +546,11 @@ public class KeyBindingsPanel extends JPanel {
private final String[] columnNames = private final String[] columnNames =
{ "Action Name", "KeyBinding", "Plugin Name" }; { "Action Name", "KeyBinding", "Plugin Name" };
KeyBindingsTableModel() { private List<DockingActionIf> actions;
KeyBindingsTableModel(List<DockingActionIf> actions) {
super(0); super(0);
this.actions = actions;
} }
@Override @Override
@ -725,9 +565,9 @@ public class KeyBindingsPanel extends JPanel {
case ACTION_NAME: case ACTION_NAME:
return action.getName(); return action.getName();
case KEY_BINDING: case KEY_BINDING:
KeyStroke ks = keyStrokesByFullName.get(action.getFullName()); KeyStroke ks = keyBindings.getKeyStroke(action.getFullName());
if (ks != null) { if (ks != null) {
return KeyEntryTextField.parseKeyStroke(ks); return KeyBindingUtils.parseKeyStroke(ks);
} }
return ""; return "";
case PLUGIN_NAME: case PLUGIN_NAME:
@ -738,7 +578,7 @@ public class KeyBindingsPanel extends JPanel {
@Override @Override
public List<DockingActionIf> getModelData() { public List<DockingActionIf> getModelData() {
return tableActions; return actions;
} }
@Override @Override
@ -758,7 +598,7 @@ public class KeyBindingsPanel extends JPanel {
@Override @Override
public int getRowCount() { public int getRowCount() {
return tableActions.size(); return actions.size();
} }
@Override @Override

View file

@ -271,7 +271,7 @@ public class OptionsManager implements OptionsService, OptionsChangeListener {
private KeyBindingsPanel panel; private KeyBindingsPanel panel;
KeyBindingOptionsEditor() { KeyBindingOptionsEditor() {
panel = new KeyBindingsPanel(tool, getOptions(DockingToolConstants.KEY_BINDINGS)); panel = new KeyBindingsPanel(tool);
} }
@Override @Override

View file

@ -95,19 +95,11 @@ public class GhidraToolTemplate implements ToolTemplate {
return iconURL; return iconURL;
} }
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hashtables such as those provided by
* <code>java.util.Hashtable</code>.
*/
@Override @Override
public int hashCode() { public int hashCode() {
return getName().hashCode(); return getName().hashCode();
} }
/**
* Indicates whether some other object is "equal to" this one.
*/
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj == null) { if (obj == null) {
@ -129,13 +121,6 @@ public class GhidraToolTemplate implements ToolTemplate {
return getName().equals(otherTemplate.getName()); return getName().equals(otherTemplate.getName());
} }
/**
* Returns a string representation of the object. In general, the
* <code>toString</code> method returns a string that
* "textually represents" this object. The result should
* be a concise but informative representation that is easy for a
* person to read.
*/
@Override @Override
public String toString() { public String toString() {
return getName() + " - " + path; return getName() + " - " + path;
@ -154,7 +139,7 @@ public class GhidraToolTemplate implements ToolTemplate {
catch (ClassNotFoundException e) { catch (ClassNotFoundException e) {
Msg.warn(this, "Tool supported content class not found: " + className); Msg.warn(this, "Tool supported content class not found: " + className);
} }
catch (Exception exc) {//TODO catch (Exception exc) {
Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc); Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc);
} }
} }

View file

@ -31,7 +31,6 @@ import docking.DialogComponentProvider;
import docking.StatusBar; import docking.StatusBar;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import docking.actions.KeyEntryDialog; import docking.actions.KeyEntryDialog;
import docking.actions.ToolActions;
import docking.options.OptionsService; import docking.options.OptionsService;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
@ -288,10 +287,8 @@ public class ToolScreenShots extends GhidraScreenShotGenerator {
public void testSetKeyBindings() { public void testSetKeyBindings() {
tool = env.launchDefaultTool(); tool = env.launchDefaultTool();
ToolActions toolActions = (ToolActions) getInstanceField("toolActions", tool);
DockingActionIf action = getAction(tool, "FunctionPlugin", "Delete Function"); DockingActionIf action = getAction(tool, "FunctionPlugin", "Delete Function");
final KeyEntryDialog keyEntryDialog = new KeyEntryDialog(action, toolActions); final KeyEntryDialog keyEntryDialog = new KeyEntryDialog(tool, action);
runSwing(() -> tool.showDialog(keyEntryDialog), false); runSwing(() -> tool.showDialog(keyEntryDialog), false);
captureDialog(); captureDialog();