@Override public void propertyChange(PropertyChangeEvent event) { boolean internal = myInternal; myInternal = true; Object value = event.getNewValue(); if (ShortcutFilteringPanel.this == event.getSource()) { if (value instanceof KeyboardShortcut) { KeyboardShortcut shortcut = (KeyboardShortcut) value; myMousePanel.setShortcut(null); myKeyboardPanel.setShortcut(shortcut); if (null != shortcut.getSecondKeyStroke()) { myKeyboardPanel.mySecondStrokeEnable.setSelected(true); } } else { MouseShortcut shortcut = value instanceof MouseShortcut ? (MouseShortcut) value : null; String text = shortcut == null ? null : KeymapUtil.getMouseShortcutText(shortcut); myMousePanel.setShortcut(shortcut); myKeyboardPanel.setShortcut(null); myKeyboardPanel.myFirstStroke.setText(text); myKeyboardPanel.mySecondStroke.setText(null); myKeyboardPanel.mySecondStroke.setEnabled(false); } } else if (value instanceof Shortcut) { setShortcut((Shortcut) value); } else if (!internal) { setShortcut(null); } myInternal = internal; }
@Nullable public static KeyStroke getKeyStroke(@NotNull final ShortcutSet shortcutSet) { final Shortcut[] shortcuts = shortcutSet.getShortcuts(); if (shortcuts.length == 0 || !(shortcuts[0] instanceof KeyboardShortcut)) return null; final KeyboardShortcut shortcut = (KeyboardShortcut) shortcuts[0]; if (shortcut.getSecondKeyStroke() != null) { return null; } return shortcut.getFirstKeyStroke(); }
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( new Pair<AnAction, KeyStroke>(action, keyShortcut.getSecondKeyStroke())); } } } } return secondKeyStrokes; }
private int getMnemonicCharIndex(String text) { final int mnemonicIndex = myPresentation.getDisplayedMnemonicIndex(); if (mnemonicIndex != -1) { return mnemonicIndex; } final ShortcutSet shortcutSet = myAction.getShortcutSet(); final Shortcut[] shortcuts = shortcutSet.getShortcuts(); for (Shortcut shortcut : shortcuts) { if (!(shortcut instanceof KeyboardShortcut)) continue; KeyboardShortcut keyboardShortcut = (KeyboardShortcut) shortcut; if (keyboardShortcut.getSecondKeyStroke() == null) { // we are interested only in "mnemonic-like" shortcuts final KeyStroke keyStroke = keyboardShortcut.getFirstKeyStroke(); final int modifiers = keyStroke.getModifiers(); if (BitUtil.isSet(modifiers, InputEvent.ALT_MASK)) { return (keyStroke.getKeyChar() != KeyEvent.CHAR_UNDEFINED) ? text.indexOf(keyStroke.getKeyChar()) : text.indexOf(KeyEvent.getKeyText(keyStroke.getKeyCode())); } } } return -1; }
public static String getShortcutText(@NotNull Shortcut shortcut) { String s = ""; if (shortcut instanceof KeyboardShortcut) { KeyboardShortcut keyboardShortcut = (KeyboardShortcut) shortcut; String acceleratorText = getKeystrokeText(keyboardShortcut.getFirstKeyStroke()); if (!acceleratorText.isEmpty()) { s = acceleratorText; } acceleratorText = getKeystrokeText(keyboardShortcut.getSecondKeyStroke()); if (!acceleratorText.isEmpty()) { s += ", " + acceleratorText; } } else if (shortcut instanceof MouseShortcut) { MouseShortcut mouseShortcut = (MouseShortcut) shortcut; s = getMouseShortcutText( mouseShortcut.getButton(), mouseShortcut.getModifiers(), mouseShortcut.getClickCount()); } else if (shortcut instanceof KeyboardModifierGestureShortcut) { final KeyboardModifierGestureShortcut gestureShortcut = (KeyboardModifierGestureShortcut) shortcut; s = gestureShortcut.getType() == KeyboardGestureAction.ModifierType.dblClick ? "Press, release and hold " : "Hold "; s += getKeystrokeText(gestureShortcut.getStroke()); } else { throw new IllegalArgumentException( "unknown shortcut class: " + shortcut.getClass().getCanonicalName()); } return s; }
protected ActionCallback _execute(final PlaybackContext context) { final String actionName = getText().substring(PREFIX.length()).trim(); final ActionManager am = ActionManager.getInstance(); final AnAction targetAction = am.getAction(actionName); if (targetAction == null) { dumpError(context, "Unknown action: " + actionName); return new ActionCallback.Rejected(); } if (!context.isUseDirectActionCall()) { final Shortcut[] sc = KeymapManager.getInstance().getActiveKeymap().getShortcuts(actionName); KeyStroke stroke = null; for (Shortcut each : sc) { if (each instanceof KeyboardShortcut) { final KeyboardShortcut ks = (KeyboardShortcut) each; final KeyStroke first = ks.getFirstKeyStroke(); final KeyStroke second = ks.getSecondKeyStroke(); if (first != null && second == null) { stroke = KeyStroke.getKeyStroke(first.getKeyCode(), first.getModifiers(), false); break; } } } if (stroke != null) { final ActionCallback result = new TimedOutCallback( Registry.intValue("actionSystem.commandProcessingTimeout"), "Timed out calling action id=" + actionName, new Throwable(), true) { @Override protected void dumpError() { context.error(getMessage(), getLine()); } }; context.message("Invoking action via shortcut: " + stroke.toString(), getLine()); final KeyStroke finalStroke = stroke; IdeFocusManager.getGlobalInstance() .doWhenFocusSettlesDown( new Runnable() { @Override public void run() { final Ref<AnActionListener> listener = new Ref<AnActionListener>(); listener.set( new AnActionListener.Adapter() { @Override public void beforeActionPerformed( final AnAction action, DataContext dataContext, AnActionEvent event) { SwingUtilities.invokeLater( new Runnable() { @Override public void run() { if (context.isDisposed()) { am.removeAnActionListener(listener.get()); return; } if (targetAction.equals(action)) { context.message( "Performed action: " + actionName, context.getCurrentLine()); am.removeAnActionListener(listener.get()); result.setDone(); } } }); } }); am.addAnActionListener(listener.get()); context.runPooledThread( new Runnable() { @Override public void run() { type(context.getRobot(), finalStroke); } }); } }); return result; } } final InputEvent input = getInputEvent(actionName); final ActionCallback result = new ActionCallback(); context.getRobot().delay(Registry.intValue("actionSystem.playback.delay")); SwingUtilities.invokeLater( new Runnable() { public void run() { am.tryToExecute(targetAction, input, null, null, false) .doWhenProcessed(result.createSetDoneRunnable()); } }); return result; }
@NotNull private JBPopup createUsagePopup( @NotNull final List<Usage> usages, @NotNull final UsageInfoToUsageConverter.TargetElementsDescriptor descriptor, @NotNull Set<UsageNode> visibleNodes, @NotNull final FindUsagesHandler handler, final Editor editor, @NotNull final RelativePoint popupPosition, final int maxUsages, @NotNull final UsageViewImpl usageView, @NotNull final FindUsagesOptions options, @NotNull final JTable table, @NotNull final UsageViewPresentation presentation, @NotNull final AsyncProcessIcon processIcon, boolean hadMoreSeparator) { table.setRowHeight(PlatformIcons.CLASS_ICON.getIconHeight() + 2); table.setShowGrid(false); table.setShowVerticalLines(false); table.setShowHorizontalLines(false); table.setTableHeader(null); table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); table.setIntercellSpacing(new Dimension(0, 0)); PopupChooserBuilder builder = new PopupChooserBuilder(table); final String title = presentation.getTabText(); if (title != null) { String result = getFullTitle(usages, title, hadMoreSeparator, visibleNodes.size() - 1, true); builder.setTitle(result); builder.setAdText(getSecondInvocationTitle(options, handler)); } builder.setMovable(true).setResizable(true); builder.setItemChoosenCallback( new Runnable() { @Override public void run() { int[] selected = table.getSelectedRows(); for (int i : selected) { Object value = table.getValueAt(i, 0); if (value instanceof UsageNode) { Usage usage = ((UsageNode) value).getUsage(); if (usage == MORE_USAGES_SEPARATOR) { appendMoreUsages(editor, popupPosition, handler, maxUsages); return; } navigateAndHint(usage, null, handler, popupPosition, maxUsages, options); } } } }); final JBPopup[] popup = new JBPopup[1]; KeyboardShortcut shortcut = UsageViewImpl.getShowUsagesWithSettingsShortcut(); if (shortcut != null) { new DumbAwareAction() { @Override public void actionPerformed(AnActionEvent e) { popup[0].cancel(); showDialogAndFindUsages(handler, popupPosition, editor, maxUsages); } }.registerCustomShortcutSet(new CustomShortcutSet(shortcut.getFirstKeyStroke()), table); } shortcut = getShowUsagesShortcut(); if (shortcut != null) { new DumbAwareAction() { @Override public void actionPerformed(AnActionEvent e) { popup[0].cancel(); searchEverywhere(options, handler, editor, popupPosition, maxUsages); } }.registerCustomShortcutSet(new CustomShortcutSet(shortcut.getFirstKeyStroke()), table); } InplaceButton settingsButton = createSettingsButton( handler, popupPosition, editor, maxUsages, new Runnable() { @Override public void run() { popup[0].cancel(); } }); ActiveComponent spinningProgress = new ActiveComponent() { @Override public void setActive(boolean active) {} @Override public JComponent getComponent() { return processIcon; } }; builder.setCommandButton(new CompositeActiveComponent(spinningProgress, settingsButton)); DefaultActionGroup toolbar = new DefaultActionGroup(); usageView.addFilteringActions(toolbar); toolbar.add(UsageGroupingRuleProviderImpl.createGroupByFileStructureAction(usageView)); toolbar.add( new AnAction( "Open Find Usages Toolwindow", "Show all usages in a separate toolwindow", AllIcons.Toolwindows.ToolWindowFind) { { AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_USAGES); setShortcutSet(action.getShortcutSet()); } @Override public void actionPerformed(AnActionEvent e) { hideHints(); popup[0].cancel(); FindUsagesManager findUsagesManager = ((FindManagerImpl) FindManager.getInstance(usageView.getProject())) .getFindUsagesManager(); findUsagesManager.findUsages( handler.getPrimaryElements(), handler.getSecondaryElements(), handler, options, FindSettings.getInstance().isSkipResultsWithOneUsage()); } }); ActionToolbar actionToolbar = ActionManager.getInstance() .createActionToolbar(ActionPlaces.USAGE_VIEW_TOOLBAR, toolbar, true); actionToolbar.setReservePlaceAutoPopupIcon(false); final JComponent toolBar = actionToolbar.getComponent(); toolBar.setOpaque(false); builder.setSettingButton(toolBar); popup[0] = builder.createPopup(); JComponent content = popup[0].getContent(); myWidth = (int) (toolBar.getPreferredSize().getWidth() + new JLabel( getFullTitle( usages, title, hadMoreSeparator, visibleNodes.size() - 1, true)) .getPreferredSize() .getWidth() + settingsButton.getPreferredSize().getWidth()); myWidth = -1; for (AnAction action : toolbar.getChildren(null)) { action.unregisterCustomShortcutSet(usageView.getComponent()); action.registerCustomShortcutSet(action.getShortcutSet(), content); } return popup[0]; }
/** * 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() { public void run() { if (myState == KeyState.STATE_WAIT_FOR_SECOND_KEYSTROKE) { resetState(); if (myContext != null) { final DataContext dataContext = myContext.getDataContext(); StatusBar.Info.set( null, dataContext == null ? null : PlatformDataKeys.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; } // 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(); } } /** * @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()) { return isModalContext(dialog.getOwner()); } } 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(AbstractPopup.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 = new Object[] { 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 = PlatformDataKeys.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 = PlatformDataKeys.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(); } 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 (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 = PlatformDataKeys.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, PlatformDataKeys.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( new Pair<AnAction, KeyStroke>(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() { public AnActionEvent createEvent( final InputEvent inputEvent, final DataContext context, final String place, final Presentation presentation, final ActionManager manager) { return new AnActionEvent(inputEvent, context, place, presentation, manager, 0); } public void onUpdatePassed( final InputEvent inputEvent, final AnAction action, final AnActionEvent actionEvent) { setState(KeyState.STATE_PROCESSED); setPressedWasProcessed(inputEvent.getID() == KeyEvent.KEY_PRESSED); } public void performAction( final InputEvent e, final AnAction action, 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, ActionProcessor processor) { ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); final Project project = PlatformDataKeys.PROJECT.getData(myContext.getDataContext()); final boolean dumb = project != null && DumbService.getInstance(project).isDumb(); List<AnActionEvent> nonDumbAwareAction = new ArrayList<AnActionEvent>(); for (final AnAction action : myContext.getActions()) { final 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))) { continue; } nonDumbAwareAction.add(actionEvent); continue; } if (!presentation.isEnabled()) { continue; } processor.onUpdatePassed(e, action, actionEvent); ((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() { 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; } ArrayList listOfActions = (ArrayList) ((JComponent) component).getClientProperty(AnAction.ourClientProperty); if (listOfActions == null) { 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); Comparator<? super AnAction> comparator = PlatformDataKeys.ACTIONS_SORTER.getData(myContext.getDataContext()); if (comparator != null) { Collections.sort(myContext.getActions(), comparator); } 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; } 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(); 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, (Presentation) action.getTemplatePresentation().clone(), ActionManager.getInstance(), 0); if (ActionUtil.lastUpdateAndCheckDumb(action, event, true)) { action.actionPerformed(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 = (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); appendAlign(30); final String text = pair.getFirst().getTemplatePresentation().getText(); append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES); } } } } }
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; }
/** * 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; } ArrayList listOfActions = (ArrayList) ((JComponent) component).getClientProperty(AnAction.ourClientProperty); if (listOfActions == null) { 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); Comparator<? super AnAction> comparator = PlatformDataKeys.ACTIONS_SORTER.getData(myContext.getDataContext()); if (comparator != null) { Collections.sort(myContext.getActions(), comparator); } return myContext; }