/* ### * 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 static docking.KeyBindingPrecedence.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.*; import javax.swing.text.JTextComponent; import docking.actions.KeyBindingUtils; import docking.menu.keys.MenuKeyProcessor; import ghidra.util.bean.GGlassPane; import ghidra.util.exception.AssertException; /** * Allows Ghidra to give preference to its key event processing over the default Java key event * processing. See {@link #dispatchKeyEvent(KeyEvent)} for a more detailed explanation of how * Ghidra processes key events. *

* {@link #install()} must be called in order to install this Singleton into Java's * key event processing system. */ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher { private static KeyBindingOverrideKeyEventDispatcher instance = null; /** * We use this action as a signal that we intend to process a key * binding and that no other Java component should try to handle it (sometimes Java processes * bindings on key typed, after we have processed a binding on key pressed, which is not * what we want). *

* This action is one that is triggered by a key pressed, but will be processed on a * key released. We need to do this for because on some systems, when we perform the * action on a key pressed, we do not get the follow-on key events, which we need to reset * our state (SCR 7040). *

* Posterity Note: While debugging we will not get a KeyEvent.KEY_RELEASED event if * the focus changes from the application to the debugger tool. */ private ExecutableAction inProgressAction; /** * Provides the current focus owner. This allows for dependency injection. */ private FocusOwnerProvider focusProvider = new DefaultFocusOwnerProvider(); /** * Installs this key event dispatcher into Java's key event processing system. Calling this * method more than once has no effect. */ static void install() { if (instance == null) { instance = new KeyBindingOverrideKeyEventDispatcher(); KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); kfm.addKeyEventDispatcher(instance); } } /** * Overridden to change the Java's key event processing to insert Ghidra's top level tool * key bindings into the event processing. Java's normal key event processing is: *

    *
  1. KeyListeners on the focused Component
  2. *
  3. InputMap and ActionMap actions for the Component
  4. *
  5. InputMap and ActionMap actions for the Component's parent, and so on up the * Swing hierarchy
  6. *
* Ghidra has altered this flow to be: *
    *
  1. Reserved keybinding actions
  2. *
  3. KeyListeners on the focused Component
  4. *
  5. InputMap and ActionMap actions for the Component
  6. *
  7. Ghidra tool-level actions
  8. *
  9. InputMap and ActionMap actions for the Component's parent, and so on up the * Swing hierarchy
  10. *
* This updated key event processing allows individual components to handle key events first, * but then allows global Ghidra key bindings to be processed, allowing normal Java processing * after Ghidra has had its chance to process the event. *

* There are some exceptions to this processing chain: *

    *
  1. We don't do any processing when the focused component is an instance of * JTextComponent.
  2. *
  3. We don't do any processing if the active window is an instance of * DockingDialog.
  4. *
* * @see java.awt.KeyEventDispatcher#dispatchKeyEvent(java.awt.event.KeyEvent) */ @Override public boolean dispatchKeyEvent(KeyEvent event) { if (blockKeyInput(event)) { return true; // let NO events through! } // always let Ghidra finish processing key events that it started if (actionInProgress(event)) { return true; } // Special case for when we use one of our built-in menu navigation actions. We ignore // docking actions if a menu is open (this is done in action.getEnabledState()). if (MenuKeyProcessor.processMenuKeyEvent(event)) { return true; } DockingKeyBindingAction action = getActionForEvent(event); if (action == null) { return false; // let the normal event flow continue } // *Special*, System key bindings--these can always be processed and are a higher priority Component focusOwner = focusProvider.getFocusOwner(); ExecutableAction executableAction = action.getExecutableAction(focusOwner); if (processSystemActionPrecedence(executableAction, event)) { return true; } if (willBeHandledByTextComponent(event)) { return false; } if (!executableAction.isValid()) { // The action is not currently valid for the given focus owner. Let all key strokes go // to Java when we have no valid context. This allows keys like Escape to work on Java // widgets. return false; } if (!executableAction.isEnabled()) { // The action is valid, but is not enabled. In this case, we do not want Java to get // the event. Instead, let the user know that they cannot perform the requested action. // We keep the action from Java in this case to keep the event processing consistent // whether or not the action is enabled. executableAction.reportNotEnabled(focusOwner); return true; } // Process the key event in precedence order. // If it processes the event at any given level, the short-circuit operator will kick out. // Finally, if the exception statement is reached, then someone has added a new level // of precedence that this algorithm has not taken into account! // @formatter:off return processKeyListenerPrecedence(executableAction, event) || processComponentActionMapPrecedence(executableAction, event) || processActionAtPrecedence(DefaultLevel, executableAction, event) || throwAssertException(); // @formatter:on } /** * Returns true if the given key event should be blocked (i.e., not processed by us or Java). */ private boolean blockKeyInput(KeyEvent event) { Component component = event.getComponent(); if (component == null) { // We are for managing GUI keyboard input--don't care about the event if this happens return false; } JRootPane rootPane = SwingUtilities.getRootPane(component); if (rootPane == null) { // This can happen when the source component of the key event has been hidden as a // result of processing the key event earlier, like on a key pressed event; for // example, when the user presses the ESC key to close a dialog. return true; // don't let Java process the remaining event chain } Component glassPane = rootPane.getGlassPane(); if (glassPane instanceof GGlassPane) { if (((GGlassPane) glassPane).isBusy()) { return true; // out parent's glass pane is blocking..don't let events through } } // else { // Msg.debug( KeyBindingOverrideKeyEventDispatcher.this, // "Found a window with a non-standard glass pane--this should be fixed to " + // "use the Docking windowing system" ); // } return false; } /** * Used to clear the flag that signals we are in the middle of processing a Ghidra action. */ private boolean actionInProgress(KeyEvent event) { boolean wasInProgress = inProgressAction != null; if (event.getID() == KeyEvent.KEY_RELEASED) { ExecutableAction action = inProgressAction; inProgressAction = null; if (action != null) { action.execute(); } } return wasInProgress; } private boolean isSettingKeyBindings(KeyEvent event) { Component destination = event.getComponent(); if (destination == null) { Component focusOwner = focusProvider.getFocusOwner(); destination = focusOwner; } return destination instanceof KeyEntryTextField; } private boolean willBeHandledByTextComponent(KeyEvent event) { Component destination = event.getComponent(); if (destination == null) { Component focusOwner = focusProvider.getFocusOwner(); destination = focusOwner; } if (!(destination instanceof JTextComponent textComponent)) { return false; // we only handle text components } // Note: don't do this--it breaks key event handling for text components, as they do // not get to handle key events when they are not editable (they still should // though, so things like built-in copy/paste still work). // JTextComponent textComponent = (JTextComponent) focusOwner; // if (!textComponent.isEditable()) { // return false; // } // Special Case: We allow Escape to go through. This doesn't seem useful to text widgets // but does allow for closing of windows. If we find text widgets that need Escape, then // we will have to update how we make this decision, such as by having the concerned text // widgets register actions for Escape and then check for that action. int code = event.getKeyCode(); if (code == KeyEvent.VK_ESCAPE) { // Cell editors will process the Escape key, so let them have it. Otherwise, allow the // system to process the Escape key as, described above. return isCellEditing(textComponent); } // We've made the executive decision to allow all keys to go through to the text component // unless they are modified with the 'Alt'/'Ctrl'/etc keys, unless they directly used // by the text component if (!isModified(event)) { return true; // unmodified keys will be given to the text component } // the key is modified; let it through if the component has a mapping for the key return hasRegisteredKeyBinding(textComponent, event); } private boolean isCellEditing(JTextComponent c) { Container parent = c.getParent(); while (parent != null) { if (parent instanceof JTree tree) { return tree.isEditing(); } else if (parent instanceof JTable table) { return table.isEditing(); } parent = parent.getParent(); } return false; } /** * A test to see if the given event is modified in such a way as a text component would not * handle the event * @param e the event * @return true if modified */ private boolean isModified(KeyEvent e) { return e.isAltDown() || e.isAltGraphDown() || e.isMetaDown() || e.isControlDown(); } private boolean hasRegisteredKeyBinding(JComponent c, KeyEvent event) { KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(event); Action action = getJavaActionForComponent(c, keyStroke); return action != null; } /** * This method should only be called if a programmer adds a new precedence to * {@link KeyBindingPrecedence} and does not update the algorithm of * {@link #dispatchKeyEvent(KeyEvent)} to take into account the new precedence. */ private boolean throwAssertException() { throw new AssertException("New precedence added to KeyBindingPrecedence?"); } private boolean processSystemActionPrecedence(ExecutableAction executableAction, KeyEvent event) { if (isSettingKeyBindings(event)) { // This means the user is setting keybindings. Do not process System actions during // this operation so that the user can assign those keybindings. return false; } KeyBindingPrecedence precedence = executableAction.getKeyBindingPrecedence(); if (precedence != SystemActionsLevel) { return false; } inProgressAction = executableAction; // this will be handled on the release return true; } private boolean processKeyListenerPrecedence(ExecutableAction action, KeyEvent e) { if (processActionAtPrecedence(KeyBindingPrecedence.KeyListenerLevel, action, e)) { return true; } // O.K., there is an action for the KeyStroke, but before we process it, we have to // check the proper ordering of key events (see method JavaDoc) if (processComponentKeyListeners(e)) { return true; } return false; } private boolean processComponentActionMapPrecedence(ExecutableAction action, KeyEvent event) { if (processActionAtPrecedence(ActionMapLevel, action, event)) { return true; } KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(event); if (processInputAndActionMaps(event, keyStroke)) { return true; } return false; } private boolean processActionAtPrecedence(KeyBindingPrecedence keyBindingPrecedence, ExecutableAction action, KeyEvent event) { KeyBindingPrecedence actionPrecedence = action.getKeyBindingPrecedence(); if (keyBindingPrecedence != actionPrecedence) { return false; } if (inProgressAction != null) { return true; } inProgressAction = action; // this will be handled on the release event.consume(); // don't let this event be used later return true; } private boolean processComponentKeyListeners(KeyEvent keyEvent) { Component focusOwner = focusProvider.getFocusOwner(); if (focusOwner == null) { return false; } KeyListener[] keyListeners = focusOwner.getKeyListeners(); for (KeyListener listener : keyListeners) { int id = keyEvent.getID(); switch (id) { case KeyEvent.KEY_TYPED: listener.keyTyped(keyEvent); break; case KeyEvent.KEY_PRESSED: listener.keyPressed(keyEvent); break; case KeyEvent.KEY_RELEASED: listener.keyReleased(keyEvent); break; } } return keyEvent.isConsumed(); } // note: this code is taken from the JComponent method: // protected boolean processKeyBinding(KeyStroke, KeyEvent, int, boolean ) // // returns true if there is a focused component that has an action for the given keystroke // and it processes that action. private boolean processInputAndActionMaps(KeyEvent keyEvent, KeyStroke keyStroke) { Component focusOwner = focusProvider.getFocusOwner(); if (focusOwner == null || !focusOwner.isEnabled() || !(focusOwner instanceof JComponent)) { return false; } JComponent jComponent = (JComponent) focusOwner; Action action = getJavaActionForComponent(jComponent, keyStroke); if (action != null) { return SwingUtilities.notifyAction(action, keyStroke, keyEvent, keyEvent.getSource(), keyEvent.getModifiersEx()); } return false; } private Action getJavaActionForComponent(JComponent jComponent, KeyStroke keyStroke) { // first see if there is a Java key binding for when the component is in the focused // window... Action action = KeyBindingUtils.getAction(jComponent, keyStroke, JComponent.WHEN_FOCUSED); if (action != null) { return action; } // ...next see if there is a key binding for when the component is the child of the focus // owner action = KeyBindingUtils.getAction(jComponent, keyStroke, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); return action; } /** * Gets a {@link DockingKeyBindingAction} that is registered for the given key event. This * method is aware of context for things like {@link DockingWindowManager} and active windows. * @param event The key event to check. * @return An action, if one is available for the given key event, in the current context. */ private DockingKeyBindingAction getActionForEvent(KeyEvent event) { DockingWindowManager activeManager = getActiveDockingWindowManager(); if (activeManager == null) { return null; } KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(event); DockingKeyBindingAction bindingAction = (DockingKeyBindingAction) activeManager.getActionForKeyStroke(keyStroke); 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; } void setFocusOwnerProvider(FocusOwnerProvider focusProvider) { this.focusProvider = focusProvider; } }