mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-08-28 05:20:21 +00:00
GP-4436 - Mouse Bindings
This commit is contained in:
parent
eca5195dea
commit
8aeebf919a
|
@ -21,6 +21,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.action.*;
|
||||
|
@ -39,6 +40,7 @@ import ghidra.app.services.NavigationHistoryService;
|
|||
import ghidra.app.util.HelpTopics;
|
||||
import ghidra.app.util.viewer.field.BrowserCodeUnitFormat;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.model.address.Address;
|
||||
|
@ -46,6 +48,7 @@ import ghidra.program.model.listing.*;
|
|||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
import ghidra.util.HelpLocation;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* <CODE>NextPrevAddressPlugin</CODE> allows the user to go back and forth in
|
||||
|
@ -83,7 +86,7 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
|
||||
/**
|
||||
* Creates a new instance of the plugin
|
||||
*
|
||||
*
|
||||
* @param tool the tool
|
||||
*/
|
||||
public NextPrevAddressPlugin(PluginTool tool) {
|
||||
|
@ -119,7 +122,7 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
|
||||
//==================================================================================================
|
||||
// Private Methods
|
||||
//==================================================================================================
|
||||
//==================================================================================================
|
||||
|
||||
private List<DockingActionIf> getPreviousActions(Navigatable navigatable) {
|
||||
Program lastProgram = null;
|
||||
|
@ -297,6 +300,9 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
|
||||
private class NextPreviousAction extends MultiActionDockingAction {
|
||||
|
||||
private static final int MOUSE_BUTTON_4 = 4;
|
||||
private static final int MOUSE_BUTTON_5 = 5;
|
||||
|
||||
private final boolean isNext;
|
||||
|
||||
NextPreviousAction(String name, String owner, boolean isNext) {
|
||||
|
@ -306,8 +312,15 @@ public class NextPrevAddressPlugin extends Plugin {
|
|||
setToolBarData(new ToolBarData(isNext ? NEXT_ICON : PREVIOUS_ICON,
|
||||
ToolConstants.TOOLBAR_GROUP_TWO));
|
||||
setHelpLocation(new HelpLocation(HelpTopics.NAVIGATION, name));
|
||||
int keycode = isNext ? KeyEvent.VK_RIGHT : KeyEvent.VK_LEFT;
|
||||
setKeyBindingData(new KeyBindingData(keycode, InputEvent.ALT_DOWN_MASK));
|
||||
|
||||
int keyCode = isNext ? KeyEvent.VK_RIGHT : KeyEvent.VK_LEFT;
|
||||
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, InputEvent.ALT_DOWN_MASK);
|
||||
|
||||
int mouseButton = isNext ? MOUSE_BUTTON_5 : MOUSE_BUTTON_4;
|
||||
MouseBinding mouseBinding = new MouseBinding(mouseButton);
|
||||
|
||||
setKeyBindingData(new KeyBindingData(new ActionTrigger(keyStroke, mouseBinding)));
|
||||
|
||||
setDescription(isNext ? "Go to next location" : "Go to previous location");
|
||||
addToWindowWhen(NavigatableActionContext.class);
|
||||
}
|
||||
|
|
|
@ -345,7 +345,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
|
|||
plugin.getTool().setStatusInfo("User cancelled keybinding.");
|
||||
return;
|
||||
}
|
||||
action.setKeyBindingData(new KeyBindingData(dialog.getKeyStroke()));
|
||||
KeyStroke newKs = dialog.getKeyStroke();
|
||||
action.setKeyBindingData(newKs == null ? null : new KeyBindingData(newKs));
|
||||
scriptTable.repaint();
|
||||
}
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ class KeyBindingInputDialog extends DialogComponentProvider implements KeyEntryL
|
|||
}
|
||||
|
||||
void setKeyStroke(KeyStroke ks) {
|
||||
this.ks = ks;
|
||||
kbField.setKeyStroke(ks);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,10 +85,13 @@ class ScriptAction extends DockingAction {
|
|||
}
|
||||
|
||||
private KeyBindingData checkForFallbackKeybindingCondition(KeyBindingData keyBindingData) {
|
||||
KeyStroke newKeyStroke = keyBindingData.getKeyBinding();
|
||||
if (newKeyStroke != null) {
|
||||
// we have a valid value; the current keybinding data is what we want
|
||||
return keyBindingData;
|
||||
|
||||
if (keyBindingData != null) {
|
||||
KeyStroke newKeyStroke = keyBindingData.getKeyBinding();
|
||||
if (newKeyStroke != null) {
|
||||
// we have a valid value; the current keybinding data is what we want
|
||||
return keyBindingData;
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if we have a fallback value
|
||||
|
@ -106,7 +109,10 @@ class ScriptAction extends DockingAction {
|
|||
private void updateUserDefinedKeybindingStatus(KeyBindingData keyBindingData) {
|
||||
// we have a user defined keybinding if the keystroke for the action differs from
|
||||
// that which is defined in the metadata of the script
|
||||
KeyStroke actionKeyStroke = keyBindingData.getKeyBinding();
|
||||
KeyStroke actionKeyStroke = null;
|
||||
if (keyBindingData != null) {
|
||||
actionKeyStroke = keyBindingData.getKeyBinding();
|
||||
}
|
||||
ScriptInfo info = infoManager.getExistingScriptInfo(script);
|
||||
KeyStroke metadataKeyBinding = info.getKeyBinding();
|
||||
isUserDefinedKeyBinding = !SystemUtilities.isEqual(actionKeyStroke, metadataKeyBinding);
|
||||
|
@ -128,8 +134,9 @@ class ScriptAction extends DockingAction {
|
|||
ScriptInfo info = infoManager.getScriptInfo(script);
|
||||
KeyStroke stroke = info.getKeyBinding();
|
||||
if (!isUserDefinedKeyBinding) {
|
||||
setKeyBindingData(new KeyBindingData(stroke));
|
||||
setKeyBindingData(stroke == null ? null : new KeyBindingData(stroke));
|
||||
}
|
||||
|
||||
Icon icon = info.getToolBarImage(false);
|
||||
if (icon != null) {
|
||||
ToolBarData data = getToolBarData();
|
||||
|
|
|
@ -588,8 +588,8 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
if (isRunningHeadless()) {
|
||||
// only change client authenticator in headless mode
|
||||
try {
|
||||
HeadlessClientAuthenticator
|
||||
.installHeadlessClientAuthenticator(ClientUtil.getUserName(), null, false);
|
||||
HeadlessClientAuthenticator.installHeadlessClientAuthenticator(
|
||||
ClientUtil.getUserName(), null, false);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Unexpected Exception", e);
|
||||
|
@ -1316,6 +1316,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
case FILE_TYPE:
|
||||
case FONT_TYPE:
|
||||
case KEYSTROKE_TYPE:
|
||||
case ACTION_TRIGGER:
|
||||
// do nothing; don't allow user to set these options (doesn't make any sense)
|
||||
break;
|
||||
|
||||
|
@ -2333,22 +2334,22 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
|
||||
/**
|
||||
* Prompts for multiple values at the same time. To use this method, you must first
|
||||
* create a {@link GhidraValuesMap} and define the values that will be supplied by this method.
|
||||
* create a {@link GhidraValuesMap} and define the values that will be supplied by this method.
|
||||
* In the GUI environment, this will result in a single dialog with an entry for each value
|
||||
* defined in the values map. This method returns a GhidraValuesMap with the values supplied by
|
||||
* the user in GUI mode or command line arguments in headless mode. If the user cancels the
|
||||
* dialog, a cancelled exception will be thrown, and unless it is explicity caught by the
|
||||
* script, will terminate the script. Also, if the values map has a {@link ValuesMapValidator},
|
||||
* the values will be validated when the user presses the "OK" button and will only exit the
|
||||
* the user in GUI mode or command line arguments in headless mode. If the user cancels the
|
||||
* dialog, a cancelled exception will be thrown, and unless it is explicity caught by the
|
||||
* script, will terminate the script. Also, if the values map has a {@link ValuesMapValidator},
|
||||
* the values will be validated when the user presses the "OK" button and will only exit the
|
||||
* dialog if the validate check passes. Otherwise, the validator should have reported an error
|
||||
* message in the dialog and the dialog will remain visible.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* Regardless of environment -- if script arguments have been set, this method will use the
|
||||
* next arguments in the array and advance the array index until all values in the values map
|
||||
* have been satisfied and so the next call to an ask method will get the next argument after
|
||||
* those consumed by this call.
|
||||
*
|
||||
* those consumed by this call.
|
||||
*
|
||||
* @param title the title of the dialog if in GUI mode
|
||||
* @param optionalMessage an optional message that is displayed in the dialog, just above the
|
||||
* list of name/value pairs
|
||||
|
@ -2616,7 +2617,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
* (in headless mode or when using .properties file)
|
||||
* @param message the message to display next to the input field (in GUI mode) or the
|
||||
* second part of the variable name (in headless mode or when using .properties file)
|
||||
* @param defaultValue the optional default address as a String - if null is passed or an invalid
|
||||
* @param defaultValue the optional default address as a String - if null is passed or an invalid
|
||||
* address is given no default will be shown in dialog
|
||||
* @return the user-specified Address value
|
||||
* @throws CancelledException if the user hit the 'cancel' button in GUI mode
|
||||
|
@ -2759,14 +2760,14 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
*
|
||||
* @param title the title of the pop-up dialog (in GUI mode) or the variable name (in
|
||||
* headless mode)
|
||||
* @return the user-selected Program with this script as the consumer if a program was
|
||||
* @return the user-selected Program with this script as the consumer if a program was
|
||||
* selected. Null is returned if a program is not selected. NOTE: It is very important that
|
||||
* the program instance returned by this method ALWAYS be properly released when no longer
|
||||
* the program instance returned by this method ALWAYS be properly released when no longer
|
||||
* needed. The script which invoked this method must be
|
||||
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
|
||||
* properly release the program may result in improper project disposal. If the program was
|
||||
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
|
||||
* properly release the program may result in improper project disposal. If the program was
|
||||
* opened by the tool, the tool will be a second consumer responsible for its own release.
|
||||
* @throws VersionException if the Program is out-of-date from the version of Ghidra and an
|
||||
* @throws VersionException if the Program is out-of-date from the version of Ghidra and an
|
||||
* upgrade was not been performed. In non-headless mode, the user will have already been
|
||||
* notified via a popup dialog.
|
||||
* @throws IOException if there is an error accessing the Program's DomainObject
|
||||
|
@ -2781,7 +2782,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
|
||||
/**
|
||||
* Returns a Program, using the title parameter for guidance with the option to upgrade
|
||||
* if needed. The actual behavior of the method depends on your environment, which can be
|
||||
* if needed. The actual behavior of the method depends on your environment, which can be
|
||||
* GUI or headless. You can control whether or not the program is allowed to upgrade via
|
||||
* the {@code upgradeIfNeeded} parameter.
|
||||
* <br>
|
||||
|
@ -2811,14 +2812,14 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
* @param upgradeIfNeeded if true, program will be upgraded if needed and possible. If false,
|
||||
* the program will only be upgraded after first prompting the user. In headless mode, it will
|
||||
* attempt to upgrade only if the parameter is true.
|
||||
* @return the user-selected Program with this script as the consumer if a program was
|
||||
* @return the user-selected Program with this script as the consumer if a program was
|
||||
* selected. Null is returned if a program is not selected. NOTE: It is very important that
|
||||
* the program instance returned by this method ALWAYS be properly released when no longer
|
||||
* the program instance returned by this method ALWAYS be properly released when no longer
|
||||
* needed. The script which invoked this method must be
|
||||
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
|
||||
* properly release the program may result in improper project disposal. If the program was
|
||||
* specified as the consumer upon release (i.e., {@code program.release(this) } - failure to
|
||||
* properly release the program may result in improper project disposal. If the program was
|
||||
* opened by the tool, the tool will be a second consumer responsible for its own release.
|
||||
* @throws VersionException if the Program is out-of-date from the version of GHIDRA and an
|
||||
* @throws VersionException if the Program is out-of-date from the version of GHIDRA and an
|
||||
* upgrade was not been performed. In non-headless mode, the user will have already been
|
||||
* notified via a popup dialog.
|
||||
* @throws IOException if there is an error accessing the Program's DomainObject
|
||||
|
@ -3139,13 +3140,13 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
* only be used in headed mode.
|
||||
* <p>
|
||||
* In the GUI environment, this method displays a password popup dialog that prompts the user
|
||||
* for a password. There is no pre-population of the input. If the user cancels the dialog, it
|
||||
* is immediately disposed, and any input to that dialog is cleared from memory. If the user
|
||||
* completes the dialog, then the password is returned in a wrapped buffer. The buffer can be
|
||||
* cleared by calling {@link Password#close()}; however, it is meant to be used in a
|
||||
* {@code try-with-resources} block. The pattern does not guarantee protection of the password,
|
||||
* for a password. There is no pre-population of the input. If the user cancels the dialog, it
|
||||
* is immediately disposed, and any input to that dialog is cleared from memory. If the user
|
||||
* completes the dialog, then the password is returned in a wrapped buffer. The buffer can be
|
||||
* cleared by calling {@link Password#close()}; however, it is meant to be used in a
|
||||
* {@code try-with-resources} block. The pattern does not guarantee protection of the password,
|
||||
* but it will help you avoid some typical pitfalls:
|
||||
*
|
||||
*
|
||||
* <pre>
|
||||
* String user = askString("Login", "Username:");
|
||||
* Project project;
|
||||
|
@ -3153,12 +3154,12 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
* project = doLoginAndOpenProject(user, password.getPasswordChars());
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* The buffer will be zero-filled upon leaving the {@code try-with-resources} block. If, in the
|
||||
* sample, the {@code doLoginAndOpenProject} method or any part of its implementation needs to
|
||||
* retain the password, it must make a copy. It is then the implementation's responsibility to
|
||||
* protect its copy.
|
||||
*
|
||||
*
|
||||
* @param title the title of the dialog
|
||||
* @param prompt the prompt to the left of the input field, or null to display "Password:"
|
||||
* @return the password
|
||||
|
@ -3639,10 +3640,10 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
* null is returned. For more control over the import process, {@link AutoImporter} may be
|
||||
* directly called.
|
||||
* <p>
|
||||
* NOTE: The returned {@link Program} is not automatically saved into the current project.
|
||||
* NOTE: The returned {@link Program} is not automatically saved into the current project.
|
||||
* <p>
|
||||
* NOTE: It is the responsibility of the script that calls this method to release the returned
|
||||
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
|
||||
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
|
||||
* needed, where <code>consumer</code> is <code>this</code>.
|
||||
*
|
||||
* @param file the file to import
|
||||
|
@ -3662,11 +3663,11 @@ public abstract class GhidraScript extends FlatProgramAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Imports the specified file as raw binary. For more control over the import process,
|
||||
* Imports the specified file as raw binary. For more control over the import process,
|
||||
* {@link AutoImporter} may be directly called.
|
||||
* <p>
|
||||
* NOTE: It is the responsibility of the script that calls this method to release the returned
|
||||
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
|
||||
* {@link Program} with {@link DomainObject#release(Object consumer)} when it is no longer
|
||||
* needed, where <code>consumer</code> is <code>this</code>.
|
||||
*
|
||||
* @param file the file to import
|
||||
|
|
|
@ -18,13 +18,13 @@ package ghidra.app.script;
|
|||
import org.apache.logging.log4j.message.Message;
|
||||
|
||||
/**
|
||||
* A simple {@link Message} implementation that allows us to use the filtering capability
|
||||
* A simple {@link Message} implementation that allows us to use the filtering capability
|
||||
* of log4j. This class has a formatted and unformatted message. log4j writes the the formatted
|
||||
* message out. Our formatted message is the original message given to us. We use the
|
||||
* unformatted message, in conjunction with a regex filter to allow for filtering such that
|
||||
* message out. Our formatted message is the original message given to us. We use the
|
||||
* unformatted message, in conjunction with a regex filter to allow for filtering such that
|
||||
* the script log file only has script messages.
|
||||
*
|
||||
* <P>See logj4-appender-rolling-file-scripts.xml
|
||||
*
|
||||
* <P>See log4j-appender-rolling-file-scripts.xml
|
||||
*/
|
||||
public class ScriptMessage implements Message {
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ class PropertiesXmlMgr {
|
|||
strMap.add(addr, str);
|
||||
}
|
||||
else if ("bookmarks".equals(type)) {
|
||||
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
|
||||
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
|
||||
// were saved as simple properties
|
||||
BookmarkManager bmMgr = program.getBookmarkManager();
|
||||
if (!overwrite) {
|
||||
|
@ -254,8 +254,7 @@ class PropertiesXmlMgr {
|
|||
list.setDate(name, new Date(value));
|
||||
}
|
||||
else if ("color".equals(type)) {
|
||||
Color color =
|
||||
ColorUtils.getColor(XmlUtilities.parseInt(element.getAttribute("VALUE")));
|
||||
Color color = ColorUtils.getColor(XmlUtilities.parseInt(element.getAttribute("VALUE")));
|
||||
list.setColor(name, color);
|
||||
}
|
||||
else if ("file".equals(type)) {
|
||||
|
@ -280,7 +279,19 @@ class PropertiesXmlMgr {
|
|||
String xmlString = XmlUtilities.unEscapeElementEntities(escapedXML);
|
||||
KeyStroke keyStroke =
|
||||
(KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(xmlString);
|
||||
list.setKeyStroke(name, keyStroke);
|
||||
|
||||
ActionTrigger trigger = null;
|
||||
if (keyStroke != null) {
|
||||
trigger = new ActionTrigger(keyStroke);
|
||||
}
|
||||
list.setActionTrigger(name, trigger);
|
||||
}
|
||||
else if ("actionTrigger".equals(type)) {
|
||||
String escapedXML = element.getAttribute("VALUE");
|
||||
String xmlString = XmlUtilities.unEscapeElementEntities(escapedXML);
|
||||
ActionTrigger actionTrigger =
|
||||
(ActionTrigger) OptionType.ACTION_TRIGGER.convertStringToObject(xmlString);
|
||||
list.setActionTrigger(name, actionTrigger);
|
||||
}
|
||||
else if ("custom".equals(type)) {
|
||||
String escapedXML = element.getAttribute("VALUE");
|
||||
|
@ -401,9 +412,15 @@ class PropertiesXmlMgr {
|
|||
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
|
||||
break;
|
||||
case KEYSTROKE_TYPE:
|
||||
attrs.addAttribute("TYPE", "keyStroke");
|
||||
KeyStroke keyStroke = propList.getKeyStroke(name, null);
|
||||
xmlString = OptionType.KEYSTROKE_TYPE.convertObjectToString(keyStroke);
|
||||
attrs.addAttribute("TYPE", "actionTrigger");
|
||||
ActionTrigger trigger = propList.getActionTrigger(name, null);
|
||||
xmlString = OptionType.ACTION_TRIGGER.convertObjectToString(trigger);
|
||||
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
|
||||
break;
|
||||
case ACTION_TRIGGER:
|
||||
attrs.addAttribute("TYPE", "actionTrigger");
|
||||
ActionTrigger actionTrigger = propList.getActionTrigger(name, null);
|
||||
xmlString = OptionType.ACTION_TRIGGER.convertObjectToString(actionTrigger);
|
||||
attrs.addAttribute("VALUE", XmlUtilities.escapeElementEntities(xmlString));
|
||||
break;
|
||||
case CUSTOM_TYPE:
|
||||
|
|
|
@ -30,6 +30,7 @@ import docking.actions.KeyEntryDialog;
|
|||
import docking.actions.ToolActions;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
|
@ -443,7 +444,8 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio
|
|||
ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
// shared option name/format: "Provider Name (Shared)" - the shared action's owner is the Tool
|
||||
runSwing(() -> keyOptions.setKeyStroke(provider.getName() + " (Shared)", newKs));
|
||||
runSwing(() -> keyOptions.setActionTrigger(provider.getName() + " (Shared)",
|
||||
new ActionTrigger(newKs)));
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
|
@ -491,7 +493,11 @@ public class ComponentProviderActionsTest extends AbstractGhidraHeadedIntegratio
|
|||
|
||||
// Option name: the action name with the 'Shared' owner
|
||||
String fullName = provider.getName() + " (Shared)";
|
||||
KeyStroke optionsKs = runSwing(() -> options.getKeyStroke(fullName, null));
|
||||
ActionTrigger actionTrigger = runSwing(() -> options.getActionTrigger(fullName, null));
|
||||
KeyStroke optionsKs = null;
|
||||
if (actionTrigger != null) {
|
||||
optionsKs = actionTrigger.getKeyStroke();
|
||||
}
|
||||
assertEquals("Key stroke in options does not match expected key stroke", expectedKs,
|
||||
optionsKs);
|
||||
}
|
||||
|
|
|
@ -807,6 +807,7 @@ public class CommentsPluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
assertNotNull(button);
|
||||
pressButton(button, false);
|
||||
waitForSwing();
|
||||
waitForBusyTool(tool);
|
||||
}
|
||||
|
||||
private CommentsDialog editComment(Address a) {
|
||||
|
|
|
@ -36,6 +36,7 @@ import docking.tool.util.DockingToolConstants;
|
|||
import docking.widgets.table.TableSortState;
|
||||
import ghidra.app.nav.Navigatable;
|
||||
import ghidra.app.nav.TestDummyNavigatable;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.DummyPluginTool;
|
||||
import ghidra.program.database.ProgramBuilder;
|
||||
|
@ -466,7 +467,7 @@ public class TableChooserDialogTest extends AbstractGhidraHeadedIntegrationTest
|
|||
ToolOptions keyOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
String name = action.getName() + " (" + action.getOwner() + ")";
|
||||
runSwing(() -> keyOptions.setKeyStroke(name, newKs));
|
||||
runSwing(() -> keyOptions.setActionTrigger(name, new ActionTrigger(newKs)));
|
||||
waitForSwing();
|
||||
|
||||
KeyStroke actual = action.getKeyBinding();
|
||||
|
|
|
@ -346,9 +346,13 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
private String clearKeyBinding(Options options) {
|
||||
String keyBindingName = "Go To Next Function (CodeBrowserPlugin)";
|
||||
KeyStroke ks = options.getKeyStroke(keyBindingName, null);
|
||||
ActionTrigger actionTrigger = options.getActionTrigger(keyBindingName, null);
|
||||
assertNotNull(actionTrigger);
|
||||
|
||||
KeyStroke ks = actionTrigger.getKeyStroke();
|
||||
assertNotNull(ks);
|
||||
options.setKeyStroke(keyBindingName, null);
|
||||
|
||||
options.setActionTrigger(keyBindingName, null);
|
||||
return keyBindingName;
|
||||
}
|
||||
|
||||
|
@ -378,7 +382,12 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
|
||||
private void verifyKeyBindingIsStillCleared(Options options, String optionName) {
|
||||
KeyStroke ksValue = options.getKeyStroke(optionName, null);
|
||||
ActionTrigger actionTrigger = options.getActionTrigger(optionName, null);
|
||||
if (actionTrigger == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyStroke ksValue = actionTrigger.getKeyStroke();
|
||||
assertNull(ksValue);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,8 +48,7 @@ import ghidra.app.plugin.core.memory.MemoryMapPlugin;
|
|||
import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin;
|
||||
import ghidra.app.plugin.core.navigation.NavigationHistoryPlugin;
|
||||
import ghidra.framework.model.ToolServices;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.mgr.OptionsManager;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
|
@ -194,12 +193,11 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
debug("d");
|
||||
|
||||
// now repeat the above test with changing some values before writing out
|
||||
invokeInstanceMethod("putObject", defaultKeyBindings,
|
||||
new Class[] { String.class, Object.class },
|
||||
new Object[] { "TestAction1 (Owner1)", KeyStroke.getKeyStroke(65, 0) });
|
||||
invokeInstanceMethod("putObject", defaultKeyBindings,
|
||||
new Class[] { String.class, Object.class },
|
||||
new Object[] { "TestAction2 (Owner 2)", KeyStroke.getKeyStroke(66, 0) });
|
||||
defaultKeyBindings.putObject("TestAction1 (Owner1)",
|
||||
new ActionTrigger(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0)));
|
||||
|
||||
defaultKeyBindings.putObject("TestAction2 (Owner 2)",
|
||||
new ActionTrigger(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0)));
|
||||
|
||||
debug("e");
|
||||
|
||||
|
@ -366,8 +364,9 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
setKeyBindingsUpDialog(tool);
|
||||
ToolOptions options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
KeyStroke optionBinding = options.getKeyStroke(action.getFullName(), null);
|
||||
assertEquals(appliedBinding, optionBinding);
|
||||
ActionTrigger actionTrigger = options.getActionTrigger(action.getFullName(), null);
|
||||
KeyStroke optionKeyStroke = actionTrigger.getKeyStroke();
|
||||
assertEquals(appliedBinding, optionKeyStroke);
|
||||
|
||||
closeAllWindows();
|
||||
}
|
||||
|
@ -429,7 +428,8 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
// setup our test variables
|
||||
panel = (KeyBindingsPanel) getEditorPanel(keyBindingsNode, optionsDialog);
|
||||
table = findComponent(panel, JTable.class);
|
||||
keyField = (JTextField) getInstanceField("ksField", panel);
|
||||
Object actionBindingPanel = getInstanceField("actionBindingPanel", panel);
|
||||
keyField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
|
||||
model = table.getModel();
|
||||
|
||||
debug("ff");
|
||||
|
@ -518,8 +518,7 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
String owner = action.getOwnerDescription();
|
||||
|
||||
for (int i = 0; i < model.getRowCount(); i++) {
|
||||
if (actionName.equals(model.getValueAt(i, 0)) &&
|
||||
owner.equals(model.getValueAt(i, 2))) {
|
||||
if (actionName.equals(model.getValueAt(i, 0)) && owner.equals(model.getValueAt(i, 2))) {
|
||||
final int idx = i;
|
||||
runSwing(() -> {
|
||||
table.setRowSelectionInterval(idx, idx);
|
||||
|
@ -627,7 +626,12 @@ public class KeyBindingUtilsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
for (String name : propertyNames) {
|
||||
|
||||
boolean match = panelKeyStrokeMap.containsKey(name);
|
||||
KeyStroke optionsKs = oldOptions.getKeyStroke(name, null);
|
||||
ActionTrigger actionTrigger = oldOptions.getActionTrigger(name, null);
|
||||
KeyStroke optionsKs = null;
|
||||
if (actionTrigger != null) {
|
||||
optionsKs = actionTrigger.getKeyStroke();
|
||||
}
|
||||
|
||||
KeyStroke panelKs = panelKeyStrokeMap.get(name);
|
||||
|
||||
// if the value is null, then it would not have been placed into the options map
|
||||
|
|
|
@ -94,10 +94,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
// look for the info panel
|
||||
MultiLineLabel label = findComponent(panel, MultiLineLabel.class);
|
||||
String str = "To add or change a key binding, select an action\n" +
|
||||
"and type any key combination\n" +
|
||||
" \n" +
|
||||
"To remove a key binding, select an action and\n" +
|
||||
"press <Enter> or <Backspace>";
|
||||
"and type any key combination\n" + " \n" +
|
||||
"To remove a key binding, select an action and\n" + "press <Enter> or <Backspace>";
|
||||
|
||||
assertEquals(str, label.getLabel());
|
||||
|
||||
|
@ -215,9 +213,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
// verify that no action is mapped to the new binding
|
||||
int keyCode = KeyEvent.VK_0;
|
||||
int modifiers = InputEvent.ALT_DOWN_MASK | InputEvent.ALT_GRAPH_DOWN_MASK;
|
||||
KeyEvent keyEvent =
|
||||
new KeyEvent(dialog, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), modifiers,
|
||||
keyCode, KeyEvent.CHAR_UNDEFINED);
|
||||
KeyEvent keyEvent = new KeyEvent(dialog, KeyEvent.KEY_PRESSED, System.currentTimeMillis(),
|
||||
modifiers, keyCode, KeyEvent.CHAR_UNDEFINED);
|
||||
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent);
|
||||
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
|
||||
Action action =
|
||||
|
@ -233,8 +230,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
assertEquals(ks, getKeyStroke(action1));
|
||||
|
||||
// verify the additional binding for 'Alt Graph'
|
||||
action =
|
||||
(Action) TestUtils.invokeInstanceMethod("getActionForKeyStroke", dwm, keyStroke);
|
||||
action = (Action) TestUtils.invokeInstanceMethod("getActionForKeyStroke", dwm, keyStroke);
|
||||
assertNotNull(action);
|
||||
}
|
||||
|
||||
|
@ -283,8 +279,8 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
boolean success = msg.contains(action1.getName()) && msg.contains(action2.getName());
|
||||
|
||||
assertTrue("In-use action message incorrect.\n\tIt should contain these 2 actions:\n\t\t" +
|
||||
action1.getName() + "\n\t\t" + action2.getName() + ".\nActual message:\n" +
|
||||
msg + "\n", success);
|
||||
action1.getName() + "\n\t\t" + action2.getName() + ".\nActual message:\n" + msg + "\n",
|
||||
success);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -560,8 +556,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
dialog.setVisible(true);
|
||||
});
|
||||
table = findComponent(panel, JTable.class);
|
||||
keyField = findComponent(panel, JTextField.class);
|
||||
keyField = (JTextField) getInstanceField("ksField", panel);
|
||||
keyField = (JTextField) findComponentByName(panel, "Key Entry Text Field");
|
||||
statusPane = findComponent(panel, JTextPane.class);
|
||||
model = table.getModel();
|
||||
waitForSwing();
|
||||
|
|
|
@ -24,6 +24,8 @@ import java.beans.PropertyEditor;
|
|||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
@ -60,6 +62,7 @@ import ghidra.framework.preferences.Preferences;
|
|||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.test.TestEnv;
|
||||
import ghidra.util.ColorUtils;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* Tests for the options dialog.
|
||||
|
@ -420,25 +423,154 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreDefaultsForKeybindings() throws Exception {
|
||||
String actionName = "Clear Cut";
|
||||
String pluginName = "DataTypeManagerPlugin";
|
||||
KeyStroke defaultKeyStroke = getKeyBinding(actionName);
|
||||
assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
|
||||
public void testKeybindings_SetMouseBounding_NoDefaultBindings() throws Exception {
|
||||
|
||||
int keyCode = KeyEvent.VK_Q;
|
||||
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
|
||||
String actionName = "Clear Color";
|
||||
String actionOwner = "ColorizingPlugin";
|
||||
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultKeyStroke);
|
||||
|
||||
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultMouseBinding);
|
||||
|
||||
int button = 1;
|
||||
int modifiers = 0;
|
||||
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
|
||||
|
||||
apply();
|
||||
assertOptionsKeyStroke(tool, actionName, pluginName, newKeyStroke);
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
KeyStroke currentBinding = getKeyBinding(actionName);
|
||||
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
|
||||
assertEquals("Mouse binding not restored after a call to restore defautls",
|
||||
defaultMouseBinding, currentMouseBinding);
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeybindings_SetMouseBoundingAndKeyBinding_NoDefaultBindings() throws Exception {
|
||||
|
||||
String actionName = "Clear Color";
|
||||
String actionOwner = "ColorizingPlugin";
|
||||
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultKeyStroke);
|
||||
|
||||
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultMouseBinding);
|
||||
|
||||
int button = 1;
|
||||
int modifiers = 0;
|
||||
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
|
||||
|
||||
int keyCode = KeyEvent.VK_Q;
|
||||
modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
|
||||
|
||||
apply();
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
|
||||
assertEquals("Mouse binding not restored after a call to restore defautls",
|
||||
defaultMouseBinding, currentMouseBinding);
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeybindings_SetMouseBoundingAndKeyBinding_ClearKeyBinding() throws Exception {
|
||||
|
||||
String actionName = "Clear Color";
|
||||
String actionOwner = "ColorizingPlugin";
|
||||
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultKeyStroke);
|
||||
|
||||
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultMouseBinding);
|
||||
|
||||
int keyCode = KeyEvent.VK_Q;
|
||||
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
|
||||
|
||||
int button = 1;
|
||||
modifiers = 0;
|
||||
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
|
||||
|
||||
apply();
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
|
||||
|
||||
clearKeyBinding(actionName, actionOwner);
|
||||
apply();
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding); // unchanged
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
|
||||
assertEquals("Mouse binding not restored after a call to restore defautls",
|
||||
defaultMouseBinding, currentMouseBinding);
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeybindings_SetMouseBounding_DefaultKeyBinding() throws Exception {
|
||||
|
||||
String actionName = "Clear Cut";
|
||||
String actionOwner = "DataTypeManagerPlugin";
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertNotNull(defaultKeyStroke);
|
||||
|
||||
MouseBinding defaultMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
assertNull(defaultMouseBinding);
|
||||
|
||||
int button = 1;
|
||||
int modifiers = 0;
|
||||
MouseBinding newMouseBinding = setMouseBinding(actionName, actionOwner, modifiers, button);
|
||||
|
||||
apply();
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, newMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
|
||||
assertEquals("Mouse binding not restored after a call to restore defautls",
|
||||
defaultMouseBinding, currentMouseBinding);
|
||||
assertOptionsMouseBinding(tool, actionName, actionOwner, defaultMouseBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreDefaultsForKeybindings() throws Exception {
|
||||
String actionName = "Clear Cut";
|
||||
String actionOwner = "DataTypeManagerPlugin";
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
|
||||
int keyCode = KeyEvent.VK_Q;
|
||||
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
|
||||
|
||||
apply();
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, newKeyStroke);
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
|
||||
currentBinding);
|
||||
assertOptionsKeyStroke(tool, actionName, pluginName, defaultKeyStroke);
|
||||
assertOptionsKeyStroke(tool, actionName, actionOwner, defaultKeyStroke);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -449,23 +581,23 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
setUpDialog(frontEndTool);
|
||||
|
||||
String actionName = "Archive Project";
|
||||
String pluginName = "ArchivePlugin";
|
||||
KeyStroke defaultKeyStroke = getKeyBinding(actionName);
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
|
||||
String actionOwner = "ArchivePlugin";
|
||||
KeyStroke defaultKeyStroke = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, defaultKeyStroke);
|
||||
|
||||
int keyCode = KeyEvent.VK_Q;
|
||||
int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK;
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, modifiers, keyCode, 'Q');
|
||||
KeyStroke newKeyStroke = setKeyBinding(actionName, actionOwner, modifiers, keyCode, 'Q');
|
||||
|
||||
apply();
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, newKeyStroke);
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, newKeyStroke);
|
||||
|
||||
restoreDefaults();
|
||||
|
||||
KeyStroke currentBinding = getKeyBinding(actionName);
|
||||
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertEquals("Key binding not restored after a call to restore defautls", defaultKeyStroke,
|
||||
currentBinding);
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, pluginName, defaultKeyStroke);
|
||||
assertOptionsKeyStroke(frontEndTool, actionName, actionOwner, defaultKeyStroke);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -745,11 +877,13 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
// Inner Classes
|
||||
//=================================================================================================
|
||||
|
||||
private KeyStroke getKeyBinding(String actionName) throws Exception {
|
||||
private MouseBinding getMouseBindingFromTable(String actionName, String actionOwner)
|
||||
throws Exception {
|
||||
|
||||
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
|
||||
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
|
||||
int row = selectRowForAction(panel, actionName);
|
||||
int row = selectRowForAction(panel, actionName, actionOwner);
|
||||
|
||||
JTable table = (JTable) getInstanceField("actionTable", panel);
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -763,36 +897,150 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
if (StringUtils.isBlank(keyBindingColumnValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String mouseBinding = keyBindingColumnValue;
|
||||
Pattern p = Pattern.compile(".*\\((.*)\\)");
|
||||
Matcher matcher = p.matcher(keyBindingColumnValue);
|
||||
if (matcher.matches()) {
|
||||
mouseBinding = matcher.group(1);
|
||||
}
|
||||
|
||||
return MouseBinding.getMouseBinding(mouseBinding);
|
||||
}
|
||||
|
||||
private KeyStroke getKeyBindingFromTable(String actionName, String actionOwner)
|
||||
throws Exception {
|
||||
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
|
||||
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
|
||||
int row = selectRowForAction(panel, actionName, actionOwner);
|
||||
|
||||
JTable table = (JTable) getInstanceField("actionTable", panel);
|
||||
@SuppressWarnings("unchecked")
|
||||
RowObjectFilterModel<DockingActionIf> model =
|
||||
(RowObjectFilterModel<DockingActionIf>) table.getModel();
|
||||
|
||||
DockingActionIf rowValue = model.getModelData().get(row);
|
||||
|
||||
String keyBindingColumnValue =
|
||||
(String) model.getColumnValueForRow(rowValue, 1 /* key binding column */);
|
||||
if (StringUtils.isBlank(keyBindingColumnValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = keyBindingColumnValue.indexOf("(");
|
||||
if (index != -1) {
|
||||
int endIndex = keyBindingColumnValue.indexOf(")");
|
||||
if (endIndex != -1) {
|
||||
keyBindingColumnValue = keyBindingColumnValue.substring(0, index);
|
||||
}
|
||||
}
|
||||
|
||||
return KeyBindingUtils.parseKeyStroke(keyBindingColumnValue);
|
||||
}
|
||||
|
||||
private void assertOptionsMouseBinding(PluginTool pluginTool, String actionName,
|
||||
String pluginName, MouseBinding value) {
|
||||
Options options = pluginTool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
ActionTrigger actionTrigger =
|
||||
options.getActionTrigger(actionName + " (" + pluginName + ")", null);
|
||||
if (actionTrigger == null) {
|
||||
assertNull("The options mouse binding does not match the value in the options table",
|
||||
value);
|
||||
return;
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = actionTrigger.getMouseBinding();
|
||||
assertEquals("The options mouse binding does not match the value in the options table",
|
||||
value, mouseBinding);
|
||||
}
|
||||
|
||||
private void assertOptionsKeyStroke(PluginTool pluginTool, String actionName, String pluginName,
|
||||
KeyStroke value) throws Exception {
|
||||
Options options = pluginTool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
KeyStroke optionsKeyStroke =
|
||||
options.getKeyStroke(actionName + " (" + pluginName + ")", null);
|
||||
assertEquals("The options keystroke does not match the value in keybinding options table",
|
||||
value, optionsKeyStroke);
|
||||
ActionTrigger actionTrigger =
|
||||
options.getActionTrigger(actionName + " (" + pluginName + ")", null);
|
||||
if (actionTrigger == null) {
|
||||
assertNull("The options keystroke does not match the value in the options table",
|
||||
value);
|
||||
return;
|
||||
}
|
||||
|
||||
KeyStroke keyStroke = actionTrigger.getKeyStroke();
|
||||
assertEquals("The options keystroke does not match the value in the options table", value,
|
||||
keyStroke);
|
||||
}
|
||||
|
||||
private KeyStroke setKeyBinding(String actionName, int modifiers, int keyCode, char keyChar)
|
||||
throws Exception {
|
||||
private MouseBinding setMouseBinding(String actionName, String actionOwner, int modifiers,
|
||||
int button) throws Exception {
|
||||
|
||||
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
|
||||
final KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
|
||||
selectRowForAction(panel, actionName);
|
||||
selectRowForAction(panel, actionName, actionOwner);
|
||||
|
||||
setToggleButtonSelected(panel, "Enter Mouse Binding", true);
|
||||
|
||||
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
|
||||
JTextField textField = (JTextField) getInstanceField("mouseEntryField", actionBindingPanel);
|
||||
|
||||
clickMouse(textField, button, 5, 5, 1, modifiers);
|
||||
waitForSwing();
|
||||
|
||||
MouseBinding expectedMouseBinding = new MouseBinding(button, modifiers);
|
||||
|
||||
waitForSwing();
|
||||
waitForSwing();
|
||||
waitForSwing();
|
||||
waitForSwing();
|
||||
|
||||
MouseBinding currentMouseBinding = getMouseBindingFromTable(actionName, actionOwner);
|
||||
assertEquals("Did not properly set mouse binding", expectedMouseBinding,
|
||||
currentMouseBinding);
|
||||
return currentMouseBinding;
|
||||
}
|
||||
|
||||
private KeyStroke setKeyBinding(String actionName, String actionOwner, int modifiers,
|
||||
int keyCode, char keyChar) throws Exception {
|
||||
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
|
||||
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
|
||||
selectRowForAction(panel, actionName, actionOwner);
|
||||
|
||||
setToggleButtonSelected(panel, "Enter Mouse Binding", false);
|
||||
|
||||
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
|
||||
JTextField textField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
|
||||
|
||||
JTextField textField = (JTextField) getInstanceField("ksField", panel);
|
||||
triggerKey(textField, modifiers, keyCode, keyChar);
|
||||
waitForSwing();
|
||||
|
||||
KeyStroke expectedKeyStroke = KeyStroke.getKeyStroke(keyCode, modifiers, false);
|
||||
KeyStroke currentBinding = getKeyBinding(actionName);
|
||||
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertEquals("Did not properly set new keybinding", expectedKeyStroke, currentBinding);
|
||||
return currentBinding;
|
||||
}
|
||||
|
||||
private int selectRowForAction(KeyBindingsPanel panel, String actionName) {
|
||||
private void clearKeyBinding(String actionName, String actionOwner) throws Exception {
|
||||
|
||||
OptionsEditor editor = seleNodeWithCustomEditor("Key Bindings");
|
||||
KeyBindingsPanel panel = (KeyBindingsPanel) getInstanceField("panel", editor);
|
||||
|
||||
selectRowForAction(panel, actionName, actionOwner);
|
||||
|
||||
setToggleButtonSelected(panel, "Enter Mouse Binding", false);
|
||||
|
||||
JPanel actionBindingPanel = (JPanel) getInstanceField("actionBindingPanel", panel);
|
||||
JTextField textField = (JTextField) getInstanceField("keyEntryField", actionBindingPanel);
|
||||
|
||||
triggerBackspaceKey(textField);
|
||||
waitForSwing();
|
||||
|
||||
KeyStroke currentBinding = getKeyBindingFromTable(actionName, actionOwner);
|
||||
assertNull(currentBinding);
|
||||
}
|
||||
|
||||
private int selectRowForAction(KeyBindingsPanel panel, String actionName, String actionOwner) {
|
||||
final JTable table = (JTable) getInstanceField("actionTable", panel);
|
||||
@SuppressWarnings("unchecked")
|
||||
final RowObjectFilterModel<DockingActionIf> model =
|
||||
|
@ -806,15 +1054,25 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
String rowActionName =
|
||||
(String) model.getColumnValueForRow(rowData, 0 /* action name column */);
|
||||
if (rowActionName.equals(actionName)) {
|
||||
actionRow = i;
|
||||
break;
|
||||
|
||||
String rowActionOwner =
|
||||
(String) model.getColumnValueForRow(rowData, 2 /* owner column */);
|
||||
if (rowActionOwner.equals(actionOwner)) {
|
||||
actionRow = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue("Could not find row for action: " + actionName, actionRow != -1);
|
||||
assertTrue("Could not find row for action: " + actionName + " (" + actionOwner + ")",
|
||||
actionRow != -1);
|
||||
|
||||
final int row = actionRow;
|
||||
runSwing(() -> table.setRowSelectionInterval(row, row));
|
||||
int row = actionRow;
|
||||
runSwing(() -> {
|
||||
table.setRowSelectionInterval(row, row);
|
||||
Rectangle cellRectangle = table.getCellRect(row, row, true);
|
||||
table.scrollRectToVisible(cellRectangle);
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
@ -1039,17 +1297,21 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
showOptionsDialog(pluginTool);
|
||||
}
|
||||
|
||||
private void showOptionsDialog(PluginTool pluginTool) throws Exception {
|
||||
// TODO change to getAction("Edit Options")
|
||||
private void editOptions(PluginTool pluginTool) {
|
||||
Set<DockingActionIf> list = pluginTool.getAllActions();
|
||||
for (DockingActionIf action : list) {
|
||||
if (action.getName().equals("Edit Options")) {
|
||||
performAction(action, false);
|
||||
break;
|
||||
waitForSwing();
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Unable to find action 'Edit Options'");
|
||||
}
|
||||
|
||||
waitForSwing();
|
||||
private void showOptionsDialog(PluginTool pluginTool) throws Exception {
|
||||
|
||||
editOptions(pluginTool);
|
||||
dialog = waitForDialogComponent(OptionsDialog.class);
|
||||
optionsPanel = (OptionsPanel) getInstanceField("panel", dialog);
|
||||
Container pane = dialog.getComponent();
|
||||
|
|
|
@ -19,6 +19,8 @@ import static org.junit.Assert.*;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.io.File;
|
||||
|
@ -30,7 +32,6 @@ import javax.swing.KeyStroke;
|
|||
import org.junit.*;
|
||||
|
||||
import docking.test.AbstractDockingTest;
|
||||
import generic.theme.GColor;
|
||||
import generic.theme.GThemeDefaults.Colors.Palette;
|
||||
import generic.theme.ThemeManager;
|
||||
import ghidra.framework.options.*;
|
||||
|
@ -39,20 +40,19 @@ import ghidra.program.database.ProgramBuilder;
|
|||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class OptionsDBTest extends AbstractDockingTest {
|
||||
|
||||
private OptionsDB options;
|
||||
private ProgramBuilder builder;
|
||||
private int txID;
|
||||
private GColor testColor;
|
||||
|
||||
public enum fruit {
|
||||
Apple, Pear, Orange
|
||||
}
|
||||
|
||||
public OptionsDBTest() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -62,7 +62,6 @@ public class OptionsDBTest extends AbstractDockingTest {
|
|||
txID = program.startTransaction("Test");
|
||||
options = new OptionsDB(program);
|
||||
ThemeManager.getInstance().setColor("color.test", Palette.MAGENTA);
|
||||
testColor = new GColor("color.test");
|
||||
}
|
||||
|
||||
private void saveAndRestoreOptions() {
|
||||
|
@ -223,10 +222,38 @@ public class OptionsDBTest extends AbstractDockingTest {
|
|||
|
||||
@Test
|
||||
public void testSaveKeyStrokeOption() {
|
||||
options.setKeyStroke("Foo", KeyStroke.getKeyStroke('a', 0));
|
||||
options.setKeyStroke("Foo", KeyStroke.getKeyStroke(KeyEvent.VK_A, 0));
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(KeyStroke.getKeyStroke('a', 0),
|
||||
options.getKeyStroke("Foo", KeyStroke.getKeyStroke('b', 0)));
|
||||
KeyStroke savedKs = options.getKeyStroke("Foo", null);
|
||||
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), savedKs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_KeyStroke() {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke('a', 0);
|
||||
ActionTrigger trigger = new ActionTrigger(ks);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_MouseBinding() {
|
||||
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
|
||||
ActionTrigger trigger = new ActionTrigger(mb);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_KeyStrokeAndMouseBinding() {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
|
||||
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
|
||||
ActionTrigger trigger = new ActionTrigger(ks, mb);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -678,7 +705,7 @@ public class OptionsDBTest extends AbstractDockingTest {
|
|||
}
|
||||
|
||||
public static class MyPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import static org.junit.Assert.*;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.io.File;
|
||||
|
@ -35,6 +37,7 @@ import generic.theme.GThemeDefaults.Colors.Palette;
|
|||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.bean.opteditor.OptionsVetoException;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class OptionsTest extends AbstractGuiTest {
|
||||
|
||||
|
@ -161,10 +164,37 @@ public class OptionsTest extends AbstractGuiTest {
|
|||
|
||||
@Test
|
||||
public void testSaveKeyStrokeOption() {
|
||||
options.setKeyStroke("Foo", KeyStroke.getKeyStroke('a', 0));
|
||||
options.setKeyStroke("Foo", KeyStroke.getKeyStroke(KeyEvent.VK_A, 0));
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(KeyStroke.getKeyStroke('a', 0),
|
||||
options.getKeyStroke("Foo", KeyStroke.getKeyStroke('b', 0)));
|
||||
assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), options.getKeyStroke("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_KeyStroke() {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
|
||||
ActionTrigger trigger = new ActionTrigger(ks);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_MouseBinding() {
|
||||
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
|
||||
ActionTrigger trigger = new ActionTrigger(mb);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveActionTrigger_KeyStrokeAndMouseBinding() {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_A, 0);
|
||||
MouseBinding mb = new MouseBinding(1, InputEvent.CTRL_DOWN_MASK);
|
||||
ActionTrigger trigger = new ActionTrigger(ks, mb);
|
||||
options.setActionTrigger("Foo", trigger);
|
||||
saveAndRestoreOptions();
|
||||
assertEquals(trigger, options.getActionTrigger("Foo", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -20,11 +20,7 @@ import java.awt.Font;
|
|||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import ghidra.framework.options.CustomOption;
|
||||
import ghidra.framework.options.OptionType;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.program.model.data.ISF.IsfObject;
|
||||
import ghidra.util.exception.AssertException;
|
||||
|
||||
|
@ -39,88 +35,94 @@ public class ExtProperty implements IsfObject {
|
|||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
public ExtProperty(String name, Options propList) {
|
||||
this.name = name;
|
||||
OptionType optionType = propList.getType(name);
|
||||
switch (optionType) {
|
||||
case INT_TYPE:
|
||||
type = "int";
|
||||
value = Integer.toString(propList.getInt(name, 0));
|
||||
break;
|
||||
case LONG_TYPE:
|
||||
type = "long";
|
||||
value = Long.toString(propList.getLong(name, 0));
|
||||
break;
|
||||
case STRING_TYPE:
|
||||
type = "string";
|
||||
value = propList.getString(name, "");
|
||||
break;
|
||||
case BOOLEAN_TYPE:
|
||||
type = "bool";
|
||||
value = Boolean.toString(propList.getBoolean(name, true));
|
||||
break;
|
||||
case DOUBLE_TYPE:
|
||||
type = "double";
|
||||
value = Double.toString(propList.getDouble(name, 0));
|
||||
break;
|
||||
case FLOAT_TYPE:
|
||||
type = "float";
|
||||
value = Float.toString(propList.getFloat(name, 0f));
|
||||
break;
|
||||
case DATE_TYPE:
|
||||
type = "date";
|
||||
Date date = propList.getDate(name, (Date) null);
|
||||
long time = date == null ? 0 : date.getTime();
|
||||
value = Long.toHexString(time);
|
||||
break;
|
||||
case COLOR_TYPE:
|
||||
type = "color";
|
||||
Color color = propList.getColor(name, null);
|
||||
int rgb = color.getRGB();
|
||||
value = Integer.toHexString(rgb);
|
||||
break;
|
||||
case ENUM_TYPE:
|
||||
type = "enum";
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
Enum enuum = propList.getEnum(name, null);
|
||||
String enumString = OptionType.ENUM_TYPE.convertObjectToString(enuum);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case FILE_TYPE:
|
||||
type = "file";
|
||||
File file = propList.getFile(name, null);
|
||||
String path = file.getAbsolutePath();
|
||||
value = path;
|
||||
break;
|
||||
case FONT_TYPE:
|
||||
type = "font";
|
||||
Font font = propList.getFont(name, null);
|
||||
enumString = OptionType.FONT_TYPE.convertObjectToString(font);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case KEYSTROKE_TYPE:
|
||||
type = "keyStroke";
|
||||
KeyStroke keyStroke = propList.getKeyStroke(name, null);
|
||||
enumString = OptionType.KEYSTROKE_TYPE.convertObjectToString(keyStroke);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case CUSTOM_TYPE:
|
||||
type = "custom";
|
||||
CustomOption custom = propList.getCustomOption(name, null);
|
||||
enumString = OptionType.CUSTOM_TYPE.convertObjectToString(custom);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case BYTE_ARRAY_TYPE:
|
||||
type = "bytes";
|
||||
byte[] bytes = propList.getByteArray(name, null);
|
||||
enumString = OptionType.BYTE_ARRAY_TYPE.convertObjectToString(bytes);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case NO_TYPE:
|
||||
break;
|
||||
default:
|
||||
throw new AssertException();
|
||||
case INT_TYPE:
|
||||
type = "int";
|
||||
value = Integer.toString(propList.getInt(name, 0));
|
||||
break;
|
||||
case LONG_TYPE:
|
||||
type = "long";
|
||||
value = Long.toString(propList.getLong(name, 0));
|
||||
break;
|
||||
case STRING_TYPE:
|
||||
type = "string";
|
||||
value = propList.getString(name, "");
|
||||
break;
|
||||
case BOOLEAN_TYPE:
|
||||
type = "bool";
|
||||
value = Boolean.toString(propList.getBoolean(name, true));
|
||||
break;
|
||||
case DOUBLE_TYPE:
|
||||
type = "double";
|
||||
value = Double.toString(propList.getDouble(name, 0));
|
||||
break;
|
||||
case FLOAT_TYPE:
|
||||
type = "float";
|
||||
value = Float.toString(propList.getFloat(name, 0f));
|
||||
break;
|
||||
case DATE_TYPE:
|
||||
type = "date";
|
||||
Date date = propList.getDate(name, (Date) null);
|
||||
long time = date == null ? 0 : date.getTime();
|
||||
value = Long.toHexString(time);
|
||||
break;
|
||||
case COLOR_TYPE:
|
||||
type = "color";
|
||||
Color color = propList.getColor(name, null);
|
||||
int rgb = color.getRGB();
|
||||
value = Integer.toHexString(rgb);
|
||||
break;
|
||||
case ENUM_TYPE:
|
||||
type = "enum";
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
Enum enuum = propList.getEnum(name, null);
|
||||
String enumString = OptionType.ENUM_TYPE.convertObjectToString(enuum);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case FILE_TYPE:
|
||||
type = "file";
|
||||
File file = propList.getFile(name, null);
|
||||
String path = file.getAbsolutePath();
|
||||
value = path;
|
||||
break;
|
||||
case FONT_TYPE:
|
||||
type = "font";
|
||||
Font font = propList.getFont(name, null);
|
||||
enumString = OptionType.FONT_TYPE.convertObjectToString(font);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case KEYSTROKE_TYPE:
|
||||
type = "actionTrigger";
|
||||
ActionTrigger trigger = propList.getActionTrigger(name, null);
|
||||
enumString = OptionType.ACTION_TRIGGER.convertObjectToString(trigger);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case ACTION_TRIGGER:
|
||||
type = "actionTrigger";
|
||||
ActionTrigger actionTrigger = propList.getActionTrigger(name, null);
|
||||
enumString = OptionType.ACTION_TRIGGER.convertObjectToString(actionTrigger);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case CUSTOM_TYPE:
|
||||
type = "custom";
|
||||
CustomOption custom = propList.getCustomOption(name, null);
|
||||
enumString = OptionType.CUSTOM_TYPE.convertObjectToString(custom);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case BYTE_ARRAY_TYPE:
|
||||
type = "bytes";
|
||||
byte[] bytes = propList.getByteArray(name, null);
|
||||
enumString = OptionType.BYTE_ARRAY_TYPE.convertObjectToString(bytes);
|
||||
value = escapeElementEntities(enumString);
|
||||
break;
|
||||
case NO_TYPE:
|
||||
break;
|
||||
default:
|
||||
throw new AssertException();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,11 +131,11 @@ public class ExtProperty implements IsfObject {
|
|||
private static final String APOSTROPHE = "'";
|
||||
private static final String QUOTE = """;
|
||||
private static final String AMPERSAND = "&";
|
||||
|
||||
|
||||
/**
|
||||
* Converts any special or reserved characters in the specified SARIF string
|
||||
* into the equivalent Unicode encoding.
|
||||
*
|
||||
*
|
||||
* @param sarif the SARIF string
|
||||
* @return the encoded SARIF string
|
||||
*/
|
||||
|
@ -143,7 +145,8 @@ public class ExtProperty implements IsfObject {
|
|||
int codePoint = sarif.codePointAt(offset);
|
||||
offset += Character.charCount(codePoint);
|
||||
|
||||
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) && (codePoint != 0x0D)) {
|
||||
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) &&
|
||||
(codePoint != 0x0D)) {
|
||||
continue;
|
||||
}
|
||||
if (codePoint >= 0x7F) {
|
||||
|
|
|
@ -15,42 +15,23 @@
|
|||
*/
|
||||
package sarif.managers;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Point;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import ghidra.framework.options.CustomOption;
|
||||
import ghidra.framework.options.OptionType;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressSetView;
|
||||
import ghidra.program.model.listing.Bookmark;
|
||||
import ghidra.program.model.listing.BookmarkManager;
|
||||
import ghidra.program.model.listing.BookmarkType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.util.IntPropertyMap;
|
||||
import ghidra.program.model.util.LongPropertyMap;
|
||||
import ghidra.program.model.util.ObjectPropertyMap;
|
||||
import ghidra.program.model.util.PropertyMap;
|
||||
import ghidra.program.model.util.PropertyMapManager;
|
||||
import ghidra.program.model.util.StringPropertyMap;
|
||||
import ghidra.program.model.util.VoidPropertyMap;
|
||||
import ghidra.util.ColorUtils;
|
||||
import ghidra.util.SaveableColor;
|
||||
import ghidra.util.SaveablePoint;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.util.*;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.DuplicateNameException;
|
||||
import ghidra.util.task.TaskLauncher;
|
||||
|
@ -79,8 +60,8 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
////////////////////////////
|
||||
|
||||
@Override
|
||||
public boolean read(Map<String, Object> result, SarifProgramOptions options, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
public boolean read(Map<String, Object> result, SarifProgramOptions options,
|
||||
TaskMonitor monitor) throws CancelledException {
|
||||
processProperty(result, options == null || options.isOverwritePropertyConflicts());
|
||||
return true;
|
||||
}
|
||||
|
@ -92,18 +73,20 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
Address addr = getLocation(result);
|
||||
if (addr != null) {
|
||||
processPropertyMapEntry(addr, name, result, overwrite);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
processPropertyListEntry(name, result, overwrite);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.appendException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void processPropertyMapEntry(Address addr, String name, Map<String, Object> result, boolean overwrite)
|
||||
throws DuplicateNameException {
|
||||
private void processPropertyMapEntry(Address addr, String name, Map<String, Object> result,
|
||||
boolean overwrite) throws DuplicateNameException {
|
||||
|
||||
String type = (String) result.get("type");
|
||||
if (type != null) {
|
||||
|
@ -128,44 +111,51 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
voidMap = propMapMgr.createVoidPropertyMap(name);
|
||||
}
|
||||
voidMap.add(addr);
|
||||
} else if ("int".equals(type)) {
|
||||
}
|
||||
else if ("int".equals(type)) {
|
||||
int value = Integer.parseInt(val, 16);
|
||||
IntPropertyMap intMap = propMapMgr.getIntPropertyMap(name);
|
||||
if (intMap == null) {
|
||||
intMap = propMapMgr.createIntPropertyMap(name);
|
||||
}
|
||||
intMap.add(addr, value);
|
||||
} else if ("long".equals(type)) {
|
||||
}
|
||||
else if ("long".equals(type)) {
|
||||
long value = Long.parseLong(val, 16);
|
||||
LongPropertyMap longMap = propMapMgr.getLongPropertyMap(name);
|
||||
if (longMap == null) {
|
||||
longMap = propMapMgr.createLongPropertyMap(name);
|
||||
}
|
||||
longMap.add(addr, value);
|
||||
} else if ("string".equals(type)) {
|
||||
}
|
||||
else if ("string".equals(type)) {
|
||||
String str = val;
|
||||
StringPropertyMap strMap = propMapMgr.getStringPropertyMap(name);
|
||||
if (strMap == null) {
|
||||
strMap = propMapMgr.createStringPropertyMap(name);
|
||||
}
|
||||
strMap.add(addr, str);
|
||||
} else if ("color".equals(type)) {
|
||||
ObjectPropertyMap<SaveableColor> objMap = (ObjectPropertyMap<SaveableColor>) propMapMgr
|
||||
.getObjectPropertyMap(name);
|
||||
}
|
||||
else if ("color".equals(type)) {
|
||||
ObjectPropertyMap<SaveableColor> objMap =
|
||||
(ObjectPropertyMap<SaveableColor>) propMapMgr.getObjectPropertyMap(name);
|
||||
if (objMap == null) {
|
||||
objMap = propMapMgr.createObjectPropertyMap(name, SaveableColor.class);
|
||||
}
|
||||
objMap.add(addr, new SaveableColor(Color.decode(val)));
|
||||
} else if ("point".equals(type)) {
|
||||
}
|
||||
else if ("point".equals(type)) {
|
||||
String xstr = val.substring(val.indexOf("[x="), val.indexOf(","));
|
||||
String ystr = val.substring(val.indexOf("y="), val.indexOf("]"));
|
||||
ObjectPropertyMap<SaveablePoint> objMap = (ObjectPropertyMap<SaveablePoint>) propMapMgr
|
||||
.getObjectPropertyMap(name);
|
||||
ObjectPropertyMap<SaveablePoint> objMap =
|
||||
(ObjectPropertyMap<SaveablePoint>) propMapMgr.getObjectPropertyMap(name);
|
||||
if (objMap == null) {
|
||||
objMap = propMapMgr.createObjectPropertyMap(name, SaveablePoint.class);
|
||||
}
|
||||
objMap.add(addr, new SaveablePoint(new Point(Integer.parseInt(xstr), Integer.parseInt(ystr))));
|
||||
} else if ("bookmarks".equals(type)) {
|
||||
objMap.add(addr,
|
||||
new SaveablePoint(new Point(Integer.parseInt(xstr), Integer.parseInt(ystr))));
|
||||
}
|
||||
else if ("bookmarks".equals(type)) {
|
||||
// Must retain for backward compatibility with old Ver-1 Note bookmarks which
|
||||
// were saved as simple properties
|
||||
BookmarkManager bmMgr = program.getBookmarkManager();
|
||||
|
@ -177,7 +167,8 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
}
|
||||
}
|
||||
bmMgr.setBookmark(addr, BookmarkType.NOTE, name, val);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
log.appendMsg("Unsupported PROPERTY usage");
|
||||
}
|
||||
}
|
||||
|
@ -202,13 +193,14 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void processPropertyListEntry(String pathname, Map<String, Object> result, boolean overwrite)
|
||||
throws Exception {
|
||||
private void processPropertyListEntry(String pathname, Map<String, Object> result,
|
||||
boolean overwrite) throws Exception {
|
||||
|
||||
String listName = getPropertyList(pathname);
|
||||
String name = getPropertyName(pathname);
|
||||
if (listName == null || name == null) {
|
||||
log.appendMsg("Property NAME attribute must contain both category prefix and property name");
|
||||
log.appendMsg(
|
||||
"Property NAME attribute must contain both category prefix and property name");
|
||||
return;
|
||||
}
|
||||
Options list = program.getOptions(listName);
|
||||
|
@ -223,48 +215,76 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
Object val = result.get("value");
|
||||
if (type == null || "void".equals(type)) {
|
||||
log.appendMsg("Unsupported PROPERTY usage");
|
||||
} else if ("int".equals(type)) {
|
||||
}
|
||||
else if ("int".equals(type)) {
|
||||
list.setInt(name, Integer.parseInt((String) val, 16));
|
||||
} else if ("long".equals(type)) {
|
||||
}
|
||||
else if ("long".equals(type)) {
|
||||
list.setLong(name, Long.parseLong((String) val, 16));
|
||||
} else if ("double".equals(type)) {
|
||||
}
|
||||
else if ("double".equals(type)) {
|
||||
list.setDouble(name, Double.parseDouble((String) val));
|
||||
} else if ("float".equals(type)) {
|
||||
}
|
||||
else if ("float".equals(type)) {
|
||||
list.setFloat(name, Float.parseFloat((String) val));
|
||||
} else if ("bool".equals(type)) {
|
||||
}
|
||||
else if ("bool".equals(type)) {
|
||||
list.setBoolean(name, Boolean.parseBoolean((String) val));
|
||||
} else if ("string".equals(type)) {
|
||||
}
|
||||
else if ("string".equals(type)) {
|
||||
list.setString(name, (String) val);
|
||||
} else if ("date".equals(type)) {
|
||||
}
|
||||
else if ("date".equals(type)) {
|
||||
list.setDate(name, new Date(Long.parseLong((String) val, 16)));
|
||||
} else if ("color".equals(type)) {
|
||||
}
|
||||
else if ("color".equals(type)) {
|
||||
Color color = ColorUtils.getColor((Integer) val);
|
||||
list.setColor(name, color);
|
||||
} else if ("file".equals(type)) {
|
||||
}
|
||||
else if ("file".equals(type)) {
|
||||
File file = new File((String) val);
|
||||
list.setFile(name, file);
|
||||
} else if ("enum".equals(type)) {
|
||||
}
|
||||
else if ("enum".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
@SuppressWarnings("rawtypes")
|
||||
Enum enuum = (Enum) OptionType.ENUM_TYPE.convertStringToObject(sarifString);
|
||||
list.setEnum(name, enuum);
|
||||
} else if ("font".equals(type)) {
|
||||
}
|
||||
else if ("font".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
Font font = (Font) OptionType.FONT_TYPE.convertStringToObject(sarifString);
|
||||
list.setFont(name, font);
|
||||
} else if ("keyStroke".equals(type)) {
|
||||
}
|
||||
else if ("keyStroke".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
KeyStroke keyStroke = (KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(sarifString);
|
||||
list.setKeyStroke(name, keyStroke);
|
||||
} else if ("custom".equals(type)) {
|
||||
KeyStroke keyStroke =
|
||||
(KeyStroke) OptionType.KEYSTROKE_TYPE.convertStringToObject(sarifString);
|
||||
|
||||
ActionTrigger trigger = null;
|
||||
if (keyStroke != null) {
|
||||
trigger = new ActionTrigger(keyStroke);
|
||||
}
|
||||
list.setActionTrigger(name, trigger);
|
||||
}
|
||||
else if ("actionTrigger".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
CustomOption custom = (CustomOption) OptionType.CUSTOM_TYPE.convertStringToObject(sarifString);
|
||||
ActionTrigger actionTrigger =
|
||||
(ActionTrigger) OptionType.ACTION_TRIGGER.convertStringToObject(sarifString);
|
||||
list.setActionTrigger(name, actionTrigger);
|
||||
}
|
||||
else if ("custom".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
CustomOption custom =
|
||||
(CustomOption) OptionType.CUSTOM_TYPE.convertStringToObject(sarifString);
|
||||
list.setCustomOption(name, custom);
|
||||
} else if ("bytes".equals(type)) {
|
||||
}
|
||||
else if ("bytes".equals(type)) {
|
||||
String sarifString = unEscapeElementEntities((String) val);
|
||||
byte[] bytes = (byte[]) OptionType.BYTE_ARRAY_TYPE.convertStringToObject(sarifString);
|
||||
list.setByteArray(name, bytes);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
log.appendMsg("Unsupported PROPERTY usage");
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +293,8 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
// SARIF WRITE CURRENT DTD //
|
||||
/////////////////////////////
|
||||
|
||||
void write(JsonArray results, AddressSetView set, TaskMonitor monitor) throws IOException, CancelledException {
|
||||
void write(JsonArray results, AddressSetView set, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
monitor.setMessage("Writing PROPERTIES ...");
|
||||
|
||||
List<String> request = program.getOptionsNames();
|
||||
|
@ -290,13 +311,14 @@ public class PropertiesSarifMgr extends SarifMgr {
|
|||
writeAsSARIF(program, set, mapRequest, results);
|
||||
}
|
||||
|
||||
public static void writeAsSARIF(Program program, List<String> request, JsonArray results) throws IOException {
|
||||
public static void writeAsSARIF(Program program, List<String> request, JsonArray results)
|
||||
throws IOException {
|
||||
SarifPropertyListWriter writer = new SarifPropertyListWriter(program, request, null);
|
||||
new TaskLauncher(new SarifWriterTask(SUBKEY, writer, results), null);
|
||||
}
|
||||
|
||||
public static void writeAsSARIF(Program program, AddressSetView set, List<PropertyMap<?>> request,
|
||||
JsonArray results) throws IOException {
|
||||
public static void writeAsSARIF(Program program, AddressSetView set,
|
||||
List<PropertyMap<?>> request, JsonArray results) throws IOException {
|
||||
SarifPropertyMapWriter writer = new SarifPropertyMapWriter(request, program, set, null);
|
||||
new TaskLauncher(new SarifWriterTask(SUBKEY, writer, results), null);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ import java.awt.Component;
|
|||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jdom.Document;
|
||||
import org.jdom.output.XMLOutputter;
|
||||
|
||||
|
@ -51,7 +49,6 @@ import ghidra.framework.options.*;
|
|||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginException;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.framework.project.tool.GhidraTool;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.symbol.Reference;
|
||||
|
@ -144,8 +141,7 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
|
|||
toolTemplate = ToolUtils.readToolTemplate(toolFileName);
|
||||
}
|
||||
|
||||
PluginTool newTool =
|
||||
(GhidraTool) toolTemplate.createTool(controller.getTool().getProject());
|
||||
PluginTool newTool = toolTemplate.createTool(controller.getTool().getProject());
|
||||
try {
|
||||
VersionTrackingSubordinatePluginX pluginX =
|
||||
new VersionTrackingSubordinatePluginX(newTool, isSourceTool);
|
||||
|
@ -190,33 +186,38 @@ public class VTSubToolManager implements VTControllerListener, OptionsChangeList
|
|||
if (processingOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(newValue instanceof ActionTrigger)) {
|
||||
return;
|
||||
}
|
||||
|
||||
processingOptions = true;
|
||||
try {
|
||||
if (!(newValue instanceof KeyStroke)) {
|
||||
return;
|
||||
}
|
||||
KeyStroke keyStroke = (KeyStroke) newValue;
|
||||
if (sourceTool != null) {
|
||||
Options sourceOptions = sourceTool.getOptions(ToolConstants.KEY_BINDINGS);
|
||||
if (sourceOptions != options) {
|
||||
sourceOptions.setKeyStroke(optionName, keyStroke);
|
||||
sourceTool.refreshKeybindings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (destinationTool != null) {
|
||||
Options destinationOptions = destinationTool.getOptions(ToolConstants.KEY_BINDINGS);
|
||||
if (destinationOptions != options) {
|
||||
destinationOptions.setKeyStroke(optionName, keyStroke);
|
||||
destinationTool.refreshKeybindings();
|
||||
}
|
||||
}
|
||||
updateActionTrigger(options, optionName, (ActionTrigger) newValue);
|
||||
}
|
||||
finally {
|
||||
processingOptions = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateActionTrigger(ToolOptions options, String optionName,
|
||||
ActionTrigger trigger) {
|
||||
if (sourceTool != null) {
|
||||
Options sourceOptions = sourceTool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
if (sourceOptions != options) {
|
||||
sourceOptions.setActionTrigger(optionName, trigger);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (destinationTool != null) {
|
||||
Options destinationOptions =
|
||||
destinationTool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
if (destinationOptions != options) {
|
||||
destinationOptions.setActionTrigger(optionName, trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createMatchActions(final PluginTool newTool) {
|
||||
newTool.setMenuGroup(new String[] { VTPlugin.MATCH_POPUP_MENU_NAME }, "1", "1");
|
||||
newTool.setMenuGroup(new String[] { VTPlugin.MARKUP_POPUP_MENU_NAME }, "1", "2");
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import docking.widgets.checkbox.GCheckBox;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A panel that displays inputs for key strokes and mouse bindings.
|
||||
*/
|
||||
public class ActionBindingPanel extends JPanel {
|
||||
|
||||
private static final String DISABLED_HINT = "Select an action";
|
||||
|
||||
private KeyEntryTextField keyEntryField;
|
||||
private JCheckBox useMouseBindingCheckBox;
|
||||
private MouseEntryTextField mouseEntryField;
|
||||
private JPanel textFieldPanel;
|
||||
|
||||
private DockingActionInputBindingListener listener;
|
||||
|
||||
public ActionBindingPanel(DockingActionInputBindingListener listener) {
|
||||
|
||||
this.listener = Objects.requireNonNull(listener);
|
||||
build();
|
||||
}
|
||||
|
||||
private void build() {
|
||||
|
||||
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
|
||||
|
||||
textFieldPanel = new JPanel(new BorderLayout());
|
||||
|
||||
keyEntryField = new KeyEntryTextField(20, ks -> listener.keyStrokeChanged(ks));
|
||||
keyEntryField.setDisabledHint(DISABLED_HINT);
|
||||
keyEntryField.setEnabled(false); // enabled on action selection
|
||||
mouseEntryField = new MouseEntryTextField(20, mb -> listener.mouseBindingChanged(mb));
|
||||
mouseEntryField.setDisabledHint(DISABLED_HINT);
|
||||
mouseEntryField.setEnabled(false); // enabled on action selection
|
||||
|
||||
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
|
||||
|
||||
String checkBoxText = "Enter Mouse Binding";
|
||||
useMouseBindingCheckBox = new GCheckBox(checkBoxText);
|
||||
useMouseBindingCheckBox
|
||||
.setToolTipText("When checked, the text field accepts mouse buttons");
|
||||
useMouseBindingCheckBox.setName(checkBoxText);
|
||||
useMouseBindingCheckBox.addItemListener(e -> updateTextField());
|
||||
|
||||
add(textFieldPanel);
|
||||
add(Box.createHorizontalStrut(5));
|
||||
add(useMouseBindingCheckBox);
|
||||
}
|
||||
|
||||
private void updateTextField() {
|
||||
|
||||
if (useMouseBindingCheckBox.isSelected()) {
|
||||
textFieldPanel.remove(keyEntryField);
|
||||
textFieldPanel.add(mouseEntryField, BorderLayout.NORTH);
|
||||
}
|
||||
else {
|
||||
textFieldPanel.remove(mouseEntryField);
|
||||
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
|
||||
}
|
||||
|
||||
validate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setKeyBindingData(KeyStroke ks, MouseBinding mb) {
|
||||
|
||||
keyEntryField.setKeyStroke(ks);
|
||||
mouseEntryField.setMouseBinding(mb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
keyEntryField.clearField();
|
||||
mouseEntryField.clearField();
|
||||
|
||||
keyEntryField.setEnabled(enabled);
|
||||
mouseEntryField.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void clearKeyStroke() {
|
||||
keyEntryField.clearField();
|
||||
}
|
||||
|
||||
public KeyStroke getKeyStroke() {
|
||||
return keyEntryField.getKeyStroke();
|
||||
}
|
||||
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseEntryField.getMouseBinding();
|
||||
}
|
||||
|
||||
public void clearMouseBinding() {
|
||||
mouseEntryField.clearField();
|
||||
}
|
||||
|
||||
public boolean isMouseBinding() {
|
||||
return useMouseBindingCheckBox.isSelected();
|
||||
}
|
||||
|
||||
}
|
|
@ -47,13 +47,14 @@ public class ActionToGuiMapper {
|
|||
popupActionManager = new PopupActionManager(winMgr, menuGroupMap);
|
||||
|
||||
DockingWindowsContextSensitiveHelpListener.install();
|
||||
MouseBindingMouseEventDispatcher.install();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a specific Help content location for a component.
|
||||
* The DocWinListener will be notified with the help location if the specified
|
||||
* component 'c' has focus and the help key is pressed.
|
||||
*
|
||||
*
|
||||
* @param c component
|
||||
* @param helpLocation the help location
|
||||
*/
|
||||
|
|
|
@ -230,6 +230,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
|
|||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dockingTool.toFront();
|
||||
if (defaultFocusComponent != null) {
|
||||
DockingWindowManager.requestFocus(defaultFocusComponent);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A simple listener interface to notify clients of changes to key strokes and mouse bindings.
|
||||
*/
|
||||
public interface DockingActionInputBindingListener {
|
||||
|
||||
/**
|
||||
* Called when the key stroke is changed.
|
||||
* @param ks the key stroke.
|
||||
*/
|
||||
public void keyStrokeChanged(KeyStroke ks);
|
||||
|
||||
/**
|
||||
* Called when the mouse binding is changed.
|
||||
* @param mb the mouse binding.
|
||||
*/
|
||||
public void mouseBindingChanged(MouseBinding mb);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.ToggleDockingActionIf;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
/**
|
||||
* A simple class to handle executing the given action. This class will generate the action context
|
||||
* as needed and validate the context before executing the action.
|
||||
*/
|
||||
public class DockingActionPerformer {
|
||||
|
||||
private DockingActionPerformer() {
|
||||
// static only
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given action later on the Swing thread.
|
||||
* @param action the action.
|
||||
* @param event the event that triggered the action.
|
||||
*/
|
||||
public static void perform(DockingActionIf action, ActionEvent event) {
|
||||
perform(action, event, DockingWindowManager.getActiveInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given action later on the Swing thread.
|
||||
* @param action the action.
|
||||
* @param event the event that triggered the action.
|
||||
* @param windowManager the window manager containing the action being processed.
|
||||
*/
|
||||
public static void perform(DockingActionIf action, ActionEvent event,
|
||||
DockingWindowManager windowManager) {
|
||||
|
||||
if (windowManager == null) {
|
||||
// not sure if this can happen
|
||||
Msg.error(DockingActionPerformer.class,
|
||||
"No window manager found; unable to execute action: " + action.getFullName());
|
||||
}
|
||||
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
ActionContext context = windowManager.createActionContext(action);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
context.setEventClickModifiers(event.getModifiers());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
windowManager.setStatusText("");
|
||||
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
|
||||
if (action instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
action.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -30,10 +30,9 @@ import docking.actions.KeyBindingUtils;
|
|||
*/
|
||||
public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||
|
||||
protected Tool tool;
|
||||
protected DockingActionIf dockingAction;
|
||||
|
||||
protected final KeyStroke keyStroke;
|
||||
protected final Tool tool;
|
||||
protected KeyStroke keyStroke;
|
||||
|
||||
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
|
||||
super(KeyBindingUtils.parseKeyStroke(keyStroke));
|
||||
|
@ -42,14 +41,9 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
|||
this.keyStroke = keyStroke;
|
||||
}
|
||||
|
||||
KeyStroke getKeyStroke() {
|
||||
return keyStroke;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
// always enable; this is a reserved binding and cannot be disabled
|
||||
return true;
|
||||
return true; // always enable; this is a internal action that cannot be disabled
|
||||
}
|
||||
|
||||
public abstract KeyBindingPrecedence getKeyBindingPrecedence();
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A class for using actions associated with mouse bindings. This class is meant to only by used by
|
||||
* internal Ghidra mouse event processing.
|
||||
*/
|
||||
public class DockingMouseBindingAction extends AbstractAction {
|
||||
|
||||
private DockingActionIf dockingAction;
|
||||
private MouseBinding mouseBinding;
|
||||
|
||||
public DockingMouseBindingAction(DockingActionIf action, MouseBinding mouseBinding) {
|
||||
this.dockingAction = Objects.requireNonNull(action);
|
||||
this.mouseBinding = Objects.requireNonNull(mouseBinding);
|
||||
}
|
||||
|
||||
public String getFullActionName() {
|
||||
return dockingAction.getFullName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true; // always enable; this is a internal action that cannot be disabled
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
DockingActionPerformer.perform(dockingAction, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFullActionName() + " (" + mouseBinding + ")";
|
||||
}
|
||||
}
|
|
@ -29,8 +29,7 @@ import javax.swing.*;
|
|||
import org.apache.commons.collections4.map.LazyMap;
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.action.ActionContextProvider;
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.*;
|
||||
import docking.actions.*;
|
||||
import docking.widgets.PasswordDialog;
|
||||
import generic.util.WindowUtilities;
|
||||
|
@ -41,6 +40,7 @@ import ghidra.util.*;
|
|||
import ghidra.util.datastruct.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import gui.event.MouseBinding;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import util.CollectionUtils;
|
||||
|
@ -707,7 +707,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/**
|
||||
* Get the local actions installed on the given provider
|
||||
*
|
||||
*
|
||||
* @param provider the provider
|
||||
* @return an iterator over the actions
|
||||
*/
|
||||
|
@ -754,7 +754,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* Returns any action that is bound to the given keystroke for the tool associated with this
|
||||
* DockingWindowManager instance.
|
||||
*
|
||||
* @param keyStroke The keystroke to check for key bindings.
|
||||
* @param keyStroke The keystroke to check for a bound action.
|
||||
* @return The action that is bound to the keystroke, or null of there is no binding for the
|
||||
* given keystroke.
|
||||
*/
|
||||
|
@ -768,6 +768,24 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any action that is bound to the given mouse binding for the tool associated with this
|
||||
* DockingWindowManager instance.
|
||||
*
|
||||
* @param mouseBinding The mouse binding to check for a bound action.
|
||||
* @return The action associated with the mouse binding , or null of there is no binding for the
|
||||
* given keystroke.
|
||||
*/
|
||||
Action getActionForMouseBinding(MouseBinding mouseBinding) {
|
||||
DockingToolActions toolActions = tool.getToolActions();
|
||||
if (toolActions instanceof ToolActions) {
|
||||
// Using a cast here; it didn't make sense to include this 'getAction' on the
|
||||
// DockingToolActions
|
||||
return ((ToolActions) toolActions).getAction(mouseBinding);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// End Package-level Methods
|
||||
//==================================================================================================
|
||||
|
@ -1189,8 +1207,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return;
|
||||
}
|
||||
|
||||
tool.getToolActions()
|
||||
.removeActions(DOCKING_WINDOWS_OWNER);
|
||||
tool.getToolActions().removeActions(DOCKING_WINDOWS_OWNER);
|
||||
|
||||
Map<String, List<ComponentPlaceholder>> permanentMap =
|
||||
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
|
||||
|
@ -1206,12 +1223,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
String subMenuName = provider.getWindowSubMenuName();
|
||||
if (provider.isTransient() && !provider.isSnapshot()) {
|
||||
transientMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
transientMap.get(subMenuName).add(placeholder);
|
||||
}
|
||||
else {
|
||||
permanentMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
permanentMap.get(subMenuName).add(placeholder);
|
||||
}
|
||||
}
|
||||
promoteSingleMenuGroups(permanentMap);
|
||||
|
@ -1225,8 +1240,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
}
|
||||
|
||||
private boolean isWindowMenuShowing() {
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager()
|
||||
.getSelectedPath();
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath();
|
||||
if (selectedPath == null || selectedPath.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1282,8 +1296,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
List<ComponentPlaceholder> list = lazyMap.get(key);
|
||||
if (list.size() == 1) {
|
||||
lazyMap.get(null /*submenu name*/)
|
||||
.add(list.get(0));
|
||||
lazyMap.get(null /*submenu name*/).add(list.get(0));
|
||||
lazyMap.remove(key);
|
||||
}
|
||||
}
|
||||
|
@ -1422,10 +1435,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
|
||||
ComponentProvider provider = entry.getKey();
|
||||
ComponentPlaceholder placeholder = entry.getValue();
|
||||
if (provider.getOwner()
|
||||
.equals(focusOwner) &&
|
||||
provider.getName()
|
||||
.equals(focusName)) {
|
||||
if (provider.getOwner().equals(focusOwner) && provider.getName().equals(focusName)) {
|
||||
focusReplacement = placeholder;
|
||||
break; // found one!
|
||||
}
|
||||
|
@ -1502,7 +1512,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/**
|
||||
* Clears the docking window manager's notion of the active provider. This is used
|
||||
* when a component that is not contained within a dockable component gets focus
|
||||
* when a component that is not contained within a dockable component gets focus
|
||||
* (e.g., JTabbedPanes for stacked components).
|
||||
*/
|
||||
private void deactivateFocusedComponent() {
|
||||
|
@ -1552,8 +1562,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getActiveWindow();
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
|
||||
if (!isMyWindow(win)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1693,8 +1702,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
toolPreferencesElement.getChildren(PreferenceState.PREFERENCE_STATE_NAME);
|
||||
for (Object name : children) {
|
||||
Element preferencesElement = (Element) name;
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME")
|
||||
.getValue(),
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME").getValue(),
|
||||
new PreferenceState(preferencesElement));
|
||||
}
|
||||
}
|
||||
|
@ -1954,7 +1962,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/*
|
||||
Note: Which window should be the parent of the dialog when the user does not specify?
|
||||
|
||||
|
||||
Some use cases; a dialog is shown from:
|
||||
1) A toolbar action
|
||||
2) A component provider's code
|
||||
|
@ -1962,7 +1970,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
4) A background thread
|
||||
5) The help window
|
||||
6) A modal password dialog appears over the splash screen
|
||||
|
||||
|
||||
It seems like the parent should be the active window for 1-2.
|
||||
Case 3 should probably use the window of the dialog provider.
|
||||
Case 4 should probably use the main tool frame, since the user may be
|
||||
|
@ -1970,12 +1978,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
active window, we can default to the tool's frame.
|
||||
Case 5 should use the help window.
|
||||
Case 6 should use the splash screen as the parent.
|
||||
|
||||
|
||||
We have not yet solidified how we should parent. This documentation is meant to
|
||||
move us towards clarity as we find Use Cases that don't make sense. (Once we
|
||||
finalize our understanding, we should update the javadoc to list exactly where
|
||||
the given Dialog Component will be shown.)
|
||||
|
||||
|
||||
Use Case
|
||||
A -The user presses an action on a toolbar from a window on screen 1, while the
|
||||
main tool frame is on screen 2. We want the popup window to appear on screen
|
||||
|
@ -1994,12 +2002,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
E -A long-running API shows a non-modal progress dialog. This API then shows a
|
||||
results dialog which is also non-modal. We do not want to parent the new dialog
|
||||
to the original dialog, since it is a progress dialog that will go away.
|
||||
|
||||
|
||||
|
||||
|
||||
For now, the easiest mental model to use is to always prefer the active non-transient
|
||||
window so that a dialog will appear in the user's view. If we find a case where this is
|
||||
not desired, then document it here.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
DockingWindowManager dwm = getActiveInstance();
|
||||
|
@ -2183,8 +2191,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
setStatusText(text);
|
||||
if (beep) {
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2201,8 +2208,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* A convenience method to make an attention-grabbing noise to the user
|
||||
*/
|
||||
public static void beep() {
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2274,8 +2280,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
if (includeMain) {
|
||||
winList.add(root.getMainWindow());
|
||||
}
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows()
|
||||
.iterator();
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows().iterator();
|
||||
while (it.hasNext()) {
|
||||
DetachedWindowNode node = it.next();
|
||||
Window win = node.getWindow();
|
||||
|
@ -2450,8 +2455,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
defaultContextProviderMap.entrySet();
|
||||
|
||||
for (Entry<Class<? extends ActionContext>, ActionContextProvider> entry : entrySet) {
|
||||
contextMap.put(entry.getKey(), entry.getValue()
|
||||
.getActionContext(null));
|
||||
contextMap.put(entry.getKey(), entry.getValue().getActionContext(null));
|
||||
}
|
||||
return contextMap;
|
||||
}
|
||||
|
@ -2472,7 +2476,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return context;
|
||||
}
|
||||
|
||||
// Some actions work on a non-active, default component provider. See if this action
|
||||
// Some actions work on a non-active, default component provider. See if this action
|
||||
// supports that.
|
||||
if (action.supportsDefaultContext()) {
|
||||
context = getDefaultContext(action.getContextClass());
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,12 +18,8 @@ package docking;
|
|||
import javax.swing.KeyStroke;
|
||||
|
||||
/**
|
||||
* Interface used to notify listener when a keystroke was entered in the
|
||||
* KeyEntryPanel.
|
||||
*
|
||||
*
|
||||
* Interface used to notify listener when a keystroke has changed.
|
||||
*/
|
||||
public interface KeyEntryListener {
|
||||
|
||||
public void processEntry(KeyStroke keyStroke);
|
||||
}
|
||||
|
|
|
@ -17,16 +17,20 @@ package docking;
|
|||
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.widgets.textfield.HintTextField;
|
||||
|
||||
/**
|
||||
* Text field captures key strokes and notifies a listener to process the key entry.
|
||||
*/
|
||||
public class KeyEntryTextField extends JTextField {
|
||||
public class KeyEntryTextField extends HintTextField {
|
||||
|
||||
private static final String HINT = "Type a key";
|
||||
private String disabledHint = HINT;
|
||||
|
||||
private KeyEntryListener listener;
|
||||
private String ksName;
|
||||
|
@ -38,11 +42,28 @@ public class KeyEntryTextField extends JTextField {
|
|||
* @param listener listener that is notified when the a key is pressed
|
||||
*/
|
||||
public KeyEntryTextField(int columns, KeyEntryListener listener) {
|
||||
super(columns);
|
||||
super(HINT);
|
||||
setName("Key Entry Text Field");
|
||||
getAccessibleContext().setAccessibleName(getName());
|
||||
setColumns(columns);
|
||||
this.listener = listener;
|
||||
addKeyListener(new MyKeyListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
setHint(enabled ? HINT : disabledHint);
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint text that will be displayed when this field is disabled
|
||||
* @param disabledHint the hint text
|
||||
*/
|
||||
public void setDisabledHint(String disabledHint) {
|
||||
this.disabledHint = Objects.requireNonNull(disabledHint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current key stroke
|
||||
* @return the key stroke
|
||||
|
@ -56,7 +77,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
* @param ks the new key stroke
|
||||
*/
|
||||
public void setKeyStroke(KeyStroke ks) {
|
||||
processEntry(ks);
|
||||
processKeyStroke(ks, false);
|
||||
setText(KeyBindingUtils.parseKeyStroke(ks));
|
||||
}
|
||||
|
||||
|
@ -66,7 +87,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
currentKeyStroke = null;
|
||||
}
|
||||
|
||||
private void processEntry(KeyStroke ks) {
|
||||
private void processKeyStroke(KeyStroke ks, boolean notify) {
|
||||
ksName = null;
|
||||
|
||||
currentKeyStroke = ks;
|
||||
|
@ -79,7 +100,10 @@ public class KeyEntryTextField extends JTextField {
|
|||
ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
}
|
||||
}
|
||||
listener.processEntry(ks);
|
||||
|
||||
if (notify) {
|
||||
listener.processEntry(ks);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyKeyListener implements KeyListener {
|
||||
|
@ -107,7 +131,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
if (!isClearKey(keyCode) && !isModifiersOnly(e)) {
|
||||
keyStroke = KeyStroke.getKeyStroke(keyCode, e.getModifiersEx());
|
||||
}
|
||||
processEntry(keyStroke);
|
||||
processKeyStroke(keyStroke, true);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@ package docking;
|
|||
import java.awt.event.ActionEvent;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.ToggleDockingActionIf;
|
||||
import docking.menu.MenuHandler;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
public class MenuBarMenuHandler extends MenuHandler {
|
||||
|
||||
|
@ -41,24 +39,7 @@ public class MenuBarMenuHandler extends MenuHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void processMenuAction(final DockingActionIf action, final ActionEvent event) {
|
||||
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
|
||||
ActionContext context = windowManager.createActionContext(action);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
windowManager.setStatusText("");
|
||||
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
|
||||
if (action instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
action.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
public void processMenuAction(DockingActionIf action, ActionEvent event) {
|
||||
DockingActionPerformer.perform(action, event, windowManager);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* Allows Ghidra to give preference to its mouse event processing over the default Java mouse event
|
||||
* processing. This class allows us to assign mouse bindings to actions.
|
||||
* <p>
|
||||
* {@link #install()} must be called in order to install this <code>Singleton</code> into Java's
|
||||
* mouse event processing system.
|
||||
*
|
||||
* @see KeyBindingOverrideKeyEventDispatcher
|
||||
*/
|
||||
public class MouseBindingMouseEventDispatcher {
|
||||
|
||||
private static MouseBindingMouseEventDispatcher instance;
|
||||
|
||||
static synchronized void install() {
|
||||
if (instance == null) {
|
||||
instance = new MouseBindingMouseEventDispatcher();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the current focus owner. This allows for dependency injection.
|
||||
*/
|
||||
private FocusOwnerProvider focusProvider = new DefaultFocusOwnerProvider();
|
||||
|
||||
/**
|
||||
* We use this action as a signal that we intend to process a mouse binding and that no other
|
||||
* Java component should try to handle it.
|
||||
* <p>
|
||||
* This action is one that is triggered by a mouse pressed, but will be processed on a
|
||||
* mouse released. We do this to ensure that we consume all related mouse events (press and
|
||||
* release) and to be consistent with the {@link KeyBindingOverrideKeyEventDispatcher}.
|
||||
*/
|
||||
private PendingActionInfo inProgressAction;
|
||||
|
||||
private MouseBindingMouseEventDispatcher() {
|
||||
// Note: see the documentation on addAWTEventListener() for limitations of using this
|
||||
// listener mechanism
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
AWTEventListener listener = new AWTEventListener() {
|
||||
@Override
|
||||
public void eventDispatched(AWTEvent event) {
|
||||
process((MouseEvent) event);
|
||||
}
|
||||
};
|
||||
toolkit.addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK);
|
||||
}
|
||||
|
||||
private void process(MouseEvent e) {
|
||||
|
||||
int id = e.getID();
|
||||
if (id == MouseEvent.MOUSE_ENTERED || id == MouseEvent.MOUSE_EXITED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// always let the application finish processing key events that it started
|
||||
if (actionInProgress(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = MouseBinding.getMouseBinding(e);
|
||||
DockingMouseBindingAction action = getDockingKeyBindingActionForEvent(mouseBinding);
|
||||
Msg.trace(this,
|
||||
new ParameterizedMessage("Mouse binding to action: {} to {}", mouseBinding, action));
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inProgressAction = new PendingActionInfo(action, mouseBinding);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to clear the flag that signals we are in the middle of processing a Ghidra action.
|
||||
*/
|
||||
private boolean actionInProgress(MouseEvent e) {
|
||||
|
||||
if (inProgressAction == null) {
|
||||
Msg.trace(this, "No mouse binding action in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: mouse buttons can be simultaneously clicked. This means that the order of pressed
|
||||
// and released events may arrive intermixed. To handle this correctly, we allow the
|
||||
// MouseBinding to check for the matching release event.
|
||||
MouseBinding mouseBinding = inProgressAction.mouseBinding();
|
||||
boolean isMatching = mouseBinding.isMatchingRelease(e);
|
||||
Msg.trace(this,
|
||||
new ParameterizedMessage(
|
||||
"Is release event for in-progress mouse binding action? {} for {}", isMatching,
|
||||
inProgressAction.action()));
|
||||
if (isMatching) {
|
||||
DockingMouseBindingAction action = inProgressAction.action();
|
||||
inProgressAction = null;
|
||||
|
||||
String command = null;
|
||||
Object source = e.getSource();
|
||||
long when = e.getWhen();
|
||||
int modifiers = e.getModifiersEx();
|
||||
|
||||
action.actionPerformed(
|
||||
new ActionEvent(source, ActionEvent.ACTION_PERFORMED, command, when, modifiers));
|
||||
}
|
||||
|
||||
e.consume();
|
||||
return true;
|
||||
}
|
||||
|
||||
private DockingMouseBindingAction getDockingKeyBindingActionForEvent(
|
||||
MouseBinding mouseBinding) {
|
||||
DockingWindowManager activeManager = getActiveDockingWindowManager();
|
||||
if (activeManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingMouseBindingAction bindingAction =
|
||||
(DockingMouseBindingAction) activeManager.getActionForMouseBinding(mouseBinding);
|
||||
return bindingAction;
|
||||
}
|
||||
|
||||
private DockingWindowManager getActiveDockingWindowManager() {
|
||||
// we need an active window to process events
|
||||
Window activeWindow = focusProvider.getActiveWindow();
|
||||
if (activeWindow == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingWindowManager activeManager = DockingWindowManager.getActiveInstance();
|
||||
if (activeManager == null) {
|
||||
// this can happen if clients use DockingWindows Look and Feel settings or
|
||||
// DockingWindows widgets without using the DockingWindows system (like in tests or
|
||||
// in stand-alone, non-Ghidra apps).
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingWindowManager managingInstance = getDockingWindowManagerForWindow(activeWindow);
|
||||
if (managingInstance != null) {
|
||||
return managingInstance;
|
||||
}
|
||||
|
||||
// this is a case where the current window is unaffiliated with a DockingWindowManager,
|
||||
// like a JavaHelp window
|
||||
return activeManager;
|
||||
}
|
||||
|
||||
private static DockingWindowManager getDockingWindowManagerForWindow(Window activeWindow) {
|
||||
DockingWindowManager manager = DockingWindowManager.getInstance(activeWindow);
|
||||
if (manager != null) {
|
||||
return manager;
|
||||
}
|
||||
if (activeWindow instanceof DockingDialog) {
|
||||
DockingDialog dockingDialog = (DockingDialog) activeWindow;
|
||||
return dockingDialog.getOwningWindowManager();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record PendingActionInfo(DockingMouseBindingAction action, MouseBinding mouseBinding) {
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.*;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import docking.widgets.textfield.HintTextField;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class MouseEntryTextField extends HintTextField {
|
||||
|
||||
private static final String HINT = "Press a mouse button";
|
||||
private String disabledHint = HINT;
|
||||
|
||||
private MouseBinding mouseBinding;
|
||||
private Consumer<MouseBinding> listener;
|
||||
|
||||
public MouseEntryTextField(int columns, Consumer<MouseBinding> listener) {
|
||||
super(HINT);
|
||||
setColumns(columns);
|
||||
setName("Mouse Entry Text Field");
|
||||
getAccessibleContext().setAccessibleName(getName());
|
||||
this.listener = Objects.requireNonNull(listener);
|
||||
|
||||
addMouseListener(new MyMouseListener());
|
||||
addKeyListener(new MyKeyListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
setHint(enabled ? HINT : disabledHint);
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint text that will be displayed when this field is disabled
|
||||
* @param disabledHint the hint text
|
||||
*/
|
||||
public void setDisabledHint(String disabledHint) {
|
||||
this.disabledHint = Objects.requireNonNull(disabledHint);
|
||||
}
|
||||
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseBinding;
|
||||
}
|
||||
|
||||
public void setMouseBinding(MouseBinding mb) {
|
||||
processMouseBinding(mb, false);
|
||||
}
|
||||
|
||||
public void clearField() {
|
||||
processMouseBinding(null, false);
|
||||
}
|
||||
|
||||
private void processMouseBinding(MouseBinding mb, boolean notify) {
|
||||
|
||||
this.mouseBinding = mb;
|
||||
if (mouseBinding == null) {
|
||||
setText("");
|
||||
}
|
||||
else {
|
||||
setText(mouseBinding.getDisplayText());
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
listener.accept(mb);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyMouseListener extends MouseAdapter {
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (!MouseEntryTextField.this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int modifiersEx = e.getModifiersEx();
|
||||
int button = e.getButton();
|
||||
processMouseBinding(new MouseBinding(button, modifiersEx), true);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private class MyKeyListener implements KeyListener {
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
int keyCode = e.getKeyCode();
|
||||
if (isClearKey(keyCode)) {
|
||||
processMouseBinding(null, true);
|
||||
}
|
||||
e.consume();
|
||||
}
|
||||
|
||||
private boolean isClearKey(int keyCode) {
|
||||
return keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ import ghidra.util.*;
|
|||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||
import ghidra.util.datastruct.WeakSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
import resources.ResourceManager;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
|
@ -323,6 +324,10 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
return menuItem;
|
||||
}
|
||||
|
||||
private MouseBinding getMouseBinding() {
|
||||
return keyBindingData == null ? null : keyBindingData.getMouseBinding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyBindingType getKeyBindingType() {
|
||||
return keyBindingType;
|
||||
|
@ -383,6 +388,10 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
@Override
|
||||
public void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData) {
|
||||
if (Objects.equals(keyBindingData, newKeyBindingData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData oldData = keyBindingData;
|
||||
keyBindingData = newKeyBindingData;
|
||||
firePropertyChanged(KEYBINDING_DATA_PROPERTY, oldData, keyBindingData);
|
||||
|
@ -492,8 +501,8 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
// menu path
|
||||
if (menuBarData != null) {
|
||||
buffer.append(" MENU PATH: ")
|
||||
.append(menuBarData.getMenuPathAsString());
|
||||
buffer.append(" MENU PATH: ").append(
|
||||
menuBarData.getMenuPathAsString());
|
||||
buffer.append('\n');
|
||||
buffer.append(" MENU GROUP: ").append(menuBarData.getMenuGroup());
|
||||
buffer.append('\n');
|
||||
|
@ -519,8 +528,8 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
// popup menu path
|
||||
if (popupMenuData != null) {
|
||||
buffer.append(" POPUP PATH: ")
|
||||
.append(popupMenuData.getMenuPathAsString());
|
||||
buffer.append(" POPUP PATH: ").append(
|
||||
popupMenuData.getMenuPathAsString());
|
||||
buffer.append('\n');
|
||||
buffer.append(" POPUP GROUP: ").append(popupMenuData.getMenuGroup());
|
||||
buffer.append('\n');
|
||||
|
@ -569,10 +578,15 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
KeyStroke keyStroke = getKeyBinding();
|
||||
if (keyStroke != null) {
|
||||
buffer.append(" KEYBINDING: ").append(keyStroke.toString());
|
||||
buffer.append(" KEYBINDING: ").append(keyStroke);
|
||||
buffer.append('\n');
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = getMouseBinding();
|
||||
if (mouseBinding != null) {
|
||||
buffer.append(" MOUSE BINDING: ").append(mouseBinding);
|
||||
}
|
||||
|
||||
String inception = getInceptionInformation();
|
||||
if (inception != null) {
|
||||
buffer.append("\n \n");
|
||||
|
|
|
@ -15,21 +15,27 @@
|
|||
*/
|
||||
package docking.action;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.KeyBindingPrecedence;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* An object that contains a key stroke and the precedence for when that key stroke should be used.
|
||||
*
|
||||
* <p>Note: this class creates key strokes that work on key {@code pressed}. This effectively
|
||||
* A class for storing an action's key stroke, mouse binding or both.
|
||||
* <p>
|
||||
* Note: this class creates key strokes that work on key {@code pressed}. This effectively
|
||||
* normalizes all client key bindings to work on the same type of key stroke (pressed, typed or
|
||||
* released).
|
||||
*/
|
||||
public class KeyBindingData {
|
||||
private KeyStroke keyStroke;
|
||||
private KeyBindingPrecedence keyBindingPrecedence;
|
||||
private KeyBindingPrecedence keyBindingPrecedence = KeyBindingPrecedence.DefaultLevel;
|
||||
|
||||
private MouseBinding mouseBinding;
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke) {
|
||||
this(keyStroke, KeyBindingPrecedence.DefaultLevel);
|
||||
|
@ -43,17 +49,44 @@ public class KeyBindingData {
|
|||
this(KeyStroke.getKeyStroke(keyCode, modifiers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance of this class that uses a mouse binding instead of a key stroke.
|
||||
* @param mouseBinding the mouse binding.
|
||||
*/
|
||||
public KeyBindingData(MouseBinding mouseBinding) {
|
||||
this.mouseBinding = Objects.requireNonNull(mouseBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key stroke from the given text. See
|
||||
* {@link KeyBindingUtils#parseKeyStroke(KeyStroke)}. The key stroke created for this class
|
||||
* will always be a key {@code pressed} key stroke.
|
||||
*
|
||||
*
|
||||
* @param keyStrokeString the key stroke string to parse
|
||||
*/
|
||||
public KeyBindingData(String keyStrokeString) {
|
||||
this(parseKeyStrokeString(keyStrokeString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key binding data with the given action trigger.
|
||||
* @param actionTrigger the trigger; may not be null
|
||||
*/
|
||||
public KeyBindingData(ActionTrigger actionTrigger) {
|
||||
Objects.requireNonNull(actionTrigger);
|
||||
this.keyStroke = actionTrigger.getKeyStroke();
|
||||
this.mouseBinding = actionTrigger.getMouseBinding();
|
||||
}
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't set precedence to System KeyBindingPrecedence");
|
||||
}
|
||||
this.keyStroke = Objects.requireNonNull(keyStroke);
|
||||
this.keyBindingPrecedence = Objects.requireNonNull(precedence);
|
||||
}
|
||||
|
||||
private static KeyStroke parseKeyStrokeString(String keyStrokeString) {
|
||||
KeyStroke keyStroke = KeyBindingUtils.parseKeyStroke(keyStrokeString);
|
||||
if (keyStroke == null) {
|
||||
|
@ -62,13 +95,33 @@ public class KeyBindingData {
|
|||
return keyStroke;
|
||||
}
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't set precedence to System KeyBindingPrecedence");
|
||||
/**
|
||||
* Returns a key binding data object that matches the given trigger. If the existing key
|
||||
* binding object already matches the new trigger, then the existing key binding data is
|
||||
* returned. If the new trigger is null, the null will be returned.
|
||||
*
|
||||
* @param kbData the existing key binding data; my be null
|
||||
* @param newTrigger the new action trigger; may be null
|
||||
* @return a key binding data based on the new action trigger; may be null
|
||||
*/
|
||||
public static KeyBindingData update(KeyBindingData kbData, ActionTrigger newTrigger) {
|
||||
if (kbData == null) {
|
||||
if (newTrigger == null) {
|
||||
return null; // no change
|
||||
}
|
||||
return new KeyBindingData(newTrigger); // trigger added
|
||||
}
|
||||
this.keyStroke = keyStroke;
|
||||
this.keyBindingPrecedence = precedence;
|
||||
|
||||
if (newTrigger == null) {
|
||||
return null; // trigger has been cleared
|
||||
}
|
||||
|
||||
ActionTrigger existingTrigger = kbData.getActionTrigger();
|
||||
if (existingTrigger.equals(newTrigger)) {
|
||||
return kbData;
|
||||
}
|
||||
|
||||
return new KeyBindingData(newTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,10 +140,56 @@ public class KeyBindingData {
|
|||
return keyBindingPrecedence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mouse binding assigned to this key binding data.
|
||||
* @return the mouse binding; may be null
|
||||
*/
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new action trigger with the values of this class
|
||||
* @return the action trigger
|
||||
*/
|
||||
public ActionTrigger getActionTrigger() {
|
||||
return new ActionTrigger(keyStroke, mouseBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[KeyStroke=" + keyStroke + ", precedence=" +
|
||||
keyBindingPrecedence + "]";
|
||||
keyBindingPrecedence + ", MouseBinding=" + mouseBinding + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(keyBindingPrecedence, keyStroke, mouseBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyBindingData other = (KeyBindingData) obj;
|
||||
if (keyBindingPrecedence != other.keyBindingPrecedence) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(keyStroke, other.keyStroke)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(mouseBinding, other.mouseBinding)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static KeyBindingData createSystemKeyBindingData(KeyStroke keyStroke) {
|
||||
|
@ -118,8 +217,15 @@ public class KeyBindingData {
|
|||
|
||||
KeyBindingPrecedence precedence = newKeyBindingData.getKeyBindingPrecedence();
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
return createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
|
||||
KeyBindingData kbd =
|
||||
createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
|
||||
kbd.mouseBinding = newKeyBindingData.mouseBinding;
|
||||
return kbd;
|
||||
}
|
||||
return new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
|
||||
|
||||
KeyBindingData kbd =
|
||||
new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
|
||||
kbd.mouseBinding = newKeyBindingData.mouseBinding;
|
||||
return kbd;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,12 @@ import docking.*;
|
|||
import docking.actions.KeyBindingUtils;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A class that organizes system key bindings by mapping them to assigned {@link DockingActionIf}s.
|
||||
*
|
||||
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
|
||||
*
|
||||
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
|
||||
* class knows how to map a single key binding to multiple actions.
|
||||
*/
|
||||
public class KeyBindingsManager implements PropertyChangeListener {
|
||||
|
@ -39,8 +40,9 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
// this map exists to update the MultiKeyBindingAction when the key binding changes
|
||||
private Map<DockingActionIf, ComponentProvider> actionToProviderMap = new HashMap<>();
|
||||
private Map<KeyStroke, DockingKeyBindingAction> dockingKeyMap = new HashMap<>();
|
||||
private Map<DockingActionIf, SystemKeyBindingAction> dockingActionToSystemActionMap =
|
||||
new HashMap<>();
|
||||
private Map<MouseBinding, DockingMouseBindingAction> dockingMouseMap = new HashMap<>();
|
||||
private Map<String, DockingActionIf> systemActionsByFullName = new HashMap<>();
|
||||
|
||||
private Tool tool;
|
||||
|
||||
public KeyBindingsManager(Tool tool) {
|
||||
|
@ -53,11 +55,20 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
actionToProviderMap.put(action, optionalProvider);
|
||||
}
|
||||
|
||||
KeyStroke keyBinding = action.getKeyBinding();
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
if (kbData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyStroke keyBinding = kbData.getKeyBinding();
|
||||
if (keyBinding != null) {
|
||||
addKeyBinding(optionalProvider, action, keyBinding);
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = kbData.getMouseBinding();
|
||||
if (mouseBinding != null) {
|
||||
doAddMouseBinding(action, mouseBinding);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSystemAction(DockingActionIf action) {
|
||||
|
@ -90,7 +101,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
// map standard keystroke to action
|
||||
// map standard keystroke to action
|
||||
doAddKeyBinding(provider, action, keyStroke);
|
||||
|
||||
// map workaround keystroke to action
|
||||
|
@ -103,7 +114,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return null; // clearing the key stroke
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// 1) Handle case with given key stroke already in use by a system action
|
||||
//
|
||||
Action existingAction = dockingKeyMap.get(ks);
|
||||
|
@ -118,12 +129,16 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return ksString + " in use by System action '" + systemDockingAction.getName() + "'";
|
||||
}
|
||||
|
||||
//
|
||||
// 2) Handle the case where a system action key stroke is being set to something that is
|
||||
if (dockingAction == null) {
|
||||
return null; // the client is only checking the keystroke and not any associated action
|
||||
}
|
||||
|
||||
//
|
||||
// 2) Handle the case where a system action key stroke is being set to something that is
|
||||
// already in-use by some other action
|
||||
//
|
||||
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(dockingAction);
|
||||
if (systemAction != null && existingAction != null) {
|
||||
//
|
||||
boolean hasSystemAction = systemActionsByFullName.containsKey(dockingAction.getFullName());
|
||||
if (hasSystemAction && existingAction != null) {
|
||||
return "System action cannot be set to in-use key stroke";
|
||||
}
|
||||
|
||||
|
@ -133,12 +148,12 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
private void fixupAltGraphKeyStrokeMapping(ComponentProvider provider, DockingActionIf action,
|
||||
KeyStroke keyStroke) {
|
||||
|
||||
// special case
|
||||
// special case
|
||||
int modifiers = keyStroke.getModifiers();
|
||||
if ((modifiers & InputEvent.ALT_DOWN_MASK) == InputEvent.ALT_DOWN_MASK) {
|
||||
//
|
||||
// Also register the 'Alt' binding with the 'Alt Graph' mask. This fixes the but
|
||||
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
|
||||
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
|
||||
// that have different key codes for the left and right Alt keys.
|
||||
//
|
||||
modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK;
|
||||
|
@ -170,8 +185,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(action);
|
||||
if (systemAction != null) {
|
||||
if (systemActionsByFullName.containsKey(action.getFullName())) {
|
||||
// the user has updated the binding for a System action; re-install it
|
||||
registerSystemKeyBinding(action, mappingKeyStroke);
|
||||
return;
|
||||
|
@ -182,6 +196,23 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
new MultipleKeyAction(tool, provider, action, actionKeyStroke));
|
||||
}
|
||||
|
||||
private void doAddMouseBinding(DockingActionIf action, MouseBinding mouseBinding) {
|
||||
|
||||
DockingMouseBindingAction mouseBindingAction = dockingMouseMap.get(mouseBinding);
|
||||
if (mouseBindingAction != null) {
|
||||
String existingName = mouseBindingAction.getFullActionName();
|
||||
String message = """
|
||||
Attempted to use the same mouse binding for multiple actions. \
|
||||
Multiple mouse bindings are not supported. Binding: %s \
|
||||
New action: %s; existing action: %s
|
||||
""".formatted(mouseBinding, action.getFullName(), existingName);
|
||||
Msg.error(this, message);
|
||||
return;
|
||||
}
|
||||
|
||||
dockingMouseMap.put(mouseBinding, new DockingMouseBindingAction(action, mouseBinding));
|
||||
}
|
||||
|
||||
private void addSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
|
||||
KeyBindingData binding = KeyBindingData.createSystemKeyBindingData(keyStroke);
|
||||
action.setKeyBindingData(binding);
|
||||
|
@ -191,7 +222,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
private void registerSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
|
||||
SystemKeyBindingAction systemAction = new SystemKeyBindingAction(tool, action, keyStroke);
|
||||
dockingKeyMap.put(keyStroke, systemAction);
|
||||
dockingActionToSystemActionMap.put(action, systemAction);
|
||||
systemActionsByFullName.put(action.getFullName(), action);
|
||||
}
|
||||
|
||||
private void removeKeyBinding(KeyStroke keyStroke, DockingActionIf action) {
|
||||
|
@ -242,17 +273,30 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
public Action getDockingKeyAction(KeyStroke keyStroke) {
|
||||
public Action getDockingAction(KeyStroke keyStroke) {
|
||||
return dockingKeyMap.get(keyStroke);
|
||||
}
|
||||
|
||||
public Action getDockingAction(MouseBinding mouseBinding) {
|
||||
return dockingMouseMap.get(mouseBinding);
|
||||
}
|
||||
|
||||
public boolean isSystemAction(DockingActionIf action) {
|
||||
return systemActionsByFullName.containsKey(action.getFullName());
|
||||
}
|
||||
|
||||
public DockingActionIf getSystemAction(String fullName) {
|
||||
return systemActionsByFullName.get(fullName);
|
||||
}
|
||||
|
||||
public Set<DockingActionIf> getSystemActions() {
|
||||
return new HashSet<>(dockingActionToSystemActionMap.keySet());
|
||||
return new HashSet<>(systemActionsByFullName.values());
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
dockingKeyMap.clear();
|
||||
dockingMouseMap.clear();
|
||||
actionToProviderMap.clear();
|
||||
dockingActionToSystemActionMap.clear();
|
||||
systemActionsByFullName.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,13 +46,12 @@ import ghidra.util.filechooser.GhidraFileFilter;
|
|||
import ghidra.util.xml.GenericXMLOutputter;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A class to provide utilities for system key bindings, such as importing and
|
||||
* exporting key binding configurations.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @since Tracker Id 329
|
||||
*/
|
||||
public class KeyBindingUtils {
|
||||
|
@ -100,7 +99,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* If there is a problem reading the data then the user will be shown an
|
||||
* error dialog.
|
||||
*
|
||||
*
|
||||
* @param inputStream the input stream from which to read options
|
||||
* @return An options object that is composed of key binding names and their
|
||||
* associated keystrokes.
|
||||
|
@ -141,7 +140,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* If there is a problem writing the data then the user will be shown an
|
||||
* error dialog.
|
||||
*
|
||||
*
|
||||
* @param keyBindingOptions The options that contains key binding data.
|
||||
*/
|
||||
public static void exportKeyBindings(ToolOptions keyBindingOptions) {
|
||||
|
@ -177,14 +176,14 @@ public class KeyBindingUtils {
|
|||
* Changes the given key event to the new source component and then dispatches that event.
|
||||
* This method is intended for clients that wish to effectively take a key event given to
|
||||
* one component and give it to another component.
|
||||
*
|
||||
*
|
||||
* <p>This method exists to deal with the complicated nature of key event processing and
|
||||
* how our (not Java's) framework processes key event bindings to trigger actions. If not
|
||||
* for our special processing of action key bindings, then this method would not be
|
||||
* necessary.
|
||||
*
|
||||
*
|
||||
* <p><b>This is seldom-used code; if you don't know when to use this code, then don't.</b>
|
||||
*
|
||||
*
|
||||
* @param newSource the new target of the event
|
||||
* @param e the existing event
|
||||
*/
|
||||
|
@ -199,7 +198,7 @@ public class KeyBindingUtils {
|
|||
|
||||
/*
|
||||
Unusual Code Alert!
|
||||
|
||||
|
||||
The KeyboardFocusManager is a complicated beast. Here we use knowledge of one such
|
||||
complication to correctly route key events. If the client of this method passes
|
||||
a component whose 'isShowing()' returns false, then the manager will not send the
|
||||
|
@ -208,13 +207,13 @@ public class KeyBindingUtils {
|
|||
attached; for example, when we are using said components with a renderer to perform
|
||||
our own painting. In the case of non-attached components, we must call the
|
||||
redispatchEvent() method ourselves.
|
||||
|
||||
|
||||
Why don't we just always call redispatchEvent()? Well, that
|
||||
method will not pass the new cloned event we just created back through the full
|
||||
key event pipeline. This means that tool-level (our Tool API, not Java)
|
||||
actions will not work, as tool-level actions are handled at the beginning of the
|
||||
key event pipeline, not by the components themselves.
|
||||
|
||||
|
||||
Also, we have here guilty knowledge that the aforementioned tool-level key processing
|
||||
will check to see if the event was consumed. If consumed, then no further processing
|
||||
will happen; if not consumed, then the framework will continue to process the event
|
||||
|
@ -245,7 +244,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
*/
|
||||
|
@ -263,12 +262,12 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* A typical use-case is to register an existing docking action with a text
|
||||
* component, which is needed because the docking key event processing will
|
||||
* not execute docking- registered actions if a text component has focus.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
* @param contextProvider the provider of the context
|
||||
|
@ -289,12 +288,12 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* A typical use-case is to register an existing docking action with a text
|
||||
* component, which is needed because the docking key event processing will
|
||||
* not execute docking- registered actions if a text component has focus.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
* @param contextProvider the provider of the context
|
||||
|
@ -311,7 +310,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Registers the given action with the given key binding on the given
|
||||
* component.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the action will be registered
|
||||
* @param keyStroke the keystroke for to which the action will be bound
|
||||
* @param action the action to execute when the given keystroke is triggered
|
||||
|
@ -353,7 +352,7 @@ public class KeyBindingUtils {
|
|||
* action with the same key binding from firing. This is useful when your
|
||||
* application is using tool-level key bindings that share the same
|
||||
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param action the action from which to get the key binding
|
||||
*/
|
||||
|
@ -373,7 +372,7 @@ public class KeyBindingUtils {
|
|||
* Note: this method clears the key binding for the
|
||||
* {@link JComponent#WHEN_FOCUSED} and
|
||||
* {@link JComponent#WHEN_ANCESTOR_OF_FOCUSED_COMPONENT} focus conditions.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param keyStroke the keystroke of the binding to be cleared
|
||||
* @see #clearKeyBinding(JComponent, KeyStroke, int)
|
||||
|
@ -387,7 +386,7 @@ public class KeyBindingUtils {
|
|||
* Allows clients to clear Java key bindings. This is useful when your
|
||||
* application is using tool-level key bindings that share the same
|
||||
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param keyStroke the keystroke of the binding to be cleared
|
||||
* @param focusCondition the particular focus condition under which the
|
||||
|
@ -405,7 +404,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Clears the currently assigned Java key binding for the action by the given name. This
|
||||
* method will find the currently assigned key binding, if any, and then remove it.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param actionName the name of the action that should not have a key binding
|
||||
* @see LookAndFeel
|
||||
|
@ -420,9 +419,8 @@ public class KeyBindingUtils {
|
|||
KeyStroke keyStroke = null;
|
||||
KeyStroke[] keys = inputMap.allKeys();
|
||||
if (keys == null) {
|
||||
Msg.debug(KeyBindingUtils.class,
|
||||
"Cannot remove action by name; does not exist: '" + actionName + "' " +
|
||||
"on component: " + component.getClass().getSimpleName());
|
||||
Msg.debug(KeyBindingUtils.class, "Cannot remove action by name; does not exist: '" +
|
||||
actionName + "' " + "on component: " + component.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -442,7 +440,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Returns the registered action for the given keystroke, or null of no
|
||||
* action is bound to that keystroke.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to check the binding
|
||||
* @param keyStroke the keystroke for which to find a bound action
|
||||
* @param focusCondition the focus condition under which to check for the
|
||||
|
@ -464,12 +462,12 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* A utility method to get all key binding actions. This method will
|
||||
* only return actions that support {@link KeyBindingType key bindings}.
|
||||
*
|
||||
*
|
||||
* <p>The mapping returned provides a list of items because it is possible for there to
|
||||
* exists multiple actions with the same name and owner. (This can happen when multiple copies
|
||||
* of a component provider are shown, each with their own set of actions that share the
|
||||
* same name.)
|
||||
*
|
||||
*
|
||||
* @param tool the tool containing the actions
|
||||
* @return the actions mapped by their full name (e.g., 'Name (OwnerName)')
|
||||
*/
|
||||
|
@ -496,13 +494,12 @@ public class KeyBindingUtils {
|
|||
* A utility method to get all key binding actions that have the given owner.
|
||||
* This method will remove duplicate actions and will only return actions
|
||||
* that support {@link KeyBindingType key bindings}.
|
||||
*
|
||||
*
|
||||
* @param tool the tool containing the actions
|
||||
* @param owner the action owner name
|
||||
* @return the actions
|
||||
*/
|
||||
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool,
|
||||
String owner) {
|
||||
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool, String owner) {
|
||||
|
||||
Map<String, DockingActionIf> deduper = new HashMap<>();
|
||||
Set<DockingActionIf> actions = tool.getDockingActionsByOwnerName(owner);
|
||||
|
@ -522,7 +519,7 @@ public class KeyBindingUtils {
|
|||
|
||||
/**
|
||||
* Returns all actions that match the given owner and name
|
||||
*
|
||||
*
|
||||
* @param allActions the universe of actions
|
||||
* @param owner the owner
|
||||
* @param name the name
|
||||
|
@ -539,13 +536,13 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Takes the existing docking action and allows it to be registered with
|
||||
* Swing components
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* The new action will not be correctly wired into the Docking Action
|
||||
* Context system. This means that the given docking action should not rely
|
||||
* on {@link DockingAction#isEnabledForContext(docking.ActionContext)} to
|
||||
* work when called from the Swing widget.
|
||||
*
|
||||
*
|
||||
* @param action the docking action to adapt to a Swing {@link Action}
|
||||
* @return the new action
|
||||
*/
|
||||
|
@ -553,66 +550,10 @@ public class KeyBindingUtils {
|
|||
return new ActionAdapter(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each action in the given collection against the given new action to make sure that
|
||||
* they share the same default key binding.
|
||||
*
|
||||
* @param newAction the action to check
|
||||
* @param existingActions the actions that have already been checked
|
||||
*/
|
||||
public static void assertSameDefaultKeyBindings(DockingActionIf newAction,
|
||||
Collection<DockingActionIf> existingActions) {
|
||||
|
||||
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
|
||||
KeyStroke defaultKs = getKeyStroke(newDefaultBinding);
|
||||
for (DockingActionIf action : existingActions) {
|
||||
if (!action.getKeyBindingType().supportsKeyBindings()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
|
||||
KeyStroke existingKs = getKeyStroke(existingDefaultBinding);
|
||||
if (!Objects.equals(defaultKs, existingKs)) {
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, action, existingKs);
|
||||
break; // one warning seems like enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message for the two given actions to signal that they do not share the
|
||||
* same default key binding
|
||||
*
|
||||
* @param newAction the new action
|
||||
* @param existingAction the action that has already been validated
|
||||
* @param existingDefaultKs the current validated key stroke
|
||||
*/
|
||||
public static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, KeyStroke existingDefaultKs) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+"'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tKey Binding: " + existingDefaultKs +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tKey Binding: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultKs;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(KeyBindingUtils.class, s, ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given data with system-independent versions of key modifiers. For example,
|
||||
* the <code>control</code> key will be converted to the <code>command</code> key on the Mac.
|
||||
*
|
||||
*
|
||||
* @param keyStroke the keystroke to validate
|
||||
* @return the potentially changed keystroke
|
||||
*/
|
||||
|
@ -676,7 +617,7 @@ public class KeyBindingUtils {
|
|||
* and we want it to look like: "Ctrl-M".
|
||||
* <br>In Java 11 we have seen toString() values get printed with repeated text, such
|
||||
* as: "shift ctrl pressed SHIFT". We want to trim off the repeated modifiers.
|
||||
*
|
||||
*
|
||||
* @param keyStroke the key stroke
|
||||
* @return the string value; the empty string if the key stroke is null
|
||||
*/
|
||||
|
@ -792,14 +733,18 @@ public class KeyBindingUtils {
|
|||
* Ctrl-Alt-Z
|
||||
* ctrl Z
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* <p><b>Note:</b> The returned keystroke will always correspond to a {@code pressed} event,
|
||||
* regardless of the value passed in (pressed, typed or released).
|
||||
*
|
||||
*
|
||||
* @param keyStroke the key stroke
|
||||
* @return the new key stroke (as returned by {@link KeyStroke#getKeyStroke(String)}
|
||||
*/
|
||||
public static KeyStroke parseKeyStroke(String keyStroke) {
|
||||
if (StringUtils.isBlank(keyStroke)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> pieces = new ArrayList<>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(keyStroke, "- ");
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
|
@ -873,13 +818,6 @@ public class KeyBindingUtils {
|
|||
return !action.getKeyBindingType().isManaged();
|
||||
}
|
||||
|
||||
private static KeyStroke getKeyStroke(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getKeyBinding();
|
||||
}
|
||||
|
||||
// prompts the user for a file location from which to read key binding data
|
||||
private static InputStream getInputStreamForFile(File startingDir) {
|
||||
File selectedFile = getFileFromUser(startingDir);
|
||||
|
|
|
@ -24,35 +24,40 @@ import docking.Tool;
|
|||
import docking.action.DockingActionIf;
|
||||
import docking.action.KeyBindingData;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import gui.event.MouseBinding;
|
||||
import util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* An object that maps actions to key strokes.
|
||||
* An object that maps actions to key strokes and mouse bindings.
|
||||
* <p>
|
||||
* This class knows how to load all system actions and how to load any key bindings for those
|
||||
* actions from the tool's options. Clients can make changes to the state of this class that can
|
||||
* then be applied to the system by calling {@link #applyChanges()}.
|
||||
* This class knows how to load all system actions and how to load any key and mouse bindings for
|
||||
* those actions from the tool's options. Clients can make changes to the state of this class that
|
||||
* can then be applied to the system by calling {@link #applyChanges()}.
|
||||
*/
|
||||
public class KeyBindings {
|
||||
|
||||
private Tool tool;
|
||||
private ToolOptions keyBindingOptions;
|
||||
|
||||
private Map<String, List<DockingActionIf>> actionsByFullName;
|
||||
private Map<String, List<String>> actionNamesByKeyStroke = new HashMap<>();
|
||||
private Map<String, KeyStroke> keyStrokesByFullName = new HashMap<>();
|
||||
// allows clients to populate a table of all actions
|
||||
private List<DockingActionIf> uniqueActions = new ArrayList<>();
|
||||
|
||||
// to know what has been changed
|
||||
private Map<String, KeyStroke> originalKeyStrokesByFullName = new HashMap<>();
|
||||
private String longestActionName = "";
|
||||
// allows clients to know if a given key stroke or mouse binding is in use
|
||||
private Map<KeyStroke, List<String>> actionNamesByKeyStroke = new HashMap<>();
|
||||
private Map<MouseBinding, String> actionNameByMouseBinding = new HashMap<>();
|
||||
|
||||
private ToolOptions options;
|
||||
// tracks all changes to an action's key stroke and mouse bindings, which allows us to apply
|
||||
// and restore options values
|
||||
private Map<String, ActionKeyBindingState> actionInfoByFullName = new HashMap<>();
|
||||
|
||||
private String longestActionName = "";
|
||||
|
||||
public KeyBindings(Tool tool) {
|
||||
this.tool = tool;
|
||||
|
||||
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
init();
|
||||
}
|
||||
|
@ -61,22 +66,40 @@ public class KeyBindings {
|
|||
return Collections.unmodifiableList(uniqueActions);
|
||||
}
|
||||
|
||||
/* used for testing */
|
||||
public Map<String, KeyStroke> getKeyStrokesByFullActionName() {
|
||||
return Collections.unmodifiableMap(keyStrokesByFullName);
|
||||
Map<String, KeyStroke> result = new HashMap<>();
|
||||
Set<Entry<String, ActionKeyBindingState>> entries = actionInfoByFullName.entrySet();
|
||||
for (Entry<String, ActionKeyBindingState> entry : entries) {
|
||||
String key = entry.getKey();
|
||||
KeyStroke value = entry.getValue().getCurrentKeyStroke();
|
||||
result.put(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean containsAction(String fullName) {
|
||||
return actionsByFullName.containsKey(fullName);
|
||||
return actionInfoByFullName.containsKey(fullName);
|
||||
}
|
||||
|
||||
public KeyStroke getKeyStroke(String fullName) {
|
||||
return keyStrokesByFullName.get(fullName);
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
return info.getCurrentKeyStroke();
|
||||
}
|
||||
|
||||
public String getActionsForKeyStrokeText(String keyStrokeText) {
|
||||
public MouseBinding getMouseBinding(String fullName) {
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
return info.getCurrentMouseBinding();
|
||||
}
|
||||
|
||||
public String getActionForMouseBinding(MouseBinding mouseBinding) {
|
||||
return actionNameByMouseBinding.get(mouseBinding);
|
||||
}
|
||||
|
||||
public String getActionsForKeyStrokeText(KeyStroke keyStroke) {
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
List<String> names = actionNamesByKeyStroke.get(keyStrokeText);
|
||||
List<String> names = actionNamesByKeyStroke.get(keyStroke);
|
||||
if (CollectionUtils.isBlank(names)) {
|
||||
return sb.toString();
|
||||
}
|
||||
|
@ -85,14 +108,16 @@ public class KeyBindings {
|
|||
return n1.compareToIgnoreCase(n2);
|
||||
});
|
||||
|
||||
sb.append("Actions mapped to key " + keyStrokeText + ":\n");
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
|
||||
sb.append("Actions mapped to key " + ksName + ":\n");
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
sb.append(" ");
|
||||
|
||||
String name = names.get(i);
|
||||
List<DockingActionIf> actions = actionsByFullName.get(name);
|
||||
DockingActionIf action = actions.get(0);
|
||||
sb.append(action.getName());
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(name);
|
||||
DockingActionIf action = info.getRepresentativeAction();
|
||||
String shortName = action.getName();
|
||||
sb.append(shortName);
|
||||
sb.append(" (").append(action.getOwnerDescription()).append(')');
|
||||
if (i < names.size() - 1) {
|
||||
sb.append("\n");
|
||||
|
@ -105,58 +130,79 @@ public class KeyBindings {
|
|||
return longestActionName;
|
||||
}
|
||||
|
||||
public boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
|
||||
public boolean isMouseBindingInUse(String fullName, MouseBinding newBinding) {
|
||||
|
||||
// remove old keystroke for action name
|
||||
KeyStroke oldKs = keyStrokesByFullName.get(actionName);
|
||||
if (oldKs != null) {
|
||||
String oldName = KeyBindingUtils.parseKeyStroke(oldKs);
|
||||
if (oldName.equals(ksName)) {
|
||||
String existingName = actionNameByMouseBinding.get(newBinding);
|
||||
if (existingName == null || newBinding == null) {
|
||||
return false; // no new binding, or not in use
|
||||
}
|
||||
|
||||
return !Objects.equals(existingName, fullName);
|
||||
}
|
||||
|
||||
public boolean setActionMouseBinding(String fullName, MouseBinding newBinding) {
|
||||
|
||||
MouseBinding currentBinding = getMouseBinding(fullName);
|
||||
if (currentBinding != null) {
|
||||
if (currentBinding.equals(newBinding)) {
|
||||
return false;
|
||||
}
|
||||
removeFromKeyMap(oldKs, actionName);
|
||||
}
|
||||
addActionKeyStroke(keyStroke, actionName);
|
||||
|
||||
keyStrokesByFullName.put(actionName, keyStroke);
|
||||
actionNameByMouseBinding.remove(currentBinding);
|
||||
}
|
||||
|
||||
if (newBinding != null) {
|
||||
actionNameByMouseBinding.put(newBinding, fullName);
|
||||
}
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
info.setCurrentMouseBinding(newBinding);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeKeyStroke(String actionName) {
|
||||
if (keyStrokesByFullName.containsKey(actionName)) {
|
||||
KeyStroke stroke = keyStrokesByFullName.get(actionName);
|
||||
if (stroke == null) {
|
||||
// nothing to remove; nothing has changed
|
||||
public boolean setActionKeyStroke(String fullName, KeyStroke newKs) {
|
||||
String newKsName = KeyBindingUtils.parseKeyStroke(newKs);
|
||||
|
||||
// remove old keystroke for action name
|
||||
KeyStroke currentKs = getKeyStroke(fullName);
|
||||
if (currentKs != null) {
|
||||
String currentName = KeyBindingUtils.parseKeyStroke(currentKs);
|
||||
if (currentName.equals(newKsName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
removeFromKeyMap(stroke, actionName);
|
||||
keyStrokesByFullName.put(actionName, null);
|
||||
return true;
|
||||
removeFromKeyMap(fullName, currentKs);
|
||||
}
|
||||
return false;
|
||||
addActionKeyStroke(fullName, newKs);
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
info.setCurrentKeyStroke(newKs);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeKeyStroke(String fullName) {
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
if (info == null) {
|
||||
return false; // not sure if this can happen
|
||||
}
|
||||
|
||||
KeyStroke currentKeyStroke = info.getCurrentKeyStroke();
|
||||
if (currentKeyStroke == null) {
|
||||
return false; // nothing to remove; nothing has changed
|
||||
}
|
||||
|
||||
removeFromKeyMap(fullName, currentKeyStroke);
|
||||
info.setCurrentKeyStroke(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the tool options key bindings to the default values originally loaded when the
|
||||
* Restores the tool options key bindings to the default values originally loaded when the
|
||||
* system started.
|
||||
*/
|
||||
public void restoreOptions() {
|
||||
|
||||
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
|
||||
for (Entry<String, List<DockingActionIf>> entry : entries) {
|
||||
List<DockingActionIf> actions = entry.getValue();
|
||||
|
||||
// pick one action, they are all conceptually the same
|
||||
DockingActionIf action = actions.get(0);
|
||||
String actionName = entry.getKey();
|
||||
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
|
||||
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
|
||||
KeyStroke newKeyStroke =
|
||||
(defaultBinding == null) ? null : defaultBinding.getKeyBinding();
|
||||
|
||||
updateOptions(actionName, currentKeyStroke, newKeyStroke);
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.restore(keyBindingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,14 +210,8 @@ public class KeyBindings {
|
|||
* Cancels any pending changes that have not yet been applied.
|
||||
*/
|
||||
public void cancelChanges() {
|
||||
Iterator<String> iter = originalKeyStrokesByFullName.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
String actionName = iter.next();
|
||||
KeyStroke originalKS = originalKeyStrokesByFullName.get(actionName);
|
||||
KeyStroke modifiedKS = keyStrokesByFullName.get(actionName);
|
||||
if (modifiedKS != null && !modifiedKS.equals(originalKS)) {
|
||||
keyStrokesByFullName.put(actionName, originalKS);
|
||||
}
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.cancelChanges();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,84 +219,203 @@ public class KeyBindings {
|
|||
* Applies any pending changes.
|
||||
*/
|
||||
public void applyChanges() {
|
||||
Iterator<String> iter = keyStrokesByFullName.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
String actionName = iter.next();
|
||||
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
|
||||
KeyStroke originalKeyStroke = originalKeyStrokesByFullName.get(actionName);
|
||||
updateOptions(actionName, originalKeyStroke, currentKeyStroke);
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.apply(keyBindingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFromKeyMap(KeyStroke ks, String actionName) {
|
||||
private void removeFromKeyMap(String actionName, KeyStroke ks) {
|
||||
if (ks == null) {
|
||||
return;
|
||||
}
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
List<String> list = actionNamesByKeyStroke.get(ksName);
|
||||
|
||||
List<String> list = actionNamesByKeyStroke.get(ks);
|
||||
if (list != null) {
|
||||
list.remove(actionName);
|
||||
if (list.isEmpty()) {
|
||||
actionNamesByKeyStroke.remove(ksName);
|
||||
actionNamesByKeyStroke.remove(ks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptions(String fullActionName, KeyStroke currentKeyStroke,
|
||||
KeyStroke newKeyStroke) {
|
||||
|
||||
if (Objects.equals(currentKeyStroke, newKeyStroke)) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.setKeyStroke(fullActionName, newKeyStroke);
|
||||
originalKeyStrokesByFullName.put(fullActionName, newKeyStroke);
|
||||
keyStrokesByFullName.put(fullActionName, newKeyStroke);
|
||||
|
||||
List<DockingActionIf> actions = actionsByFullName.get(fullActionName);
|
||||
for (DockingActionIf action : actions) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKeyStroke));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void init() {
|
||||
|
||||
actionsByFullName = KeyBindingUtils.getAllActionsByFullName(tool);
|
||||
actionInfoByFullName = new HashMap<>();
|
||||
|
||||
Map<String, List<DockingActionIf>> actionsByFullName =
|
||||
KeyBindingUtils.getAllActionsByFullName(tool);
|
||||
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
|
||||
for (Entry<String, List<DockingActionIf>> entry : entries) {
|
||||
|
||||
// pick one action, they are all conceptually the same
|
||||
List<DockingActionIf> actions = entry.getValue();
|
||||
DockingActionIf action = actions.get(0);
|
||||
uniqueActions.add(action);
|
||||
|
||||
String actionName = entry.getKey();
|
||||
KeyStroke ks = options.getKeyStroke(actionName, null);
|
||||
keyStrokesByFullName.put(actionName, ks);
|
||||
addActionKeyStroke(ks, actionName);
|
||||
originalKeyStrokesByFullName.put(actionName, ks);
|
||||
String fullName = entry.getKey();
|
||||
ActionTrigger trigger = keyBindingOptions.getActionTrigger(fullName, null);
|
||||
|
||||
String shortName = action.getName();
|
||||
KeyStroke ks = null;
|
||||
MouseBinding mb = null;
|
||||
|
||||
if (trigger != null) {
|
||||
ks = trigger.getKeyStroke();
|
||||
mb = trigger.getMouseBinding();
|
||||
}
|
||||
|
||||
ActionKeyBindingState info = new ActionKeyBindingState(actions, ks, mb);
|
||||
actionInfoByFullName.put(fullName, info);
|
||||
|
||||
uniqueActions.add(info.getRepresentativeAction());
|
||||
|
||||
addActionKeyStroke(fullName, ks);
|
||||
|
||||
String shortName = info.getShortName();
|
||||
if (shortName.length() > longestActionName.length()) {
|
||||
longestActionName = shortName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addActionKeyStroke(KeyStroke ks, String actionName) {
|
||||
private void addActionKeyStroke(String actionName, KeyStroke ks) {
|
||||
if (ks == null) {
|
||||
return;
|
||||
}
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
List<String> list = actionNamesByKeyStroke.get(ksName);
|
||||
|
||||
List<String> list = actionNamesByKeyStroke.get(ks);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
actionNamesByKeyStroke.put(ksName, list);
|
||||
actionNamesByKeyStroke.put(ks, list);
|
||||
}
|
||||
if (!list.contains(actionName)) {
|
||||
list.add(actionName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to store current and original values for key strokes and mouse bindings. This is
|
||||
* used to apply changes and restore default values.
|
||||
*/
|
||||
private class ActionKeyBindingState {
|
||||
|
||||
private List<DockingActionIf> actions = new ArrayList<>();
|
||||
private KeyStroke originalKeyStroke;
|
||||
private KeyStroke currentKeyStroke;
|
||||
private MouseBinding originalMouseBinding;
|
||||
private MouseBinding currentMouseBinding;
|
||||
|
||||
ActionKeyBindingState(List<DockingActionIf> actions, KeyStroke ks, MouseBinding mb) {
|
||||
this.actions.addAll(actions);
|
||||
this.originalKeyStroke = ks;
|
||||
this.currentKeyStroke = ks;
|
||||
this.originalMouseBinding = mb;
|
||||
this.currentMouseBinding = mb;
|
||||
}
|
||||
|
||||
public DockingActionIf getRepresentativeAction() {
|
||||
// pick one action, they are all conceptually the same
|
||||
return actions.get(0);
|
||||
}
|
||||
|
||||
String getShortName() {
|
||||
// pick one action, they are all conceptually the same
|
||||
return actions.get(0).getName();
|
||||
}
|
||||
|
||||
String getFullName() {
|
||||
return getRepresentativeAction().getFullName();
|
||||
}
|
||||
|
||||
public MouseBinding getCurrentMouseBinding() {
|
||||
return currentMouseBinding;
|
||||
}
|
||||
|
||||
public void setCurrentMouseBinding(MouseBinding newMouseBinding) {
|
||||
this.currentMouseBinding = newMouseBinding;
|
||||
}
|
||||
|
||||
public KeyStroke getCurrentKeyStroke() {
|
||||
return currentKeyStroke;
|
||||
}
|
||||
|
||||
public void setCurrentKeyStroke(KeyStroke newKeyStroke) {
|
||||
this.currentKeyStroke = newKeyStroke;
|
||||
}
|
||||
|
||||
public void cancelChanges() {
|
||||
currentKeyStroke = originalKeyStroke;
|
||||
currentMouseBinding = originalMouseBinding;
|
||||
}
|
||||
|
||||
public void apply(ToolOptions keyStrokeOptions) {
|
||||
if (!hasChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData kbd = getCurrentKeyBindingData();
|
||||
apply(keyStrokeOptions, kbd);
|
||||
}
|
||||
|
||||
private void apply(ToolOptions keyStrokeOptions, KeyBindingData keyBinding) {
|
||||
|
||||
if (keyBinding == null) {
|
||||
// no bindings; bindings have been cleared
|
||||
for (DockingActionIf action : actions) {
|
||||
action.setUnvalidatedKeyBindingData(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ActionTrigger newTrigger = keyBinding.getActionTrigger();
|
||||
String fullName = getFullName();
|
||||
keyStrokeOptions.setActionTrigger(fullName, newTrigger);
|
||||
}
|
||||
|
||||
private boolean hasChanged() {
|
||||
return !Objects.equals(originalKeyStroke, currentKeyStroke) ||
|
||||
!Objects.equals(originalMouseBinding, currentMouseBinding);
|
||||
}
|
||||
|
||||
private boolean matches(KeyBindingData kbData) {
|
||||
|
||||
if (CollectionUtils.isAllNull(kbData, currentKeyStroke, currentMouseBinding)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kbData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyStroke otherKs = kbData.getKeyBinding();
|
||||
if (!Objects.equals(otherKs, currentKeyStroke)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MouseBinding otherMb = kbData.getMouseBinding();
|
||||
return Objects.equals(otherMb, currentMouseBinding);
|
||||
}
|
||||
|
||||
private KeyBindingData getCurrentKeyBindingData() {
|
||||
|
||||
if (currentKeyStroke == null && currentMouseBinding == null) {
|
||||
return null; // the key binding data does not exist or has been cleared
|
||||
}
|
||||
|
||||
DockingActionIf action = getRepresentativeAction();
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
ActionTrigger trigger = new ActionTrigger(currentKeyStroke, currentMouseBinding);
|
||||
return KeyBindingData.update(kbData, trigger);
|
||||
}
|
||||
|
||||
// restores the options to their default values
|
||||
public void restore(ToolOptions options) {
|
||||
DockingActionIf action = getRepresentativeAction();
|
||||
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
|
||||
|
||||
if (!matches(defaultBinding)) {
|
||||
apply(options, defaultBinding);
|
||||
}
|
||||
|
||||
cancelChanges();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
*/
|
||||
public void setKeyStroke(KeyStroke ks) {
|
||||
keyEntryField.setKeyStroke(ks);
|
||||
updateCollisionPane(ks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -169,7 +170,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
action.setUnvalidatedKeyBindingData(newKs == null ? null : new KeyBindingData(newKs));
|
||||
|
||||
close();
|
||||
}
|
||||
|
@ -192,8 +193,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ksName);
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ks);
|
||||
try {
|
||||
doc.insertString(0, text, textAttrs);
|
||||
collisionPane.setCaretPosition(0);
|
||||
|
|
|
@ -18,8 +18,6 @@ package docking.actions;
|
|||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.apache.commons.collections4.Bag;
|
||||
import org.apache.commons.collections4.bag.HashBag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -28,8 +26,9 @@ import docking.ActionContext;
|
|||
import docking.DockingWindowManager;
|
||||
import docking.action.*;
|
||||
import docking.tool.ToolConstants;
|
||||
import ghidra.framework.options.OptionsChangeListener;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.util.Msg;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A stub action that allows key bindings to be edited through the key bindings options. This
|
||||
|
@ -63,7 +62,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
* Note: This collection is weak; the actions will stay as long as they are
|
||||
* registered in the tool.
|
||||
*/
|
||||
private WeakHashMap<DockingActionIf, KeyStroke> clientActions = new WeakHashMap<>();
|
||||
private WeakHashMap<DockingActionIf, ActionTrigger> clientActions = new WeakHashMap<>();
|
||||
|
||||
private ToolOptions keyBindingOptions;
|
||||
private Bag<String> actionOwners = new HashBag<>();
|
||||
|
@ -73,11 +72,13 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
*
|
||||
* @param name The name of the action--this will be displayed in the options as the name of
|
||||
* key binding's action
|
||||
* @param defaultKs the default key stroke for this stub. The key stroke will be validated
|
||||
* each time an action is added to this stub to ensure that the defaults are in sync.
|
||||
* @param defaultActionTrigger the default action trigger for this stub. The action trigger
|
||||
* will be validated each time an action is added to this stub to ensure that the
|
||||
* defaults are in sync.
|
||||
* @param options the tool's key binding options
|
||||
*/
|
||||
SharedStubKeyBindingAction(String name, KeyStroke defaultKs, ToolOptions options) {
|
||||
SharedStubKeyBindingAction(String name, ActionTrigger defaultActionTrigger,
|
||||
ToolOptions options) {
|
||||
// Note: we need to have this stub registered to use key bindings so that the options will
|
||||
// restore the saved key binding to this class, which will then notify any of the
|
||||
// shared actions using this stub.
|
||||
|
@ -87,7 +88,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
// Dummy keybinding actions don't have help--the real action does
|
||||
DockingWindowManager.getHelpService().excludeFromHelp(this);
|
||||
|
||||
setUnvalidatedKeyBindingData(new KeyBindingData(defaultKs));
|
||||
setKeyBindingData(this, defaultActionTrigger);
|
||||
|
||||
// A listener to keep the shared, stub keybindings in sync with their clients
|
||||
options.addOptionsChangeListener(this);
|
||||
|
@ -119,7 +120,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
void addClientAction(DockingActionIf action) {
|
||||
|
||||
// 1) Validate new action keystroke against existing actions
|
||||
KeyStroke defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
|
||||
ActionTrigger defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
|
||||
|
||||
// 2) Add the action and the validated keystroke, as this is the default keystroke
|
||||
clientActions.put(action, defaultKs);
|
||||
|
@ -159,61 +160,69 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
return super.getDescription();
|
||||
}
|
||||
|
||||
private KeyStroke validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
|
||||
private ActionTrigger validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
|
||||
|
||||
// this value may be null
|
||||
KeyBindingData defaultBinding = newAction.getDefaultKeyBindingData();
|
||||
KeyStroke newDefaultKs = getKeyStroke(defaultBinding);
|
||||
ActionTrigger newDefaulTrigger = getActionTrigger(defaultBinding);
|
||||
|
||||
Set<Entry<DockingActionIf, KeyStroke>> entries = clientActions.entrySet();
|
||||
for (Entry<DockingActionIf, KeyStroke> entry : entries) {
|
||||
Set<Entry<DockingActionIf, ActionTrigger>> entries = clientActions.entrySet();
|
||||
for (Entry<DockingActionIf, ActionTrigger> entry : entries) {
|
||||
DockingActionIf existingAction = entry.getKey();
|
||||
KeyStroke existingDefaultKs = entry.getValue();
|
||||
if (Objects.equals(existingDefaultKs, newDefaultKs)) {
|
||||
ActionTrigger existingDefaultTrigger = entry.getValue();
|
||||
if (Objects.equals(existingDefaultTrigger, newDefaulTrigger)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingUtils.logDifferentKeyBindingsWarnigMessage(newAction, existingAction,
|
||||
existingDefaultKs);
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, existingAction, existingDefaultTrigger);
|
||||
|
||||
//
|
||||
// Not sure which keystroke to prefer here--keep the first one that was set
|
||||
//
|
||||
|
||||
// set the new action's keystroke to be the winner
|
||||
newAction.setKeyBindingData(new KeyBindingData(existingDefaultKs));
|
||||
// set the existing action's keystroke to be the winner
|
||||
newAction.setKeyBindingData(existingAction.getKeyBindingData());
|
||||
|
||||
// one message is probably enough;
|
||||
return existingDefaultKs;
|
||||
return existingDefaultTrigger;
|
||||
}
|
||||
|
||||
return newDefaultKs;
|
||||
return newDefaulTrigger;
|
||||
}
|
||||
|
||||
private void updateActionKeyStrokeFromOptions(DockingActionIf action, KeyStroke defaultKs) {
|
||||
private void updateActionKeyStrokeFromOptions(DockingActionIf action,
|
||||
ActionTrigger defaultTrigger) {
|
||||
|
||||
KeyStroke stubKs = defaultKs;
|
||||
KeyStroke optionsKs = getKeyStrokeFromOptions(defaultKs);
|
||||
if (!Objects.equals(defaultKs, optionsKs)) {
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(optionsKs));
|
||||
stubKs = optionsKs;
|
||||
ActionTrigger stubTrigger = defaultTrigger;
|
||||
ActionTrigger optionsTrigger = getActionTriggerFromOptions(defaultTrigger);
|
||||
if (!Objects.equals(defaultTrigger, optionsTrigger)) {
|
||||
setKeyBindingData(action, optionsTrigger);
|
||||
stubTrigger = optionsTrigger;
|
||||
}
|
||||
|
||||
setUnvalidatedKeyBindingData(new KeyBindingData(stubKs));
|
||||
setKeyBindingData(this, stubTrigger);
|
||||
}
|
||||
|
||||
private KeyStroke getKeyStrokeFromOptions(KeyStroke validatedKeyStroke) {
|
||||
KeyStroke ks = keyBindingOptions.getKeyStroke(getFullName(), validatedKeyStroke);
|
||||
return ks;
|
||||
private void setKeyBindingData(DockingActionIf action, ActionTrigger actionTrigger) {
|
||||
KeyBindingData kbData = null;
|
||||
if (actionTrigger != null) {
|
||||
kbData = new KeyBindingData(actionTrigger);
|
||||
}
|
||||
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(kbData);
|
||||
}
|
||||
|
||||
private KeyStroke getKeyStroke(KeyBindingData data) {
|
||||
private ActionTrigger getActionTriggerFromOptions(ActionTrigger validatedTrigger) {
|
||||
return keyBindingOptions.getActionTrigger(getFullName(), validatedTrigger);
|
||||
}
|
||||
|
||||
private ActionTrigger getActionTrigger(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getKeyBinding();
|
||||
return data.getActionTrigger();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -224,11 +233,11 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
return; // not my binding
|
||||
}
|
||||
|
||||
KeyStroke newKs = (KeyStroke) newValue;
|
||||
ActionTrigger newTrigger = (ActionTrigger) newValue;
|
||||
setKeyBindingData(this, newTrigger);
|
||||
|
||||
for (DockingActionIf action : clientActions.keySet()) {
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
setKeyBindingData(action, newTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,4 +262,23 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
clientActions.clear();
|
||||
keyBindingOptions.removeOptionsChangeListener(this);
|
||||
}
|
||||
|
||||
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+ "'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + existingDefaultTrigger +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultTrigger;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(SharedStubKeyBindingAction.class, s,
|
||||
ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,9 @@ import docking.tool.util.DockingToolConstants;
|
|||
import ghidra.framework.options.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* An class to manage actions registered with the tool
|
||||
|
@ -51,7 +53,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
/*
|
||||
Map of Maps of Sets
|
||||
|
||||
|
||||
Owner Name ->
|
||||
Action Name -> Set of Actions
|
||||
*/
|
||||
|
@ -60,15 +62,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
private Map<String, SharedStubKeyBindingAction> sharedActionMap = new HashMap<>();
|
||||
|
||||
private ToolOptions keyBindingOptions;
|
||||
private ToolOptions options;
|
||||
private Tool tool;
|
||||
private KeyBindingsManager keyBindingsManager;
|
||||
private OptionsChangeListener optionChangeListener = (options, optionName, oldValue,
|
||||
newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue);
|
||||
private OptionsChangeListener optionChangeListener = (toolOptions, optionName, oldValue,
|
||||
newValue) -> updateKeyBindingsFromOptions(optionName, (ActionTrigger) newValue);
|
||||
|
||||
/**
|
||||
* Construct an ActionManager
|
||||
*
|
||||
*
|
||||
* @param tool tool using this ActionManager
|
||||
* @param actionToGuiHelper the class that takes actions and maps them to GUI widgets
|
||||
*/
|
||||
|
@ -76,8 +78,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
this.tool = tool;
|
||||
this.actionGuiHelper = actionToGuiHelper;
|
||||
this.keyBindingsManager = new KeyBindingsManager(tool);
|
||||
this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
this.keyBindingOptions.addOptionsChangeListener(optionChangeListener);
|
||||
this.options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
this.options.addOptionsChangeListener(optionChangeListener);
|
||||
|
||||
createSystemActions();
|
||||
SharedActionRegistry.installSharedActions(tool, this);
|
||||
|
@ -112,12 +114,12 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
// Some System actions support changing the keybinding. In the future, all System actions
|
||||
// may support this.
|
||||
if (action.getKeyBindingType().isManaged()) {
|
||||
KeyBindingData kbd = action.getKeyBindingData();
|
||||
KeyStroke ks = kbd.getKeyBinding();
|
||||
loadKeyBindingFromOptions(action, ks);
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
loadKeyBindingFromOptions(action, actionTrigger);
|
||||
}
|
||||
|
||||
keyBindingsManager.addSystemAction(action);
|
||||
addActionToMap(action);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
@ -127,12 +129,64 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
|
||||
private void addActionToMap(DockingActionIf action) {
|
||||
|
||||
Set<DockingActionIf> actions = getActionStorage(action);
|
||||
KeyBindingUtils.assertSameDefaultKeyBindings(action, actions);
|
||||
assertSameDefaultActionTrigger(action, actions);
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
private static void assertSameDefaultActionTrigger(DockingActionIf newAction,
|
||||
Collection<DockingActionIf> existingActions) {
|
||||
|
||||
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
|
||||
ActionTrigger defaultTrigger = getActionTrigger(newDefaultBinding);
|
||||
for (DockingActionIf action : existingActions) {
|
||||
if (!action.getKeyBindingType().supportsKeyBindings()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
|
||||
ActionTrigger existingTrigger = getActionTrigger(existingDefaultBinding);
|
||||
if (!Objects.equals(defaultTrigger, existingTrigger)) {
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, action, existingTrigger);
|
||||
break; // one warning seems like enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that two equivalent actions (same name and owner) share the same default action
|
||||
* trigger. It is considered a programming mistake for two equivalent actions to have different
|
||||
* triggers.
|
||||
*/
|
||||
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+ "'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + existingDefaultTrigger +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultTrigger;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(ToolActions.class, s, ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
|
||||
private static ActionTrigger getActionTrigger(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getActionTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action that works specifically with a component provider.
|
||||
* @param provider provider associated with the action
|
||||
|
@ -170,32 +224,45 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
KeyStroke ks = action.getKeyBinding();
|
||||
loadKeyBindingFromOptions(action, ks);
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
loadKeyBindingFromOptions(action, actionTrigger);
|
||||
|
||||
keyBindingsManager.addAction(provider, action);
|
||||
}
|
||||
|
||||
private void loadKeyBindingFromOptions(DockingActionIf action, KeyStroke ks) {
|
||||
String description = "Keybinding for " + action.getFullName();
|
||||
keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null,
|
||||
description);
|
||||
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
|
||||
if (!Objects.equals(ks, newKs)) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
private ActionTrigger getActionTrigger(DockingActionIf action) {
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
if (kbData != null) {
|
||||
return kbData.getActionTrigger();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadKeyBindingFromOptions(DockingActionIf action, ActionTrigger actionTrigger) {
|
||||
|
||||
String fullName = action.getFullName();
|
||||
String description = "Keybinding for " + fullName;
|
||||
options.registerOption(fullName, OptionType.ACTION_TRIGGER, actionTrigger, null,
|
||||
description);
|
||||
|
||||
KeyBindingData existingKbData = action.getKeyBindingData();
|
||||
|
||||
ActionTrigger newTrigger = options.getActionTrigger(fullName, actionTrigger);
|
||||
KeyBindingData newKbData = KeyBindingData.update(existingKbData, newTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
|
||||
private void installSharedKeyBinding(ComponentProvider provider, DockingActionIf action) {
|
||||
|
||||
String name = action.getName();
|
||||
KeyStroke defaultKeyStroke = action.getKeyBinding();
|
||||
|
||||
// get or create the stub to which we will add the action
|
||||
SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> {
|
||||
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
SharedStubKeyBindingAction newStub =
|
||||
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
|
||||
registerStub(newStub, defaultKeyStroke);
|
||||
new SharedStubKeyBindingAction(name, actionTrigger, options);
|
||||
registerStub(newStub, actionTrigger);
|
||||
return newStub;
|
||||
});
|
||||
|
||||
|
@ -209,10 +276,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void registerStub(SharedStubKeyBindingAction stub, KeyStroke defaultKeyStroke) {
|
||||
private void registerStub(SharedStubKeyBindingAction stub, ActionTrigger defaultActionTrigger) {
|
||||
stub.addPropertyChangeListener(this);
|
||||
|
||||
loadKeyBindingFromOptions(stub, defaultKeyStroke);
|
||||
loadKeyBindingFromOptions(stub, defaultActionTrigger);
|
||||
|
||||
keyBindingsManager.addAction(null, stub);
|
||||
}
|
||||
|
@ -243,10 +310,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return; // no actions registered for this owner
|
||||
}
|
||||
|
||||
// Note: this method is called when plugins are removed. 'owner' is the name of the plugin.
|
||||
// This method will also get called while passing the system owner. In that case, we do
|
||||
// not want to remove system actions in this method. We check below for system actions.
|
||||
|
||||
//@formatter:off
|
||||
toCleanup.values()
|
||||
.stream()
|
||||
.flatMap(set -> set.stream())
|
||||
.filter(action -> !keyBindingsManager.isSystemAction(action)) // (see note above)
|
||||
.forEach(action -> removeGlobalAction(action))
|
||||
;
|
||||
//@formatter:on
|
||||
|
@ -312,32 +384,33 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Iterator<DockingActionIf> getAllActionsIterator() {
|
||||
// chain all items together, rather than copy the data
|
||||
// Note: do not use Apache's IteratorUtils.chainedIterator. It degrades exponentially
|
||||
return Stream
|
||||
.concat(
|
||||
actionsByNameByOwner.values()
|
||||
.stream()
|
||||
.flatMap(actionsByName -> actionsByName.values()
|
||||
.stream())
|
||||
.flatMap(actions -> actions.stream()),
|
||||
sharedActionMap.values()
|
||||
.stream())
|
||||
.iterator();
|
||||
//@formatter:off
|
||||
return Stream.concat(
|
||||
actionsByNameByOwner.values().stream()
|
||||
.flatMap(actionsByName -> actionsByName.values().stream())
|
||||
.flatMap(actions -> actions.stream()),
|
||||
sharedActionMap.values().stream()).iterator();
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keybindings for each action so that they are still registered as being used;
|
||||
* otherwise the options will be removed because they are noted as not being used.
|
||||
/*
|
||||
* An odd method that really shoulnd't be on the interface. This is a call that allows the
|
||||
* framework to signal that the ToolOptions have been rebuilt, such as when restoring from xml.
|
||||
* During a rebuild, ToolOptions does not send out events, so this class does not get any of the
|
||||
* values from the new options. This method tells us to get the new version of the options from
|
||||
* the tool.
|
||||
*/
|
||||
public synchronized void restoreKeyBindings() {
|
||||
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
public synchronized void optionsRebuilt() {
|
||||
|
||||
// grab the new, rebuilt options
|
||||
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
Iterator<DockingActionIf> it = getKeyBindingActionsIterator();
|
||||
for (DockingActionIf action : CollectionUtils.asIterable(it)) {
|
||||
KeyStroke ks = action.getKeyBinding();
|
||||
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
|
||||
if (!Objects.equals(ks, newKs)) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
}
|
||||
KeyBindingData currentKbData = action.getKeyBindingData();
|
||||
ActionTrigger optionsTrigger = options.getActionTrigger(action.getFullName(), null);
|
||||
KeyBindingData newKbData = KeyBindingData.update(currentKbData, optionsTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,8 +450,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
keyBindingsManager.removeAction(action);
|
||||
|
||||
getActionStorage(action).remove(action);
|
||||
if (!action.getKeyBindingType()
|
||||
.isShared()) {
|
||||
if (!action.getKeyBindingType().isShared()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -391,12 +463,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Set<DockingActionIf> getActionStorage(DockingActionIf action) {
|
||||
String owner = action.getOwner();
|
||||
String name = action.getName();
|
||||
return actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
return actionsByNameByOwner.get(owner).get(name);
|
||||
}
|
||||
|
||||
private void updateKeyBindingsFromOptions(ToolOptions options, String optionName,
|
||||
KeyStroke newKs) {
|
||||
private void updateKeyBindingsFromOptions(String optionName, ActionTrigger newTrigger) {
|
||||
|
||||
// note: the 'shared actions' update themselves, so we only need to handle standard actions
|
||||
|
||||
|
@ -405,21 +475,30 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
String name = matcher.group(1);
|
||||
String owner = matcher.group(2);
|
||||
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
for (DockingActionIf action : actions) {
|
||||
KeyStroke oldKs = action.getKeyBinding();
|
||||
if (Objects.equals(oldKs, newKs)) {
|
||||
continue; // prevent bouncing
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner).get(name);
|
||||
if (actions.isEmpty()) {
|
||||
// An empty actions list implies that the action changed in the options is a shared
|
||||
// action or a system action. Shared actions will update themselves. Here we will
|
||||
// handle system actions.
|
||||
DockingActionIf systemAction = keyBindingsManager.getSystemAction(optionName);
|
||||
if (systemAction != null) {
|
||||
KeyBindingData oldKbData = systemAction.getKeyBindingData();
|
||||
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
|
||||
systemAction.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
return;
|
||||
}
|
||||
|
||||
for (DockingActionIf action : actions) {
|
||||
KeyBindingData oldKbData = action.getKeyBindingData();
|
||||
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!evt.getPropertyName()
|
||||
.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -431,15 +510,19 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check to see if we need to update the options to reflect the change to the action's key
|
||||
// binding data.
|
||||
//
|
||||
KeyBindingData newKeyBindingData = (KeyBindingData) evt.getNewValue();
|
||||
KeyStroke newKs = null;
|
||||
ActionTrigger newTrigger = null;
|
||||
if (newKeyBindingData != null) {
|
||||
newKs = newKeyBindingData.getKeyBinding();
|
||||
newTrigger = newKeyBindingData.getActionTrigger();
|
||||
}
|
||||
|
||||
KeyStroke currentKs = keyBindingOptions.getKeyStroke(action.getFullName(), null);
|
||||
if (!Objects.equals(currentKs, newKs)) {
|
||||
keyBindingOptions.setKeyStroke(action.getFullName(), newKs);
|
||||
ActionTrigger currentTrigger = options.getActionTrigger(action.getFullName(), null);
|
||||
if (!Objects.equals(currentTrigger, newTrigger)) {
|
||||
options.setActionTrigger(action.getFullName(), newTrigger);
|
||||
keyBindingsChanged();
|
||||
}
|
||||
}
|
||||
|
@ -456,8 +539,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
Iterator<DockingActionIf> it = actionGuiHelper.getComponentActions(provider);
|
||||
while (it.hasNext()) {
|
||||
DockingActionIf action = it.next();
|
||||
if (action.getName()
|
||||
.equals(actionName)) {
|
||||
if (action.getName().equals(actionName)) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
@ -476,7 +558,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
|
||||
public Action getAction(KeyStroke ks) {
|
||||
return keyBindingsManager.getDockingKeyAction(ks);
|
||||
return keyBindingsManager.getDockingAction(ks);
|
||||
}
|
||||
|
||||
public Action getAction(MouseBinding mb) {
|
||||
return keyBindingsManager.getDockingAction(mb);
|
||||
}
|
||||
|
||||
DockingActionIf getSharedStubKeyBindingAction(String name) {
|
||||
|
@ -487,23 +573,22 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
* Allows clients to register an action by using a placeholder. This is useful when
|
||||
* an API wishes to have a central object (like a plugin) register actions for transient
|
||||
* providers, that may not be loaded until needed.
|
||||
*
|
||||
*
|
||||
* <p>This method may be called multiple times with the same conceptual placeholder--the
|
||||
* placeholder will only be added once.
|
||||
*
|
||||
*
|
||||
* @param placeholder the placeholder containing information related to the action it represents
|
||||
*/
|
||||
@Override
|
||||
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) {
|
||||
|
||||
String name = placeholder.getName();
|
||||
KeyStroke defaultKeyStroke = placeholder.getKeyBinding();
|
||||
|
||||
SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> {
|
||||
|
||||
ActionTrigger actionTrigger = getActionTrigger(placeholder);
|
||||
SharedStubKeyBindingAction newStub =
|
||||
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
|
||||
registerStub(newStub, defaultKeyStroke);
|
||||
new SharedStubKeyBindingAction(name, actionTrigger, options);
|
||||
registerStub(newStub, actionTrigger);
|
||||
return newStub;
|
||||
});
|
||||
|
||||
|
@ -511,4 +596,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
stub.addActionOwner(owner);
|
||||
}
|
||||
|
||||
private ActionTrigger getActionTrigger(SharedDockingActionPlaceholder placeholder) {
|
||||
KeyStroke defaultKs = placeholder.getKeyBinding();
|
||||
if (defaultKs != null) {
|
||||
return new ActionTrigger(defaultKs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,9 @@ import java.beans.PropertyChangeListener;
|
|||
|
||||
import javax.swing.JButton;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.DockingActionPerformer;
|
||||
import docking.DockingWindowManager;
|
||||
import docking.action.*;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
/**
|
||||
* Class to manager toolbar buttons.
|
||||
|
@ -113,23 +112,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
ActionContext context = getWindowManager().createActionContext(toolBarAction);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
context.setEventClickModifiers(event.getModifiers());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
if (toolBarAction.isValidContext(context) &&
|
||||
toolBarAction.isEnabledForContext(context)) {
|
||||
if (toolBarAction instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) toolBarAction;
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
toolBarAction.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
DockingActionPerformer.perform(toolBarAction, event, getWindowManager());
|
||||
}
|
||||
|
||||
private DockingWindowManager getWindowManager() {
|
||||
|
|
|
@ -149,9 +149,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
|
||||
public static Window getWindowByTitleContaining(Window parentWindow, String text) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if (!w.isShowing()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -169,9 +167,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
|
||||
protected static Window getWindowByTitle(Window parentWindow, String title) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if (!w.isShowing()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -212,9 +208,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= timeout) {
|
||||
|
||||
Set<Window> winList = getAllWindows();
|
||||
Iterator<Window> it = winList.iterator();
|
||||
while (it.hasNext()) {
|
||||
Window w = it.next();
|
||||
for (Window w : winList) {
|
||||
if (windowClass.isAssignableFrom(w.getClass()) && w.isShowing()) {
|
||||
return w;
|
||||
}
|
||||
|
@ -499,9 +493,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= DEFAULT_WINDOW_TIMEOUT) {
|
||||
|
||||
Set<Window> winList = getAllWindows();
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if ((w instanceof JDialog) && w.isShowing()) {
|
||||
String windowTitle = getTitleForWindow(w);
|
||||
if (title.equals(windowTitle)) {
|
||||
|
@ -534,9 +526,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= DEFAULT_WAIT_TIMEOUT) {
|
||||
|
||||
Set<Window> winList = getWindows(window);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if ((w instanceof JDialog) && w.isShowing()) {
|
||||
String windowTitle = getTitleForWindow(w);
|
||||
if (title.equals(windowTitle)) {
|
||||
|
@ -637,9 +627,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
private static <T extends DialogComponentProvider> T getDialogComponent(Window parentWindow,
|
||||
Class<T> ghidraClass) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
DialogComponentProvider dialogComponentProvider =
|
||||
getDialogComponentProvider(w, ghidraClass);
|
||||
if (dialogComponentProvider != null) {
|
||||
|
@ -953,9 +941,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
// So, just ignore the exception. Client code that *really* wants all windows,
|
||||
// like that which waits for windows, should be calling this method repeatedly anyway.
|
||||
}
|
||||
Iterator<Window> iter = dockableWinList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : dockableWinList) {
|
||||
windowSet.add(w);
|
||||
findOwnedWindows(w, windowSet);
|
||||
}
|
||||
|
@ -1123,9 +1109,8 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
public static Set<DockingActionIf> getActionsByOwnerAndName(Tool tool, String owner,
|
||||
String name) {
|
||||
Set<DockingActionIf> ownerActions = tool.getDockingActionsByOwnerName(owner);
|
||||
return ownerActions.stream()
|
||||
.filter(action -> action.getName().equals(name))
|
||||
.collect(Collectors.toSet());
|
||||
return ownerActions.stream().filter(action -> action.getName().equals(name)).collect(
|
||||
Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,8 +23,7 @@ import docking.Tool;
|
|||
public interface DockingToolConstants {
|
||||
|
||||
/**
|
||||
* Name of options for key bindings that map action name to a
|
||||
* key stroke object.
|
||||
*/
|
||||
* Name of options for key bindings that map action name to a key stroke or mouse binding.
|
||||
*/
|
||||
public final static String KEY_BINDINGS = "Key Bindings";
|
||||
}
|
||||
|
|
|
@ -81,6 +81,14 @@ public class HintTextField extends JTextField {
|
|||
validateField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint for this text field
|
||||
* @param hint the hint text
|
||||
*/
|
||||
public void setHint(String hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key listener allows us to check field validity on every key typed
|
||||
*/
|
||||
|
|
|
@ -32,10 +32,12 @@ import docking.*;
|
|||
import docking.action.*;
|
||||
import docking.test.AbstractDockingTest;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SpyErrorLogger;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
||||
|
||||
|
@ -468,7 +470,15 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
|||
|
||||
private void setSharedKeyBinding(KeyStroke newKs) {
|
||||
ToolOptions options = getKeyBindingOptions();
|
||||
runSwing(() -> options.setKeyStroke(SHARED_FULL_NAME, newKs));
|
||||
runSwing(() -> {
|
||||
ActionTrigger actionTrigger = options.getActionTrigger(SHARED_FULL_NAME, null);
|
||||
MouseBinding existingMouseBinding = null;
|
||||
if (actionTrigger != null) {
|
||||
existingMouseBinding = actionTrigger.getMouseBinding();
|
||||
}
|
||||
ActionTrigger newTrigger = new ActionTrigger(newKs, existingMouseBinding);
|
||||
options.setActionTrigger(SHARED_FULL_NAME, newTrigger);
|
||||
});
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
|
@ -496,7 +506,10 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
|||
|
||||
public SharedNameAction(String owner, KeyStroke ks) {
|
||||
super(SHARED_NAME, owner, KeyBindingType.SHARED);
|
||||
setKeyBindingData(new KeyBindingData(ks));
|
||||
|
||||
if (ks != null) {
|
||||
setKeyBindingData(new KeyBindingData(ks));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -32,13 +32,10 @@
|
|||
<logger name="org.jdom" level="WARN"/>
|
||||
|
||||
<logger name="generic.help" level="DEBUG"/>
|
||||
<logger name="generic.random" level="WARN"/>
|
||||
<logger name="generic.watchdog" level="DEBUG" />
|
||||
|
||||
<logger name="docking.help" level="DEBUG"/>
|
||||
<logger name="docking.event.mouse" level="DEBUG" />
|
||||
<logger name="docking.framework" level="DEBUG" />
|
||||
<logger name="docking.widgets.table" level="DEBUG" />
|
||||
<logger name="docking.widgets.filechooser" level="DEBUG" />
|
||||
|
||||
<logger name="docking" level="DEBUG"/>
|
||||
|
||||
<logger name="ghidra.feature.fid" level="INFO" />
|
||||
<logger name="ghidra.framework" level="DEBUG"/>
|
||||
|
@ -57,8 +54,6 @@
|
|||
<!-- Ignore warnings about missing content classes in test env -->
|
||||
<logger name="ghidra.framework.project.tool.GhidraToolTemplate" level="ERROR"/>
|
||||
|
||||
<logger name="functioncalls" level="DEBUG" />
|
||||
<logger name="generic.random" level="WARN"/>
|
||||
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
|
||||
<logger name="ghidra.net" level="WARN"/>
|
||||
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
|
||||
|
@ -77,8 +72,10 @@
|
|||
<logger name="ghidra.app.script" level="INFO" />
|
||||
<logger name="ghidra.app.util.importer" level="DEBUG" />
|
||||
<logger name="ghidra.app.util.opinion" level="DEBUG" />
|
||||
<logger name="ghidra.util.classfinder" level="DEBUG" />
|
||||
<logger name="ghidra.util.classfinder" level="DEBUG" />
|
||||
<logger name="ghidra.util.extensions" level="DEBUG" />
|
||||
<logger name="ghidra.util.task" level="DEBUG" />
|
||||
<logger name="functioncalls" level="DEBUG" />
|
||||
<logger name="org.jungrapht.visualization" level="WARN" />
|
||||
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
|
||||
|
||||
|
|
|
@ -30,15 +30,11 @@
|
|||
<logger name="org.jdom" level="WARN"/>
|
||||
|
||||
<logger name="generic.help" level="DEBUG"/>
|
||||
<logger name="generic.random" level="WARN"/>
|
||||
<logger name="generic.watchdog" level="DEBUG" />
|
||||
|
||||
<logger name="docking.help" level="DEBUG"/>
|
||||
<logger name="docking.event.mouse" level="DEBUG" />
|
||||
<logger name="docking.framework" level="DEBUG" />
|
||||
<logger name="docking.framework.SplashScreen" level="TRACE" />
|
||||
<logger name="docking.widgets.table" level="DEBUG" />
|
||||
<logger name="docking.widgets.filechooser" level="DEBUG" />
|
||||
|
||||
|
||||
<logger name="docking" level="DEBUG"/>
|
||||
|
||||
<logger name="ghidra.feature.fid" level="INFO" />
|
||||
<logger name="ghidra.framework" level="DEBUG"/>
|
||||
<logger name="ghidra.graph" level="DEBUG" />
|
||||
|
@ -55,9 +51,7 @@
|
|||
|
||||
<!-- Ignore warnings about missing content classes in test env -->
|
||||
<logger name="ghidra.framework.project.tool.GhidraToolTemplate" level="ERROR"/>
|
||||
|
||||
<logger name="functioncalls" level="DEBUG" />
|
||||
<logger name="generic.random" level="WARN"/>
|
||||
|
||||
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
|
||||
<logger name="ghidra.net" level="WARN"/>
|
||||
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
|
||||
|
@ -78,6 +72,7 @@
|
|||
<logger name="ghidra.app.util.opinion" level="DEBUG" />
|
||||
<logger name="ghidra.util.classfinder" level="DEBUG" />
|
||||
<logger name="ghidra.util.task" level="DEBUG" />
|
||||
<logger name="functioncalls" level="DEBUG" />
|
||||
<logger name="org.jungrapht.visualization" level="WARN" />
|
||||
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ public abstract class AbstractOptions implements Options {
|
|||
set.add(Color.class);
|
||||
set.add(Font.class);
|
||||
set.add(KeyStroke.class);
|
||||
set.add(ActionTrigger.class);
|
||||
set.add(File.class);
|
||||
set.add(Date.class);
|
||||
return set;
|
||||
|
@ -154,6 +155,17 @@ public abstract class AbstractOptions implements Options {
|
|||
if (type == OptionType.FONT_TYPE) {
|
||||
warnShouldUseTheme("font");
|
||||
}
|
||||
if (type == OptionType.KEYSTROKE_TYPE) {
|
||||
type = OptionType.ACTION_TRIGGER;
|
||||
if (defaultValue instanceof KeyStroke) {
|
||||
defaultValue = new ActionTrigger((KeyStroke) defaultValue);
|
||||
}
|
||||
if (editorSupplier != null) {
|
||||
Msg.error(this, "Custom KeyStroke property editors are no longer supported. " +
|
||||
"Use ActionTrigger instead");
|
||||
editorSupplier = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!type.isCompatible(defaultValue)) {
|
||||
throw new IllegalStateException(
|
||||
|
@ -187,8 +199,7 @@ public abstract class AbstractOptions implements Options {
|
|||
}
|
||||
|
||||
Option option =
|
||||
createRegisteredOption(optionName, type, description, help, defaultValue,
|
||||
editor);
|
||||
createRegisteredOption(optionName, type, description, help, defaultValue, editor);
|
||||
|
||||
valueMap.put(optionName, option);
|
||||
}
|
||||
|
@ -235,7 +246,7 @@ public abstract class AbstractOptions implements Options {
|
|||
|
||||
// There are several cases where an existing option may exist when registering an option
|
||||
// 1) the option was accessed before it was registered
|
||||
// 2) the option was loaded from a store (database or toolstate)
|
||||
// 2) the option was loaded from a store (database or tool state)
|
||||
// 3) the option was registered more than once.
|
||||
//
|
||||
// The only time this is a problem is if the exiting option type is not compatible with
|
||||
|
@ -288,13 +299,21 @@ public abstract class AbstractOptions implements Options {
|
|||
valueMap.put(optionName, option);
|
||||
}
|
||||
}
|
||||
else if (type != OptionType.NO_TYPE && type != option.getOptionType()) {
|
||||
throw new IllegalStateException(
|
||||
"Expected option type: " + type + ", but was type: " + option.getOptionType());
|
||||
}
|
||||
|
||||
validateOptionType(option, type);
|
||||
return option;
|
||||
}
|
||||
|
||||
private void validateOptionType(Option option, OptionType type) {
|
||||
|
||||
if (type == option.getOptionType() || type == OptionType.NO_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Expected option type: " + type + ", but was type: " + option.getOptionType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(String optionName, Object newValue) {
|
||||
if (newValue == null) {
|
||||
|
@ -503,9 +522,30 @@ public abstract class AbstractOptions implements Options {
|
|||
|
||||
@Override
|
||||
public KeyStroke getKeyStroke(String optionName, KeyStroke defaultValue) {
|
||||
Option option = getOption(optionName, OptionType.KEYSTROKE_TYPE, defaultValue);
|
||||
|
||||
ActionTrigger defaultTrigger = null;
|
||||
if (defaultValue != null) {
|
||||
defaultTrigger = new ActionTrigger(defaultValue);
|
||||
}
|
||||
|
||||
Option option = getOption(optionName, OptionType.ACTION_TRIGGER, defaultTrigger);
|
||||
try {
|
||||
return (KeyStroke) option.getValue(defaultValue);
|
||||
ActionTrigger actionTrigger = (ActionTrigger) option.getValue(defaultTrigger);
|
||||
if (actionTrigger != null) {
|
||||
return actionTrigger.getKeyStroke();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (ClassCastException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue) {
|
||||
Option option = getOption(optionName, OptionType.ACTION_TRIGGER, defaultValue);
|
||||
try {
|
||||
return (ActionTrigger) option.getValue(defaultValue);
|
||||
}
|
||||
catch (ClassCastException e) {
|
||||
return defaultValue;
|
||||
|
@ -592,7 +632,16 @@ public abstract class AbstractOptions implements Options {
|
|||
|
||||
@Override
|
||||
public void setKeyStroke(String optionName, KeyStroke value) {
|
||||
putObject(optionName, value, OptionType.KEYSTROKE_TYPE);
|
||||
ActionTrigger actionTrigger = null;
|
||||
if (value != null) {
|
||||
actionTrigger = new ActionTrigger(value);
|
||||
}
|
||||
setActionTrigger(optionName, actionTrigger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActionTrigger(String optionName, ActionTrigger value) {
|
||||
putObject(optionName, value, OptionType.ACTION_TRIGGER);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.options;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import gui.event.MouseBinding;
|
||||
import util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a way to trigger an action in the system. A trigger is based on a key stroke, a mouse
|
||||
* binding or both.
|
||||
*/
|
||||
public class ActionTrigger {
|
||||
|
||||
private final static Pattern TO_STRING_PATTERN =
|
||||
Pattern.compile(".*Key Stroke\\[(.*)\\].*Mouse Binding\\[(.*)\\]");
|
||||
|
||||
private final static String KEY_STROKE = "KeyStroke";
|
||||
private final static String MOUSE_BINDING = "MouseBinding";
|
||||
|
||||
private KeyStroke keyStroke;
|
||||
private MouseBinding mouseBinding;
|
||||
|
||||
/**
|
||||
* Creates an action trigger with the given key stroke.
|
||||
* @param keyStroke the key stroke
|
||||
*/
|
||||
public ActionTrigger(KeyStroke keyStroke) {
|
||||
this(keyStroke, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action trigger with the given mouse binding.
|
||||
* @param mouseBinding the mouse binding
|
||||
*/
|
||||
public ActionTrigger(MouseBinding mouseBinding) {
|
||||
this(null, mouseBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience constructor for creating an action trigger with either or both values set. At
|
||||
* least one of the values must be non-null.
|
||||
*
|
||||
* @param keyStroke the key stroke; may be null
|
||||
* @param mouseBinding the mouse binding; may be null
|
||||
*/
|
||||
public ActionTrigger(KeyStroke keyStroke, MouseBinding mouseBinding) {
|
||||
if (CollectionUtils.isAllNull(keyStroke, mouseBinding)) {
|
||||
throw new NullPointerException("Both the key stroke and mouse bindng cannot be null");
|
||||
}
|
||||
this.keyStroke = keyStroke;
|
||||
this.mouseBinding = mouseBinding;
|
||||
}
|
||||
|
||||
public KeyStroke getKeyStroke() {
|
||||
return keyStroke;
|
||||
}
|
||||
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buffy = new StringBuilder("ActionTrigger: ");
|
||||
|
||||
buffy.append("Key Stroke[");
|
||||
if (keyStroke != null) {
|
||||
buffy.append(keyStroke.toString());
|
||||
}
|
||||
buffy.append("], Mouse Binding[");
|
||||
|
||||
if (mouseBinding != null) {
|
||||
buffy.append(mouseBinding.toString());
|
||||
}
|
||||
buffy.append(']');
|
||||
|
||||
return buffy.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new action trigger from the given string. The string is expected to be the result
|
||||
* of calling {@link #toString()} on an instance of this class.
|
||||
*
|
||||
* @param string the string to parse.
|
||||
* @return the new instance or null of the string is invalid.
|
||||
*/
|
||||
public static ActionTrigger getActionTrigger(String string) {
|
||||
|
||||
Matcher matcher = TO_STRING_PATTERN.matcher(string);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String ksString = matcher.group(1);
|
||||
String mbString = matcher.group(2);
|
||||
|
||||
KeyStroke ks = null;
|
||||
if (!StringUtils.isBlank(ksString)) {
|
||||
ks = KeyStroke.getKeyStroke(ksString);
|
||||
}
|
||||
|
||||
MouseBinding mb = null;
|
||||
if (!StringUtils.isBlank(mbString)) {
|
||||
mb = MouseBinding.getMouseBinding(mbString);
|
||||
}
|
||||
|
||||
return create(ks, mb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes this action trigger's data into the given save state.
|
||||
* @param saveState the save state
|
||||
*/
|
||||
public void writeState(SaveState saveState) {
|
||||
|
||||
String ksString = "";
|
||||
if (keyStroke != null) {
|
||||
ksString = keyStroke.toString();
|
||||
}
|
||||
saveState.putString(KEY_STROKE, ksString);
|
||||
|
||||
String mbString = "";
|
||||
if (mouseBinding != null) {
|
||||
mbString = mouseBinding.toString();
|
||||
}
|
||||
saveState.putString(MOUSE_BINDING, mbString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new action trigger by reading data from the given save state.
|
||||
* @param saveState the save state
|
||||
* @return the new action trigger
|
||||
*/
|
||||
public static ActionTrigger create(SaveState saveState) {
|
||||
|
||||
KeyStroke ks = null;
|
||||
String value = saveState.getString(KEY_STROKE, null);
|
||||
if (!StringUtils.isBlank(value)) {
|
||||
ks = KeyStroke.getKeyStroke(value);
|
||||
}
|
||||
|
||||
MouseBinding mb = null;
|
||||
value = saveState.getString(MOUSE_BINDING, null);
|
||||
if (value != null) {
|
||||
mb = MouseBinding.getMouseBinding(value);
|
||||
}
|
||||
|
||||
return create(ks, mb);
|
||||
}
|
||||
|
||||
private static ActionTrigger create(KeyStroke ks, MouseBinding mb) {
|
||||
if (ks == null && mb == null) {
|
||||
return null;
|
||||
}
|
||||
return new ActionTrigger(ks, mb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((keyStroke == null) ? 0 : keyStroke.hashCode());
|
||||
result = prime * result + ((mouseBinding == null) ? 0 : mouseBinding.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ActionTrigger other = (ActionTrigger) obj;
|
||||
if (!Objects.equals(keyStroke, other.keyStroke)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(mouseBinding, other.mouseBinding);
|
||||
}
|
||||
|
||||
}
|
|
@ -19,6 +19,8 @@ import java.beans.PropertyEditor;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
|
||||
import ghidra.util.HelpLocation;
|
||||
|
@ -104,6 +106,15 @@ public class FileOptions extends AbstractOptions {
|
|||
@Override
|
||||
protected Option createUnregisteredOption(String optionName, OptionType type,
|
||||
Object defaultValue) {
|
||||
|
||||
if (type == OptionType.KEYSTROKE_TYPE) {
|
||||
// convert key strokes to action triggers
|
||||
type = OptionType.ACTION_TRIGGER;
|
||||
if (defaultValue instanceof KeyStroke keyStroke) {
|
||||
defaultValue = new ActionTrigger(keyStroke);
|
||||
}
|
||||
}
|
||||
|
||||
return new FileOption(optionName, type, null, null, defaultValue, false, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@ public enum OptionType {
|
|||
FILE_TYPE(File.class, new FileStringAdapter()),
|
||||
COLOR_TYPE(Color.class, new ColorStringAdapter()),
|
||||
FONT_TYPE(Font.class, new FontStringAdapter()),
|
||||
KEYSTROKE_TYPE(KeyStroke.class, new KeyStrokeStringAdapter());
|
||||
KEYSTROKE_TYPE(KeyStroke.class, new KeyStrokeStringAdapter()),
|
||||
ACTION_TRIGGER(ActionTrigger.class, new ActionTriggerStringAdapter());
|
||||
|
||||
private Class<?> clazz;
|
||||
private StringAdapter stringAdapter;
|
||||
|
@ -241,8 +242,7 @@ public enum OptionType {
|
|||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this,
|
||||
"Can't create customOption instance for: " + customOptionClassName +
|
||||
e);
|
||||
"Can't create customOption instance for: " + customOptionClassName + e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -331,4 +331,11 @@ public enum OptionType {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
static class ActionTriggerStringAdapter extends StringAdapter {
|
||||
@Override
|
||||
Object stringToObject(String string) {
|
||||
return ActionTrigger.getActionTrigger(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ public interface Options {
|
|||
* Note, this method should not be used for
|
||||
* colors and font as doing so will result in those colors and fonts becoming disconnected
|
||||
* to the current theme. Instead use
|
||||
*
|
||||
*
|
||||
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
|
||||
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
|
||||
* @param optionName the name of the option being registered.
|
||||
|
@ -139,7 +139,7 @@ public interface Options {
|
|||
* to the current theme. Instead use
|
||||
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
|
||||
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
|
||||
*
|
||||
*
|
||||
* @param optionName the name of the option being registered.
|
||||
* @param type the OptionType for this options.
|
||||
* @param defaultValue the defaultValue for the option. In this version of the method, the default
|
||||
|
@ -166,7 +166,7 @@ public interface Options {
|
|||
* may be thrown. This API will not use the supplier when in headless mode, this avoiding the
|
||||
* creation of GUI components. For this to work correctly, clients using custom property
|
||||
* editors must defer construction of the editor until the supplier is called.
|
||||
*
|
||||
*
|
||||
* @param optionName the name of the option being registered.
|
||||
* @param type the OptionType for this options.
|
||||
* @param defaultValue the defaultValue for the option. In this version of the method, the default
|
||||
|
@ -392,16 +392,27 @@ public interface Options {
|
|||
public Font getFont(String optionName, Font defaultValue);
|
||||
|
||||
/**
|
||||
* Get the KeyStrokg for the given action name.
|
||||
* Get the KeyStroke for the given action name.
|
||||
* @param optionName the option name
|
||||
* @param defaultValue value that is stored and returned if there is no
|
||||
* option with the given name
|
||||
* @return KeyStroke option
|
||||
* @throws IllegalArgumentException is a option exists with the given
|
||||
* name but it is not a KeyStroke
|
||||
* @deprecated use {@link #getActionTrigger(String, ActionTrigger)} instead
|
||||
*/
|
||||
@Deprecated(since = "11.1", forRemoval = true)
|
||||
public KeyStroke getKeyStroke(String optionName, KeyStroke defaultValue);
|
||||
|
||||
/**
|
||||
* Get the {@link ActionTrigger} for the given full action name.
|
||||
* @param optionName the action name
|
||||
* @param defaultValue value that is stored and returned if there is no
|
||||
* option with the given name
|
||||
* @return the action trigger
|
||||
*/
|
||||
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue);
|
||||
|
||||
/**
|
||||
* Get the string value for the given option name.
|
||||
* @param optionName option name
|
||||
|
@ -507,9 +518,20 @@ public interface Options {
|
|||
* @param value KeyStroke to set
|
||||
* @throws IllegalArgumentException if a option with the given
|
||||
* name already exists, but it is not a KeyStroke
|
||||
* @deprecated use {@link #setActionTrigger(String, ActionTrigger)} instead
|
||||
*/
|
||||
@Deprecated(since = "11.1", forRemoval = true)
|
||||
public void setKeyStroke(String optionName, KeyStroke value);
|
||||
|
||||
/**
|
||||
* Sets the action trigger value for the option
|
||||
* @param optionName name of the option
|
||||
* @param value action trigger to set
|
||||
* @throws IllegalArgumentException if a option with the given
|
||||
* name already exists, but it is not an action trigger
|
||||
*/
|
||||
public void setActionTrigger(String optionName, ActionTrigger value);
|
||||
|
||||
/**
|
||||
* Set the String value for the option.
|
||||
* @param optionName name of the option
|
||||
|
@ -570,14 +592,14 @@ public interface Options {
|
|||
|
||||
/**
|
||||
* Restores <b>all</b> options contained herein to their default values.
|
||||
*
|
||||
*
|
||||
* @see #restoreDefaultValue(String)
|
||||
*/
|
||||
public void restoreDefaultValues();
|
||||
|
||||
/**
|
||||
* Restores the option denoted by the given name to its default value.
|
||||
*
|
||||
*
|
||||
* @param optionName The name of the option to restore
|
||||
* @see #restoreDefaultValues()
|
||||
*/
|
||||
|
@ -585,11 +607,11 @@ public interface Options {
|
|||
|
||||
/**
|
||||
* Returns a Options object that is a sub-options of this options.
|
||||
*
|
||||
*
|
||||
* <p>Note: the option path can have {@link Options#DELIMITER} characters which will be
|
||||
* used to create a hierarchy with each element in the path resulting in sub-option of the
|
||||
* previous path element.
|
||||
*
|
||||
*
|
||||
* @param path the path for the sub-options object
|
||||
* @return an Options object that is a sub-options of this options
|
||||
*/
|
||||
|
|
|
@ -64,8 +64,8 @@ public class SubOptions implements Options {
|
|||
Set<String> childCategories = AbstractOptions.getChildCategories(optionPaths);
|
||||
List<Options> childOptions = new ArrayList<>(childCategories.size());
|
||||
for (String categoryName : childCategories) {
|
||||
childOptions.add(new SubOptions(options, categoryName, prefix + categoryName +
|
||||
DELIMITER));
|
||||
childOptions.add(
|
||||
new SubOptions(options, categoryName, prefix + categoryName + DELIMITER));
|
||||
}
|
||||
return childOptions;
|
||||
}
|
||||
|
@ -170,6 +170,11 @@ public class SubOptions implements Options {
|
|||
return options.getKeyStroke(prefix + optionName, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionTrigger getActionTrigger(String optionName, ActionTrigger defaultValue) {
|
||||
return options.getActionTrigger(prefix + optionName, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String optionName, String defaultValue) {
|
||||
return options.getString(prefix + optionName, defaultValue);
|
||||
|
@ -235,6 +240,11 @@ public class SubOptions implements Options {
|
|||
options.setKeyStroke(prefix + optionName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActionTrigger(String optionName, ActionTrigger value) {
|
||||
options.setActionTrigger(prefix + optionName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setString(String optionName, String value) {
|
||||
options.setString(prefix + optionName, value);
|
||||
|
|
|
@ -128,6 +128,12 @@ public class ToolOptions extends AbstractOptions {
|
|||
Class<?> c = Class.forName(element.getAttributeValue(CLASS_ATTRIBUTE));
|
||||
Constructor<?> constructor = c.getDeclaredConstructor();
|
||||
WrappedOption wo = (WrappedOption) constructor.newInstance();
|
||||
wo.readState(new SaveState(element));
|
||||
|
||||
if (wo instanceof WrappedKeyStroke wrappedKs) {
|
||||
wo = wrappedKs.toWrappedActionTrigger();
|
||||
}
|
||||
|
||||
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
|
||||
valueMap.put(optionName, option);
|
||||
|
||||
|
@ -138,7 +144,6 @@ public class ToolOptions extends AbstractOptions {
|
|||
option.doSetCurrentValue(null); // use doSet so that it is not registered
|
||||
}
|
||||
else {
|
||||
wo.readState(new SaveState(element));
|
||||
option.doSetCurrentValue(wo.getObject()); // use doSet so that it is not registered
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +261,9 @@ public class ToolOptions extends AbstractOptions {
|
|||
if (value instanceof KeyStroke) {
|
||||
return new WrappedKeyStroke((KeyStroke) value);
|
||||
}
|
||||
if (value instanceof ActionTrigger) {
|
||||
return new WrappedActionTrigger((ActionTrigger) value);
|
||||
}
|
||||
if (value instanceof File) {
|
||||
return new WrappedFile((File) value);
|
||||
}
|
||||
|
@ -415,6 +423,15 @@ public class ToolOptions extends AbstractOptions {
|
|||
@Override
|
||||
protected Option createUnregisteredOption(String optionName, OptionType type,
|
||||
Object defaultValue) {
|
||||
|
||||
if (type == OptionType.KEYSTROKE_TYPE) {
|
||||
// convert key strokes to action triggers
|
||||
type = OptionType.ACTION_TRIGGER;
|
||||
if (defaultValue instanceof KeyStroke keyStroke) {
|
||||
defaultValue = new ActionTrigger(keyStroke);
|
||||
}
|
||||
}
|
||||
|
||||
return new ToolOption(optionName, type, null, null, defaultValue, false, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.options;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class WrappedActionTrigger implements WrappedOption {
|
||||
|
||||
private ActionTrigger actionTrigger;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
WrappedActionTrigger() {
|
||||
// for reflection
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a wrapper object using the given ActionTrigger.
|
||||
* @param actionTrigger the action trigger
|
||||
*/
|
||||
WrappedActionTrigger(ActionTrigger actionTrigger) {
|
||||
this.actionTrigger = actionTrigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject() {
|
||||
return actionTrigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readState(SaveState saveState) {
|
||||
actionTrigger = ActionTrigger.create(saveState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeState(SaveState saveState) {
|
||||
if (actionTrigger == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionTrigger.writeState(saveState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionType getOptionType() {
|
||||
return OptionType.ACTION_TRIGGER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Objects.toString(actionTrigger);
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ class WrappedKeyStroke implements WrappedOption {
|
|||
|
||||
/**
|
||||
* Construct a wrapper object using the given KeyStroke.
|
||||
* @param ks the keystroke
|
||||
*/
|
||||
WrappedKeyStroke(KeyStroke ks) {
|
||||
this.keyStroke = ks;
|
||||
|
@ -48,31 +49,15 @@ class WrappedKeyStroke implements WrappedOption {
|
|||
return keyStroke;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the components for a Key Stroke from the given
|
||||
* SaveState object to restore this WrappedKeyStroke.
|
||||
*/
|
||||
@Override
|
||||
public void readState(SaveState saveState) {
|
||||
if (saveState.hasValue(KEY_CODE)) {
|
||||
int keyCode = saveState.getInt(KEY_CODE, 0);
|
||||
int modifiers = saveState.getInt(MODIFIERS, 0);
|
||||
String version = System.getProperty("java.version");
|
||||
if (version.startsWith("1.4")) {
|
||||
modifiers &= 0x0f;
|
||||
modifiers |= modifiers << 6;
|
||||
}
|
||||
else if (version.startsWith("1.3")) {
|
||||
modifiers &= 0x0f;
|
||||
}
|
||||
keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the components for the wrapped Key Stroke to the given
|
||||
* SaveState object.
|
||||
*/
|
||||
@Override
|
||||
public void writeState(SaveState saveState) {
|
||||
if (keyStroke == null) {
|
||||
|
@ -91,4 +76,17 @@ class WrappedKeyStroke implements WrappedOption {
|
|||
public String toString() {
|
||||
return Objects.toString(keyStroke);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method to allow for converting the deprecated options key stroke usage to the new action
|
||||
* trigger usage
|
||||
* @return a WrappedActionTrigger
|
||||
*/
|
||||
public WrappedActionTrigger toWrappedActionTrigger() {
|
||||
ActionTrigger trigger = null;
|
||||
if (keyStroke != null) {
|
||||
trigger = new ActionTrigger(keyStroke);
|
||||
}
|
||||
return new WrappedActionTrigger(trigger);
|
||||
}
|
||||
}
|
||||
|
|
238
Ghidra/Framework/Gui/src/main/java/gui/event/MouseBinding.java
Normal file
238
Ghidra/Framework/Gui/src/main/java/gui/event/MouseBinding.java
Normal file
|
@ -0,0 +1,238 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package gui.event;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.*;
|
||||
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A simple class that represents a mouse button and any modifiers needed to bind an action to a
|
||||
* mouse input event.
|
||||
* <P>
|
||||
* The modifiers used by this class will include the button down mask for the given button. This
|
||||
* is done to match how {@link MouseEvent} uses its modifiers.
|
||||
*/
|
||||
public class MouseBinding {
|
||||
|
||||
private static final Pattern BUTTON_PATTERN =
|
||||
Pattern.compile("button(\\d+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String SHIFT = "Shift";
|
||||
private static final String CTRL = "Ctrl";
|
||||
private static final String ALT = "Alt";
|
||||
private static final String META = "Meta";
|
||||
|
||||
private int modifiers = -1;
|
||||
private int button = -1;
|
||||
|
||||
/**
|
||||
* Construct a binding with the given button number of the desired mouse button (e.g., 1, 2,...)
|
||||
* @param button the button number
|
||||
*/
|
||||
public MouseBinding(int button) {
|
||||
this(button, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a binding with the given button number of the desired mouse button (e.g., 1, 2,...)
|
||||
* as well as any desired modifiers (e.g., {@link InputEvent#SHIFT_DOWN_MASK}).
|
||||
* @param button the button number
|
||||
* @param modifiers the event modifiers
|
||||
*/
|
||||
public MouseBinding(int button, int modifiers) {
|
||||
this.button = button;
|
||||
|
||||
// The button down mask is applied to the mouse event modifiers by Java. Thus, for us to
|
||||
// match the mouse event modifiers, we need to add the button down mask here.
|
||||
this.modifiers = InputEvent.getMaskForButton(button);
|
||||
|
||||
if (modifiers > 0) {
|
||||
this.modifiers |= modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The button used by this class
|
||||
* @return the button used by this class
|
||||
*/
|
||||
public int getButton() {
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* The modifiers used by this class
|
||||
* @return the modifiers used by this class
|
||||
*/
|
||||
public int getModifiers() {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* A user-friendly display string for this class
|
||||
* @return a user-friendly display string for this class
|
||||
*/
|
||||
public String getDisplayText() {
|
||||
String modifiersText = InputEvent.getModifiersExText(modifiers);
|
||||
if (StringUtils.isBlank(modifiersText)) {
|
||||
// not sure if this can happen, since we add the button number to the modifiers
|
||||
return "Button" + button;
|
||||
}
|
||||
return modifiersText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mouse binding for the given event
|
||||
* @param e the event
|
||||
* @return the mouse binding
|
||||
*/
|
||||
public static MouseBinding getMouseBinding(MouseEvent e) {
|
||||
return new MouseBinding(e.getButton(), e.getModifiersEx());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mouse binding from the given string. The string is expected to be of the form:
|
||||
* {@code Ctrl+Button1}, which is the form of the text generated by {@link #getDisplayText()}.
|
||||
*
|
||||
* @param mouseString the mouse string
|
||||
* @return the mouse binding or null if an invalid string was given
|
||||
*/
|
||||
public static MouseBinding getMouseBinding(String mouseString) {
|
||||
|
||||
int button = getButton(mouseString);
|
||||
if (button == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// be flexible on the tokens for splitting, even though '+' seems to be the standard
|
||||
StringTokenizer tokenizer = new StringTokenizer(mouseString, "- +");
|
||||
List<String> pieces = new ArrayList<>();
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken();
|
||||
if (!pieces.contains(token)) {
|
||||
pieces.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
int modifiers = 0;
|
||||
for (Iterator<String> iterator = pieces.iterator(); iterator.hasNext();) {
|
||||
String piece = iterator.next();
|
||||
if (indexOfIgnoreCase(piece, SHIFT) != -1) {
|
||||
modifiers |= InputEvent.SHIFT_DOWN_MASK;
|
||||
iterator.remove();
|
||||
}
|
||||
else if (indexOfIgnoreCase(piece, CTRL) != -1) {
|
||||
modifiers |= InputEvent.CTRL_DOWN_MASK;
|
||||
iterator.remove();
|
||||
}
|
||||
else if (indexOfIgnoreCase(piece, ALT) != -1) {
|
||||
modifiers |= InputEvent.ALT_DOWN_MASK;
|
||||
iterator.remove();
|
||||
}
|
||||
else if (indexOfIgnoreCase(piece, META) != -1) {
|
||||
modifiers |= InputEvent.META_DOWN_MASK;
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return new MouseBinding(button, modifiers);
|
||||
}
|
||||
|
||||
private static int getButton(String mouseString) {
|
||||
|
||||
Matcher buttonMatcher = BUTTON_PATTERN.matcher(mouseString);
|
||||
if (buttonMatcher.find()) {
|
||||
String numberString = buttonMatcher.group(1);
|
||||
try {
|
||||
int intValue = Integer.parseInt(numberString);
|
||||
if (intValue > 0) {
|
||||
return intValue;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
Msg.error(MouseBinding.class, "Unable to parse button number %s in text %s"
|
||||
.formatted(numberString, mouseString));
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given mouse event is the mouse released event for the mouse button used
|
||||
* by this class. This method will ignore modifier text, since modifiers can be pressed and
|
||||
* released independent of the mouse button's release.
|
||||
*
|
||||
* @param e the event
|
||||
* @return true if the given mouse event is the mouse released event for the mouse button used
|
||||
* by this class
|
||||
*/
|
||||
public boolean isMatchingRelease(MouseEvent e) {
|
||||
|
||||
int otherButton = e.getButton();
|
||||
if (button != otherButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int id = e.getID();
|
||||
if (id == MouseEvent.MOUSE_RELEASED || id == MouseEvent.MOUSE_CLICKED) {
|
||||
// not sure if released and clicked are sent for every OS / mouse combo
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getDisplayText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(button, modifiers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MouseBinding other = (MouseBinding) obj;
|
||||
if (button != other.button) {
|
||||
return false;
|
||||
}
|
||||
if (modifiers != other.modifiers) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package gui.event;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class MouseBindingTest {
|
||||
|
||||
private static final int CTRL = InputEvent.CTRL_DOWN_MASK;
|
||||
private static final int SHIFT = InputEvent.SHIFT_DOWN_MASK;
|
||||
|
||||
@Test
|
||||
public void testConstructor_InvalidButton() {
|
||||
|
||||
try {
|
||||
new MouseBinding(0);
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
new MouseBinding(-1);
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMouseBinding_BadButton() {
|
||||
assertNull(MouseBinding.getMouseBinding("Button"));
|
||||
assertNull(MouseBinding.getMouseBinding("Button0"));
|
||||
assertNull(MouseBinding.getMouseBinding("Cats"));
|
||||
assertNull(MouseBinding.getMouseBinding("Buttons12"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMouseBindingFromText() {
|
||||
|
||||
MouseBinding mb = MouseBinding.getMouseBinding("Button1");
|
||||
int button = 1;
|
||||
assertEquals(button, mb.getButton());
|
||||
assertModifiers(mb, buttonMask(button));
|
||||
mb = MouseBinding.getMouseBinding("Button2");
|
||||
button = 2;
|
||||
assertEquals(button, mb.getButton());
|
||||
assertModifiers(mb, buttonMask(button));
|
||||
|
||||
mb = MouseBinding.getMouseBinding("Ctrl+Button1");
|
||||
button = 1;
|
||||
assertEquals(button, mb.getButton());
|
||||
assertModifiers(mb, CTRL, buttonMask(button));
|
||||
|
||||
mb = MouseBinding.getMouseBinding("Ctrl+Shift+Button2");
|
||||
button = 2;
|
||||
assertEquals(button, mb.getButton());
|
||||
assertModifiers(mb, CTRL, SHIFT, buttonMask(button));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMouseBindingFromEvent() {
|
||||
|
||||
int button = 1;
|
||||
int modifiers = buttonMask(1);
|
||||
JPanel source = new JPanel();
|
||||
MouseEvent event = new MouseEvent(source, MouseEvent.MOUSE_PRESSED,
|
||||
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
|
||||
MouseBinding mb = MouseBinding.getMouseBinding(event);
|
||||
assertEquals(button, mb.getButton());
|
||||
assertModifiers(mb, buttonMask(button));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsMatchingRelease() {
|
||||
|
||||
int button = 1;
|
||||
MouseBinding mb = new MouseBinding(button);
|
||||
|
||||
int modifiers = buttonMask(1);
|
||||
JPanel source = new JPanel();
|
||||
MouseEvent pressed = new MouseEvent(source, MouseEvent.MOUSE_PRESSED,
|
||||
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
|
||||
assertFalse(mb.isMatchingRelease(pressed));
|
||||
|
||||
MouseEvent released = new MouseEvent(source, MouseEvent.MOUSE_RELEASED,
|
||||
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
|
||||
assertTrue(mb.isMatchingRelease(released));
|
||||
|
||||
MouseEvent clicked = new MouseEvent(source, MouseEvent.MOUSE_RELEASED,
|
||||
System.currentTimeMillis(), modifiers, 0, 0, 1, false, button);
|
||||
assertTrue(mb.isMatchingRelease(clicked));
|
||||
|
||||
// test that modifiers are ignored when determining what is a matching release
|
||||
modifiers = InputEvent.SHIFT_DOWN_MASK ^ buttonMask(button);
|
||||
released = new MouseEvent(source, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
modifiers, 0, 0, 1, false, button);
|
||||
assertTrue(mb.isMatchingRelease(released));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDisplayString() {
|
||||
|
||||
int button = 1;
|
||||
MouseBinding mb = new MouseBinding(button);
|
||||
assertEquals("Button1", mb.getDisplayText());
|
||||
|
||||
mb = MouseBinding.getMouseBinding("Button1");
|
||||
assertEquals("Button1", mb.getDisplayText());
|
||||
|
||||
mb = MouseBinding.getMouseBinding("Button1 pressed");
|
||||
assertEquals("Button1", mb.getDisplayText());
|
||||
|
||||
mb = MouseBinding.getMouseBinding("Shift+Button2");
|
||||
assertEquals("Shift+Button2", mb.getDisplayText());
|
||||
}
|
||||
|
||||
private void assertModifiers(MouseBinding mb, int... expected) {
|
||||
int actual = mb.getModifiers();
|
||||
int allMods = 0;
|
||||
for (int mod : expected) {
|
||||
allMods ^= mod;
|
||||
}
|
||||
assertEquals(allMods, actual);
|
||||
}
|
||||
|
||||
private int buttonMask(int buttonNumber) {
|
||||
return InputEvent.getMaskForButton(buttonNumber);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ import java.io.IOException;
|
|||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import db.*;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.util.*;
|
||||
|
@ -362,6 +364,15 @@ class OptionsDB extends AbstractOptions {
|
|||
type = OptionType.values()[record.getByteValue(TYPE_COL)];
|
||||
}
|
||||
}
|
||||
|
||||
else if (type == OptionType.KEYSTROKE_TYPE) {
|
||||
// convert key strokes to action triggers
|
||||
type = OptionType.ACTION_TRIGGER;
|
||||
if (defaultValue instanceof KeyStroke keyStroke) {
|
||||
defaultValue = new ActionTrigger(keyStroke);
|
||||
}
|
||||
}
|
||||
|
||||
return new DBOption(optionName, type, null, null, defaultValue, false, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -1402,7 +1402,7 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
|
||||
protected void restoreOptionsFromXml(Element root) {
|
||||
optionsMgr.setConfigState(root.getChild("OPTIONS"));
|
||||
toolActions.restoreKeyBindings();
|
||||
toolActions.optionsRebuilt();
|
||||
setToolOptionsHelpLocation();
|
||||
}
|
||||
|
||||
|
@ -1418,7 +1418,6 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
|
||||
protected void restorePluginsFromXml(Element elem) throws PluginException {
|
||||
pluginMgr.restorePluginsFromXml(elem);
|
||||
|
||||
}
|
||||
|
||||
PluginEvent[] getLastEvents() {
|
||||
|
@ -1553,10 +1552,6 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
return winMgr.getActiveComponentProvider();
|
||||
}
|
||||
|
||||
public void refreshKeybindings() {
|
||||
toolActions.restoreKeyBindings();
|
||||
}
|
||||
|
||||
public void setUnconfigurable() {
|
||||
isConfigurable = false;
|
||||
}
|
||||
|
|
|
@ -28,8 +28,7 @@ import javax.swing.table.TableColumn;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import docking.DockingUtils;
|
||||
import docking.KeyEntryTextField;
|
||||
import docking.*;
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.actions.*;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
|
@ -37,13 +36,12 @@ import docking.widgets.*;
|
|||
import docking.widgets.label.GIconLabel;
|
||||
import docking.widgets.table.*;
|
||||
import generic.theme.Gui;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
import ghidra.util.layout.VerticalLayout;
|
||||
import gui.event.MouseBinding;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import resources.Icons;
|
||||
|
@ -66,7 +64,8 @@ public class KeyBindingsPanel extends JPanel {
|
|||
private JPanel infoPanel;
|
||||
private MultiLineLabel collisionLabel;
|
||||
private KeyBindingsTableModel tableModel;
|
||||
private KeyEntryTextField ksField;
|
||||
private ActionBindingListener actionBindingListener = new ActionBindingListener();
|
||||
private ActionBindingPanel actionBindingPanel;
|
||||
private GTableFilterPanel<DockingActionIf> tableFilterPanel;
|
||||
private EmptyBorderButton helpButton;
|
||||
|
||||
|
@ -207,11 +206,11 @@ public class KeyBindingsPanel extends JPanel {
|
|||
}
|
||||
|
||||
private JPanel createKeyEntryPanel() {
|
||||
ksField = new KeyEntryTextField(20, keyStroke -> keyStrokeChanged(keyStroke));
|
||||
actionBindingPanel = new ActionBindingPanel(actionBindingListener);
|
||||
|
||||
// this is the lower panel that holds the key entry text field
|
||||
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
p.add(ksField);
|
||||
p.add(actionBindingPanel);
|
||||
|
||||
JPanel keyPanel = new JPanel(new BorderLayout());
|
||||
|
||||
|
@ -221,8 +220,7 @@ public class KeyBindingsPanel extends JPanel {
|
|||
MultiLineLabel mlabel =
|
||||
new MultiLineLabel("To add or change a key binding, select an action\n" +
|
||||
"and type any key combination\n \n" +
|
||||
"To remove a key binding, select an action and\n" +
|
||||
"press <Enter> or <Backspace>");
|
||||
"To remove a key binding, select an action and\n" + "press <Enter> or <Backspace>");
|
||||
JPanel labelPanel = new JPanel();
|
||||
labelPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0));
|
||||
BoxLayout bl = new BoxLayout(labelPanel, BoxLayout.X_AXIS);
|
||||
|
@ -334,8 +332,12 @@ public class KeyBindingsPanel extends JPanel {
|
|||
Map<String, KeyStroke> localActionMap = new HashMap<>();
|
||||
List<String> optionNames = keyBindingOptions.getOptionNames();
|
||||
for (String name : optionNames) {
|
||||
KeyStroke newKeyStroke = keyBindingOptions.getKeyStroke(name, null);
|
||||
localActionMap.put(name, newKeyStroke);
|
||||
ActionTrigger actionTrigger = keyBindingOptions.getActionTrigger(name, null);
|
||||
KeyStroke optionsKs = null;
|
||||
if (actionTrigger != null) {
|
||||
optionsKs = actionTrigger.getKeyStroke();
|
||||
}
|
||||
localActionMap.put(name, optionsKs);
|
||||
}
|
||||
return localActionMap;
|
||||
}
|
||||
|
@ -383,9 +385,9 @@ public class KeyBindingsPanel extends JPanel {
|
|||
return action.getFullName();
|
||||
}
|
||||
|
||||
private void showActionsMappedToKeyStroke(String ksName) {
|
||||
private void showActionsMappedToKeyStroke(KeyStroke ks) {
|
||||
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ksName);
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ks);
|
||||
if (StringUtils.isBlank(text)) {
|
||||
text = " ";
|
||||
}
|
||||
|
@ -413,9 +415,6 @@ public class KeyBindingsPanel extends JPanel {
|
|||
|
||||
Map<String, KeyStroke> keyStrokesByActionName =
|
||||
createActionNameToKeyStrokeMap(keyBindingOptions);
|
||||
if (keyStrokesByActionName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean changes = false;
|
||||
|
||||
|
@ -445,7 +444,7 @@ public class KeyBindingsPanel extends JPanel {
|
|||
/**
|
||||
* Processes KeyStroke entry from the text field.
|
||||
*/
|
||||
private void keyStrokeChanged(KeyStroke ks) {
|
||||
private void updateKeyStroke(KeyStroke ks) {
|
||||
clearInfoPanel();
|
||||
|
||||
DockingActionIf action = getSelectedAction();
|
||||
|
@ -458,25 +457,56 @@ public class KeyBindingsPanel extends JPanel {
|
|||
String errorMessage = toolActions.validateActionKeyBinding(action, ks);
|
||||
if (errorMessage != null) {
|
||||
statusLabel.setText(errorMessage);
|
||||
ksField.clearField();
|
||||
actionBindingPanel.clearKeyStroke();
|
||||
return;
|
||||
}
|
||||
|
||||
String selectedActionName = getSelectedActionName();
|
||||
if (selectedActionName != null) {
|
||||
if (setActionKeyStroke(selectedActionName, ks)) {
|
||||
String keyStrokeText = KeyBindingUtils.parseKeyStroke(ks);
|
||||
showActionsMappedToKeyStroke(keyStrokeText);
|
||||
tableModel.fireTableDataChanged();
|
||||
changesMade(true);
|
||||
}
|
||||
String selectedActionName = action.getFullName();
|
||||
if (setActionKeyStroke(selectedActionName, ks)) {
|
||||
showActionsMappedToKeyStroke(ks);
|
||||
tableModel.fireTableDataChanged();
|
||||
changesMade(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMouseBinding(MouseBinding mb) {
|
||||
|
||||
clearInfoPanel();
|
||||
|
||||
DockingActionIf action = getSelectedAction();
|
||||
if (action == null) {
|
||||
statusLabel.setText("No action is selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
String selectedActionName = action.getFullName();
|
||||
if (setMouseBinding(selectedActionName, mb)) {
|
||||
tableModel.fireTableDataChanged();
|
||||
changesMade(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setMouseBinding(String actionName, MouseBinding mouseBinding) {
|
||||
|
||||
if (keyBindings.isMouseBindingInUse(actionName, mouseBinding)) {
|
||||
|
||||
String existingName = keyBindings.getActionForMouseBinding(mouseBinding);
|
||||
String message = """
|
||||
Mouse binding '%s' already in use by '%s'.
|
||||
The existing binding must be cleared before it can be used again.
|
||||
""".formatted(mouseBinding, existingName);
|
||||
Msg.showInfo(this, actionBindingPanel, "Mouse Binding In Use", message);
|
||||
actionBindingPanel.clearMouseBinding();
|
||||
return false;
|
||||
}
|
||||
|
||||
return keyBindings.setActionMouseBinding(actionName, mouseBinding);
|
||||
}
|
||||
|
||||
// returns true if the key stroke is a new value
|
||||
private boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
|
||||
if (!isValidKeyStroke(keyStroke)) {
|
||||
ksField.setText("");
|
||||
actionBindingPanel.clearKeyStroke();
|
||||
return keyBindings.removeKeyStroke(actionName);
|
||||
}
|
||||
|
||||
|
@ -513,20 +543,22 @@ public class KeyBindingsPanel extends JPanel {
|
|||
String fullActionName = getSelectedActionName();
|
||||
if (fullActionName == null) {
|
||||
statusLabel.setText("");
|
||||
actionBindingPanel.setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
actionBindingPanel.setEnabled(true);
|
||||
|
||||
helpButton.setEnabled(true);
|
||||
KeyStroke ks = keyBindings.getKeyStroke(fullActionName);
|
||||
String ksName = "";
|
||||
clearInfoPanel();
|
||||
|
||||
KeyStroke ks = keyBindings.getKeyStroke(fullActionName);
|
||||
if (ks != null) {
|
||||
ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
showActionsMappedToKeyStroke(ksName);
|
||||
showActionsMappedToKeyStroke(ks);
|
||||
}
|
||||
|
||||
ksField.setText(ksName);
|
||||
MouseBinding mb = keyBindings.getMouseBinding(fullActionName);
|
||||
actionBindingPanel.setKeyBindingData(ks, mb);
|
||||
|
||||
// make sure the label gets enough space
|
||||
statusLabel.setPreferredSize(
|
||||
|
@ -543,8 +575,7 @@ public class KeyBindingsPanel extends JPanel {
|
|||
}
|
||||
|
||||
private class KeyBindingsTableModel extends AbstractSortedTableModel<DockingActionIf> {
|
||||
private final String[] columnNames =
|
||||
{ "Action Name", "KeyBinding", "Plugin Name" };
|
||||
private final String[] columnNames = { "Action Name", "KeyBinding", "Plugin Name" };
|
||||
|
||||
private List<DockingActionIf> actions;
|
||||
|
||||
|
@ -561,15 +592,23 @@ public class KeyBindingsPanel extends JPanel {
|
|||
@Override
|
||||
public Object getColumnValueForRow(DockingActionIf action, int columnIndex) {
|
||||
|
||||
String fullName = action.getFullName();
|
||||
switch (columnIndex) {
|
||||
case ACTION_NAME:
|
||||
return action.getName();
|
||||
case KEY_BINDING:
|
||||
KeyStroke ks = keyBindings.getKeyStroke(action.getFullName());
|
||||
String text = "";
|
||||
KeyStroke ks = keyBindings.getKeyStroke(fullName);
|
||||
if (ks != null) {
|
||||
return KeyBindingUtils.parseKeyStroke(ks);
|
||||
text += KeyBindingUtils.parseKeyStroke(ks);
|
||||
}
|
||||
return "";
|
||||
|
||||
MouseBinding mb = keyBindings.getMouseBinding(fullName);
|
||||
if (mb != null) {
|
||||
text += " (" + mb.getDisplayText() + ")";
|
||||
}
|
||||
|
||||
return text.trim();
|
||||
case PLUGIN_NAME:
|
||||
return action.getOwnerDescription();
|
||||
}
|
||||
|
@ -606,4 +645,17 @@ public class KeyBindingsPanel extends JPanel {
|
|||
return String.class;
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionBindingListener implements DockingActionInputBindingListener {
|
||||
|
||||
@Override
|
||||
public void keyStrokeChanged(KeyStroke ks) {
|
||||
updateKeyStroke(ks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseBindingChanged(MouseBinding mb) {
|
||||
updateMouseBinding(mb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue