/* * Copyright 2000-2017 JetBrains s.r.o. * * 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 com.intellij.openapi.keymap.impl; import com.intellij.ide.DataManager; import com.intellij.ide.IdeEventQueue; import com.intellij.ide.ProhibitAWTEvents; import com.intellij.ide.impl.DataManagerImpl; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.actionSystem.impl.ActionButtonWithText; import com.intellij.openapi.actionSystem.impl.PresentationFactory; import com.intellij.openapi.application.*; import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.keymap.KeyMapBundle; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.keymap.impl.keyGestures.KeyboardGestureProcessor; import com.intellij.openapi.keymap.impl.ui.ShortcutTextField; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.ListPopupStep; import com.intellij.openapi.ui.popup.PopupStep; import com.intellij.openapi.ui.popup.util.BaseListPopupStep; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.StatusBar; import com.intellij.openapi.wm.WindowManager; import com.intellij.openapi.wm.ex.StatusBarEx; import com.intellij.openapi.wm.impl.FloatingDecorator; import com.intellij.openapi.wm.impl.IdeFrameImpl; import com.intellij.openapi.wm.impl.IdeGlassPaneEx; import com.intellij.ui.ColoredListCellRenderer; import com.intellij.ui.ComponentWithMnemonics; import com.intellij.ui.KeyStrokeAdapter; import com.intellij.ui.SimpleTextAttributes; import com.intellij.ui.components.JBOptionButton; import com.intellij.ui.popup.list.ListPopupImpl; import com.intellij.ui.speedSearch.SpeedSearchSupply; import com.intellij.util.Alarm; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.KeyboardLayoutUtil; import com.intellij.util.ui.MacUIUtil; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.plaf.basic.ComboPopup; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.im.InputContext; import java.lang.reflect.Method; import java.util.*; import java.util.List; /** * This class is automaton with finite number of state. * * @author Anton Katilin * @author Vladimir Kondratyev */ public final class IdeKeyEventDispatcher implements Disposable { @NonNls private static final String GET_CACHED_STROKE_METHOD_NAME = "getCachedStroke"; private KeyStroke myFirstKeyStroke; /** * When we "dispatch" key event via keymap, i.e. when registered action has been executed * instead of event dispatching, then we have to consume all following KEY_RELEASED and * KEY_TYPED event because they are not valid. */ private boolean myPressedWasProcessed; private KeyState myState = KeyState.STATE_INIT; private final PresentationFactory myPresentationFactory = new PresentationFactory(); private boolean myDisposed; private boolean myLeftCtrlPressed; private boolean myRightAltPressed; private final KeyboardGestureProcessor myKeyGestureProcessor = new KeyboardGestureProcessor(this); private final KeyProcessorContext myContext = new KeyProcessorContext(); private final IdeEventQueue myQueue; private final Alarm mySecondStrokeTimeout = new Alarm(); private final Runnable mySecondStrokeTimeoutRunnable = () -> { if (myState == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) { resetState(); final DataContext dataContext = myContext.getDataContext(); StatusBar.Info.set(null, dataContext == null ? null : CommonDataKeys.PROJECT.getData(dataContext)); } }; private final Alarm mySecondKeystrokePopupTimeout = new Alarm(); public IdeKeyEventDispatcher(IdeEventQueue queue){ myQueue = queue; Application parent = ApplicationManager.getApplication(); // Application is null on early start when e.g. license dialog is shown if (parent != null) Disposer.register(parent, this); } public boolean isWaitingForSecondKeyStroke(){ return getState() == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE || isPressedWasProcessed(); } /** * @return {@code true} if and only if the passed event is already dispatched by the * {@code IdeKeyEventDispatcher} and there is no need for any other processing of the event. */ public boolean dispatchKeyEvent(final KeyEvent e){ if (myDisposed) return false; KeyboardLayoutUtil.storeAsciiForChar(e); if (e.isConsumed()) { return false; } if (isSpeedSearchEditing(e)) { return false; } // http://www.jetbrains.net/jira/browse/IDEADEV-12372 if (e.getKeyCode() == KeyEvent.VK_CONTROL) { if (e.getID() == KeyEvent.KEY_PRESSED) { myLeftCtrlPressed = e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT; } else if (e.getID() == KeyEvent.KEY_RELEASED) { myLeftCtrlPressed = false; } } else if (e.getKeyCode() == KeyEvent.VK_ALT) { if (e.getID() == KeyEvent.KEY_PRESSED) { myRightAltPressed = e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT; } else if (e.getID() == KeyEvent.KEY_RELEASED) { myRightAltPressed = false; } } KeyboardFocusManager focusManager=KeyboardFocusManager.getCurrentKeyboardFocusManager(); Component focusOwner = focusManager.getFocusOwner(); // shortcuts should not work in shortcut setup fields if (focusOwner instanceof ShortcutTextField) { return false; } if (focusOwner instanceof JTextComponent && ((JTextComponent)focusOwner).isEditable()) { if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED && e.getKeyCode() != KeyEvent.VK_ESCAPE) { MacUIUtil.hideCursor(); } } MenuSelectionManager menuSelectionManager=MenuSelectionManager.defaultManager(); MenuElement[] selectedPath = menuSelectionManager.getSelectedPath(); if(selectedPath.length>0){ if (!(selectedPath[0] instanceof ComboPopup)) { // The following couple of lines of code is a PATCH!!! // It is needed to ignore ENTER KEY_TYPED events which sometimes can reach editor when an action // is invoked from main menu via Enter key. setState(KeyState.STATE_PROCESSED); setPressedWasProcessed(true); return false; } } // Keymap shortcuts (i.e. not local shortcuts) should work only in: // - main frame // - floating focusedWindow // - when there's an editor in contexts Window focusedWindow = focusManager.getFocusedWindow(); boolean isModalContext = focusedWindow != null && isModalContext(focusedWindow); if (ApplicationManager.getApplication() == null) return false; //EA-39114 final DataManager dataManager = DataManager.getInstance(); if (dataManager == null) return false; DataContext dataContext = dataManager.getDataContext(); myContext.setDataContext(dataContext); myContext.setFocusOwner(focusOwner); myContext.setModalContext(isModalContext); myContext.setInputEvent(e); try { if (getState() == KeyState.STATE_INIT) { return inInitState(); } else if (getState() == KeyState.STATE_PROCESSED) { return inProcessedState(); } else if (getState() == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) { return inWaitForSecondStrokeState(); } else if (getState() == KeyState.STATE_SECOND_STROKE_IN_PROGRESS) { return inSecondStrokeInProgressState(); } else if (getState() == KeyState.STATE_KEY_GESTURE_PROCESSOR) { return myKeyGestureProcessor.process(); } else { throw new IllegalStateException("state = " + getState()); } } finally { myContext.clear(); } } private static boolean isSpeedSearchEditing(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_BACK_SPACE) { Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); if (owner instanceof JComponent) { SpeedSearchSupply supply = SpeedSearchSupply.getSupply((JComponent)owner); return supply != null && supply.isPopupActive(); } } return false; } /** * @return {@code true} if and only if the {@code component} represents * modal context. * @throws IllegalArgumentException if {@code component} is {@code null}. */ public static boolean isModalContext(@NotNull Component component) { Window window = UIUtil.getWindow(component); if (window instanceof IdeFrameImpl) { final Component pane = ((IdeFrameImpl) window).getGlassPane(); if (pane instanceof IdeGlassPaneEx) { return ((IdeGlassPaneEx) pane).isInModalContext(); } } if (window instanceof JDialog) { final JDialog dialog = (JDialog)window; if (!dialog.isModal()) { final Window owner = dialog.getOwner(); return owner != null && isModalContext(owner); } } if (window instanceof JFrame) { return false; } boolean isFloatingDecorator = window instanceof FloatingDecorator; boolean isPopup = !(component instanceof JFrame) && !(component instanceof JDialog); if (isPopup) { if (component instanceof JWindow) { JBPopup popup = (JBPopup)((JWindow)component).getRootPane().getClientProperty(JBPopup.KEY); if (popup != null) { return popup.isModalContext(); } } } return !isFloatingDecorator; } private boolean inWaitForSecondStrokeState() { // a key pressed means that the user starts to enter the second stroke... if (KeyEvent.KEY_PRESSED==myContext.getInputEvent().getID()) { setState(KeyState.STATE_SECOND_STROKE_IN_PROGRESS); return inSecondStrokeInProgressState(); } // looks like RELEASEs (from the first stroke) go here... skip them return true; } /** * This is hack. AWT doesn't allow to create KeyStroke with specified key code and key char * simultaneously. Therefore we are using reflection. */ private static KeyStroke getKeyStrokeWithoutMouseModifiers(KeyStroke originalKeyStroke){ int modifier=originalKeyStroke.getModifiers()&~InputEvent.BUTTON1_DOWN_MASK&~InputEvent.BUTTON1_MASK& ~InputEvent.BUTTON2_DOWN_MASK&~InputEvent.BUTTON2_MASK& ~InputEvent.BUTTON3_DOWN_MASK&~InputEvent.BUTTON3_MASK; try { Method[] methods=AWTKeyStroke.class.getDeclaredMethods(); Method getCachedStrokeMethod=null; for (Method method : methods) { if (GET_CACHED_STROKE_METHOD_NAME.equals(method.getName())) { getCachedStrokeMethod = method; getCachedStrokeMethod.setAccessible(true); break; } } if(getCachedStrokeMethod==null){ throw new IllegalStateException("not found method with name getCachedStrokeMethod"); } Object[] getCachedStrokeMethodArgs= {originalKeyStroke.getKeyChar(), originalKeyStroke.getKeyCode(), modifier, originalKeyStroke.isOnKeyRelease()}; return (KeyStroke)getCachedStrokeMethod.invoke(originalKeyStroke, getCachedStrokeMethodArgs); } catch(Exception exc){ throw new IllegalStateException(exc.getMessage()); } } private boolean inSecondStrokeInProgressState() { KeyEvent e = myContext.getInputEvent(); // when any key is released, we stop waiting for the second stroke if(KeyEvent.KEY_RELEASED==e.getID()){ myFirstKeyStroke=null; setState(KeyState.STATE_INIT); Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext()); StatusBar.Info.set(null, project); return false; } KeyStroke originalKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(e); if (originalKeyStroke == null) { return false; } KeyStroke keyStroke=getKeyStrokeWithoutMouseModifiers(originalKeyStroke); updateCurrentContext(myContext.getFoundComponent(), new KeyboardShortcut(myFirstKeyStroke, keyStroke), myContext.isModalContext()); // consume the wrong second stroke and keep on waiting if (myContext.getActions().isEmpty()) { return true; } // finally user had managed to enter the second keystroke, so let it be processed Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext()); StatusBarEx statusBar = (StatusBarEx) WindowManager.getInstance().getStatusBar(project); if (processAction(e, myActionProcessor)) { if (statusBar != null) { statusBar.setInfo(null); } return true; } else { return false; } } private boolean inProcessedState() { KeyEvent e = myContext.getInputEvent(); // ignore typed events which come after processed pressed event if (KeyEvent.KEY_TYPED == e.getID() && isPressedWasProcessed()) { return true; } if (KeyEvent.KEY_RELEASED == e.getID() && KeyEvent.VK_ALT == e.getKeyCode() && isPressedWasProcessed()) { //see IDEADEV-8615 return true; } setState(KeyState.STATE_INIT); setPressedWasProcessed(false); return inInitState(); } @NonNls private static final Set<String> ALT_GR_LAYOUTS = new HashSet<>(Arrays.asList( "pl", "de", "fi", "fr", "no", "da", "se", "pt", "nl", "tr", "sl", "hu", "bs", "hr", "sr", "sk", "lv" )); private boolean inInitState() { Component focusOwner = myContext.getFocusOwner(); boolean isModalContext = myContext.isModalContext(); DataContext dataContext = myContext.getDataContext(); KeyEvent e = myContext.getInputEvent(); // http://www.jetbrains.net/jira/browse/IDEADEV-12372 if (myLeftCtrlPressed && myRightAltPressed && focusOwner != null && e.getModifiers() == (InputEvent.CTRL_MASK | InputEvent.ALT_MASK)) { if (Registry.is("actionSystem.force.alt.gr")) { return false; } final InputContext inputContext = focusOwner.getInputContext(); if (inputContext != null) { Locale locale = inputContext.getLocale(); if (locale != null) { @NonNls final String language = locale.getLanguage(); if (ALT_GR_LAYOUTS.contains(language)) { // don't search for shortcuts return false; } } } } KeyStroke originalKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(e); if (originalKeyStroke == null) { return false; } KeyStroke keyStroke=getKeyStrokeWithoutMouseModifiers(originalKeyStroke); if (myKeyGestureProcessor.processInitState()) { return true; } if (SystemInfo.isMac) { boolean keyTyped = e.getID() == KeyEvent.KEY_TYPED; boolean hasMnemonicsInWindow = e.getID() == KeyEvent.KEY_PRESSED && hasMnemonicInWindow(focusOwner, e.getKeyCode()) || keyTyped && hasMnemonicInWindow(focusOwner, e.getKeyChar()); boolean imEnabled = IdeEventQueue.getInstance().isInputMethodEnabled(); if (e.getModifiersEx() == InputEvent.ALT_DOWN_MASK && (hasMnemonicsInWindow || !imEnabled && keyTyped)) { setPressedWasProcessed(true); setState(KeyState.STATE_PROCESSED); return false; } } updateCurrentContext(focusOwner, new KeyboardShortcut(keyStroke, null), isModalContext); if(myContext.getActions().isEmpty()) { // there's nothing mapped for this stroke return false; } if(myContext.isHasSecondStroke()){ myFirstKeyStroke=keyStroke; final ArrayList<Pair<AnAction, KeyStroke>> secondKeyStrokes = getSecondKeystrokeActions(); final Project project = CommonDataKeys.PROJECT.getData(dataContext); StringBuilder message = new StringBuilder(); message.append(KeyMapBundle.message("prefix.key.pressed.message")); message.append(' '); for (int i = 0; i < secondKeyStrokes.size(); i++) { Pair<AnAction, KeyStroke> pair = secondKeyStrokes.get(i); if (i > 0) message.append(", "); message.append(pair.getFirst().getTemplatePresentation().getText()); message.append(" ("); message.append(KeymapUtil.getKeystrokeText(pair.getSecond())); message.append(")"); } StatusBar.Info.set(message.toString(), project); mySecondStrokeTimeout.cancelAllRequests(); mySecondStrokeTimeout.addRequest(mySecondStrokeTimeoutRunnable, Registry.intValue("actionSystem.secondKeystrokeTimeout")); if (Registry.is("actionSystem.secondKeystrokeAutoPopupEnabled")) { mySecondKeystrokePopupTimeout.cancelAllRequests(); if (secondKeyStrokes.size() > 1) { final DataContext oldContext = myContext.getDataContext(); mySecondKeystrokePopupTimeout.addRequest(() -> { if (myState == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) { StatusBar.Info.set(null, CommonDataKeys.PROJECT.getData(oldContext)); new SecondaryKeystrokePopup(myFirstKeyStroke, secondKeyStrokes, oldContext).showInBestPositionFor(oldContext); } }, Registry.intValue("actionSystem.secondKeystrokePopupTimeout")); } } setState(KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE); return true; }else{ return processAction(e, myActionProcessor); } } private ArrayList<Pair<AnAction, KeyStroke>> getSecondKeystrokeActions() { ArrayList<Pair<AnAction, KeyStroke>> secondKeyStrokes = new ArrayList<>(); for (AnAction action : myContext.getActions()) { Shortcut[] shortcuts = action.getShortcutSet().getShortcuts(); for (Shortcut shortcut : shortcuts) { if (shortcut instanceof KeyboardShortcut) { KeyboardShortcut keyShortcut = (KeyboardShortcut)shortcut; if (keyShortcut.getFirstKeyStroke().equals(myFirstKeyStroke)) { secondKeyStrokes.add(Pair.create(action, keyShortcut.getSecondKeyStroke())); } } } } return secondKeyStrokes; } private static boolean hasMnemonicInWindow(Component focusOwner, int keyCode) { if (keyCode == KeyEvent.VK_ALT || keyCode == 0) return false; // Optimization final Container container = focusOwner == null ? null : UIUtil.getWindow(focusOwner); return hasMnemonic(container, keyCode) || hasMnemonicInBalloons(container, keyCode); } private static boolean hasMnemonic(final Container container, final int keyCode) { if (container == null) return false; final Component[] components = container.getComponents(); for (Component component : components) { if (component instanceof AbstractButton) { final AbstractButton button = (AbstractButton)component; if (button instanceof JBOptionButton) { if (((JBOptionButton)button).isOkToProcessDefaultMnemonics() || button.getMnemonic() == keyCode) { return true; } } else { if (button.getMnemonic() == keyCode) return true; } } if (component instanceof JLabel) { final JLabel label = (JLabel)component; if (label.getDisplayedMnemonic() == keyCode) return true; } if (component instanceof ActionButtonWithText) { if (((ActionButtonWithText)component).getMnemonic() == keyCode) return true; } if (component instanceof Container) { if (hasMnemonic((Container)component, keyCode)) return true; } } return false; } private static boolean hasMnemonicInBalloons(Container container, int code) { final Component parent = UIUtil.findUltimateParent(container); if (parent instanceof RootPaneContainer) { final JLayeredPane pane = ((RootPaneContainer)parent).getLayeredPane(); for (Component component : pane.getComponents()) { if (component instanceof ComponentWithMnemonics && component instanceof Container && hasMnemonic((Container)component, code)) { return true; } } } return false; } private final ActionProcessor myActionProcessor = new ActionProcessor() { @NotNull @Override public AnActionEvent createEvent(final InputEvent inputEvent, @NotNull final DataContext context, @NotNull final String place, @NotNull final Presentation presentation, final ActionManager manager) { return new AnActionEvent(inputEvent, context, place, presentation, manager, 0); } @Override public void onUpdatePassed(final InputEvent inputEvent, @NotNull final AnAction action, @NotNull final AnActionEvent actionEvent) { setState(KeyState.STATE_PROCESSED); setPressedWasProcessed(inputEvent.getID() == KeyEvent.KEY_PRESSED); } @Override public void performAction(@NotNull InputEvent e, @NotNull AnAction action, @NotNull AnActionEvent actionEvent) { e.consume(); DataContext ctx = actionEvent.getDataContext(); if (action instanceof ActionGroup && !((ActionGroup)action).canBePerformed(ctx)) { ActionGroup group = (ActionGroup)action; JBPopupFactory.getInstance() .createActionGroupPopup(group.getTemplatePresentation().getText(), group, ctx, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false) .showInBestPositionFor(ctx); } else { ActionUtil.performActionDumbAware(action, actionEvent); } if (Registry.is("actionSystem.fixLostTyping")) { IdeEventQueue.getInstance().doWhenReady(() -> IdeEventQueue.getInstance().getKeyEventDispatcher().resetState()); } } }; public boolean processAction(final InputEvent e, @NotNull ActionProcessor processor) { ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); final Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext()); final boolean dumb = project != null && DumbService.getInstance(project).isDumb(); List<AnActionEvent> nonDumbAwareAction = new ArrayList<>(); List<AnAction> actions = myContext.getActions(); for (final AnAction action : actions.toArray(new AnAction[actions.size()])) { Presentation presentation = myPresentationFactory.getPresentation(action); // Mouse modifiers are 0 because they have no any sense when action is invoked via keyboard final AnActionEvent actionEvent = processor.createEvent(e, myContext.getDataContext(), ActionPlaces.MAIN_MENU, presentation, ActionManager.getInstance()); try (AccessToken ignored = ProhibitAWTEvents.start("update")) { ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, actionEvent, true); } if (dumb && !action.isDumbAware()) { if (!Boolean.FALSE.equals(presentation.getClientProperty(ActionUtil.WOULD_BE_ENABLED_IF_NOT_DUMB_MODE))) { nonDumbAwareAction.add(actionEvent); } continue; } if (!presentation.isEnabled()) { continue; } processor.onUpdatePassed(e, action, actionEvent); if (myContext.getDataContext() instanceof DataManagerImpl.MyDataContext) { // this is not true for test data contexts ((DataManagerImpl.MyDataContext)myContext.getDataContext()).setEventCount(IdeEventQueue.getInstance().getEventCount(), this); } actionManager.fireBeforeActionPerformed(action, actionEvent.getDataContext(), actionEvent); Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(actionEvent.getDataContext()); if (component != null && !component.isShowing()) { return true; } ((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity( () -> processor.performAction(e, action, actionEvent)); actionManager.fireAfterActionPerformed(action, actionEvent.getDataContext(), actionEvent); return true; } if (!nonDumbAwareAction.isEmpty()) { showDumbModeWarningLaterIfNobodyConsumesEvent(e, nonDumbAwareAction.toArray(new AnActionEvent[nonDumbAwareAction.size()])); } return false; } private static void showDumbModeWarningLaterIfNobodyConsumesEvent(final InputEvent e, final AnActionEvent... actionEvents) { if (ModalityState.current() == ModalityState.NON_MODAL) { ApplicationManager.getApplication().invokeLater(() -> { if (e.isConsumed()) return; ActionUtil.showDumbModeWarning(actionEvents); }); } } /** * This method fills {@code myActions} list. * @return true if there is a shortcut with second stroke found. */ public KeyProcessorContext updateCurrentContext(Component component, Shortcut sc, boolean isModalContext){ myContext.setFoundComponent(null); myContext.getActions().clear(); if (isControlEnterOnDialog(component, sc)) return myContext; boolean hasSecondStroke = false; // here we try to find "local" shortcuts for (; component != null; component = component.getParent()) { if (!(component instanceof JComponent)) { continue; } List<AnAction> listOfActions = ActionUtil.getActions((JComponent)component); if (listOfActions.isEmpty()) { continue; } for (AnAction action : listOfActions) { hasSecondStroke |= addAction(action, sc); } // once we've found a proper local shortcut(s), we continue with non-local shortcuts if (!myContext.getActions().isEmpty()) { myContext.setFoundComponent((JComponent)component); break; } } // search in main keymap Keymap keymap = KeymapManager.getInstance().getActiveKeymap(); String[] actionIds = keymap.getActionIds(sc); ActionManager actionManager = ActionManager.getInstance(); for (String actionId : actionIds) { AnAction action = actionManager.getAction(actionId); if (action != null) { if (isModalContext && !action.isEnabledInModalContext()) { continue; } hasSecondStroke |= addAction(action, sc); } } if (!hasSecondStroke && sc instanceof KeyboardShortcut) { // little trick to invoke action which second stroke is a key w/o modifiers, but user still // holds the modifier key(s) of the first stroke final KeyboardShortcut keyboardShortcut = (KeyboardShortcut)sc; final KeyStroke firstKeyStroke = keyboardShortcut.getFirstKeyStroke(); final KeyStroke secondKeyStroke = keyboardShortcut.getSecondKeyStroke(); if (secondKeyStroke != null && secondKeyStroke.getModifiers() != 0 && firstKeyStroke.getModifiers() != 0) { final KeyboardShortcut altShortCut = new KeyboardShortcut(firstKeyStroke, KeyStroke.getKeyStroke(secondKeyStroke.getKeyCode(), 0)); final String[] additionalActions = keymap.getActionIds(altShortCut); for (final String actionId : additionalActions) { AnAction action = actionManager.getAction(actionId); if (action != null) { if (isModalContext && !action.isEnabledInModalContext()) { continue; } hasSecondStroke |= addAction(action, altShortCut); } } } } myContext.setHasSecondStroke(hasSecondStroke); final List<AnAction> actions = myContext.getActions(); if (actions.size() > 1) { final List<AnAction> readOnlyActions = Collections.unmodifiableList(actions); for (ActionPromoter promoter : ActionPromoter.EP_NAME.getExtensions()) { final List<AnAction> promoted = promoter.promote(readOnlyActions, myContext.getDataContext()); if (promoted == null || promoted.isEmpty()) continue; actions.removeAll(promoted); actions.addAll(0, promoted); } } return myContext; } private static final KeyboardShortcut CONTROL_ENTER = KeyboardShortcut.fromString("control ENTER"); private static boolean isControlEnterOnDialog(Component component, Shortcut sc) { return CONTROL_ENTER.equals(sc) && !IdeEventQueue.getInstance().isPopupActive() //avoid Control+Enter in completion && DialogWrapper.findInstance(component) != null; } /** * @return true if action is added and has second stroke */ private boolean addAction(AnAction action, Shortcut sc) { boolean hasSecondStroke = false; Shortcut[] shortcuts = action.getShortcutSet().getShortcuts(); for (Shortcut each : shortcuts) { if (!each.isKeyboard()) continue; if (each.startsWith(sc)) { if (!myContext.getActions().contains(action)) { myContext.getActions().add(action); } if (each instanceof KeyboardShortcut) { hasSecondStroke |= ((KeyboardShortcut)each).getSecondKeyStroke() != null; } } } return hasSecondStroke; } public KeyProcessorContext getContext() { return myContext; } @Override public void dispose() { myDisposed = true; } public KeyState getState() { return myState; } public void setState(final KeyState state) { myState = state; if (myQueue != null) { myQueue.maybeReady(); } } public void resetState() { setState(KeyState.STATE_INIT); setPressedWasProcessed(false); } private boolean isPressedWasProcessed() { return myPressedWasProcessed; } private void setPressedWasProcessed(boolean pressedWasProcessed) { myPressedWasProcessed = pressedWasProcessed; } public boolean isReady() { return myState == KeyState.STATE_INIT || myState == KeyState.STATE_PROCESSED; } private static class SecondaryKeystrokePopup extends ListPopupImpl { private SecondaryKeystrokePopup(@NotNull final KeyStroke firstKeystroke, @NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext context) { super(buildStep(actions, context)); registerActions(firstKeystroke, actions, context); } private void registerActions(@NotNull final KeyStroke firstKeyStroke, @NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext ctx) { ContainerUtil.process(actions, pair -> { final String actionText = pair.getFirst().getTemplatePresentation().getText(); final AbstractAction a = new AbstractAction() { @Override public void actionPerformed(final ActionEvent e) { cancel(); invokeAction(pair.getFirst(), ctx); } }; final KeyStroke keyStroke = pair.getSecond(); if (keyStroke != null) { registerAction(actionText, keyStroke, a); if (keyStroke.getModifiers() == 0) { // do a little trick here, so if I will press Command+R and the second keystroke is just 'R', // I want to be able to hold the Command while pressing 'R' final KeyStroke additionalKeyStroke = KeyStroke.getKeyStroke(keyStroke.getKeyCode(), firstKeyStroke.getModifiers()); final String _existing = getActionForKeyStroke(additionalKeyStroke); if (_existing == null) registerAction("__additional__" + actionText, additionalKeyStroke, a); } } return true; }); } private static void invokeAction(@NotNull final AnAction action, final DataContext ctx) { TransactionGuard.submitTransaction(ApplicationManager.getApplication(), () -> { final AnActionEvent event = new AnActionEvent(null, ctx, ActionPlaces.UNKNOWN, action.getTemplatePresentation().clone(), ActionManager.getInstance(), 0); if (ActionUtil.lastUpdateAndCheckDumb(action, event, true)) { ActionUtil.performActionDumbAware(action, event); } }); } @Override protected ListCellRenderer getListElementRenderer() { return new ActionListCellRenderer(); } private static ListPopupStep buildStep(@NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext ctx) { return new BaseListPopupStep<Pair<AnAction, KeyStroke>>("Choose an action", ContainerUtil.findAll(actions, pair -> { final AnAction action = pair.getFirst(); final Presentation presentation = action.getTemplatePresentation().clone(); AnActionEvent event = new AnActionEvent(null, ctx, ActionPlaces.UNKNOWN, presentation, ActionManager.getInstance(), 0); ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, event, true); return presentation.isEnabled() && presentation.isVisible(); })) { @Override public PopupStep onChosen(Pair<AnAction, KeyStroke> selectedValue, boolean finalChoice) { invokeAction(selectedValue.getFirst(), ctx); return FINAL_CHOICE; } }; } private static class ActionListCellRenderer extends ColoredListCellRenderer { @Override protected void customizeCellRenderer(@NotNull final JList list, final Object value, final int index, final boolean selected, final boolean hasFocus) { if (value instanceof Pair) { //noinspection unchecked final Pair<AnAction, KeyStroke> pair = (Pair<AnAction, KeyStroke>) value; append(KeymapUtil.getShortcutText(new KeyboardShortcut(pair.getSecond(), null)), SimpleTextAttributes.GRAY_ATTRIBUTES); appendTextPadding(30); final String text = pair.getFirst().getTemplatePresentation().getText(); if (text != null) { append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES); } } } } } }