IDEA-161281 Find in Path dialog: Mnemonics misbehaviour on OS X
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / keymap / impl / IdeKeyEventDispatcher.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.keymap.impl;
17
18 import com.intellij.ide.DataManager;
19 import com.intellij.ide.IdeEventQueue;
20 import com.intellij.ide.ProhibitAWTEvents;
21 import com.intellij.ide.impl.DataManagerImpl;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.actionSystem.*;
24 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
25 import com.intellij.openapi.actionSystem.ex.ActionUtil;
26 import com.intellij.openapi.actionSystem.impl.PresentationFactory;
27 import com.intellij.openapi.application.*;
28 import com.intellij.openapi.application.impl.LaterInvocator;
29 import com.intellij.openapi.keymap.KeyMapBundle;
30 import com.intellij.openapi.keymap.Keymap;
31 import com.intellij.openapi.keymap.KeymapManager;
32 import com.intellij.openapi.keymap.KeymapUtil;
33 import com.intellij.openapi.keymap.impl.keyGestures.KeyboardGestureProcessor;
34 import com.intellij.openapi.keymap.impl.ui.ShortcutTextField;
35 import com.intellij.openapi.project.DumbService;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.ui.DialogWrapper;
38 import com.intellij.openapi.ui.popup.JBPopup;
39 import com.intellij.openapi.ui.popup.JBPopupFactory;
40 import com.intellij.openapi.ui.popup.ListPopupStep;
41 import com.intellij.openapi.ui.popup.PopupStep;
42 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
43 import com.intellij.openapi.util.Disposer;
44 import com.intellij.openapi.util.Pair;
45 import com.intellij.openapi.util.SystemInfo;
46 import com.intellij.openapi.util.registry.Registry;
47 import com.intellij.openapi.wm.StatusBar;
48 import com.intellij.openapi.wm.WindowManager;
49 import com.intellij.openapi.wm.ex.StatusBarEx;
50 import com.intellij.openapi.wm.impl.FloatingDecorator;
51 import com.intellij.openapi.wm.impl.IdeFrameImpl;
52 import com.intellij.openapi.wm.impl.IdeGlassPaneEx;
53 import com.intellij.ui.ColoredListCellRenderer;
54 import com.intellij.ui.ComponentWithMnemonics;
55 import com.intellij.ui.KeyStrokeAdapter;
56 import com.intellij.ui.SimpleTextAttributes;
57 import com.intellij.ui.components.JBOptionButton;
58 import com.intellij.ui.popup.list.ListPopupImpl;
59 import com.intellij.ui.speedSearch.SpeedSearchSupply;
60 import com.intellij.util.Alarm;
61 import com.intellij.util.containers.ContainerUtil;
62 import com.intellij.util.ui.KeyboardLayoutUtil;
63 import com.intellij.util.ui.MacUIUtil;
64 import com.intellij.util.ui.UIUtil;
65 import org.jetbrains.annotations.NonNls;
66 import org.jetbrains.annotations.NotNull;
67 import org.jetbrains.annotations.Nullable;
68
69 import javax.swing.*;
70 import javax.swing.plaf.basic.ComboPopup;
71 import javax.swing.text.JTextComponent;
72 import java.awt.*;
73 import java.awt.event.ActionEvent;
74 import java.awt.event.InputEvent;
75 import java.awt.event.KeyEvent;
76 import java.awt.im.InputContext;
77 import java.lang.reflect.Method;
78 import java.util.*;
79 import java.util.List;
80
81 /**
82  * This class is automaton with finite number of state.
83  *
84  * @author Anton Katilin
85  * @author Vladimir Kondratyev
86  */
87 public final class IdeKeyEventDispatcher implements Disposable {
88   @NonNls
89   private static final String GET_CACHED_STROKE_METHOD_NAME = "getCachedStroke";
90
91   private KeyStroke myFirstKeyStroke;
92   /**
93    * When we "dispatch" key event via keymap, i.e. when registered action has been executed
94    * instead of event dispatching, then we have to consume all following KEY_RELEASED and
95    * KEY_TYPED event because they are not valid.
96    */
97   private boolean myPressedWasProcessed;
98   private KeyState myState = KeyState.STATE_INIT;
99
100   private final PresentationFactory myPresentationFactory = new PresentationFactory();
101   private boolean myDisposed = false;
102   private boolean myLeftCtrlPressed = false;
103   private boolean myRightAltPressed = false;
104
105   private final KeyboardGestureProcessor myKeyGestureProcessor = new KeyboardGestureProcessor(this);
106
107   private final KeyProcessorContext myContext = new KeyProcessorContext();
108   private final IdeEventQueue myQueue;
109
110   private final Alarm mySecondStrokeTimeout = new Alarm();
111   private final Runnable mySecondStrokeTimeoutRunnable = () -> {
112     if (myState == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) {
113       resetState();
114       final DataContext dataContext = myContext.getDataContext();
115       StatusBar.Info.set(null, dataContext == null ? null : CommonDataKeys.PROJECT.getData(dataContext));
116     }
117   };
118
119   private final Alarm mySecondKeystrokePopupTimeout = new Alarm();
120
121   public IdeKeyEventDispatcher(IdeEventQueue queue){
122     myQueue = queue;
123     Application parent = ApplicationManager.getApplication();  // Application is null on early start when e.g. license dialog is shown
124     if (parent != null) Disposer.register(parent, this);
125   }
126
127   public boolean isWaitingForSecondKeyStroke(){
128     return getState() == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE || isPressedWasProcessed();
129   }
130
131   /**
132    * @return <code>true</code> if and only if the passed event is already dispatched by the
133    * <code>IdeKeyEventDispatcher</code> and there is no need for any other processing of the event.
134    */
135   public boolean dispatchKeyEvent(final KeyEvent e){
136     if (myDisposed) return false;
137     KeyboardLayoutUtil.storeAsciiForChar(e);
138
139     if (e.isConsumed()) {
140       return false;
141     }
142
143     if (isSpeedSearchEditing(e)) {
144       return false;
145     }
146
147     // http://www.jetbrains.net/jira/browse/IDEADEV-12372
148     if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
149       if (e.getID() == KeyEvent.KEY_PRESSED) {
150         myLeftCtrlPressed = e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT;
151       }
152       else if (e.getID() == KeyEvent.KEY_RELEASED) {
153         myLeftCtrlPressed = false;
154       }
155     }
156     else if (e.getKeyCode() == KeyEvent.VK_ALT) {
157       if (e.getID() == KeyEvent.KEY_PRESSED) {
158         myRightAltPressed = e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT;
159       }
160       else if (e.getID() == KeyEvent.KEY_RELEASED) {
161         myRightAltPressed = false;
162       }
163     }
164
165     KeyboardFocusManager focusManager=KeyboardFocusManager.getCurrentKeyboardFocusManager();
166     Component focusOwner = focusManager.getFocusOwner();
167
168     // shortcuts should not work in shortcut setup fields
169     if (focusOwner instanceof ShortcutTextField) {
170       return false;
171     }
172     if (focusOwner instanceof JTextComponent && ((JTextComponent)focusOwner).isEditable()) {
173       if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED && e.getKeyCode() != KeyEvent.VK_ESCAPE) {
174         MacUIUtil.hideCursor();
175       }
176     }
177
178     MenuSelectionManager menuSelectionManager=MenuSelectionManager.defaultManager();
179     MenuElement[] selectedPath = menuSelectionManager.getSelectedPath();
180     if(selectedPath.length>0){
181       if (!(selectedPath[0] instanceof ComboPopup)) {
182         // The following couple of lines of code is a PATCH!!!
183         // It is needed to ignore ENTER KEY_TYPED events which sometimes can reach editor when an action
184         // is invoked from main menu via Enter key.
185         setState(KeyState.STATE_PROCESSED);
186         setPressedWasProcessed(true);
187         return false;
188       }
189     }
190
191     // Keymap shortcuts (i.e. not local shortcuts) should work only in:
192     // - main frame
193     // - floating focusedWindow
194     // - when there's an editor in contexts
195     Window focusedWindow = focusManager.getFocusedWindow();
196     boolean isModalContext = focusedWindow != null && isModalContext(focusedWindow);
197
198     if (ApplicationManager.getApplication() == null) return false; //EA-39114
199
200     final DataManager dataManager = DataManager.getInstance();
201     if (dataManager == null) return false;
202
203     DataContext dataContext = dataManager.getDataContext();
204
205     myContext.setDataContext(dataContext);
206     myContext.setFocusOwner(focusOwner);
207     myContext.setModalContext(isModalContext);
208     myContext.setInputEvent(e);
209
210     try {
211       if (getState() == KeyState.STATE_INIT) {
212         return inInitState();
213       }
214       else if (getState() == KeyState.STATE_PROCESSED) {
215         return inProcessedState();
216       }
217       else if (getState() == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) {
218         return inWaitForSecondStrokeState();
219       }
220       else if (getState() == KeyState.STATE_SECOND_STROKE_IN_PROGRESS) {
221         return inSecondStrokeInProgressState();
222       }
223       else if (getState() == KeyState.STATE_KEY_GESTURE_PROCESSOR) {
224         return myKeyGestureProcessor.process();
225       }
226       else {
227         throw new IllegalStateException("state = " + getState());
228       }
229     }
230     finally {
231       myContext.clear();
232     }
233   }
234
235   private static boolean isSpeedSearchEditing(KeyEvent e) {
236     int keyCode = e.getKeyCode();
237     if (keyCode == KeyEvent.VK_BACK_SPACE) {
238       Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
239       if (owner instanceof JComponent) {
240         SpeedSearchSupply supply = SpeedSearchSupply.getSupply((JComponent)owner);
241         return supply != null && supply.isPopupActive();
242       }
243     }
244     return false;
245   }
246
247   /**
248    * @return <code>true</code> if and only if the <code>component</code> represents
249    * modal context.
250    * @throws IllegalArgumentException if <code>component</code> is <code>null</code>.
251    */
252   public static boolean isModalContext(@NotNull Component component) {
253     Window window = UIUtil.getWindow(component);
254
255     if (window instanceof IdeFrameImpl) {
256       final Component pane = ((IdeFrameImpl) window).getGlassPane();
257       if (pane instanceof IdeGlassPaneEx) {
258         return ((IdeGlassPaneEx) pane).isInModalContext();
259       }
260     }
261
262     if (window instanceof JDialog) {
263       final JDialog dialog = (JDialog)window;
264       if (!dialog.isModal()) {
265         final Window owner = dialog.getOwner();
266         return owner != null && isModalContext(owner);
267       }
268     }
269
270     if (window instanceof JFrame) {
271       return false;
272     }
273
274     boolean isFloatingDecorator = window instanceof FloatingDecorator;
275
276     boolean isPopup = !(component instanceof JFrame) && !(component instanceof JDialog);
277     if (isPopup) {
278       if (component instanceof JWindow) {
279         JBPopup popup = (JBPopup)((JWindow)component).getRootPane().getClientProperty(JBPopup.KEY);
280         if (popup != null) {
281           return popup.isModalContext();
282         }
283       }
284     }
285
286     return !isFloatingDecorator;
287   }
288
289   private boolean inWaitForSecondStrokeState() {
290     // a key pressed means that the user starts to enter the second stroke...
291     if (KeyEvent.KEY_PRESSED==myContext.getInputEvent().getID()) {
292       setState(KeyState.STATE_SECOND_STROKE_IN_PROGRESS);
293       return inSecondStrokeInProgressState();
294     }
295     // looks like RELEASEs (from the first stroke) go here...  skip them
296     return true;
297   }
298
299   /**
300    * This is hack. AWT doesn't allow to create KeyStroke with specified key code and key char
301    * simultaneously. Therefore we are using reflection.
302    */
303   private static KeyStroke getKeyStrokeWithoutMouseModifiers(KeyStroke originalKeyStroke){
304     int modifier=originalKeyStroke.getModifiers()&~InputEvent.BUTTON1_DOWN_MASK&~InputEvent.BUTTON1_MASK&
305                  ~InputEvent.BUTTON2_DOWN_MASK&~InputEvent.BUTTON2_MASK&
306                  ~InputEvent.BUTTON3_DOWN_MASK&~InputEvent.BUTTON3_MASK;
307     try {
308       Method[] methods=AWTKeyStroke.class.getDeclaredMethods();
309       Method getCachedStrokeMethod=null;
310       for (Method method : methods) {
311         if (GET_CACHED_STROKE_METHOD_NAME.equals(method.getName())) {
312           getCachedStrokeMethod = method;
313           getCachedStrokeMethod.setAccessible(true);
314           break;
315         }
316       }
317       if(getCachedStrokeMethod==null){
318         throw new IllegalStateException("not found method with name getCachedStrokeMethod");
319       }
320       Object[] getCachedStrokeMethodArgs=
321         {originalKeyStroke.getKeyChar(), originalKeyStroke.getKeyCode(), modifier, originalKeyStroke.isOnKeyRelease()};
322       return (KeyStroke)getCachedStrokeMethod.invoke(originalKeyStroke, getCachedStrokeMethodArgs);
323     }
324     catch(Exception exc){
325       throw new IllegalStateException(exc.getMessage());
326     }
327   }
328
329   private boolean inSecondStrokeInProgressState() {
330     KeyEvent e = myContext.getInputEvent();
331
332     // when any key is released, we stop waiting for the second stroke
333     if(KeyEvent.KEY_RELEASED==e.getID()){
334       myFirstKeyStroke=null;
335       setState(KeyState.STATE_INIT);
336       Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext());
337       StatusBar.Info.set(null, project);
338       return false;
339     }
340
341     KeyStroke originalKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(e);
342     if (originalKeyStroke == null) {
343       return false;
344     }
345     KeyStroke keyStroke=getKeyStrokeWithoutMouseModifiers(originalKeyStroke);
346
347     updateCurrentContext(myContext.getFoundComponent(), new KeyboardShortcut(myFirstKeyStroke, keyStroke), myContext.isModalContext());
348
349     // consume the wrong second stroke and keep on waiting
350     if (myContext.getActions().isEmpty()) {
351       return true;
352     }
353
354     // finally user had managed to enter the second keystroke, so let it be processed
355     Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext());
356     StatusBarEx statusBar = (StatusBarEx) WindowManager.getInstance().getStatusBar(project);
357     if (processAction(e, myActionProcessor)) {
358       if (statusBar != null) {
359         statusBar.setInfo(null);
360       }
361       return true;
362     } else {
363       return false;
364     }
365   }
366
367   private boolean inProcessedState() {
368     KeyEvent e = myContext.getInputEvent();
369
370     // ignore typed events which come after processed pressed event
371     if (KeyEvent.KEY_TYPED == e.getID() && isPressedWasProcessed()) {
372       return true;
373     }
374     if (KeyEvent.KEY_RELEASED == e.getID() && KeyEvent.VK_ALT == e.getKeyCode() && isPressedWasProcessed()) {
375       //see IDEADEV-8615
376       return true;
377     }
378     setState(KeyState.STATE_INIT);
379     setPressedWasProcessed(false);
380     return inInitState();
381   }
382
383   @NonNls private static final Set<String> ALT_GR_LAYOUTS = new HashSet<>(Arrays.asList(
384     "pl", "de", "fi", "fr", "no", "da", "se", "pt", "nl", "tr", "sl", "hu", "bs", "hr", "sr", "sk", "lv"
385   ));
386
387   private boolean inInitState() {
388     Component focusOwner = myContext.getFocusOwner();
389     boolean isModalContext = myContext.isModalContext();
390     DataContext dataContext = myContext.getDataContext();
391     KeyEvent e = myContext.getInputEvent();
392
393     // http://www.jetbrains.net/jira/browse/IDEADEV-12372
394     if (myLeftCtrlPressed && myRightAltPressed && focusOwner != null && e.getModifiers() == (InputEvent.CTRL_MASK | InputEvent.ALT_MASK)) {
395       if (Registry.is("actionSystem.force.alt.gr")) {
396         return false;
397       }
398       final InputContext inputContext = focusOwner.getInputContext();
399       if (inputContext != null) {
400         Locale locale = inputContext.getLocale();
401         if (locale != null) {
402           @NonNls final String language = locale.getLanguage();
403           if (ALT_GR_LAYOUTS.contains(language)) {
404             // don't search for shortcuts
405             return false;
406           }
407         }
408       }
409     }
410
411     KeyStroke originalKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(e);
412     if (originalKeyStroke == null) {
413       return false;
414     }
415     KeyStroke keyStroke=getKeyStrokeWithoutMouseModifiers(originalKeyStroke);
416
417     if (myKeyGestureProcessor.processInitState()) {
418       return true;
419     }
420
421     if (SystemInfo.isMac) {
422       boolean keyTyped = e.getID() == KeyEvent.KEY_TYPED;
423       boolean hasMnemonicsInWindow = (e.getID() == KeyEvent.KEY_PRESSED
424                                       || e.getID() == KeyEvent.KEY_RELEASED)
425                                      && hasMnemonicInWindow(focusOwner, e.getKeyCode());
426
427       boolean imEnabled = IdeEventQueue.getInstance().isInputMethodEnabled();
428
429       if (e.getModifiersEx() == InputEvent.ALT_DOWN_MASK && (hasMnemonicsInWindow || !imEnabled && keyTyped))  {
430         setPressedWasProcessed(true);
431         setState(KeyState.STATE_PROCESSED);
432         return false;
433       }
434     }
435
436     updateCurrentContext(focusOwner, new KeyboardShortcut(keyStroke, null), isModalContext);
437     if(myContext.getActions().isEmpty()) {
438       // there's nothing mapped for this stroke
439       return false;
440     }
441
442     if(myContext.isHasSecondStroke()){
443       myFirstKeyStroke=keyStroke;
444       final ArrayList<Pair<AnAction, KeyStroke>> secondKeyStrokes = getSecondKeystrokeActions();
445
446       final Project project = CommonDataKeys.PROJECT.getData(dataContext);
447       StringBuilder message = new StringBuilder();
448       message.append(KeyMapBundle.message("prefix.key.pressed.message"));
449       message.append(' ');
450       for (int i = 0; i < secondKeyStrokes.size(); i++) {
451         Pair<AnAction, KeyStroke> pair = secondKeyStrokes.get(i);
452         if (i > 0) message.append(", ");
453         message.append(pair.getFirst().getTemplatePresentation().getText());
454         message.append(" (");
455         message.append(KeymapUtil.getKeystrokeText(pair.getSecond()));
456         message.append(")");
457       }
458
459       StatusBar.Info.set(message.toString(), project);
460
461       mySecondStrokeTimeout.cancelAllRequests();
462       mySecondStrokeTimeout.addRequest(mySecondStrokeTimeoutRunnable, Registry.intValue("actionSystem.secondKeystrokeTimeout"));
463
464       if (Registry.is("actionSystem.secondKeystrokeAutoPopupEnabled")) {
465         mySecondKeystrokePopupTimeout.cancelAllRequests();
466         if (secondKeyStrokes.size() > 1) {
467           final DataContext oldContext = myContext.getDataContext();
468           mySecondKeystrokePopupTimeout.addRequest(() -> {
469             if (myState == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) {
470               StatusBar.Info.set(null, CommonDataKeys.PROJECT.getData(oldContext));
471               new SecondaryKeystrokePopup(myFirstKeyStroke, secondKeyStrokes, oldContext).showInBestPositionFor(oldContext);
472             }
473           }, Registry.intValue("actionSystem.secondKeystrokePopupTimeout"));
474         }
475       }
476
477       setState(KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE);
478       return true;
479     }else{
480       return processAction(e, myActionProcessor);
481     }
482   }
483
484   private ArrayList<Pair<AnAction, KeyStroke>> getSecondKeystrokeActions() {
485     ArrayList<Pair<AnAction, KeyStroke>> secondKeyStrokes = new ArrayList<>();
486     for (AnAction action : myContext.getActions()) {
487       Shortcut[] shortcuts = action.getShortcutSet().getShortcuts();
488       for (Shortcut shortcut : shortcuts) {
489         if (shortcut instanceof KeyboardShortcut) {
490           KeyboardShortcut keyShortcut = (KeyboardShortcut)shortcut;
491           if (keyShortcut.getFirstKeyStroke().equals(myFirstKeyStroke)) {
492             secondKeyStrokes.add(Pair.create(action, keyShortcut.getSecondKeyStroke()));
493           }
494         }
495       }
496     }
497     return secondKeyStrokes;
498   }
499
500   private static boolean hasMnemonicInWindow(Component focusOwner, int keyCode) {
501     if (keyCode == KeyEvent.VK_ALT || keyCode == 0) return false; // Optimization
502     final Container container = SwingUtilities.getWindowAncestor(focusOwner);
503     return hasMnemonic(container, keyCode) || hasMnemonicInBalloons(container, keyCode);
504   }
505
506   private static boolean hasMnemonic(final Container container, final int keyCode) {
507     if (container == null) return false;
508
509     final Component[] components = container.getComponents();
510     for (Component component : components) {
511       if (component instanceof AbstractButton) {
512         final AbstractButton button = (AbstractButton)component;
513         if (button instanceof JBOptionButton) {
514           if (((JBOptionButton)button).isOkToProcessDefaultMnemonics() ||
515               button.getMnemonic() == keyCode)
516           {
517             return true;
518           }
519         } else {
520           if (button.getMnemonic() == keyCode) return true;
521         }
522       }
523       if (component instanceof JLabel) {
524         final JLabel label = (JLabel)component;
525         if (label.getDisplayedMnemonic() == keyCode) return true;
526       }
527       if (component instanceof Container) {
528         if (hasMnemonic((Container)component, keyCode)) return true;
529       }
530     }
531     return false;
532   }
533
534   private static boolean hasMnemonicInBalloons(Container container, int code) {
535     final Component parent = UIUtil.findUltimateParent(container);
536     if (parent instanceof RootPaneContainer) {
537       final JLayeredPane pane = ((RootPaneContainer)parent).getLayeredPane();
538       for (Component component : pane.getComponents()) {
539         if (component instanceof ComponentWithMnemonics && component instanceof Container && hasMnemonic((Container)component, code)) {
540           return true;
541         }
542       }
543     }
544     return false;
545   }
546
547   private final ActionProcessor myActionProcessor = new ActionProcessor() {
548     @NotNull
549     @Override
550     public AnActionEvent createEvent(final InputEvent inputEvent, @NotNull final DataContext context, @NotNull final String place, @NotNull final Presentation presentation,
551                                      final ActionManager manager) {
552       return new AnActionEvent(inputEvent, context, place, presentation, manager, 0);
553     }
554
555     @Override
556     public void onUpdatePassed(final InputEvent inputEvent, @NotNull final AnAction action, @NotNull final AnActionEvent actionEvent) {
557       setState(KeyState.STATE_PROCESSED);
558       setPressedWasProcessed(inputEvent.getID() == KeyEvent.KEY_PRESSED);
559     }
560
561     @Override
562     public void performAction(@NotNull InputEvent e, @NotNull AnAction action, @NotNull AnActionEvent actionEvent) {
563       e.consume();
564
565       DataContext ctx = actionEvent.getDataContext();
566       if (action instanceof ActionGroup && !((ActionGroup)action).canBePerformed(ctx)) {
567         ActionGroup group = (ActionGroup)action;
568         JBPopupFactory.getInstance()
569           .createActionGroupPopup(group.getTemplatePresentation().getText(), group, ctx, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false)
570           .showInBestPositionFor(ctx);
571       }
572       else {
573         ActionUtil.performActionDumbAware(action, actionEvent);
574       }
575
576       if (Registry.is("actionSystem.fixLostTyping")) {
577         IdeEventQueue.getInstance().doWhenReady(() -> IdeEventQueue.getInstance().getKeyEventDispatcher().resetState());
578       }
579     }
580   };
581
582   public boolean processAction(final InputEvent e, @NotNull ActionProcessor processor) {
583     ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
584     final Project project = CommonDataKeys.PROJECT.getData(myContext.getDataContext());
585     final boolean dumb = project != null && DumbService.getInstance(project).isDumb();
586     List<AnActionEvent> nonDumbAwareAction = new ArrayList<>();
587     List<AnAction> actions = myContext.getActions();
588     for (final AnAction action : actions.toArray(new AnAction[actions.size()])) {
589       Presentation presentation = myPresentationFactory.getPresentation(action);
590
591       // Mouse modifiers are 0 because they have no any sense when action is invoked via keyboard
592       final AnActionEvent actionEvent =
593         processor.createEvent(e, myContext.getDataContext(), ActionPlaces.MAIN_MENU, presentation, ActionManager.getInstance());
594
595       try (AccessToken ignored = ProhibitAWTEvents.start("update")) {
596         ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, actionEvent, true);
597       }
598
599       if (dumb && !action.isDumbAware()) {
600         if (!Boolean.FALSE.equals(presentation.getClientProperty(ActionUtil.WOULD_BE_ENABLED_IF_NOT_DUMB_MODE))) {
601           nonDumbAwareAction.add(actionEvent);
602         }
603         continue;
604       }
605
606       if (!presentation.isEnabled()) {
607         continue;
608       }
609
610       processor.onUpdatePassed(e, action, actionEvent);
611
612       if (myContext.getDataContext() instanceof DataManagerImpl.MyDataContext) { // this is not true for test data contexts
613         ((DataManagerImpl.MyDataContext)myContext.getDataContext()).setEventCount(IdeEventQueue.getInstance().getEventCount(), this);
614       }
615       actionManager.fireBeforeActionPerformed(action, actionEvent.getDataContext(), actionEvent);
616       Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(actionEvent.getDataContext());
617       if (component != null && !component.isShowing()) {
618         return true;
619       }
620
621       ((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(
622         () -> processor.performAction(e, action, actionEvent));
623       actionManager.fireAfterActionPerformed(action, actionEvent.getDataContext(), actionEvent);
624       return true;
625     }
626
627     if (!nonDumbAwareAction.isEmpty()) {
628       showDumbModeWarningLaterIfNobodyConsumesEvent(e, nonDumbAwareAction.toArray(new AnActionEvent[nonDumbAwareAction.size()]));
629     }
630
631     return false;
632   }
633
634   private static void showDumbModeWarningLaterIfNobodyConsumesEvent(final InputEvent e, final AnActionEvent... actionEvents) {
635     if (ModalityState.current() == ModalityState.NON_MODAL) {
636         ApplicationManager.getApplication().invokeLater(() -> {
637           if (e.isConsumed()) return;
638
639           ActionUtil.showDumbModeWarning(actionEvents);
640         });
641       }
642   }
643
644   /**
645    * This method fills <code>myActions</code> list.
646    * @return true if there is a shortcut with second stroke found.
647    */
648   public KeyProcessorContext updateCurrentContext(Component component, Shortcut sc, boolean isModalContext){
649     myContext.setFoundComponent(null);
650     myContext.getActions().clear();
651
652     if (isControlEnterOnDialog(component, sc)) return myContext;
653
654     boolean hasSecondStroke = false;
655
656     // here we try to find "local" shortcuts
657
658     for (; component != null; component = component.getParent()) {
659       if (!(component instanceof JComponent)) {
660         continue;
661       }
662       List<AnAction> listOfActions = ActionUtil.getActions((JComponent)component);
663       if (listOfActions.isEmpty()) {
664         continue;
665       }
666       for (Object listOfAction : listOfActions) {
667         if (!(listOfAction instanceof AnAction)) {
668           continue;
669         }
670         AnAction action = (AnAction)listOfAction;
671         hasSecondStroke |= addAction(action, sc);
672       }
673       // once we've found a proper local shortcut(s), we continue with non-local shortcuts
674       if (!myContext.getActions().isEmpty()) {
675         myContext.setFoundComponent((JComponent)component);
676         break;
677       }
678     }
679
680     // search in main keymap
681
682     Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
683     String[] actionIds = keymap.getActionIds(sc);
684
685     ActionManager actionManager = ActionManager.getInstance();
686     for (String actionId : actionIds) {
687       AnAction action = actionManager.getAction(actionId);
688       if (action != null) {
689         if (isModalContext && !action.isEnabledInModalContext()) {
690           continue;
691         }
692         hasSecondStroke |= addAction(action, sc);
693       }
694     }
695
696     if (!hasSecondStroke && sc instanceof KeyboardShortcut) {
697       // little trick to invoke action which second stroke is a key w/o modifiers, but user still
698       // holds the modifier key(s) of the first stroke
699
700       final KeyboardShortcut keyboardShortcut = (KeyboardShortcut)sc;
701       final KeyStroke firstKeyStroke = keyboardShortcut.getFirstKeyStroke();
702       final KeyStroke secondKeyStroke = keyboardShortcut.getSecondKeyStroke();
703
704       if (secondKeyStroke != null && secondKeyStroke.getModifiers() != 0 && firstKeyStroke.getModifiers() != 0) {
705         final KeyboardShortcut altShortCut = new KeyboardShortcut(firstKeyStroke, KeyStroke
706           .getKeyStroke(secondKeyStroke.getKeyCode(), 0));
707         final String[] additionalActions = keymap.getActionIds(altShortCut);
708
709         for (final String actionId : additionalActions) {
710           AnAction action = actionManager.getAction(actionId);
711           if (action != null) {
712             if (isModalContext && !action.isEnabledInModalContext()) {
713               continue;
714             }
715             hasSecondStroke |= addAction(action, altShortCut);
716           }
717         }
718       }
719
720     }
721
722     myContext.setHasSecondStroke(hasSecondStroke);
723     final List<AnAction> actions = myContext.getActions();
724
725     if (actions.size() > 1) {
726       final List<AnAction> readOnlyActions = Collections.unmodifiableList(actions);
727       for (ActionPromoter promoter : ActionPromoter.EP_NAME.getExtensions()) {
728         final List<AnAction> promoted = promoter.promote(readOnlyActions, myContext.getDataContext());
729         if (promoted == null || promoted.isEmpty()) continue;
730
731         actions.removeAll(promoted);
732         actions.addAll(0, promoted);
733       }
734     }
735
736     return myContext;
737   }
738
739   private static KeyboardShortcut CONTROL_ENTER = KeyboardShortcut.fromString("control ENTER");
740   private static boolean isControlEnterOnDialog(Component component, Shortcut sc) {
741     return CONTROL_ENTER.equals(sc)
742            && !IdeEventQueue.getInstance().isPopupActive() //avoid Control+Enter in completion
743            && DialogWrapper.findInstance(component) != null;
744   }
745
746   /**
747    * @return true if action is added and has second stroke
748    */
749   private boolean addAction(AnAction action, Shortcut sc) {
750     boolean hasSecondStroke = false;
751
752     Shortcut[] shortcuts = action.getShortcutSet().getShortcuts();
753     for (Shortcut each : shortcuts) {
754       if (!each.isKeyboard()) continue;
755
756       if (each.startsWith(sc)) {
757         if (!myContext.getActions().contains(action)) {
758           myContext.getActions().add(action);
759         }
760
761         if (each instanceof KeyboardShortcut) {
762           hasSecondStroke |= ((KeyboardShortcut)each).getSecondKeyStroke() != null;
763         }
764       }
765     }
766
767     return hasSecondStroke;
768   }
769
770   public KeyProcessorContext getContext() {
771     return myContext;
772   }
773
774   @Override
775   public void dispose() {
776     myDisposed = true;
777   }
778
779   public KeyState getState() {
780     return myState;
781   }
782
783   public void setState(final KeyState state) {
784     myState = state;
785     if (myQueue != null) {
786       myQueue.maybeReady();
787     }
788   }
789
790   public void resetState() {
791     setState(KeyState.STATE_INIT);
792     setPressedWasProcessed(false);
793   }
794
795   public boolean isPressedWasProcessed() {
796     return myPressedWasProcessed;
797   }
798
799   public void setPressedWasProcessed(boolean pressedWasProcessed) {
800     myPressedWasProcessed = pressedWasProcessed;
801   }
802
803   public boolean isReady() {
804     return myState == KeyState.STATE_INIT || myState == KeyState.STATE_PROCESSED;
805   }
806
807   private static class SecondaryKeystrokePopup extends ListPopupImpl {
808     private SecondaryKeystrokePopup(@NotNull final KeyStroke firstKeystroke, @NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext context) {
809       super(buildStep(actions, context));
810       registerActions(firstKeystroke, actions, context);
811     }
812
813     private void registerActions(@NotNull final KeyStroke firstKeyStroke, @NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext ctx) {
814       ContainerUtil.process(actions, pair -> {
815         final String actionText = pair.getFirst().getTemplatePresentation().getText();
816         final AbstractAction a = new AbstractAction() {
817           @Override
818           public void actionPerformed(final ActionEvent e) {
819             cancel();
820             invokeAction(pair.getFirst(), ctx);
821           }
822         };
823
824         final KeyStroke keyStroke = pair.getSecond();
825         if (keyStroke != null) {
826           registerAction(actionText, keyStroke, a);
827
828           if (keyStroke.getModifiers() == 0) {
829             // do a little trick here, so if I will press Command+R and the second keystroke is just 'R',
830             // I want to be able to hold the Command while pressing 'R'
831
832             final KeyStroke additionalKeyStroke = KeyStroke.getKeyStroke(keyStroke.getKeyCode(), firstKeyStroke.getModifiers());
833             final String _existing = getActionForKeyStroke(additionalKeyStroke);
834             if (_existing == null) registerAction("__additional__" + actionText, additionalKeyStroke, a);
835           }
836         }
837
838         return true;
839       });
840     }
841
842     private static void invokeAction(@NotNull final AnAction action, final DataContext ctx) {
843       TransactionGuard.submitTransaction(ApplicationManager.getApplication(), () -> {
844           final AnActionEvent event =
845             new AnActionEvent(null, ctx, ActionPlaces.UNKNOWN, action.getTemplatePresentation().clone(),
846                               ActionManager.getInstance(), 0);
847           if (ActionUtil.lastUpdateAndCheckDumb(action, event, true)) {
848             ActionUtil.performActionDumbAware(action, event);
849           }
850         });
851     }
852
853     @Override
854     protected ListCellRenderer getListElementRenderer() {
855       return new ActionListCellRenderer();
856     }
857
858     private static ListPopupStep buildStep(@NotNull final List<Pair<AnAction, KeyStroke>> actions, final DataContext ctx) {
859       return new BaseListPopupStep<Pair<AnAction, KeyStroke>>("Choose an action", ContainerUtil.findAll(actions, pair -> {
860         final AnAction action = pair.getFirst();
861         final Presentation presentation = action.getTemplatePresentation().clone();
862         AnActionEvent event = new AnActionEvent(null, ctx,
863                                                 ActionPlaces.UNKNOWN,
864                                                 presentation,
865                                                 ActionManager.getInstance(),
866                                                 0);
867
868         ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, event, true);
869         return presentation.isEnabled() && presentation.isVisible();
870       })) {
871         @Override
872         public PopupStep onChosen(Pair<AnAction, KeyStroke> selectedValue, boolean finalChoice) {
873           invokeAction(selectedValue.getFirst(), ctx);
874           return FINAL_CHOICE;
875         }
876       };
877     }
878
879     private static class ActionListCellRenderer extends ColoredListCellRenderer {
880       @Override
881       protected void customizeCellRenderer(@NotNull final JList list, final Object value, final int index, final boolean selected, final boolean hasFocus) {
882         if (value instanceof Pair) {
883           //noinspection unchecked
884           final Pair<AnAction, KeyStroke> pair = (Pair<AnAction, KeyStroke>) value;
885           append(KeymapUtil.getShortcutText(new KeyboardShortcut(pair.getSecond(), null)), SimpleTextAttributes.GRAY_ATTRIBUTES);
886           appendTextPadding(30);
887           final String text = pair.getFirst().getTemplatePresentation().getText();
888           if (text != null) {
889             append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
890           }
891         }
892       }
893     }
894   }
895 }