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