blob: 4b4d19136f016c485d473a23485a504fe3257fe4 [file] [log] [blame]
/*
* 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.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.DataManager;
import com.intellij.ide.IdeEventQueue;
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.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.impl.FocusManagerImpl;
import com.intellij.openapi.wm.impl.IdeGlassPaneImpl;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.awt.event.MouseEvent.*;
/**
* Current implementation of the dispatcher is intended to filter mouse event addressed to
* the editor. Also it allows to map middle mouse's button to some action.
*
* @author Vladimir Kondratyev
* @author Konstantin Bulenkov
*/
public final class IdeMouseEventDispatcher {
private final PresentationFactory myPresentationFactory = new PresentationFactory();
private final ArrayList<AnAction> myActions = new ArrayList<AnAction>(1);
private final Map<Container, BlockState> myRootPane2BlockedId = new HashMap<Container, BlockState>();
private int myLastHorScrolledComponentHash = 0;
// Don't compare MouseEvent ids. Swing has wrong sequence of events: first is mouse_clicked(500)
// then mouse_pressed(501), mouse_released(502) etc. Here, mouse events sorted so we can compare
// theirs ids to properly use method blockNextEvents(MouseEvent)
private static final List<Integer> SWING_EVENTS_PRIORITY = Arrays.asList(MOUSE_PRESSED,
MOUSE_ENTERED,
MOUSE_EXITED,
MOUSE_MOVED,
MOUSE_DRAGGED,
MOUSE_WHEEL,
MOUSE_RELEASED,
MOUSE_CLICKED);
public IdeMouseEventDispatcher() {
}
private void fillActionsList(Component component, MouseShortcut mouseShortcut, boolean isModalContext) {
myActions.clear();
// here we try to find "local" shortcuts
if (component instanceof JComponent) {
for (AnAction action : ActionUtil.getActions((JComponent)component)) {
for (Shortcut shortcut : action.getShortcutSet().getShortcuts()) {
if (mouseShortcut.equals(shortcut) && !myActions.contains(action)) {
myActions.add(action);
}
}
}
// once we've found a proper local shortcut(s), we exit
if (!myActions.isEmpty()) {
return;
}
}
// search in main keymap
if (KeymapManagerImpl.ourKeymapManagerInitialized) {
final KeymapManager keymapManager = KeymapManager.getInstance();
if (keymapManager != null) {
final Keymap keymap = keymapManager.getActiveKeymap();
final String[] actionIds = keymap.getActionIds(mouseShortcut);
ActionManager actionManager = ActionManager.getInstance();
for (String actionId : actionIds) {
AnAction action = actionManager.getAction(actionId);
if (action == null) continue;
if (isModalContext && !action.isEnabledInModalContext()) continue;
if (!myActions.contains(action)) {
myActions.add(action);
}
}
}
}
}
/**
* @return <code>true</code> if and only if the passed event is already dispatched by the
* <code>IdeMouseEventDispatcher</code> and there is no need for any other processing of the event.
* If the method returns <code>false</code> then it means that the event should be delivered
* to normal event dispatching.
*/
public boolean dispatchMouseEvent(MouseEvent e) {
Component c = e.getComponent();
//frame activation by mouse click
if (e.getID() == MOUSE_PRESSED && c instanceof IdeFrame && !c.hasFocus()) {
IdeFocusManager focusManager = IdeFocusManager.getGlobalInstance();
if (focusManager instanceof FocusManagerImpl) {
Component at = SwingUtilities.getDeepestComponentAt(c, e.getX(), e.getY());
if (at != null && at.isFocusable()) {
((FocusManagerImpl)focusManager).setLastFocusedAtDeactivation((IdeFrame)c, at);
}
}
}
if (SystemInfo.isXWindow && e.isPopupTrigger() && e.getButton() != 3) {
// we can do better than silly triggering popup on everything but left click
resetPopupTrigger(e);
}
boolean ignore = false;
if (!(e.getID() == MouseEvent.MOUSE_PRESSED ||
e.getID() == MouseEvent.MOUSE_RELEASED ||
e.getID() == MOUSE_CLICKED)) {
ignore = true;
}
patchClickCount(e);
if (e.isConsumed()
|| e.isPopupTrigger()
|| (e.getButton() > 3 ? e.getID() != MOUSE_PRESSED : e.getID() != MOUSE_RELEASED)
|| e.getClickCount() < 1
|| e.getButton() == MouseEvent.NOBUTTON) { // See #16995. It did happen
ignore = true;
}
final JRootPane root = findRoot(e);
if (root != null) {
BlockState blockState = myRootPane2BlockedId.get(root);
if (blockState != null) {
if (SWING_EVENTS_PRIORITY.indexOf(blockState.currentEventId) < SWING_EVENTS_PRIORITY.indexOf(e.getID())) {
blockState.currentEventId = e.getID();
if (blockState.blockMode == IdeEventQueue.BlockMode.COMPLETE) {
return true;
}
else {
ignore = true;
}
} else {
myRootPane2BlockedId.remove(root);
}
}
}
if (c == null) {
throw new IllegalStateException("component cannot be null");
}
c = SwingUtilities.getDeepestComponentAt(c, e.getX(), e.getY());
if (c instanceof IdeGlassPaneImpl) {
c = ((IdeGlassPaneImpl)c).getTargetComponentFor(e);
}
if (c == null) { // do nothing if component doesn't contains specified point
return false;
}
if (isHorizontalScrolling(c, e)) {
boolean done = doHorizontalScrolling(c, (MouseWheelEvent)e);
if (done) return true;
}
if (ignore) return false;
// avoid "cyclic component initialization error" in case of dialogs shown because of component initialization failure
if (!KeymapManagerImpl.ourKeymapManagerInitialized) {
return false;
}
final MouseShortcut shortcut = new MouseShortcut(e.getButton(), e.getModifiersEx(), e.getClickCount());
fillActionsList(c, shortcut, IdeKeyEventDispatcher.isModalContext(c));
ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
if (actionManager != null) {
AnAction[] actions = myActions.toArray(new AnAction[myActions.size()]);
for (AnAction action : actions) {
DataContext dataContext = DataManager.getInstance().getDataContext(c);
Presentation presentation = myPresentationFactory.getPresentation(action);
AnActionEvent actionEvent = new AnActionEvent(e, dataContext, ActionPlaces.MAIN_MENU, presentation,
ActionManager.getInstance(),
e.getModifiers());
action.beforeActionPerformedUpdate(actionEvent);
if (presentation.isEnabled()) {
actionManager.fireBeforeActionPerformed(action, dataContext, actionEvent);
final Component context = PlatformDataKeys.CONTEXT_COMPONENT.getData(dataContext);
if (context != null && !context.isShowing()) continue;
action.actionPerformed(actionEvent);
e.consume();
}
}
if (actions.length > 0 && e.isConsumed())
return true;
}
return e.getButton() > 3;
}
private static void resetPopupTrigger(final MouseEvent e) {
ReflectionUtil.setField(MouseEvent.class, e, boolean.class, "popupTrigger", false);
}
/**
* This method patches event if it concerns side buttons like 4 (Backward) or 5 (Forward)
* AND it's not single-click event. We won't support double-click for side buttons.
* Also some JDK bugs produce zero-click events for side buttons.
* @return true if event was patched
*/
public static boolean patchClickCount(final MouseEvent e) {
if (e.getClickCount() != 1 && e.getButton() > 3) {
ReflectionUtil.setField(MouseEvent.class, e, int.class, "clickCount", 1);
}
return false;
}
private boolean doHorizontalScrolling(Component c, MouseWheelEvent me) {
final JScrollBar scrollBar = findHorizontalScrollBar(c);
if (scrollBar != null) {
if (scrollBar.hashCode() != myLastHorScrolledComponentHash) {
FeatureUsageTracker.getInstance().triggerFeatureUsed("ui.horizontal.scrolling");
myLastHorScrolledComponentHash = scrollBar.hashCode();
}
scrollBar.setValue(scrollBar.getValue() + getScrollAmount(c, me, scrollBar));
return true;
}
return false;
}
public void resetHorScrollingTracker() {
myLastHorScrolledComponentHash = 0;
}
private static int getScrollAmount(Component c, MouseWheelEvent me, JScrollBar scrollBar) {
final int scrollBarWidth = scrollBar.getWidth();
final int ratio = Registry.is("ide.smart.horizontal.scrolling") && scrollBarWidth > 0
? Math.max((int)Math.pow(c.getWidth() / scrollBarWidth, 2), 10) : 10; // do annoying scrolling faster if smart scrolling is on
return me.getUnitsToScroll() * scrollBar.getUnitIncrement() * ratio;
}
private static boolean isHorizontalScrolling(Component c, MouseEvent e) {
if ( c != null
&& e instanceof MouseWheelEvent
&& (!SystemInfo.isMac || isDiagramViewComponent(c.getParent()))) {
final MouseWheelEvent mwe = (MouseWheelEvent)e;
return mwe.isShiftDown()
&& mwe.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL
&& findHorizontalScrollBar(c) != null;
}
return false;
}
@Nullable
private static JScrollBar findHorizontalScrollBar(Component c) {
if (c == null) return null;
if (c instanceof JScrollPane) {
return ((JScrollPane)c).getHorizontalScrollBar();
}
if (isDiagramViewComponent(c)) {
final JComponent view = (JComponent)c;
for (int i = 0; i < view.getComponentCount(); i++) {
if (view.getComponent(i) instanceof JScrollBar) {
final JScrollBar scrollBar = (JScrollBar)view.getComponent(i);
if (scrollBar.getOrientation() == Adjustable.HORIZONTAL) {
return scrollBar;
}
}
}
}
return findHorizontalScrollBar(c.getParent());
}
private static boolean isDiagramViewComponent(Component c) {
return c != null && "y.view.Graph2DView".equals(c.getClass().getName());
}
public void blockNextEvents(final MouseEvent e, IdeEventQueue.BlockMode blockMode) {
final JRootPane root = findRoot(e);
if (root == null) return;
myRootPane2BlockedId.put(root, new BlockState(e.getID(), blockMode));
}
@Nullable
private static JRootPane findRoot(MouseEvent e) {
final Component parent = UIUtil.findUltimateParent(e.getComponent());
JRootPane root = null;
if (parent instanceof JWindow) {
root = ((JWindow)parent).getRootPane();
}
else if (parent instanceof JDialog) {
root = ((JDialog)parent).getRootPane();
}
else if (parent instanceof JFrame) {
root = ((JFrame)parent).getRootPane();
}
return root;
}
private static class BlockState {
private int currentEventId;
private final IdeEventQueue.BlockMode blockMode;
private BlockState(int id, IdeEventQueue.BlockMode mode) {
currentEventId = id;
blockMode = mode;
}
}
}