Merge remote-tracking branch

'origin/GP-4436-dragonmacher-mouse-bindings-unified-options--SQUASHED'
(Closes #208)
This commit is contained in:
Ryan Kurtz 2024-04-10 13:46:33 -04:00
commit cc30e48b6b
61 changed files with 3136 additions and 919 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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