| /* |
| * Copyright 2000-2014 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.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.PresentationFactory; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| 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.ListPopupStep; |
| import com.intellij.openapi.ui.popup.PopupStep; |
| import com.intellij.openapi.ui.popup.util.BaseListPopupStep; |
| import com.intellij.openapi.util.Condition; |
| 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.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.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.ui.MacUIUtil; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| 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 = false; |
| private boolean myLeftCtrlPressed = false; |
| private boolean myRightAltPressed = false; |
| |
| 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 = new Runnable() { |
| @Override |
| public void run() { |
| 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</code> if and only if the passed event is already dispatched by the |
| * <code>IdeKeyEventDispatcher</code> and there is no need for any other processing of the event. |
| */ |
| public boolean dispatchKeyEvent(final KeyEvent e){ |
| if (myDisposed) return false; |
| |
| 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.getKeyChar() != 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); |
| |
| 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</code> if and only if the <code>component</code> represents |
| * modal context. |
| * @throws IllegalArgumentException if <code>component</code> is <code>null</code>. |
| */ |
| public static boolean isModalContext(@NotNull Component component) { |
| Window window; |
| if (component instanceof Window) { |
| window = (Window)component; |
| } else { |
| window = SwingUtilities.getWindowAncestor(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 isMainFrame = window instanceof IdeFrameImpl; |
| 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 !isMainFrame && !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 static KeyStroke getKeyStrokeWithoutCtrlModifier(KeyStroke originalKeyStroke){ |
| int modifier=originalKeyStroke.getModifiers()&~InputEvent.CTRL_MASK&~InputEvent.CTRL_DOWN_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=KeyStroke.getKeyStrokeForEvent(e); |
| 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<String>(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=KeyStroke.getKeyStrokeForEvent(e); |
| KeyStroke keyStroke=getKeyStrokeWithoutMouseModifiers(originalKeyStroke); |
| |
| |
| |
| if (Registry.is("fix.jdk7.alt.shortcuts") && SystemInfo.isMac && SystemInfo.isOracleJvm && (keyStroke.getModifiers() & InputEvent.ALT_MASK) == InputEvent.ALT_MASK) |
| { |
| if (KeymapManager.getInstance().getActiveKeymap().getActionIds(new KeyboardShortcut(keyStroke, null)).length == 0) { |
| keyStroke = getKeyStrokeWithoutCtrlModifier(keyStroke); |
| } |
| } |
| |
| 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(new Runnable() { |
| @Override |
| public void run() { |
| 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<Pair<AnAction,KeyStroke>>(); |
| 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 = getContainer(focusOwner); |
| return hasMnemonic(container, keyCode) || hasMnemonicInBalloons(container, keyCode); |
| } |
| |
| @Nullable |
| private static Container getContainer(@Nullable final Component focusOwner) { |
| if (focusOwner == null) return null; |
| if (focusOwner.isLightweight()) { |
| Container container = focusOwner.getParent(); |
| while (container != null) { |
| final Container parent = container.getParent(); |
| if (parent instanceof JLayeredPane) break; |
| if (parent != null && parent.isLightweight()) { |
| container = parent; |
| } |
| else { |
| break; |
| } |
| } |
| return container; |
| } |
| |
| return SwingUtilities.windowForComponent(focusOwner); |
| } |
| |
| 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()) 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 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(final InputEvent e, @NotNull final AnAction action, @NotNull final AnActionEvent actionEvent) { |
| e.consume(); |
| action.actionPerformed(actionEvent); |
| if (Registry.is("actionSystem.fixLostTyping")) { |
| IdeEventQueue.getInstance().doWhenReady(new Runnable() { |
| @Override |
| public void run() { |
| 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<AnActionEvent>(); |
| 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()); |
| |
| ActionUtil.performDumbAwareUpdate(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; |
| } |
| |
| 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(new Runnable() { |
| @Override |
| public void run() { |
| if (e.isConsumed()) return; |
| |
| ActionUtil.showDumbModeWarning(actionEvents); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * This method fills <code>myActions</code> 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 (Object listOfAction : listOfActions) { |
| if (!(listOfAction instanceof AnAction)) { |
| continue; |
| } |
| AnAction action = (AnAction)listOfAction; |
| 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.isEmpty()) continue; |
| |
| actions.removeAll(promoted); |
| actions.addAll(0, promoted); |
| } |
| } |
| |
| return myContext; |
| } |
| |
| private static 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); |
| } |
| |
| public boolean isPressedWasProcessed() { |
| return myPressedWasProcessed; |
| } |
| |
| public 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, new Processor<Pair<AnAction, KeyStroke>>() { |
| @Override |
| public boolean process(final Pair<AnAction, KeyStroke> 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) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| 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, new Condition<Pair<AnAction, KeyStroke>>() { |
| @Override |
| public boolean value(Pair<AnAction, KeyStroke> 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(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(final JList list, final Object value, final int index, final boolean selected, final boolean hasFocus) { |
| if (value == null) return; |
| if (value instanceof Pair) { |
| final Pair<AnAction, KeyStroke> pair = (Pair<AnAction, KeyStroke>) value; |
| append(KeymapUtil.getShortcutText(new KeyboardShortcut(pair.getSecond(), null)), SimpleTextAttributes.GRAY_ATTRIBUTES); |
| appendFixedTextFragmentWidth(30); |
| final String text = pair.getFirst().getTemplatePresentation().getText(); |
| append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES); |
| } |
| } |
| } |
| } |
| } |