mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 19:42:36 +02:00
Merge remote-tracking branch
'origin/GP-4436-dragonmacher-mouse-bindings-unified-options--SQUASHED' (Closes #208)
This commit is contained in:
commit
cc30e48b6b
61 changed files with 3136 additions and 919 deletions
|
@ -0,0 +1,123 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import docking.widgets.checkbox.GCheckBox;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A panel that displays inputs for key strokes and mouse bindings.
|
||||
*/
|
||||
public class ActionBindingPanel extends JPanel {
|
||||
|
||||
private static final String DISABLED_HINT = "Select an action";
|
||||
|
||||
private KeyEntryTextField keyEntryField;
|
||||
private JCheckBox useMouseBindingCheckBox;
|
||||
private MouseEntryTextField mouseEntryField;
|
||||
private JPanel textFieldPanel;
|
||||
|
||||
private DockingActionInputBindingListener listener;
|
||||
|
||||
public ActionBindingPanel(DockingActionInputBindingListener listener) {
|
||||
|
||||
this.listener = Objects.requireNonNull(listener);
|
||||
build();
|
||||
}
|
||||
|
||||
private void build() {
|
||||
|
||||
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
|
||||
|
||||
textFieldPanel = new JPanel(new BorderLayout());
|
||||
|
||||
keyEntryField = new KeyEntryTextField(20, ks -> listener.keyStrokeChanged(ks));
|
||||
keyEntryField.setDisabledHint(DISABLED_HINT);
|
||||
keyEntryField.setEnabled(false); // enabled on action selection
|
||||
mouseEntryField = new MouseEntryTextField(20, mb -> listener.mouseBindingChanged(mb));
|
||||
mouseEntryField.setDisabledHint(DISABLED_HINT);
|
||||
mouseEntryField.setEnabled(false); // enabled on action selection
|
||||
|
||||
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
|
||||
|
||||
String checkBoxText = "Enter Mouse Binding";
|
||||
useMouseBindingCheckBox = new GCheckBox(checkBoxText);
|
||||
useMouseBindingCheckBox
|
||||
.setToolTipText("When checked, the text field accepts mouse buttons");
|
||||
useMouseBindingCheckBox.setName(checkBoxText);
|
||||
useMouseBindingCheckBox.addItemListener(e -> updateTextField());
|
||||
|
||||
add(textFieldPanel);
|
||||
add(Box.createHorizontalStrut(5));
|
||||
add(useMouseBindingCheckBox);
|
||||
}
|
||||
|
||||
private void updateTextField() {
|
||||
|
||||
if (useMouseBindingCheckBox.isSelected()) {
|
||||
textFieldPanel.remove(keyEntryField);
|
||||
textFieldPanel.add(mouseEntryField, BorderLayout.NORTH);
|
||||
}
|
||||
else {
|
||||
textFieldPanel.remove(mouseEntryField);
|
||||
textFieldPanel.add(keyEntryField, BorderLayout.NORTH);
|
||||
}
|
||||
|
||||
validate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setKeyBindingData(KeyStroke ks, MouseBinding mb) {
|
||||
|
||||
keyEntryField.setKeyStroke(ks);
|
||||
mouseEntryField.setMouseBinding(mb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
keyEntryField.clearField();
|
||||
mouseEntryField.clearField();
|
||||
|
||||
keyEntryField.setEnabled(enabled);
|
||||
mouseEntryField.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void clearKeyStroke() {
|
||||
keyEntryField.clearField();
|
||||
}
|
||||
|
||||
public KeyStroke getKeyStroke() {
|
||||
return keyEntryField.getKeyStroke();
|
||||
}
|
||||
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseEntryField.getMouseBinding();
|
||||
}
|
||||
|
||||
public void clearMouseBinding() {
|
||||
mouseEntryField.clearField();
|
||||
}
|
||||
|
||||
public boolean isMouseBinding() {
|
||||
return useMouseBindingCheckBox.isSelected();
|
||||
}
|
||||
|
||||
}
|
|
@ -47,13 +47,14 @@ public class ActionToGuiMapper {
|
|||
popupActionManager = new PopupActionManager(winMgr, menuGroupMap);
|
||||
|
||||
DockingWindowsContextSensitiveHelpListener.install();
|
||||
MouseBindingMouseEventDispatcher.install();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a specific Help content location for a component.
|
||||
* The DocWinListener will be notified with the help location if the specified
|
||||
* component 'c' has focus and the help key is pressed.
|
||||
*
|
||||
*
|
||||
* @param c component
|
||||
* @param helpLocation the help location
|
||||
*/
|
||||
|
|
|
@ -230,6 +230,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
|
|||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dockingTool.toFront();
|
||||
if (defaultFocusComponent != null) {
|
||||
DockingWindowManager.requestFocus(defaultFocusComponent);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A simple listener interface to notify clients of changes to key strokes and mouse bindings.
|
||||
*/
|
||||
public interface DockingActionInputBindingListener {
|
||||
|
||||
/**
|
||||
* Called when the key stroke is changed.
|
||||
* @param ks the key stroke.
|
||||
*/
|
||||
public void keyStrokeChanged(KeyStroke ks);
|
||||
|
||||
/**
|
||||
* Called when the mouse binding is changed.
|
||||
* @param mb the mouse binding.
|
||||
*/
|
||||
public void mouseBindingChanged(MouseBinding mb);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.ToggleDockingActionIf;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
/**
|
||||
* A simple class to handle executing the given action. This class will generate the action context
|
||||
* as needed and validate the context before executing the action.
|
||||
*/
|
||||
public class DockingActionPerformer {
|
||||
|
||||
private DockingActionPerformer() {
|
||||
// static only
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given action later on the Swing thread.
|
||||
* @param action the action.
|
||||
* @param event the event that triggered the action.
|
||||
*/
|
||||
public static void perform(DockingActionIf action, ActionEvent event) {
|
||||
perform(action, event, DockingWindowManager.getActiveInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given action later on the Swing thread.
|
||||
* @param action the action.
|
||||
* @param event the event that triggered the action.
|
||||
* @param windowManager the window manager containing the action being processed.
|
||||
*/
|
||||
public static void perform(DockingActionIf action, ActionEvent event,
|
||||
DockingWindowManager windowManager) {
|
||||
|
||||
if (windowManager == null) {
|
||||
// not sure if this can happen
|
||||
Msg.error(DockingActionPerformer.class,
|
||||
"No window manager found; unable to execute action: " + action.getFullName());
|
||||
}
|
||||
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
ActionContext context = windowManager.createActionContext(action);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
context.setEventClickModifiers(event.getModifiers());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
windowManager.setStatusText("");
|
||||
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
|
||||
if (action instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
action.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -30,10 +30,9 @@ import docking.actions.KeyBindingUtils;
|
|||
*/
|
||||
public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||
|
||||
protected Tool tool;
|
||||
protected DockingActionIf dockingAction;
|
||||
|
||||
protected final KeyStroke keyStroke;
|
||||
protected final Tool tool;
|
||||
protected KeyStroke keyStroke;
|
||||
|
||||
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
|
||||
super(KeyBindingUtils.parseKeyStroke(keyStroke));
|
||||
|
@ -42,14 +41,9 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
|||
this.keyStroke = keyStroke;
|
||||
}
|
||||
|
||||
KeyStroke getKeyStroke() {
|
||||
return keyStroke;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
// always enable; this is a reserved binding and cannot be disabled
|
||||
return true;
|
||||
return true; // always enable; this is a internal action that cannot be disabled
|
||||
}
|
||||
|
||||
public abstract KeyBindingPrecedence getKeyBindingPrecedence();
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A class for using actions associated with mouse bindings. This class is meant to only by used by
|
||||
* internal Ghidra mouse event processing.
|
||||
*/
|
||||
public class DockingMouseBindingAction extends AbstractAction {
|
||||
|
||||
private DockingActionIf dockingAction;
|
||||
private MouseBinding mouseBinding;
|
||||
|
||||
public DockingMouseBindingAction(DockingActionIf action, MouseBinding mouseBinding) {
|
||||
this.dockingAction = Objects.requireNonNull(action);
|
||||
this.mouseBinding = Objects.requireNonNull(mouseBinding);
|
||||
}
|
||||
|
||||
public String getFullActionName() {
|
||||
return dockingAction.getFullName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true; // always enable; this is a internal action that cannot be disabled
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
DockingActionPerformer.perform(dockingAction, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFullActionName() + " (" + mouseBinding + ")";
|
||||
}
|
||||
}
|
|
@ -29,8 +29,7 @@ import javax.swing.*;
|
|||
import org.apache.commons.collections4.map.LazyMap;
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.action.ActionContextProvider;
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.*;
|
||||
import docking.actions.*;
|
||||
import docking.widgets.PasswordDialog;
|
||||
import generic.util.WindowUtilities;
|
||||
|
@ -41,6 +40,7 @@ import ghidra.util.*;
|
|||
import ghidra.util.datastruct.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import gui.event.MouseBinding;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import util.CollectionUtils;
|
||||
|
@ -707,7 +707,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/**
|
||||
* Get the local actions installed on the given provider
|
||||
*
|
||||
*
|
||||
* @param provider the provider
|
||||
* @return an iterator over the actions
|
||||
*/
|
||||
|
@ -754,7 +754,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* Returns any action that is bound to the given keystroke for the tool associated with this
|
||||
* DockingWindowManager instance.
|
||||
*
|
||||
* @param keyStroke The keystroke to check for key bindings.
|
||||
* @param keyStroke The keystroke to check for a bound action.
|
||||
* @return The action that is bound to the keystroke, or null of there is no binding for the
|
||||
* given keystroke.
|
||||
*/
|
||||
|
@ -768,6 +768,24 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any action that is bound to the given mouse binding for the tool associated with this
|
||||
* DockingWindowManager instance.
|
||||
*
|
||||
* @param mouseBinding The mouse binding to check for a bound action.
|
||||
* @return The action associated with the mouse binding , or null of there is no binding for the
|
||||
* given keystroke.
|
||||
*/
|
||||
Action getActionForMouseBinding(MouseBinding mouseBinding) {
|
||||
DockingToolActions toolActions = tool.getToolActions();
|
||||
if (toolActions instanceof ToolActions) {
|
||||
// Using a cast here; it didn't make sense to include this 'getAction' on the
|
||||
// DockingToolActions
|
||||
return ((ToolActions) toolActions).getAction(mouseBinding);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// End Package-level Methods
|
||||
//==================================================================================================
|
||||
|
@ -1189,8 +1207,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return;
|
||||
}
|
||||
|
||||
tool.getToolActions()
|
||||
.removeActions(DOCKING_WINDOWS_OWNER);
|
||||
tool.getToolActions().removeActions(DOCKING_WINDOWS_OWNER);
|
||||
|
||||
Map<String, List<ComponentPlaceholder>> permanentMap =
|
||||
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
|
||||
|
@ -1206,12 +1223,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
String subMenuName = provider.getWindowSubMenuName();
|
||||
if (provider.isTransient() && !provider.isSnapshot()) {
|
||||
transientMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
transientMap.get(subMenuName).add(placeholder);
|
||||
}
|
||||
else {
|
||||
permanentMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
permanentMap.get(subMenuName).add(placeholder);
|
||||
}
|
||||
}
|
||||
promoteSingleMenuGroups(permanentMap);
|
||||
|
@ -1225,8 +1240,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
}
|
||||
|
||||
private boolean isWindowMenuShowing() {
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager()
|
||||
.getSelectedPath();
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath();
|
||||
if (selectedPath == null || selectedPath.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1282,8 +1296,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
List<ComponentPlaceholder> list = lazyMap.get(key);
|
||||
if (list.size() == 1) {
|
||||
lazyMap.get(null /*submenu name*/)
|
||||
.add(list.get(0));
|
||||
lazyMap.get(null /*submenu name*/).add(list.get(0));
|
||||
lazyMap.remove(key);
|
||||
}
|
||||
}
|
||||
|
@ -1422,10 +1435,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
|
||||
ComponentProvider provider = entry.getKey();
|
||||
ComponentPlaceholder placeholder = entry.getValue();
|
||||
if (provider.getOwner()
|
||||
.equals(focusOwner) &&
|
||||
provider.getName()
|
||||
.equals(focusName)) {
|
||||
if (provider.getOwner().equals(focusOwner) && provider.getName().equals(focusName)) {
|
||||
focusReplacement = placeholder;
|
||||
break; // found one!
|
||||
}
|
||||
|
@ -1502,7 +1512,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/**
|
||||
* Clears the docking window manager's notion of the active provider. This is used
|
||||
* when a component that is not contained within a dockable component gets focus
|
||||
* when a component that is not contained within a dockable component gets focus
|
||||
* (e.g., JTabbedPanes for stacked components).
|
||||
*/
|
||||
private void deactivateFocusedComponent() {
|
||||
|
@ -1552,8 +1562,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getActiveWindow();
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
|
||||
if (!isMyWindow(win)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1693,8 +1702,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
toolPreferencesElement.getChildren(PreferenceState.PREFERENCE_STATE_NAME);
|
||||
for (Object name : children) {
|
||||
Element preferencesElement = (Element) name;
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME")
|
||||
.getValue(),
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME").getValue(),
|
||||
new PreferenceState(preferencesElement));
|
||||
}
|
||||
}
|
||||
|
@ -1954,7 +1962,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
/*
|
||||
Note: Which window should be the parent of the dialog when the user does not specify?
|
||||
|
||||
|
||||
Some use cases; a dialog is shown from:
|
||||
1) A toolbar action
|
||||
2) A component provider's code
|
||||
|
@ -1962,7 +1970,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
4) A background thread
|
||||
5) The help window
|
||||
6) A modal password dialog appears over the splash screen
|
||||
|
||||
|
||||
It seems like the parent should be the active window for 1-2.
|
||||
Case 3 should probably use the window of the dialog provider.
|
||||
Case 4 should probably use the main tool frame, since the user may be
|
||||
|
@ -1970,12 +1978,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
active window, we can default to the tool's frame.
|
||||
Case 5 should use the help window.
|
||||
Case 6 should use the splash screen as the parent.
|
||||
|
||||
|
||||
We have not yet solidified how we should parent. This documentation is meant to
|
||||
move us towards clarity as we find Use Cases that don't make sense. (Once we
|
||||
finalize our understanding, we should update the javadoc to list exactly where
|
||||
the given Dialog Component will be shown.)
|
||||
|
||||
|
||||
Use Case
|
||||
A -The user presses an action on a toolbar from a window on screen 1, while the
|
||||
main tool frame is on screen 2. We want the popup window to appear on screen
|
||||
|
@ -1994,12 +2002,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
E -A long-running API shows a non-modal progress dialog. This API then shows a
|
||||
results dialog which is also non-modal. We do not want to parent the new dialog
|
||||
to the original dialog, since it is a progress dialog that will go away.
|
||||
|
||||
|
||||
|
||||
|
||||
For now, the easiest mental model to use is to always prefer the active non-transient
|
||||
window so that a dialog will appear in the user's view. If we find a case where this is
|
||||
not desired, then document it here.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
DockingWindowManager dwm = getActiveInstance();
|
||||
|
@ -2183,8 +2191,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
setStatusText(text);
|
||||
if (beep) {
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2201,8 +2208,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* A convenience method to make an attention-grabbing noise to the user
|
||||
*/
|
||||
public static void beep() {
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2274,8 +2280,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
if (includeMain) {
|
||||
winList.add(root.getMainWindow());
|
||||
}
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows()
|
||||
.iterator();
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows().iterator();
|
||||
while (it.hasNext()) {
|
||||
DetachedWindowNode node = it.next();
|
||||
Window win = node.getWindow();
|
||||
|
@ -2450,8 +2455,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
defaultContextProviderMap.entrySet();
|
||||
|
||||
for (Entry<Class<? extends ActionContext>, ActionContextProvider> entry : entrySet) {
|
||||
contextMap.put(entry.getKey(), entry.getValue()
|
||||
.getActionContext(null));
|
||||
contextMap.put(entry.getKey(), entry.getValue().getActionContext(null));
|
||||
}
|
||||
return contextMap;
|
||||
}
|
||||
|
@ -2472,7 +2476,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return context;
|
||||
}
|
||||
|
||||
// Some actions work on a non-active, default component provider. See if this action
|
||||
// Some actions work on a non-active, default component provider. See if this action
|
||||
// supports that.
|
||||
if (action.supportsDefaultContext()) {
|
||||
context = getDefaultContext(action.getContextClass());
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,12 +18,8 @@ package docking;
|
|||
import javax.swing.KeyStroke;
|
||||
|
||||
/**
|
||||
* Interface used to notify listener when a keystroke was entered in the
|
||||
* KeyEntryPanel.
|
||||
*
|
||||
*
|
||||
* Interface used to notify listener when a keystroke has changed.
|
||||
*/
|
||||
public interface KeyEntryListener {
|
||||
|
||||
public void processEntry(KeyStroke keyStroke);
|
||||
}
|
||||
|
|
|
@ -17,16 +17,20 @@ package docking;
|
|||
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.widgets.textfield.HintTextField;
|
||||
|
||||
/**
|
||||
* Text field captures key strokes and notifies a listener to process the key entry.
|
||||
*/
|
||||
public class KeyEntryTextField extends JTextField {
|
||||
public class KeyEntryTextField extends HintTextField {
|
||||
|
||||
private static final String HINT = "Type a key";
|
||||
private String disabledHint = HINT;
|
||||
|
||||
private KeyEntryListener listener;
|
||||
private String ksName;
|
||||
|
@ -38,11 +42,28 @@ public class KeyEntryTextField extends JTextField {
|
|||
* @param listener listener that is notified when the a key is pressed
|
||||
*/
|
||||
public KeyEntryTextField(int columns, KeyEntryListener listener) {
|
||||
super(columns);
|
||||
super(HINT);
|
||||
setName("Key Entry Text Field");
|
||||
getAccessibleContext().setAccessibleName(getName());
|
||||
setColumns(columns);
|
||||
this.listener = listener;
|
||||
addKeyListener(new MyKeyListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
setHint(enabled ? HINT : disabledHint);
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint text that will be displayed when this field is disabled
|
||||
* @param disabledHint the hint text
|
||||
*/
|
||||
public void setDisabledHint(String disabledHint) {
|
||||
this.disabledHint = Objects.requireNonNull(disabledHint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current key stroke
|
||||
* @return the key stroke
|
||||
|
@ -56,7 +77,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
* @param ks the new key stroke
|
||||
*/
|
||||
public void setKeyStroke(KeyStroke ks) {
|
||||
processEntry(ks);
|
||||
processKeyStroke(ks, false);
|
||||
setText(KeyBindingUtils.parseKeyStroke(ks));
|
||||
}
|
||||
|
||||
|
@ -66,7 +87,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
currentKeyStroke = null;
|
||||
}
|
||||
|
||||
private void processEntry(KeyStroke ks) {
|
||||
private void processKeyStroke(KeyStroke ks, boolean notify) {
|
||||
ksName = null;
|
||||
|
||||
currentKeyStroke = ks;
|
||||
|
@ -79,7 +100,10 @@ public class KeyEntryTextField extends JTextField {
|
|||
ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
}
|
||||
}
|
||||
listener.processEntry(ks);
|
||||
|
||||
if (notify) {
|
||||
listener.processEntry(ks);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyKeyListener implements KeyListener {
|
||||
|
@ -107,7 +131,7 @@ public class KeyEntryTextField extends JTextField {
|
|||
if (!isClearKey(keyCode) && !isModifiersOnly(e)) {
|
||||
keyStroke = KeyStroke.getKeyStroke(keyCode, e.getModifiersEx());
|
||||
}
|
||||
processEntry(keyStroke);
|
||||
processKeyStroke(keyStroke, true);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@ package docking;
|
|||
import java.awt.event.ActionEvent;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.action.ToggleDockingActionIf;
|
||||
import docking.menu.MenuHandler;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
public class MenuBarMenuHandler extends MenuHandler {
|
||||
|
||||
|
@ -41,24 +39,7 @@ public class MenuBarMenuHandler extends MenuHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void processMenuAction(final DockingActionIf action, final ActionEvent event) {
|
||||
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
|
||||
ActionContext context = windowManager.createActionContext(action);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
windowManager.setStatusText("");
|
||||
if (action.isValidContext(context) && action.isEnabledForContext(context)) {
|
||||
if (action instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = ((ToggleDockingActionIf) action);
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
action.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
public void processMenuAction(DockingActionIf action, ActionEvent event) {
|
||||
DockingActionPerformer.perform(action, event, windowManager);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* Allows Ghidra to give preference to its mouse event processing over the default Java mouse event
|
||||
* processing. This class allows us to assign mouse bindings to actions.
|
||||
* <p>
|
||||
* {@link #install()} must be called in order to install this <code>Singleton</code> into Java's
|
||||
* mouse event processing system.
|
||||
*
|
||||
* @see KeyBindingOverrideKeyEventDispatcher
|
||||
*/
|
||||
public class MouseBindingMouseEventDispatcher {
|
||||
|
||||
private static MouseBindingMouseEventDispatcher instance;
|
||||
|
||||
static synchronized void install() {
|
||||
if (instance == null) {
|
||||
instance = new MouseBindingMouseEventDispatcher();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the current focus owner. This allows for dependency injection.
|
||||
*/
|
||||
private FocusOwnerProvider focusProvider = new DefaultFocusOwnerProvider();
|
||||
|
||||
/**
|
||||
* We use this action as a signal that we intend to process a mouse binding and that no other
|
||||
* Java component should try to handle it.
|
||||
* <p>
|
||||
* This action is one that is triggered by a mouse pressed, but will be processed on a
|
||||
* mouse released. We do this to ensure that we consume all related mouse events (press and
|
||||
* release) and to be consistent with the {@link KeyBindingOverrideKeyEventDispatcher}.
|
||||
*/
|
||||
private PendingActionInfo inProgressAction;
|
||||
|
||||
private MouseBindingMouseEventDispatcher() {
|
||||
// Note: see the documentation on addAWTEventListener() for limitations of using this
|
||||
// listener mechanism
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
AWTEventListener listener = new AWTEventListener() {
|
||||
@Override
|
||||
public void eventDispatched(AWTEvent event) {
|
||||
process((MouseEvent) event);
|
||||
}
|
||||
};
|
||||
toolkit.addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK);
|
||||
}
|
||||
|
||||
private void process(MouseEvent e) {
|
||||
|
||||
int id = e.getID();
|
||||
if (id == MouseEvent.MOUSE_ENTERED || id == MouseEvent.MOUSE_EXITED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// always let the application finish processing key events that it started
|
||||
if (actionInProgress(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = MouseBinding.getMouseBinding(e);
|
||||
DockingMouseBindingAction action = getDockingKeyBindingActionForEvent(mouseBinding);
|
||||
Msg.trace(this,
|
||||
new ParameterizedMessage("Mouse binding to action: {} to {}", mouseBinding, action));
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inProgressAction = new PendingActionInfo(action, mouseBinding);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to clear the flag that signals we are in the middle of processing a Ghidra action.
|
||||
*/
|
||||
private boolean actionInProgress(MouseEvent e) {
|
||||
|
||||
if (inProgressAction == null) {
|
||||
Msg.trace(this, "No mouse binding action in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: mouse buttons can be simultaneously clicked. This means that the order of pressed
|
||||
// and released events may arrive intermixed. To handle this correctly, we allow the
|
||||
// MouseBinding to check for the matching release event.
|
||||
MouseBinding mouseBinding = inProgressAction.mouseBinding();
|
||||
boolean isMatching = mouseBinding.isMatchingRelease(e);
|
||||
Msg.trace(this,
|
||||
new ParameterizedMessage(
|
||||
"Is release event for in-progress mouse binding action? {} for {}", isMatching,
|
||||
inProgressAction.action()));
|
||||
if (isMatching) {
|
||||
DockingMouseBindingAction action = inProgressAction.action();
|
||||
inProgressAction = null;
|
||||
|
||||
String command = null;
|
||||
Object source = e.getSource();
|
||||
long when = e.getWhen();
|
||||
int modifiers = e.getModifiersEx();
|
||||
|
||||
action.actionPerformed(
|
||||
new ActionEvent(source, ActionEvent.ACTION_PERFORMED, command, when, modifiers));
|
||||
}
|
||||
|
||||
e.consume();
|
||||
return true;
|
||||
}
|
||||
|
||||
private DockingMouseBindingAction getDockingKeyBindingActionForEvent(
|
||||
MouseBinding mouseBinding) {
|
||||
DockingWindowManager activeManager = getActiveDockingWindowManager();
|
||||
if (activeManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingMouseBindingAction bindingAction =
|
||||
(DockingMouseBindingAction) activeManager.getActionForMouseBinding(mouseBinding);
|
||||
return bindingAction;
|
||||
}
|
||||
|
||||
private DockingWindowManager getActiveDockingWindowManager() {
|
||||
// we need an active window to process events
|
||||
Window activeWindow = focusProvider.getActiveWindow();
|
||||
if (activeWindow == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingWindowManager activeManager = DockingWindowManager.getActiveInstance();
|
||||
if (activeManager == null) {
|
||||
// this can happen if clients use DockingWindows Look and Feel settings or
|
||||
// DockingWindows widgets without using the DockingWindows system (like in tests or
|
||||
// in stand-alone, non-Ghidra apps).
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingWindowManager managingInstance = getDockingWindowManagerForWindow(activeWindow);
|
||||
if (managingInstance != null) {
|
||||
return managingInstance;
|
||||
}
|
||||
|
||||
// this is a case where the current window is unaffiliated with a DockingWindowManager,
|
||||
// like a JavaHelp window
|
||||
return activeManager;
|
||||
}
|
||||
|
||||
private static DockingWindowManager getDockingWindowManagerForWindow(Window activeWindow) {
|
||||
DockingWindowManager manager = DockingWindowManager.getInstance(activeWindow);
|
||||
if (manager != null) {
|
||||
return manager;
|
||||
}
|
||||
if (activeWindow instanceof DockingDialog) {
|
||||
DockingDialog dockingDialog = (DockingDialog) activeWindow;
|
||||
return dockingDialog.getOwningWindowManager();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record PendingActionInfo(DockingMouseBindingAction action, MouseBinding mouseBinding) {
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.event.*;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import docking.widgets.textfield.HintTextField;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class MouseEntryTextField extends HintTextField {
|
||||
|
||||
private static final String HINT = "Press a mouse button";
|
||||
private String disabledHint = HINT;
|
||||
|
||||
private MouseBinding mouseBinding;
|
||||
private Consumer<MouseBinding> listener;
|
||||
|
||||
public MouseEntryTextField(int columns, Consumer<MouseBinding> listener) {
|
||||
super(HINT);
|
||||
setColumns(columns);
|
||||
setName("Mouse Entry Text Field");
|
||||
getAccessibleContext().setAccessibleName(getName());
|
||||
this.listener = Objects.requireNonNull(listener);
|
||||
|
||||
addMouseListener(new MyMouseListener());
|
||||
addKeyListener(new MyKeyListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
setHint(enabled ? HINT : disabledHint);
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint text that will be displayed when this field is disabled
|
||||
* @param disabledHint the hint text
|
||||
*/
|
||||
public void setDisabledHint(String disabledHint) {
|
||||
this.disabledHint = Objects.requireNonNull(disabledHint);
|
||||
}
|
||||
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseBinding;
|
||||
}
|
||||
|
||||
public void setMouseBinding(MouseBinding mb) {
|
||||
processMouseBinding(mb, false);
|
||||
}
|
||||
|
||||
public void clearField() {
|
||||
processMouseBinding(null, false);
|
||||
}
|
||||
|
||||
private void processMouseBinding(MouseBinding mb, boolean notify) {
|
||||
|
||||
this.mouseBinding = mb;
|
||||
if (mouseBinding == null) {
|
||||
setText("");
|
||||
}
|
||||
else {
|
||||
setText(mouseBinding.getDisplayText());
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
listener.accept(mb);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyMouseListener extends MouseAdapter {
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (!MouseEntryTextField.this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int modifiersEx = e.getModifiersEx();
|
||||
int button = e.getButton();
|
||||
processMouseBinding(new MouseBinding(button, modifiersEx), true);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private class MyKeyListener implements KeyListener {
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
e.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
int keyCode = e.getKeyCode();
|
||||
if (isClearKey(keyCode)) {
|
||||
processMouseBinding(null, true);
|
||||
}
|
||||
e.consume();
|
||||
}
|
||||
|
||||
private boolean isClearKey(int keyCode) {
|
||||
return keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ import ghidra.util.*;
|
|||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||
import ghidra.util.datastruct.WeakSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
import resources.ResourceManager;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
|
@ -323,6 +324,10 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
return menuItem;
|
||||
}
|
||||
|
||||
private MouseBinding getMouseBinding() {
|
||||
return keyBindingData == null ? null : keyBindingData.getMouseBinding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyBindingType getKeyBindingType() {
|
||||
return keyBindingType;
|
||||
|
@ -383,6 +388,10 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
@Override
|
||||
public void setUnvalidatedKeyBindingData(KeyBindingData newKeyBindingData) {
|
||||
if (Objects.equals(keyBindingData, newKeyBindingData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData oldData = keyBindingData;
|
||||
keyBindingData = newKeyBindingData;
|
||||
firePropertyChanged(KEYBINDING_DATA_PROPERTY, oldData, keyBindingData);
|
||||
|
@ -492,8 +501,8 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
// menu path
|
||||
if (menuBarData != null) {
|
||||
buffer.append(" MENU PATH: ")
|
||||
.append(menuBarData.getMenuPathAsString());
|
||||
buffer.append(" MENU PATH: ").append(
|
||||
menuBarData.getMenuPathAsString());
|
||||
buffer.append('\n');
|
||||
buffer.append(" MENU GROUP: ").append(menuBarData.getMenuGroup());
|
||||
buffer.append('\n');
|
||||
|
@ -519,8 +528,8 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
// popup menu path
|
||||
if (popupMenuData != null) {
|
||||
buffer.append(" POPUP PATH: ")
|
||||
.append(popupMenuData.getMenuPathAsString());
|
||||
buffer.append(" POPUP PATH: ").append(
|
||||
popupMenuData.getMenuPathAsString());
|
||||
buffer.append('\n');
|
||||
buffer.append(" POPUP GROUP: ").append(popupMenuData.getMenuGroup());
|
||||
buffer.append('\n');
|
||||
|
@ -569,10 +578,15 @@ public abstract class DockingAction implements DockingActionIf {
|
|||
|
||||
KeyStroke keyStroke = getKeyBinding();
|
||||
if (keyStroke != null) {
|
||||
buffer.append(" KEYBINDING: ").append(keyStroke.toString());
|
||||
buffer.append(" KEYBINDING: ").append(keyStroke);
|
||||
buffer.append('\n');
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = getMouseBinding();
|
||||
if (mouseBinding != null) {
|
||||
buffer.append(" MOUSE BINDING: ").append(mouseBinding);
|
||||
}
|
||||
|
||||
String inception = getInceptionInformation();
|
||||
if (inception != null) {
|
||||
buffer.append("\n \n");
|
||||
|
|
|
@ -15,21 +15,27 @@
|
|||
*/
|
||||
package docking.action;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.KeyBindingPrecedence;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* An object that contains a key stroke and the precedence for when that key stroke should be used.
|
||||
*
|
||||
* <p>Note: this class creates key strokes that work on key {@code pressed}. This effectively
|
||||
* A class for storing an action's key stroke, mouse binding or both.
|
||||
* <p>
|
||||
* Note: this class creates key strokes that work on key {@code pressed}. This effectively
|
||||
* normalizes all client key bindings to work on the same type of key stroke (pressed, typed or
|
||||
* released).
|
||||
*/
|
||||
public class KeyBindingData {
|
||||
private KeyStroke keyStroke;
|
||||
private KeyBindingPrecedence keyBindingPrecedence;
|
||||
private KeyBindingPrecedence keyBindingPrecedence = KeyBindingPrecedence.DefaultLevel;
|
||||
|
||||
private MouseBinding mouseBinding;
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke) {
|
||||
this(keyStroke, KeyBindingPrecedence.DefaultLevel);
|
||||
|
@ -43,17 +49,44 @@ public class KeyBindingData {
|
|||
this(KeyStroke.getKeyStroke(keyCode, modifiers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance of this class that uses a mouse binding instead of a key stroke.
|
||||
* @param mouseBinding the mouse binding.
|
||||
*/
|
||||
public KeyBindingData(MouseBinding mouseBinding) {
|
||||
this.mouseBinding = Objects.requireNonNull(mouseBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key stroke from the given text. See
|
||||
* {@link KeyBindingUtils#parseKeyStroke(KeyStroke)}. The key stroke created for this class
|
||||
* will always be a key {@code pressed} key stroke.
|
||||
*
|
||||
*
|
||||
* @param keyStrokeString the key stroke string to parse
|
||||
*/
|
||||
public KeyBindingData(String keyStrokeString) {
|
||||
this(parseKeyStrokeString(keyStrokeString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key binding data with the given action trigger.
|
||||
* @param actionTrigger the trigger; may not be null
|
||||
*/
|
||||
public KeyBindingData(ActionTrigger actionTrigger) {
|
||||
Objects.requireNonNull(actionTrigger);
|
||||
this.keyStroke = actionTrigger.getKeyStroke();
|
||||
this.mouseBinding = actionTrigger.getMouseBinding();
|
||||
}
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't set precedence to System KeyBindingPrecedence");
|
||||
}
|
||||
this.keyStroke = Objects.requireNonNull(keyStroke);
|
||||
this.keyBindingPrecedence = Objects.requireNonNull(precedence);
|
||||
}
|
||||
|
||||
private static KeyStroke parseKeyStrokeString(String keyStrokeString) {
|
||||
KeyStroke keyStroke = KeyBindingUtils.parseKeyStroke(keyStrokeString);
|
||||
if (keyStroke == null) {
|
||||
|
@ -62,13 +95,33 @@ public class KeyBindingData {
|
|||
return keyStroke;
|
||||
}
|
||||
|
||||
public KeyBindingData(KeyStroke keyStroke, KeyBindingPrecedence precedence) {
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't set precedence to System KeyBindingPrecedence");
|
||||
/**
|
||||
* Returns a key binding data object that matches the given trigger. If the existing key
|
||||
* binding object already matches the new trigger, then the existing key binding data is
|
||||
* returned. If the new trigger is null, the null will be returned.
|
||||
*
|
||||
* @param kbData the existing key binding data; my be null
|
||||
* @param newTrigger the new action trigger; may be null
|
||||
* @return a key binding data based on the new action trigger; may be null
|
||||
*/
|
||||
public static KeyBindingData update(KeyBindingData kbData, ActionTrigger newTrigger) {
|
||||
if (kbData == null) {
|
||||
if (newTrigger == null) {
|
||||
return null; // no change
|
||||
}
|
||||
return new KeyBindingData(newTrigger); // trigger added
|
||||
}
|
||||
this.keyStroke = keyStroke;
|
||||
this.keyBindingPrecedence = precedence;
|
||||
|
||||
if (newTrigger == null) {
|
||||
return null; // trigger has been cleared
|
||||
}
|
||||
|
||||
ActionTrigger existingTrigger = kbData.getActionTrigger();
|
||||
if (existingTrigger.equals(newTrigger)) {
|
||||
return kbData;
|
||||
}
|
||||
|
||||
return new KeyBindingData(newTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,10 +140,56 @@ public class KeyBindingData {
|
|||
return keyBindingPrecedence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mouse binding assigned to this key binding data.
|
||||
* @return the mouse binding; may be null
|
||||
*/
|
||||
public MouseBinding getMouseBinding() {
|
||||
return mouseBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new action trigger with the values of this class
|
||||
* @return the action trigger
|
||||
*/
|
||||
public ActionTrigger getActionTrigger() {
|
||||
return new ActionTrigger(keyStroke, mouseBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[KeyStroke=" + keyStroke + ", precedence=" +
|
||||
keyBindingPrecedence + "]";
|
||||
keyBindingPrecedence + ", MouseBinding=" + mouseBinding + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(keyBindingPrecedence, keyStroke, mouseBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyBindingData other = (KeyBindingData) obj;
|
||||
if (keyBindingPrecedence != other.keyBindingPrecedence) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(keyStroke, other.keyStroke)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(mouseBinding, other.mouseBinding)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static KeyBindingData createSystemKeyBindingData(KeyStroke keyStroke) {
|
||||
|
@ -118,8 +217,15 @@ public class KeyBindingData {
|
|||
|
||||
KeyBindingPrecedence precedence = newKeyBindingData.getKeyBindingPrecedence();
|
||||
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
|
||||
return createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
|
||||
KeyBindingData kbd =
|
||||
createSystemKeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding));
|
||||
kbd.mouseBinding = newKeyBindingData.mouseBinding;
|
||||
return kbd;
|
||||
}
|
||||
return new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
|
||||
|
||||
KeyBindingData kbd =
|
||||
new KeyBindingData(KeyBindingUtils.validateKeyStroke(keyBinding), precedence);
|
||||
kbd.mouseBinding = newKeyBindingData.mouseBinding;
|
||||
return kbd;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,12 @@ import docking.*;
|
|||
import docking.actions.KeyBindingUtils;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
/**
|
||||
* A class that organizes system key bindings by mapping them to assigned {@link DockingActionIf}s.
|
||||
*
|
||||
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
|
||||
*
|
||||
* <p>This class understands reserved system key bindings. For non-reserved key bindings, this
|
||||
* class knows how to map a single key binding to multiple actions.
|
||||
*/
|
||||
public class KeyBindingsManager implements PropertyChangeListener {
|
||||
|
@ -39,8 +40,9 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
// this map exists to update the MultiKeyBindingAction when the key binding changes
|
||||
private Map<DockingActionIf, ComponentProvider> actionToProviderMap = new HashMap<>();
|
||||
private Map<KeyStroke, DockingKeyBindingAction> dockingKeyMap = new HashMap<>();
|
||||
private Map<DockingActionIf, SystemKeyBindingAction> dockingActionToSystemActionMap =
|
||||
new HashMap<>();
|
||||
private Map<MouseBinding, DockingMouseBindingAction> dockingMouseMap = new HashMap<>();
|
||||
private Map<String, DockingActionIf> systemActionsByFullName = new HashMap<>();
|
||||
|
||||
private Tool tool;
|
||||
|
||||
public KeyBindingsManager(Tool tool) {
|
||||
|
@ -53,11 +55,20 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
actionToProviderMap.put(action, optionalProvider);
|
||||
}
|
||||
|
||||
KeyStroke keyBinding = action.getKeyBinding();
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
if (kbData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyStroke keyBinding = kbData.getKeyBinding();
|
||||
if (keyBinding != null) {
|
||||
addKeyBinding(optionalProvider, action, keyBinding);
|
||||
}
|
||||
|
||||
MouseBinding mouseBinding = kbData.getMouseBinding();
|
||||
if (mouseBinding != null) {
|
||||
doAddMouseBinding(action, mouseBinding);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSystemAction(DockingActionIf action) {
|
||||
|
@ -90,7 +101,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
// map standard keystroke to action
|
||||
// map standard keystroke to action
|
||||
doAddKeyBinding(provider, action, keyStroke);
|
||||
|
||||
// map workaround keystroke to action
|
||||
|
@ -103,7 +114,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return null; // clearing the key stroke
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// 1) Handle case with given key stroke already in use by a system action
|
||||
//
|
||||
Action existingAction = dockingKeyMap.get(ks);
|
||||
|
@ -118,12 +129,16 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return ksString + " in use by System action '" + systemDockingAction.getName() + "'";
|
||||
}
|
||||
|
||||
//
|
||||
// 2) Handle the case where a system action key stroke is being set to something that is
|
||||
if (dockingAction == null) {
|
||||
return null; // the client is only checking the keystroke and not any associated action
|
||||
}
|
||||
|
||||
//
|
||||
// 2) Handle the case where a system action key stroke is being set to something that is
|
||||
// already in-use by some other action
|
||||
//
|
||||
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(dockingAction);
|
||||
if (systemAction != null && existingAction != null) {
|
||||
//
|
||||
boolean hasSystemAction = systemActionsByFullName.containsKey(dockingAction.getFullName());
|
||||
if (hasSystemAction && existingAction != null) {
|
||||
return "System action cannot be set to in-use key stroke";
|
||||
}
|
||||
|
||||
|
@ -133,12 +148,12 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
private void fixupAltGraphKeyStrokeMapping(ComponentProvider provider, DockingActionIf action,
|
||||
KeyStroke keyStroke) {
|
||||
|
||||
// special case
|
||||
// special case
|
||||
int modifiers = keyStroke.getModifiers();
|
||||
if ((modifiers & InputEvent.ALT_DOWN_MASK) == InputEvent.ALT_DOWN_MASK) {
|
||||
//
|
||||
// Also register the 'Alt' binding with the 'Alt Graph' mask. This fixes the but
|
||||
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
|
||||
// on Windows (https://bugs.openjdk.java.net/browse/JDK-8194873)
|
||||
// that have different key codes for the left and right Alt keys.
|
||||
//
|
||||
modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK;
|
||||
|
@ -170,8 +185,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
SystemKeyBindingAction systemAction = dockingActionToSystemActionMap.get(action);
|
||||
if (systemAction != null) {
|
||||
if (systemActionsByFullName.containsKey(action.getFullName())) {
|
||||
// the user has updated the binding for a System action; re-install it
|
||||
registerSystemKeyBinding(action, mappingKeyStroke);
|
||||
return;
|
||||
|
@ -182,6 +196,23 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
new MultipleKeyAction(tool, provider, action, actionKeyStroke));
|
||||
}
|
||||
|
||||
private void doAddMouseBinding(DockingActionIf action, MouseBinding mouseBinding) {
|
||||
|
||||
DockingMouseBindingAction mouseBindingAction = dockingMouseMap.get(mouseBinding);
|
||||
if (mouseBindingAction != null) {
|
||||
String existingName = mouseBindingAction.getFullActionName();
|
||||
String message = """
|
||||
Attempted to use the same mouse binding for multiple actions. \
|
||||
Multiple mouse bindings are not supported. Binding: %s \
|
||||
New action: %s; existing action: %s
|
||||
""".formatted(mouseBinding, action.getFullName(), existingName);
|
||||
Msg.error(this, message);
|
||||
return;
|
||||
}
|
||||
|
||||
dockingMouseMap.put(mouseBinding, new DockingMouseBindingAction(action, mouseBinding));
|
||||
}
|
||||
|
||||
private void addSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
|
||||
KeyBindingData binding = KeyBindingData.createSystemKeyBindingData(keyStroke);
|
||||
action.setKeyBindingData(binding);
|
||||
|
@ -191,7 +222,7 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
private void registerSystemKeyBinding(DockingActionIf action, KeyStroke keyStroke) {
|
||||
SystemKeyBindingAction systemAction = new SystemKeyBindingAction(tool, action, keyStroke);
|
||||
dockingKeyMap.put(keyStroke, systemAction);
|
||||
dockingActionToSystemActionMap.put(action, systemAction);
|
||||
systemActionsByFullName.put(action.getFullName(), action);
|
||||
}
|
||||
|
||||
private void removeKeyBinding(KeyStroke keyStroke, DockingActionIf action) {
|
||||
|
@ -242,17 +273,30 @@ public class KeyBindingsManager implements PropertyChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
public Action getDockingKeyAction(KeyStroke keyStroke) {
|
||||
public Action getDockingAction(KeyStroke keyStroke) {
|
||||
return dockingKeyMap.get(keyStroke);
|
||||
}
|
||||
|
||||
public Action getDockingAction(MouseBinding mouseBinding) {
|
||||
return dockingMouseMap.get(mouseBinding);
|
||||
}
|
||||
|
||||
public boolean isSystemAction(DockingActionIf action) {
|
||||
return systemActionsByFullName.containsKey(action.getFullName());
|
||||
}
|
||||
|
||||
public DockingActionIf getSystemAction(String fullName) {
|
||||
return systemActionsByFullName.get(fullName);
|
||||
}
|
||||
|
||||
public Set<DockingActionIf> getSystemActions() {
|
||||
return new HashSet<>(dockingActionToSystemActionMap.keySet());
|
||||
return new HashSet<>(systemActionsByFullName.values());
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
dockingKeyMap.clear();
|
||||
dockingMouseMap.clear();
|
||||
actionToProviderMap.clear();
|
||||
dockingActionToSystemActionMap.clear();
|
||||
systemActionsByFullName.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,13 +46,12 @@ import ghidra.util.filechooser.GhidraFileFilter;
|
|||
import ghidra.util.xml.GenericXMLOutputter;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A class to provide utilities for system key bindings, such as importing and
|
||||
* exporting key binding configurations.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @since Tracker Id 329
|
||||
*/
|
||||
public class KeyBindingUtils {
|
||||
|
@ -100,7 +99,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* If there is a problem reading the data then the user will be shown an
|
||||
* error dialog.
|
||||
*
|
||||
*
|
||||
* @param inputStream the input stream from which to read options
|
||||
* @return An options object that is composed of key binding names and their
|
||||
* associated keystrokes.
|
||||
|
@ -141,7 +140,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* If there is a problem writing the data then the user will be shown an
|
||||
* error dialog.
|
||||
*
|
||||
*
|
||||
* @param keyBindingOptions The options that contains key binding data.
|
||||
*/
|
||||
public static void exportKeyBindings(ToolOptions keyBindingOptions) {
|
||||
|
@ -177,14 +176,14 @@ public class KeyBindingUtils {
|
|||
* Changes the given key event to the new source component and then dispatches that event.
|
||||
* This method is intended for clients that wish to effectively take a key event given to
|
||||
* one component and give it to another component.
|
||||
*
|
||||
*
|
||||
* <p>This method exists to deal with the complicated nature of key event processing and
|
||||
* how our (not Java's) framework processes key event bindings to trigger actions. If not
|
||||
* for our special processing of action key bindings, then this method would not be
|
||||
* necessary.
|
||||
*
|
||||
*
|
||||
* <p><b>This is seldom-used code; if you don't know when to use this code, then don't.</b>
|
||||
*
|
||||
*
|
||||
* @param newSource the new target of the event
|
||||
* @param e the existing event
|
||||
*/
|
||||
|
@ -199,7 +198,7 @@ public class KeyBindingUtils {
|
|||
|
||||
/*
|
||||
Unusual Code Alert!
|
||||
|
||||
|
||||
The KeyboardFocusManager is a complicated beast. Here we use knowledge of one such
|
||||
complication to correctly route key events. If the client of this method passes
|
||||
a component whose 'isShowing()' returns false, then the manager will not send the
|
||||
|
@ -208,13 +207,13 @@ public class KeyBindingUtils {
|
|||
attached; for example, when we are using said components with a renderer to perform
|
||||
our own painting. In the case of non-attached components, we must call the
|
||||
redispatchEvent() method ourselves.
|
||||
|
||||
|
||||
Why don't we just always call redispatchEvent()? Well, that
|
||||
method will not pass the new cloned event we just created back through the full
|
||||
key event pipeline. This means that tool-level (our Tool API, not Java)
|
||||
actions will not work, as tool-level actions are handled at the beginning of the
|
||||
key event pipeline, not by the components themselves.
|
||||
|
||||
|
||||
Also, we have here guilty knowledge that the aforementioned tool-level key processing
|
||||
will check to see if the event was consumed. If consumed, then no further processing
|
||||
will happen; if not consumed, then the framework will continue to process the event
|
||||
|
@ -245,7 +244,7 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
*/
|
||||
|
@ -263,12 +262,12 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* A typical use-case is to register an existing docking action with a text
|
||||
* component, which is needed because the docking key event processing will
|
||||
* not execute docking- registered actions if a text component has focus.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
* @param contextProvider the provider of the context
|
||||
|
@ -289,12 +288,12 @@ public class KeyBindingUtils {
|
|||
* <p>
|
||||
* The given action must have a keystroke assigned, or this method will do
|
||||
* nothing.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* A typical use-case is to register an existing docking action with a text
|
||||
* component, which is needed because the docking key event processing will
|
||||
* not execute docking- registered actions if a text component has focus.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the given action will be bound
|
||||
* @param action the action to bind
|
||||
* @param contextProvider the provider of the context
|
||||
|
@ -311,7 +310,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Registers the given action with the given key binding on the given
|
||||
* component.
|
||||
*
|
||||
*
|
||||
* @param component the component to which the action will be registered
|
||||
* @param keyStroke the keystroke for to which the action will be bound
|
||||
* @param action the action to execute when the given keystroke is triggered
|
||||
|
@ -353,7 +352,7 @@ public class KeyBindingUtils {
|
|||
* action with the same key binding from firing. This is useful when your
|
||||
* application is using tool-level key bindings that share the same
|
||||
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param action the action from which to get the key binding
|
||||
*/
|
||||
|
@ -373,7 +372,7 @@ public class KeyBindingUtils {
|
|||
* Note: this method clears the key binding for the
|
||||
* {@link JComponent#WHEN_FOCUSED} and
|
||||
* {@link JComponent#WHEN_ANCESTOR_OF_FOCUSED_COMPONENT} focus conditions.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param keyStroke the keystroke of the binding to be cleared
|
||||
* @see #clearKeyBinding(JComponent, KeyStroke, int)
|
||||
|
@ -387,7 +386,7 @@ public class KeyBindingUtils {
|
|||
* Allows clients to clear Java key bindings. This is useful when your
|
||||
* application is using tool-level key bindings that share the same
|
||||
* keystroke as a built-in Java action, such as Ctrl-C for the copy action.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param keyStroke the keystroke of the binding to be cleared
|
||||
* @param focusCondition the particular focus condition under which the
|
||||
|
@ -405,7 +404,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Clears the currently assigned Java key binding for the action by the given name. This
|
||||
* method will find the currently assigned key binding, if any, and then remove it.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to clear the key binding
|
||||
* @param actionName the name of the action that should not have a key binding
|
||||
* @see LookAndFeel
|
||||
|
@ -420,9 +419,8 @@ public class KeyBindingUtils {
|
|||
KeyStroke keyStroke = null;
|
||||
KeyStroke[] keys = inputMap.allKeys();
|
||||
if (keys == null) {
|
||||
Msg.debug(KeyBindingUtils.class,
|
||||
"Cannot remove action by name; does not exist: '" + actionName + "' " +
|
||||
"on component: " + component.getClass().getSimpleName());
|
||||
Msg.debug(KeyBindingUtils.class, "Cannot remove action by name; does not exist: '" +
|
||||
actionName + "' " + "on component: " + component.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -442,7 +440,7 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Returns the registered action for the given keystroke, or null of no
|
||||
* action is bound to that keystroke.
|
||||
*
|
||||
*
|
||||
* @param component the component for which to check the binding
|
||||
* @param keyStroke the keystroke for which to find a bound action
|
||||
* @param focusCondition the focus condition under which to check for the
|
||||
|
@ -464,12 +462,12 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* A utility method to get all key binding actions. This method will
|
||||
* only return actions that support {@link KeyBindingType key bindings}.
|
||||
*
|
||||
*
|
||||
* <p>The mapping returned provides a list of items because it is possible for there to
|
||||
* exists multiple actions with the same name and owner. (This can happen when multiple copies
|
||||
* of a component provider are shown, each with their own set of actions that share the
|
||||
* same name.)
|
||||
*
|
||||
*
|
||||
* @param tool the tool containing the actions
|
||||
* @return the actions mapped by their full name (e.g., 'Name (OwnerName)')
|
||||
*/
|
||||
|
@ -496,13 +494,12 @@ public class KeyBindingUtils {
|
|||
* A utility method to get all key binding actions that have the given owner.
|
||||
* This method will remove duplicate actions and will only return actions
|
||||
* that support {@link KeyBindingType key bindings}.
|
||||
*
|
||||
*
|
||||
* @param tool the tool containing the actions
|
||||
* @param owner the action owner name
|
||||
* @return the actions
|
||||
*/
|
||||
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool,
|
||||
String owner) {
|
||||
public static Set<DockingActionIf> getKeyBindingActionsForOwner(Tool tool, String owner) {
|
||||
|
||||
Map<String, DockingActionIf> deduper = new HashMap<>();
|
||||
Set<DockingActionIf> actions = tool.getDockingActionsByOwnerName(owner);
|
||||
|
@ -522,7 +519,7 @@ public class KeyBindingUtils {
|
|||
|
||||
/**
|
||||
* Returns all actions that match the given owner and name
|
||||
*
|
||||
*
|
||||
* @param allActions the universe of actions
|
||||
* @param owner the owner
|
||||
* @param name the name
|
||||
|
@ -539,13 +536,13 @@ public class KeyBindingUtils {
|
|||
/**
|
||||
* Takes the existing docking action and allows it to be registered with
|
||||
* Swing components
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* The new action will not be correctly wired into the Docking Action
|
||||
* Context system. This means that the given docking action should not rely
|
||||
* on {@link DockingAction#isEnabledForContext(docking.ActionContext)} to
|
||||
* work when called from the Swing widget.
|
||||
*
|
||||
*
|
||||
* @param action the docking action to adapt to a Swing {@link Action}
|
||||
* @return the new action
|
||||
*/
|
||||
|
@ -553,66 +550,10 @@ public class KeyBindingUtils {
|
|||
return new ActionAdapter(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each action in the given collection against the given new action to make sure that
|
||||
* they share the same default key binding.
|
||||
*
|
||||
* @param newAction the action to check
|
||||
* @param existingActions the actions that have already been checked
|
||||
*/
|
||||
public static void assertSameDefaultKeyBindings(DockingActionIf newAction,
|
||||
Collection<DockingActionIf> existingActions) {
|
||||
|
||||
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
|
||||
KeyStroke defaultKs = getKeyStroke(newDefaultBinding);
|
||||
for (DockingActionIf action : existingActions) {
|
||||
if (!action.getKeyBindingType().supportsKeyBindings()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
|
||||
KeyStroke existingKs = getKeyStroke(existingDefaultBinding);
|
||||
if (!Objects.equals(defaultKs, existingKs)) {
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, action, existingKs);
|
||||
break; // one warning seems like enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message for the two given actions to signal that they do not share the
|
||||
* same default key binding
|
||||
*
|
||||
* @param newAction the new action
|
||||
* @param existingAction the action that has already been validated
|
||||
* @param existingDefaultKs the current validated key stroke
|
||||
*/
|
||||
public static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, KeyStroke existingDefaultKs) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+"'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tKey Binding: " + existingDefaultKs +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tKey Binding: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultKs;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(KeyBindingUtils.class, s, ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given data with system-independent versions of key modifiers. For example,
|
||||
* the <code>control</code> key will be converted to the <code>command</code> key on the Mac.
|
||||
*
|
||||
*
|
||||
* @param keyStroke the keystroke to validate
|
||||
* @return the potentially changed keystroke
|
||||
*/
|
||||
|
@ -676,7 +617,7 @@ public class KeyBindingUtils {
|
|||
* and we want it to look like: "Ctrl-M".
|
||||
* <br>In Java 11 we have seen toString() values get printed with repeated text, such
|
||||
* as: "shift ctrl pressed SHIFT". We want to trim off the repeated modifiers.
|
||||
*
|
||||
*
|
||||
* @param keyStroke the key stroke
|
||||
* @return the string value; the empty string if the key stroke is null
|
||||
*/
|
||||
|
@ -792,14 +733,18 @@ public class KeyBindingUtils {
|
|||
* Ctrl-Alt-Z
|
||||
* ctrl Z
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* <p><b>Note:</b> The returned keystroke will always correspond to a {@code pressed} event,
|
||||
* regardless of the value passed in (pressed, typed or released).
|
||||
*
|
||||
*
|
||||
* @param keyStroke the key stroke
|
||||
* @return the new key stroke (as returned by {@link KeyStroke#getKeyStroke(String)}
|
||||
*/
|
||||
public static KeyStroke parseKeyStroke(String keyStroke) {
|
||||
if (StringUtils.isBlank(keyStroke)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> pieces = new ArrayList<>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(keyStroke, "- ");
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
|
@ -873,13 +818,6 @@ public class KeyBindingUtils {
|
|||
return !action.getKeyBindingType().isManaged();
|
||||
}
|
||||
|
||||
private static KeyStroke getKeyStroke(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getKeyBinding();
|
||||
}
|
||||
|
||||
// prompts the user for a file location from which to read key binding data
|
||||
private static InputStream getInputStreamForFile(File startingDir) {
|
||||
File selectedFile = getFileFromUser(startingDir);
|
||||
|
|
|
@ -24,35 +24,40 @@ import docking.Tool;
|
|||
import docking.action.DockingActionIf;
|
||||
import docking.action.KeyBindingData;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import gui.event.MouseBinding;
|
||||
import util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* An object that maps actions to key strokes.
|
||||
* An object that maps actions to key strokes and mouse bindings.
|
||||
* <p>
|
||||
* This class knows how to load all system actions and how to load any key bindings for those
|
||||
* actions from the tool's options. Clients can make changes to the state of this class that can
|
||||
* then be applied to the system by calling {@link #applyChanges()}.
|
||||
* This class knows how to load all system actions and how to load any key and mouse bindings for
|
||||
* those actions from the tool's options. Clients can make changes to the state of this class that
|
||||
* can then be applied to the system by calling {@link #applyChanges()}.
|
||||
*/
|
||||
public class KeyBindings {
|
||||
|
||||
private Tool tool;
|
||||
private ToolOptions keyBindingOptions;
|
||||
|
||||
private Map<String, List<DockingActionIf>> actionsByFullName;
|
||||
private Map<String, List<String>> actionNamesByKeyStroke = new HashMap<>();
|
||||
private Map<String, KeyStroke> keyStrokesByFullName = new HashMap<>();
|
||||
// allows clients to populate a table of all actions
|
||||
private List<DockingActionIf> uniqueActions = new ArrayList<>();
|
||||
|
||||
// to know what has been changed
|
||||
private Map<String, KeyStroke> originalKeyStrokesByFullName = new HashMap<>();
|
||||
private String longestActionName = "";
|
||||
// allows clients to know if a given key stroke or mouse binding is in use
|
||||
private Map<KeyStroke, List<String>> actionNamesByKeyStroke = new HashMap<>();
|
||||
private Map<MouseBinding, String> actionNameByMouseBinding = new HashMap<>();
|
||||
|
||||
private ToolOptions options;
|
||||
// tracks all changes to an action's key stroke and mouse bindings, which allows us to apply
|
||||
// and restore options values
|
||||
private Map<String, ActionKeyBindingState> actionInfoByFullName = new HashMap<>();
|
||||
|
||||
private String longestActionName = "";
|
||||
|
||||
public KeyBindings(Tool tool) {
|
||||
this.tool = tool;
|
||||
|
||||
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
init();
|
||||
}
|
||||
|
@ -61,22 +66,40 @@ public class KeyBindings {
|
|||
return Collections.unmodifiableList(uniqueActions);
|
||||
}
|
||||
|
||||
/* used for testing */
|
||||
public Map<String, KeyStroke> getKeyStrokesByFullActionName() {
|
||||
return Collections.unmodifiableMap(keyStrokesByFullName);
|
||||
Map<String, KeyStroke> result = new HashMap<>();
|
||||
Set<Entry<String, ActionKeyBindingState>> entries = actionInfoByFullName.entrySet();
|
||||
for (Entry<String, ActionKeyBindingState> entry : entries) {
|
||||
String key = entry.getKey();
|
||||
KeyStroke value = entry.getValue().getCurrentKeyStroke();
|
||||
result.put(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean containsAction(String fullName) {
|
||||
return actionsByFullName.containsKey(fullName);
|
||||
return actionInfoByFullName.containsKey(fullName);
|
||||
}
|
||||
|
||||
public KeyStroke getKeyStroke(String fullName) {
|
||||
return keyStrokesByFullName.get(fullName);
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
return info.getCurrentKeyStroke();
|
||||
}
|
||||
|
||||
public String getActionsForKeyStrokeText(String keyStrokeText) {
|
||||
public MouseBinding getMouseBinding(String fullName) {
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
return info.getCurrentMouseBinding();
|
||||
}
|
||||
|
||||
public String getActionForMouseBinding(MouseBinding mouseBinding) {
|
||||
return actionNameByMouseBinding.get(mouseBinding);
|
||||
}
|
||||
|
||||
public String getActionsForKeyStrokeText(KeyStroke keyStroke) {
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
List<String> names = actionNamesByKeyStroke.get(keyStrokeText);
|
||||
List<String> names = actionNamesByKeyStroke.get(keyStroke);
|
||||
if (CollectionUtils.isBlank(names)) {
|
||||
return sb.toString();
|
||||
}
|
||||
|
@ -85,14 +108,16 @@ public class KeyBindings {
|
|||
return n1.compareToIgnoreCase(n2);
|
||||
});
|
||||
|
||||
sb.append("Actions mapped to key " + keyStrokeText + ":\n");
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
|
||||
sb.append("Actions mapped to key " + ksName + ":\n");
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
sb.append(" ");
|
||||
|
||||
String name = names.get(i);
|
||||
List<DockingActionIf> actions = actionsByFullName.get(name);
|
||||
DockingActionIf action = actions.get(0);
|
||||
sb.append(action.getName());
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(name);
|
||||
DockingActionIf action = info.getRepresentativeAction();
|
||||
String shortName = action.getName();
|
||||
sb.append(shortName);
|
||||
sb.append(" (").append(action.getOwnerDescription()).append(')');
|
||||
if (i < names.size() - 1) {
|
||||
sb.append("\n");
|
||||
|
@ -105,58 +130,79 @@ public class KeyBindings {
|
|||
return longestActionName;
|
||||
}
|
||||
|
||||
public boolean setActionKeyStroke(String actionName, KeyStroke keyStroke) {
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(keyStroke);
|
||||
public boolean isMouseBindingInUse(String fullName, MouseBinding newBinding) {
|
||||
|
||||
// remove old keystroke for action name
|
||||
KeyStroke oldKs = keyStrokesByFullName.get(actionName);
|
||||
if (oldKs != null) {
|
||||
String oldName = KeyBindingUtils.parseKeyStroke(oldKs);
|
||||
if (oldName.equals(ksName)) {
|
||||
String existingName = actionNameByMouseBinding.get(newBinding);
|
||||
if (existingName == null || newBinding == null) {
|
||||
return false; // no new binding, or not in use
|
||||
}
|
||||
|
||||
return !Objects.equals(existingName, fullName);
|
||||
}
|
||||
|
||||
public boolean setActionMouseBinding(String fullName, MouseBinding newBinding) {
|
||||
|
||||
MouseBinding currentBinding = getMouseBinding(fullName);
|
||||
if (currentBinding != null) {
|
||||
if (currentBinding.equals(newBinding)) {
|
||||
return false;
|
||||
}
|
||||
removeFromKeyMap(oldKs, actionName);
|
||||
}
|
||||
addActionKeyStroke(keyStroke, actionName);
|
||||
|
||||
keyStrokesByFullName.put(actionName, keyStroke);
|
||||
actionNameByMouseBinding.remove(currentBinding);
|
||||
}
|
||||
|
||||
if (newBinding != null) {
|
||||
actionNameByMouseBinding.put(newBinding, fullName);
|
||||
}
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
info.setCurrentMouseBinding(newBinding);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeKeyStroke(String actionName) {
|
||||
if (keyStrokesByFullName.containsKey(actionName)) {
|
||||
KeyStroke stroke = keyStrokesByFullName.get(actionName);
|
||||
if (stroke == null) {
|
||||
// nothing to remove; nothing has changed
|
||||
public boolean setActionKeyStroke(String fullName, KeyStroke newKs) {
|
||||
String newKsName = KeyBindingUtils.parseKeyStroke(newKs);
|
||||
|
||||
// remove old keystroke for action name
|
||||
KeyStroke currentKs = getKeyStroke(fullName);
|
||||
if (currentKs != null) {
|
||||
String currentName = KeyBindingUtils.parseKeyStroke(currentKs);
|
||||
if (currentName.equals(newKsName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
removeFromKeyMap(stroke, actionName);
|
||||
keyStrokesByFullName.put(actionName, null);
|
||||
return true;
|
||||
removeFromKeyMap(fullName, currentKs);
|
||||
}
|
||||
return false;
|
||||
addActionKeyStroke(fullName, newKs);
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
info.setCurrentKeyStroke(newKs);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeKeyStroke(String fullName) {
|
||||
|
||||
ActionKeyBindingState info = actionInfoByFullName.get(fullName);
|
||||
if (info == null) {
|
||||
return false; // not sure if this can happen
|
||||
}
|
||||
|
||||
KeyStroke currentKeyStroke = info.getCurrentKeyStroke();
|
||||
if (currentKeyStroke == null) {
|
||||
return false; // nothing to remove; nothing has changed
|
||||
}
|
||||
|
||||
removeFromKeyMap(fullName, currentKeyStroke);
|
||||
info.setCurrentKeyStroke(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the tool options key bindings to the default values originally loaded when the
|
||||
* Restores the tool options key bindings to the default values originally loaded when the
|
||||
* system started.
|
||||
*/
|
||||
public void restoreOptions() {
|
||||
|
||||
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
|
||||
for (Entry<String, List<DockingActionIf>> entry : entries) {
|
||||
List<DockingActionIf> actions = entry.getValue();
|
||||
|
||||
// pick one action, they are all conceptually the same
|
||||
DockingActionIf action = actions.get(0);
|
||||
String actionName = entry.getKey();
|
||||
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
|
||||
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
|
||||
KeyStroke newKeyStroke =
|
||||
(defaultBinding == null) ? null : defaultBinding.getKeyBinding();
|
||||
|
||||
updateOptions(actionName, currentKeyStroke, newKeyStroke);
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.restore(keyBindingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,14 +210,8 @@ public class KeyBindings {
|
|||
* Cancels any pending changes that have not yet been applied.
|
||||
*/
|
||||
public void cancelChanges() {
|
||||
Iterator<String> iter = originalKeyStrokesByFullName.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
String actionName = iter.next();
|
||||
KeyStroke originalKS = originalKeyStrokesByFullName.get(actionName);
|
||||
KeyStroke modifiedKS = keyStrokesByFullName.get(actionName);
|
||||
if (modifiedKS != null && !modifiedKS.equals(originalKS)) {
|
||||
keyStrokesByFullName.put(actionName, originalKS);
|
||||
}
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.cancelChanges();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,84 +219,203 @@ public class KeyBindings {
|
|||
* Applies any pending changes.
|
||||
*/
|
||||
public void applyChanges() {
|
||||
Iterator<String> iter = keyStrokesByFullName.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
String actionName = iter.next();
|
||||
KeyStroke currentKeyStroke = keyStrokesByFullName.get(actionName);
|
||||
KeyStroke originalKeyStroke = originalKeyStrokesByFullName.get(actionName);
|
||||
updateOptions(actionName, originalKeyStroke, currentKeyStroke);
|
||||
for (ActionKeyBindingState info : actionInfoByFullName.values()) {
|
||||
info.apply(keyBindingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFromKeyMap(KeyStroke ks, String actionName) {
|
||||
private void removeFromKeyMap(String actionName, KeyStroke ks) {
|
||||
if (ks == null) {
|
||||
return;
|
||||
}
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
List<String> list = actionNamesByKeyStroke.get(ksName);
|
||||
|
||||
List<String> list = actionNamesByKeyStroke.get(ks);
|
||||
if (list != null) {
|
||||
list.remove(actionName);
|
||||
if (list.isEmpty()) {
|
||||
actionNamesByKeyStroke.remove(ksName);
|
||||
actionNamesByKeyStroke.remove(ks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptions(String fullActionName, KeyStroke currentKeyStroke,
|
||||
KeyStroke newKeyStroke) {
|
||||
|
||||
if (Objects.equals(currentKeyStroke, newKeyStroke)) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.setKeyStroke(fullActionName, newKeyStroke);
|
||||
originalKeyStrokesByFullName.put(fullActionName, newKeyStroke);
|
||||
keyStrokesByFullName.put(fullActionName, newKeyStroke);
|
||||
|
||||
List<DockingActionIf> actions = actionsByFullName.get(fullActionName);
|
||||
for (DockingActionIf action : actions) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKeyStroke));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void init() {
|
||||
|
||||
actionsByFullName = KeyBindingUtils.getAllActionsByFullName(tool);
|
||||
actionInfoByFullName = new HashMap<>();
|
||||
|
||||
Map<String, List<DockingActionIf>> actionsByFullName =
|
||||
KeyBindingUtils.getAllActionsByFullName(tool);
|
||||
Set<Entry<String, List<DockingActionIf>>> entries = actionsByFullName.entrySet();
|
||||
for (Entry<String, List<DockingActionIf>> entry : entries) {
|
||||
|
||||
// pick one action, they are all conceptually the same
|
||||
List<DockingActionIf> actions = entry.getValue();
|
||||
DockingActionIf action = actions.get(0);
|
||||
uniqueActions.add(action);
|
||||
|
||||
String actionName = entry.getKey();
|
||||
KeyStroke ks = options.getKeyStroke(actionName, null);
|
||||
keyStrokesByFullName.put(actionName, ks);
|
||||
addActionKeyStroke(ks, actionName);
|
||||
originalKeyStrokesByFullName.put(actionName, ks);
|
||||
String fullName = entry.getKey();
|
||||
ActionTrigger trigger = keyBindingOptions.getActionTrigger(fullName, null);
|
||||
|
||||
String shortName = action.getName();
|
||||
KeyStroke ks = null;
|
||||
MouseBinding mb = null;
|
||||
|
||||
if (trigger != null) {
|
||||
ks = trigger.getKeyStroke();
|
||||
mb = trigger.getMouseBinding();
|
||||
}
|
||||
|
||||
ActionKeyBindingState info = new ActionKeyBindingState(actions, ks, mb);
|
||||
actionInfoByFullName.put(fullName, info);
|
||||
|
||||
uniqueActions.add(info.getRepresentativeAction());
|
||||
|
||||
addActionKeyStroke(fullName, ks);
|
||||
|
||||
String shortName = info.getShortName();
|
||||
if (shortName.length() > longestActionName.length()) {
|
||||
longestActionName = shortName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addActionKeyStroke(KeyStroke ks, String actionName) {
|
||||
private void addActionKeyStroke(String actionName, KeyStroke ks) {
|
||||
if (ks == null) {
|
||||
return;
|
||||
}
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
List<String> list = actionNamesByKeyStroke.get(ksName);
|
||||
|
||||
List<String> list = actionNamesByKeyStroke.get(ks);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
actionNamesByKeyStroke.put(ksName, list);
|
||||
actionNamesByKeyStroke.put(ks, list);
|
||||
}
|
||||
if (!list.contains(actionName)) {
|
||||
list.add(actionName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to store current and original values for key strokes and mouse bindings. This is
|
||||
* used to apply changes and restore default values.
|
||||
*/
|
||||
private class ActionKeyBindingState {
|
||||
|
||||
private List<DockingActionIf> actions = new ArrayList<>();
|
||||
private KeyStroke originalKeyStroke;
|
||||
private KeyStroke currentKeyStroke;
|
||||
private MouseBinding originalMouseBinding;
|
||||
private MouseBinding currentMouseBinding;
|
||||
|
||||
ActionKeyBindingState(List<DockingActionIf> actions, KeyStroke ks, MouseBinding mb) {
|
||||
this.actions.addAll(actions);
|
||||
this.originalKeyStroke = ks;
|
||||
this.currentKeyStroke = ks;
|
||||
this.originalMouseBinding = mb;
|
||||
this.currentMouseBinding = mb;
|
||||
}
|
||||
|
||||
public DockingActionIf getRepresentativeAction() {
|
||||
// pick one action, they are all conceptually the same
|
||||
return actions.get(0);
|
||||
}
|
||||
|
||||
String getShortName() {
|
||||
// pick one action, they are all conceptually the same
|
||||
return actions.get(0).getName();
|
||||
}
|
||||
|
||||
String getFullName() {
|
||||
return getRepresentativeAction().getFullName();
|
||||
}
|
||||
|
||||
public MouseBinding getCurrentMouseBinding() {
|
||||
return currentMouseBinding;
|
||||
}
|
||||
|
||||
public void setCurrentMouseBinding(MouseBinding newMouseBinding) {
|
||||
this.currentMouseBinding = newMouseBinding;
|
||||
}
|
||||
|
||||
public KeyStroke getCurrentKeyStroke() {
|
||||
return currentKeyStroke;
|
||||
}
|
||||
|
||||
public void setCurrentKeyStroke(KeyStroke newKeyStroke) {
|
||||
this.currentKeyStroke = newKeyStroke;
|
||||
}
|
||||
|
||||
public void cancelChanges() {
|
||||
currentKeyStroke = originalKeyStroke;
|
||||
currentMouseBinding = originalMouseBinding;
|
||||
}
|
||||
|
||||
public void apply(ToolOptions keyStrokeOptions) {
|
||||
if (!hasChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData kbd = getCurrentKeyBindingData();
|
||||
apply(keyStrokeOptions, kbd);
|
||||
}
|
||||
|
||||
private void apply(ToolOptions keyStrokeOptions, KeyBindingData keyBinding) {
|
||||
|
||||
if (keyBinding == null) {
|
||||
// no bindings; bindings have been cleared
|
||||
for (DockingActionIf action : actions) {
|
||||
action.setUnvalidatedKeyBindingData(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ActionTrigger newTrigger = keyBinding.getActionTrigger();
|
||||
String fullName = getFullName();
|
||||
keyStrokeOptions.setActionTrigger(fullName, newTrigger);
|
||||
}
|
||||
|
||||
private boolean hasChanged() {
|
||||
return !Objects.equals(originalKeyStroke, currentKeyStroke) ||
|
||||
!Objects.equals(originalMouseBinding, currentMouseBinding);
|
||||
}
|
||||
|
||||
private boolean matches(KeyBindingData kbData) {
|
||||
|
||||
if (CollectionUtils.isAllNull(kbData, currentKeyStroke, currentMouseBinding)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kbData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyStroke otherKs = kbData.getKeyBinding();
|
||||
if (!Objects.equals(otherKs, currentKeyStroke)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MouseBinding otherMb = kbData.getMouseBinding();
|
||||
return Objects.equals(otherMb, currentMouseBinding);
|
||||
}
|
||||
|
||||
private KeyBindingData getCurrentKeyBindingData() {
|
||||
|
||||
if (currentKeyStroke == null && currentMouseBinding == null) {
|
||||
return null; // the key binding data does not exist or has been cleared
|
||||
}
|
||||
|
||||
DockingActionIf action = getRepresentativeAction();
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
ActionTrigger trigger = new ActionTrigger(currentKeyStroke, currentMouseBinding);
|
||||
return KeyBindingData.update(kbData, trigger);
|
||||
}
|
||||
|
||||
// restores the options to their default values
|
||||
public void restore(ToolOptions options) {
|
||||
DockingActionIf action = getRepresentativeAction();
|
||||
KeyBindingData defaultBinding = action.getDefaultKeyBindingData();
|
||||
|
||||
if (!matches(defaultBinding)) {
|
||||
apply(options, defaultBinding);
|
||||
}
|
||||
|
||||
cancelChanges();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
*/
|
||||
public void setKeyStroke(KeyStroke ks) {
|
||||
keyEntryField.setKeyStroke(ks);
|
||||
updateCollisionPane(ks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -169,7 +170,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
action.setUnvalidatedKeyBindingData(newKs == null ? null : new KeyBindingData(newKs));
|
||||
|
||||
close();
|
||||
}
|
||||
|
@ -192,8 +193,7 @@ public class KeyEntryDialog extends DialogComponentProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
String ksName = KeyBindingUtils.parseKeyStroke(ks);
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ksName);
|
||||
String text = keyBindings.getActionsForKeyStrokeText(ks);
|
||||
try {
|
||||
doc.insertString(0, text, textAttrs);
|
||||
collisionPane.setCaretPosition(0);
|
||||
|
|
|
@ -18,8 +18,6 @@ package docking.actions;
|
|||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.apache.commons.collections4.Bag;
|
||||
import org.apache.commons.collections4.bag.HashBag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -28,8 +26,9 @@ import docking.ActionContext;
|
|||
import docking.DockingWindowManager;
|
||||
import docking.action.*;
|
||||
import docking.tool.ToolConstants;
|
||||
import ghidra.framework.options.OptionsChangeListener;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.util.Msg;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A stub action that allows key bindings to be edited through the key bindings options. This
|
||||
|
@ -63,7 +62,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
* Note: This collection is weak; the actions will stay as long as they are
|
||||
* registered in the tool.
|
||||
*/
|
||||
private WeakHashMap<DockingActionIf, KeyStroke> clientActions = new WeakHashMap<>();
|
||||
private WeakHashMap<DockingActionIf, ActionTrigger> clientActions = new WeakHashMap<>();
|
||||
|
||||
private ToolOptions keyBindingOptions;
|
||||
private Bag<String> actionOwners = new HashBag<>();
|
||||
|
@ -73,11 +72,13 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
*
|
||||
* @param name The name of the action--this will be displayed in the options as the name of
|
||||
* key binding's action
|
||||
* @param defaultKs the default key stroke for this stub. The key stroke will be validated
|
||||
* each time an action is added to this stub to ensure that the defaults are in sync.
|
||||
* @param defaultActionTrigger the default action trigger for this stub. The action trigger
|
||||
* will be validated each time an action is added to this stub to ensure that the
|
||||
* defaults are in sync.
|
||||
* @param options the tool's key binding options
|
||||
*/
|
||||
SharedStubKeyBindingAction(String name, KeyStroke defaultKs, ToolOptions options) {
|
||||
SharedStubKeyBindingAction(String name, ActionTrigger defaultActionTrigger,
|
||||
ToolOptions options) {
|
||||
// Note: we need to have this stub registered to use key bindings so that the options will
|
||||
// restore the saved key binding to this class, which will then notify any of the
|
||||
// shared actions using this stub.
|
||||
|
@ -87,7 +88,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
// Dummy keybinding actions don't have help--the real action does
|
||||
DockingWindowManager.getHelpService().excludeFromHelp(this);
|
||||
|
||||
setUnvalidatedKeyBindingData(new KeyBindingData(defaultKs));
|
||||
setKeyBindingData(this, defaultActionTrigger);
|
||||
|
||||
// A listener to keep the shared, stub keybindings in sync with their clients
|
||||
options.addOptionsChangeListener(this);
|
||||
|
@ -119,7 +120,7 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
void addClientAction(DockingActionIf action) {
|
||||
|
||||
// 1) Validate new action keystroke against existing actions
|
||||
KeyStroke defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
|
||||
ActionTrigger defaultKs = validateActionsHaveTheSameDefaultKeyStroke(action);
|
||||
|
||||
// 2) Add the action and the validated keystroke, as this is the default keystroke
|
||||
clientActions.put(action, defaultKs);
|
||||
|
@ -159,61 +160,69 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
return super.getDescription();
|
||||
}
|
||||
|
||||
private KeyStroke validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
|
||||
private ActionTrigger validateActionsHaveTheSameDefaultKeyStroke(DockingActionIf newAction) {
|
||||
|
||||
// this value may be null
|
||||
KeyBindingData defaultBinding = newAction.getDefaultKeyBindingData();
|
||||
KeyStroke newDefaultKs = getKeyStroke(defaultBinding);
|
||||
ActionTrigger newDefaulTrigger = getActionTrigger(defaultBinding);
|
||||
|
||||
Set<Entry<DockingActionIf, KeyStroke>> entries = clientActions.entrySet();
|
||||
for (Entry<DockingActionIf, KeyStroke> entry : entries) {
|
||||
Set<Entry<DockingActionIf, ActionTrigger>> entries = clientActions.entrySet();
|
||||
for (Entry<DockingActionIf, ActionTrigger> entry : entries) {
|
||||
DockingActionIf existingAction = entry.getKey();
|
||||
KeyStroke existingDefaultKs = entry.getValue();
|
||||
if (Objects.equals(existingDefaultKs, newDefaultKs)) {
|
||||
ActionTrigger existingDefaultTrigger = entry.getValue();
|
||||
if (Objects.equals(existingDefaultTrigger, newDefaulTrigger)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingUtils.logDifferentKeyBindingsWarnigMessage(newAction, existingAction,
|
||||
existingDefaultKs);
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, existingAction, existingDefaultTrigger);
|
||||
|
||||
//
|
||||
// Not sure which keystroke to prefer here--keep the first one that was set
|
||||
//
|
||||
|
||||
// set the new action's keystroke to be the winner
|
||||
newAction.setKeyBindingData(new KeyBindingData(existingDefaultKs));
|
||||
// set the existing action's keystroke to be the winner
|
||||
newAction.setKeyBindingData(existingAction.getKeyBindingData());
|
||||
|
||||
// one message is probably enough;
|
||||
return existingDefaultKs;
|
||||
return existingDefaultTrigger;
|
||||
}
|
||||
|
||||
return newDefaultKs;
|
||||
return newDefaulTrigger;
|
||||
}
|
||||
|
||||
private void updateActionKeyStrokeFromOptions(DockingActionIf action, KeyStroke defaultKs) {
|
||||
private void updateActionKeyStrokeFromOptions(DockingActionIf action,
|
||||
ActionTrigger defaultTrigger) {
|
||||
|
||||
KeyStroke stubKs = defaultKs;
|
||||
KeyStroke optionsKs = getKeyStrokeFromOptions(defaultKs);
|
||||
if (!Objects.equals(defaultKs, optionsKs)) {
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(optionsKs));
|
||||
stubKs = optionsKs;
|
||||
ActionTrigger stubTrigger = defaultTrigger;
|
||||
ActionTrigger optionsTrigger = getActionTriggerFromOptions(defaultTrigger);
|
||||
if (!Objects.equals(defaultTrigger, optionsTrigger)) {
|
||||
setKeyBindingData(action, optionsTrigger);
|
||||
stubTrigger = optionsTrigger;
|
||||
}
|
||||
|
||||
setUnvalidatedKeyBindingData(new KeyBindingData(stubKs));
|
||||
setKeyBindingData(this, stubTrigger);
|
||||
}
|
||||
|
||||
private KeyStroke getKeyStrokeFromOptions(KeyStroke validatedKeyStroke) {
|
||||
KeyStroke ks = keyBindingOptions.getKeyStroke(getFullName(), validatedKeyStroke);
|
||||
return ks;
|
||||
private void setKeyBindingData(DockingActionIf action, ActionTrigger actionTrigger) {
|
||||
KeyBindingData kbData = null;
|
||||
if (actionTrigger != null) {
|
||||
kbData = new KeyBindingData(actionTrigger);
|
||||
}
|
||||
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(kbData);
|
||||
}
|
||||
|
||||
private KeyStroke getKeyStroke(KeyBindingData data) {
|
||||
private ActionTrigger getActionTriggerFromOptions(ActionTrigger validatedTrigger) {
|
||||
return keyBindingOptions.getActionTrigger(getFullName(), validatedTrigger);
|
||||
}
|
||||
|
||||
private ActionTrigger getActionTrigger(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getKeyBinding();
|
||||
return data.getActionTrigger();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -224,11 +233,11 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
return; // not my binding
|
||||
}
|
||||
|
||||
KeyStroke newKs = (KeyStroke) newValue;
|
||||
ActionTrigger newTrigger = (ActionTrigger) newValue;
|
||||
setKeyBindingData(this, newTrigger);
|
||||
|
||||
for (DockingActionIf action : clientActions.keySet()) {
|
||||
// we use the 'unvalidated' call since this value is provided by the user--we assume
|
||||
// that user input is correct; we only validate programmer input
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
setKeyBindingData(action, newTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,4 +262,23 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
|
|||
clientActions.clear();
|
||||
keyBindingOptions.removeOptionsChangeListener(this);
|
||||
}
|
||||
|
||||
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+ "'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + existingDefaultTrigger +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultTrigger;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(SharedStubKeyBindingAction.class, s,
|
||||
ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,9 @@ import docking.tool.util.DockingToolConstants;
|
|||
import ghidra.framework.options.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* An class to manage actions registered with the tool
|
||||
|
@ -51,7 +53,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
/*
|
||||
Map of Maps of Sets
|
||||
|
||||
|
||||
Owner Name ->
|
||||
Action Name -> Set of Actions
|
||||
*/
|
||||
|
@ -60,15 +62,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
private Map<String, SharedStubKeyBindingAction> sharedActionMap = new HashMap<>();
|
||||
|
||||
private ToolOptions keyBindingOptions;
|
||||
private ToolOptions options;
|
||||
private Tool tool;
|
||||
private KeyBindingsManager keyBindingsManager;
|
||||
private OptionsChangeListener optionChangeListener = (options, optionName, oldValue,
|
||||
newValue) -> updateKeyBindingsFromOptions(options, optionName, (KeyStroke) newValue);
|
||||
private OptionsChangeListener optionChangeListener = (toolOptions, optionName, oldValue,
|
||||
newValue) -> updateKeyBindingsFromOptions(optionName, (ActionTrigger) newValue);
|
||||
|
||||
/**
|
||||
* Construct an ActionManager
|
||||
*
|
||||
*
|
||||
* @param tool tool using this ActionManager
|
||||
* @param actionToGuiHelper the class that takes actions and maps them to GUI widgets
|
||||
*/
|
||||
|
@ -76,8 +78,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
this.tool = tool;
|
||||
this.actionGuiHelper = actionToGuiHelper;
|
||||
this.keyBindingsManager = new KeyBindingsManager(tool);
|
||||
this.keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
this.keyBindingOptions.addOptionsChangeListener(optionChangeListener);
|
||||
this.options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
this.options.addOptionsChangeListener(optionChangeListener);
|
||||
|
||||
createSystemActions();
|
||||
SharedActionRegistry.installSharedActions(tool, this);
|
||||
|
@ -112,12 +114,12 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
// Some System actions support changing the keybinding. In the future, all System actions
|
||||
// may support this.
|
||||
if (action.getKeyBindingType().isManaged()) {
|
||||
KeyBindingData kbd = action.getKeyBindingData();
|
||||
KeyStroke ks = kbd.getKeyBinding();
|
||||
loadKeyBindingFromOptions(action, ks);
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
loadKeyBindingFromOptions(action, actionTrigger);
|
||||
}
|
||||
|
||||
keyBindingsManager.addSystemAction(action);
|
||||
addActionToMap(action);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
@ -127,12 +129,64 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
|
||||
private void addActionToMap(DockingActionIf action) {
|
||||
|
||||
Set<DockingActionIf> actions = getActionStorage(action);
|
||||
KeyBindingUtils.assertSameDefaultKeyBindings(action, actions);
|
||||
assertSameDefaultActionTrigger(action, actions);
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
private static void assertSameDefaultActionTrigger(DockingActionIf newAction,
|
||||
Collection<DockingActionIf> existingActions) {
|
||||
|
||||
if (!newAction.getKeyBindingType().supportsKeyBindings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindingData newDefaultBinding = newAction.getDefaultKeyBindingData();
|
||||
ActionTrigger defaultTrigger = getActionTrigger(newDefaultBinding);
|
||||
for (DockingActionIf action : existingActions) {
|
||||
if (!action.getKeyBindingType().supportsKeyBindings()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBindingData existingDefaultBinding = action.getDefaultKeyBindingData();
|
||||
ActionTrigger existingTrigger = getActionTrigger(existingDefaultBinding);
|
||||
if (!Objects.equals(defaultTrigger, existingTrigger)) {
|
||||
logDifferentKeyBindingsWarnigMessage(newAction, action, existingTrigger);
|
||||
break; // one warning seems like enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that two equivalent actions (same name and owner) share the same default action
|
||||
* trigger. It is considered a programming mistake for two equivalent actions to have different
|
||||
* triggers.
|
||||
*/
|
||||
private static void logDifferentKeyBindingsWarnigMessage(DockingActionIf newAction,
|
||||
DockingActionIf existingAction, ActionTrigger existingDefaultTrigger) {
|
||||
|
||||
//@formatter:off
|
||||
String s = "Shared Key Binding Actions have different default values. These " +
|
||||
"must be the same." +
|
||||
"\n\tAction name: '"+existingAction.getName()+ "'" +
|
||||
"\n\tAction 1: " + existingAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + existingDefaultTrigger +
|
||||
"\n\tAction 2: " + newAction.getInceptionInformation() +
|
||||
"\n\t\tAction Trigger: " + newAction.getKeyBinding() +
|
||||
"\nUsing the " +
|
||||
"first value set - " + existingDefaultTrigger;
|
||||
//@formatter:on
|
||||
|
||||
Msg.warn(ToolActions.class, s, ReflectionUtilities.createJavaFilteredThrowable());
|
||||
}
|
||||
|
||||
private static ActionTrigger getActionTrigger(KeyBindingData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return data.getActionTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action that works specifically with a component provider.
|
||||
* @param provider provider associated with the action
|
||||
|
@ -170,32 +224,45 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
KeyStroke ks = action.getKeyBinding();
|
||||
loadKeyBindingFromOptions(action, ks);
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
loadKeyBindingFromOptions(action, actionTrigger);
|
||||
|
||||
keyBindingsManager.addAction(provider, action);
|
||||
}
|
||||
|
||||
private void loadKeyBindingFromOptions(DockingActionIf action, KeyStroke ks) {
|
||||
String description = "Keybinding for " + action.getFullName();
|
||||
keyBindingOptions.registerOption(action.getFullName(), OptionType.KEYSTROKE_TYPE, ks, null,
|
||||
description);
|
||||
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
|
||||
if (!Objects.equals(ks, newKs)) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
private ActionTrigger getActionTrigger(DockingActionIf action) {
|
||||
KeyBindingData kbData = action.getKeyBindingData();
|
||||
if (kbData != null) {
|
||||
return kbData.getActionTrigger();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadKeyBindingFromOptions(DockingActionIf action, ActionTrigger actionTrigger) {
|
||||
|
||||
String fullName = action.getFullName();
|
||||
String description = "Keybinding for " + fullName;
|
||||
options.registerOption(fullName, OptionType.ACTION_TRIGGER, actionTrigger, null,
|
||||
description);
|
||||
|
||||
KeyBindingData existingKbData = action.getKeyBindingData();
|
||||
|
||||
ActionTrigger newTrigger = options.getActionTrigger(fullName, actionTrigger);
|
||||
KeyBindingData newKbData = KeyBindingData.update(existingKbData, newTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
|
||||
private void installSharedKeyBinding(ComponentProvider provider, DockingActionIf action) {
|
||||
|
||||
String name = action.getName();
|
||||
KeyStroke defaultKeyStroke = action.getKeyBinding();
|
||||
|
||||
// get or create the stub to which we will add the action
|
||||
SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> {
|
||||
|
||||
ActionTrigger actionTrigger = getActionTrigger(action);
|
||||
SharedStubKeyBindingAction newStub =
|
||||
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
|
||||
registerStub(newStub, defaultKeyStroke);
|
||||
new SharedStubKeyBindingAction(name, actionTrigger, options);
|
||||
registerStub(newStub, actionTrigger);
|
||||
return newStub;
|
||||
});
|
||||
|
||||
|
@ -209,10 +276,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void registerStub(SharedStubKeyBindingAction stub, KeyStroke defaultKeyStroke) {
|
||||
private void registerStub(SharedStubKeyBindingAction stub, ActionTrigger defaultActionTrigger) {
|
||||
stub.addPropertyChangeListener(this);
|
||||
|
||||
loadKeyBindingFromOptions(stub, defaultKeyStroke);
|
||||
loadKeyBindingFromOptions(stub, defaultActionTrigger);
|
||||
|
||||
keyBindingsManager.addAction(null, stub);
|
||||
}
|
||||
|
@ -243,10 +310,15 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return; // no actions registered for this owner
|
||||
}
|
||||
|
||||
// Note: this method is called when plugins are removed. 'owner' is the name of the plugin.
|
||||
// This method will also get called while passing the system owner. In that case, we do
|
||||
// not want to remove system actions in this method. We check below for system actions.
|
||||
|
||||
//@formatter:off
|
||||
toCleanup.values()
|
||||
.stream()
|
||||
.flatMap(set -> set.stream())
|
||||
.filter(action -> !keyBindingsManager.isSystemAction(action)) // (see note above)
|
||||
.forEach(action -> removeGlobalAction(action))
|
||||
;
|
||||
//@formatter:on
|
||||
|
@ -312,32 +384,33 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Iterator<DockingActionIf> getAllActionsIterator() {
|
||||
// chain all items together, rather than copy the data
|
||||
// Note: do not use Apache's IteratorUtils.chainedIterator. It degrades exponentially
|
||||
return Stream
|
||||
.concat(
|
||||
actionsByNameByOwner.values()
|
||||
.stream()
|
||||
.flatMap(actionsByName -> actionsByName.values()
|
||||
.stream())
|
||||
.flatMap(actions -> actions.stream()),
|
||||
sharedActionMap.values()
|
||||
.stream())
|
||||
.iterator();
|
||||
//@formatter:off
|
||||
return Stream.concat(
|
||||
actionsByNameByOwner.values().stream()
|
||||
.flatMap(actionsByName -> actionsByName.values().stream())
|
||||
.flatMap(actions -> actions.stream()),
|
||||
sharedActionMap.values().stream()).iterator();
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keybindings for each action so that they are still registered as being used;
|
||||
* otherwise the options will be removed because they are noted as not being used.
|
||||
/*
|
||||
* An odd method that really shoulnd't be on the interface. This is a call that allows the
|
||||
* framework to signal that the ToolOptions have been rebuilt, such as when restoring from xml.
|
||||
* During a rebuild, ToolOptions does not send out events, so this class does not get any of the
|
||||
* values from the new options. This method tells us to get the new version of the options from
|
||||
* the tool.
|
||||
*/
|
||||
public synchronized void restoreKeyBindings() {
|
||||
keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
public synchronized void optionsRebuilt() {
|
||||
|
||||
// grab the new, rebuilt options
|
||||
options = tool.getOptions(DockingToolConstants.KEY_BINDINGS);
|
||||
|
||||
Iterator<DockingActionIf> it = getKeyBindingActionsIterator();
|
||||
for (DockingActionIf action : CollectionUtils.asIterable(it)) {
|
||||
KeyStroke ks = action.getKeyBinding();
|
||||
KeyStroke newKs = keyBindingOptions.getKeyStroke(action.getFullName(), ks);
|
||||
if (!Objects.equals(ks, newKs)) {
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
}
|
||||
KeyBindingData currentKbData = action.getKeyBindingData();
|
||||
ActionTrigger optionsTrigger = options.getActionTrigger(action.getFullName(), null);
|
||||
KeyBindingData newKbData = KeyBindingData.update(currentKbData, optionsTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,8 +450,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
keyBindingsManager.removeAction(action);
|
||||
|
||||
getActionStorage(action).remove(action);
|
||||
if (!action.getKeyBindingType()
|
||||
.isShared()) {
|
||||
if (!action.getKeyBindingType().isShared()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -391,12 +463,10 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Set<DockingActionIf> getActionStorage(DockingActionIf action) {
|
||||
String owner = action.getOwner();
|
||||
String name = action.getName();
|
||||
return actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
return actionsByNameByOwner.get(owner).get(name);
|
||||
}
|
||||
|
||||
private void updateKeyBindingsFromOptions(ToolOptions options, String optionName,
|
||||
KeyStroke newKs) {
|
||||
private void updateKeyBindingsFromOptions(String optionName, ActionTrigger newTrigger) {
|
||||
|
||||
// note: the 'shared actions' update themselves, so we only need to handle standard actions
|
||||
|
||||
|
@ -405,21 +475,30 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
String name = matcher.group(1);
|
||||
String owner = matcher.group(2);
|
||||
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
for (DockingActionIf action : actions) {
|
||||
KeyStroke oldKs = action.getKeyBinding();
|
||||
if (Objects.equals(oldKs, newKs)) {
|
||||
continue; // prevent bouncing
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner).get(name);
|
||||
if (actions.isEmpty()) {
|
||||
// An empty actions list implies that the action changed in the options is a shared
|
||||
// action or a system action. Shared actions will update themselves. Here we will
|
||||
// handle system actions.
|
||||
DockingActionIf systemAction = keyBindingsManager.getSystemAction(optionName);
|
||||
if (systemAction != null) {
|
||||
KeyBindingData oldKbData = systemAction.getKeyBindingData();
|
||||
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
|
||||
systemAction.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
action.setUnvalidatedKeyBindingData(new KeyBindingData(newKs));
|
||||
return;
|
||||
}
|
||||
|
||||
for (DockingActionIf action : actions) {
|
||||
KeyBindingData oldKbData = action.getKeyBindingData();
|
||||
KeyBindingData newKbData = KeyBindingData.update(oldKbData, newTrigger);
|
||||
action.setUnvalidatedKeyBindingData(newKbData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!evt.getPropertyName()
|
||||
.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -431,15 +510,19 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Check to see if we need to update the options to reflect the change to the action's key
|
||||
// binding data.
|
||||
//
|
||||
KeyBindingData newKeyBindingData = (KeyBindingData) evt.getNewValue();
|
||||
KeyStroke newKs = null;
|
||||
ActionTrigger newTrigger = null;
|
||||
if (newKeyBindingData != null) {
|
||||
newKs = newKeyBindingData.getKeyBinding();
|
||||
newTrigger = newKeyBindingData.getActionTrigger();
|
||||
}
|
||||
|
||||
KeyStroke currentKs = keyBindingOptions.getKeyStroke(action.getFullName(), null);
|
||||
if (!Objects.equals(currentKs, newKs)) {
|
||||
keyBindingOptions.setKeyStroke(action.getFullName(), newKs);
|
||||
ActionTrigger currentTrigger = options.getActionTrigger(action.getFullName(), null);
|
||||
if (!Objects.equals(currentTrigger, newTrigger)) {
|
||||
options.setActionTrigger(action.getFullName(), newTrigger);
|
||||
keyBindingsChanged();
|
||||
}
|
||||
}
|
||||
|
@ -456,8 +539,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
Iterator<DockingActionIf> it = actionGuiHelper.getComponentActions(provider);
|
||||
while (it.hasNext()) {
|
||||
DockingActionIf action = it.next();
|
||||
if (action.getName()
|
||||
.equals(actionName)) {
|
||||
if (action.getName().equals(actionName)) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
@ -476,7 +558,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
}
|
||||
|
||||
public Action getAction(KeyStroke ks) {
|
||||
return keyBindingsManager.getDockingKeyAction(ks);
|
||||
return keyBindingsManager.getDockingAction(ks);
|
||||
}
|
||||
|
||||
public Action getAction(MouseBinding mb) {
|
||||
return keyBindingsManager.getDockingAction(mb);
|
||||
}
|
||||
|
||||
DockingActionIf getSharedStubKeyBindingAction(String name) {
|
||||
|
@ -487,23 +573,22 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
* Allows clients to register an action by using a placeholder. This is useful when
|
||||
* an API wishes to have a central object (like a plugin) register actions for transient
|
||||
* providers, that may not be loaded until needed.
|
||||
*
|
||||
*
|
||||
* <p>This method may be called multiple times with the same conceptual placeholder--the
|
||||
* placeholder will only be added once.
|
||||
*
|
||||
*
|
||||
* @param placeholder the placeholder containing information related to the action it represents
|
||||
*/
|
||||
@Override
|
||||
public void registerSharedActionPlaceholder(SharedDockingActionPlaceholder placeholder) {
|
||||
|
||||
String name = placeholder.getName();
|
||||
KeyStroke defaultKeyStroke = placeholder.getKeyBinding();
|
||||
|
||||
SharedStubKeyBindingAction stub = sharedActionMap.computeIfAbsent(name, key -> {
|
||||
|
||||
ActionTrigger actionTrigger = getActionTrigger(placeholder);
|
||||
SharedStubKeyBindingAction newStub =
|
||||
new SharedStubKeyBindingAction(name, defaultKeyStroke, keyBindingOptions);
|
||||
registerStub(newStub, defaultKeyStroke);
|
||||
new SharedStubKeyBindingAction(name, actionTrigger, options);
|
||||
registerStub(newStub, actionTrigger);
|
||||
return newStub;
|
||||
});
|
||||
|
||||
|
@ -511,4 +596,11 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
stub.addActionOwner(owner);
|
||||
}
|
||||
|
||||
private ActionTrigger getActionTrigger(SharedDockingActionPlaceholder placeholder) {
|
||||
KeyStroke defaultKs = placeholder.getKeyBinding();
|
||||
if (defaultKs != null) {
|
||||
return new ActionTrigger(defaultKs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,9 @@ import java.beans.PropertyChangeListener;
|
|||
|
||||
import javax.swing.JButton;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.DockingActionPerformer;
|
||||
import docking.DockingWindowManager;
|
||||
import docking.action.*;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
/**
|
||||
* Class to manager toolbar buttons.
|
||||
|
@ -113,23 +112,7 @@ public class ToolBarItemManager implements PropertyChangeListener, ActionListene
|
|||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
DockingWindowManager.clearMouseOverHelp();
|
||||
ActionContext context = getWindowManager().createActionContext(toolBarAction);
|
||||
|
||||
context.setSourceObject(event.getSource());
|
||||
context.setEventClickModifiers(event.getModifiers());
|
||||
|
||||
// this gives the UI some time to repaint before executing the action
|
||||
Swing.runLater(() -> {
|
||||
if (toolBarAction.isValidContext(context) &&
|
||||
toolBarAction.isEnabledForContext(context)) {
|
||||
if (toolBarAction instanceof ToggleDockingActionIf) {
|
||||
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) toolBarAction;
|
||||
toggleAction.setSelected(!toggleAction.isSelected());
|
||||
}
|
||||
toolBarAction.actionPerformed(context);
|
||||
}
|
||||
});
|
||||
DockingActionPerformer.perform(toolBarAction, event, getWindowManager());
|
||||
}
|
||||
|
||||
private DockingWindowManager getWindowManager() {
|
||||
|
|
|
@ -149,9 +149,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
|
||||
public static Window getWindowByTitleContaining(Window parentWindow, String text) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if (!w.isShowing()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -169,9 +167,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
|
||||
protected static Window getWindowByTitle(Window parentWindow, String title) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if (!w.isShowing()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -212,9 +208,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= timeout) {
|
||||
|
||||
Set<Window> winList = getAllWindows();
|
||||
Iterator<Window> it = winList.iterator();
|
||||
while (it.hasNext()) {
|
||||
Window w = it.next();
|
||||
for (Window w : winList) {
|
||||
if (windowClass.isAssignableFrom(w.getClass()) && w.isShowing()) {
|
||||
return w;
|
||||
}
|
||||
|
@ -499,9 +493,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= DEFAULT_WINDOW_TIMEOUT) {
|
||||
|
||||
Set<Window> winList = getAllWindows();
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if ((w instanceof JDialog) && w.isShowing()) {
|
||||
String windowTitle = getTitleForWindow(w);
|
||||
if (title.equals(windowTitle)) {
|
||||
|
@ -534,9 +526,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
while (totalTime <= DEFAULT_WAIT_TIMEOUT) {
|
||||
|
||||
Set<Window> winList = getWindows(window);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
if ((w instanceof JDialog) && w.isShowing()) {
|
||||
String windowTitle = getTitleForWindow(w);
|
||||
if (title.equals(windowTitle)) {
|
||||
|
@ -637,9 +627,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
private static <T extends DialogComponentProvider> T getDialogComponent(Window parentWindow,
|
||||
Class<T> ghidraClass) {
|
||||
Set<Window> winList = getWindows(parentWindow);
|
||||
Iterator<Window> iter = winList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : winList) {
|
||||
DialogComponentProvider dialogComponentProvider =
|
||||
getDialogComponentProvider(w, ghidraClass);
|
||||
if (dialogComponentProvider != null) {
|
||||
|
@ -953,9 +941,7 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
// So, just ignore the exception. Client code that *really* wants all windows,
|
||||
// like that which waits for windows, should be calling this method repeatedly anyway.
|
||||
}
|
||||
Iterator<Window> iter = dockableWinList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Window w = iter.next();
|
||||
for (Window w : dockableWinList) {
|
||||
windowSet.add(w);
|
||||
findOwnedWindows(w, windowSet);
|
||||
}
|
||||
|
@ -1123,9 +1109,8 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
|
|||
public static Set<DockingActionIf> getActionsByOwnerAndName(Tool tool, String owner,
|
||||
String name) {
|
||||
Set<DockingActionIf> ownerActions = tool.getDockingActionsByOwnerName(owner);
|
||||
return ownerActions.stream()
|
||||
.filter(action -> action.getName().equals(name))
|
||||
.collect(Collectors.toSet());
|
||||
return ownerActions.stream().filter(action -> action.getName().equals(name)).collect(
|
||||
Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,8 +23,7 @@ import docking.Tool;
|
|||
public interface DockingToolConstants {
|
||||
|
||||
/**
|
||||
* Name of options for key bindings that map action name to a
|
||||
* key stroke object.
|
||||
*/
|
||||
* Name of options for key bindings that map action name to a key stroke or mouse binding.
|
||||
*/
|
||||
public final static String KEY_BINDINGS = "Key Bindings";
|
||||
}
|
||||
|
|
|
@ -81,6 +81,14 @@ public class HintTextField extends JTextField {
|
|||
validateField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint for this text field
|
||||
* @param hint the hint text
|
||||
*/
|
||||
public void setHint(String hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key listener allows us to check field validity on every key typed
|
||||
*/
|
||||
|
|
|
@ -32,10 +32,12 @@ import docking.*;
|
|||
import docking.action.*;
|
||||
import docking.test.AbstractDockingTest;
|
||||
import docking.tool.util.DockingToolConstants;
|
||||
import ghidra.framework.options.ActionTrigger;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SpyErrorLogger;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import gui.event.MouseBinding;
|
||||
|
||||
public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
||||
|
||||
|
@ -468,7 +470,15 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
|||
|
||||
private void setSharedKeyBinding(KeyStroke newKs) {
|
||||
ToolOptions options = getKeyBindingOptions();
|
||||
runSwing(() -> options.setKeyStroke(SHARED_FULL_NAME, newKs));
|
||||
runSwing(() -> {
|
||||
ActionTrigger actionTrigger = options.getActionTrigger(SHARED_FULL_NAME, null);
|
||||
MouseBinding existingMouseBinding = null;
|
||||
if (actionTrigger != null) {
|
||||
existingMouseBinding = actionTrigger.getMouseBinding();
|
||||
}
|
||||
ActionTrigger newTrigger = new ActionTrigger(newKs, existingMouseBinding);
|
||||
options.setActionTrigger(SHARED_FULL_NAME, newTrigger);
|
||||
});
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
|
@ -496,7 +506,10 @@ public class SharedKeyBindingDockingActionTest extends AbstractDockingTest {
|
|||
|
||||
public SharedNameAction(String owner, KeyStroke ks) {
|
||||
super(SHARED_NAME, owner, KeyBindingType.SHARED);
|
||||
setKeyBindingData(new KeyBindingData(ks));
|
||||
|
||||
if (ks != null) {
|
||||
setKeyBindingData(new KeyBindingData(ks));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue