bind 'clone caret' actions to ControlDoubleClick+Up/Down

This commit is contained in:
Dmitry Batrak
2014-07-22 12:21:07 +04:00
parent 41034f6b5d
commit 222d8ef5fe
6 changed files with 395 additions and 86 deletions
@@ -56,6 +56,7 @@ import com.intellij.openapi.fileEditor.impl.EditorHistoryManager;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.keymap.MacKeymapUtil;
import com.intellij.openapi.keymap.impl.ModifierKeyDoubleClickHandler;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.progress.ProcessCanceledException;
@@ -151,106 +152,25 @@ public class SearchEverywhereAction extends AnAction implements CustomComponentA
private Component myContextComponent;
private CalcThread myCalcThread;
private static AtomicBoolean ourShiftIsPressed = new AtomicBoolean(false);
private final static Couple<AtomicBoolean> ourPressed = Couple.of(new AtomicBoolean(false), new AtomicBoolean(false));
private final static Couple<AtomicBoolean> ourReleased = Couple.of(new AtomicBoolean(false), new AtomicBoolean(false));
private static AtomicBoolean ourOtherKeyWasPressed = new AtomicBoolean(false);
private static AtomicLong ourLastTimePressed = new AtomicLong(0);
private static AtomicBoolean showAll = new AtomicBoolean(false);
private volatile ActionCallback myCurrentWorker = ActionCallback.DONE;
private int myHistoryIndex = 0;
boolean mySkipFocusGain = false;
static {
ModifierKeyDoubleClickHandler.getInstance().registerAction(IdeActions.ACTION_SEARCH_EVERYWHERE, KeyEvent.VK_SHIFT, -1);
IdeEventQueue.getInstance().addPostprocessor(new IdeEventQueue.EventDispatcher() {
@Override
public boolean dispatch(AWTEvent event) {
if (event instanceof KeyEvent) {
final KeyEvent keyEvent = (KeyEvent)event;
final int keyCode = keyEvent.getKeyCode();
final int keyCode = ((KeyEvent)event).getKeyCode();
if (keyCode == KeyEvent.VK_SHIFT) {
ourShiftIsPressed.set(event.getID() == KeyEvent.KEY_PRESSED);
if (keyEvent.isControlDown() || keyEvent.isAltDown() || keyEvent.isMetaDown()) {
resetState();
return false;
}
if (ourOtherKeyWasPressed.get() && System.currentTimeMillis() - ourLastTimePressed.get() < 500) {
resetState();
return false;
}
ourOtherKeyWasPressed.set(false);
if (ourPressed.first.get() && System.currentTimeMillis() - ourLastTimePressed.get() > 500) {
resetState();
}
handleShift((KeyEvent)event);
return false;
} else {
ourLastTimePressed.set(System.currentTimeMillis());
ourOtherKeyWasPressed.set(true);
if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_TAB) {
ourLastTimePressed.set(0);
}
}
resetState();
}
return false;
}
private void resetState() {
ourPressed.first.set(false);
ourPressed.second.set(false);
ourReleased.first.set(false);
ourReleased.second.set(false);
}
private void handleShift(KeyEvent event) {
if (ourPressed.first.get() && System.currentTimeMillis() - ourLastTimePressed.get() > 300) {
resetState();
return;
}
if (event.getID() == KeyEvent.KEY_PRESSED) {
if (!ourPressed.first.get()) {
resetState();
ourPressed.first.set(true);
ourLastTimePressed.set(System.currentTimeMillis());
return;
} else {
if (ourPressed.first.get() && ourReleased.first.get()) {
ourPressed.second.set(true);
ourLastTimePressed.set(System.currentTimeMillis());
return;
}
}
} else if (event.getID() == KeyEvent.KEY_RELEASED) {
if (ourPressed.first.get() && !ourReleased.first.get()) {
ourReleased.first.set(true);
ourLastTimePressed.set(System.currentTimeMillis());
return;
} else if (ourPressed.first.get() && ourReleased.first.get() && ourPressed.second.get()) {
resetState();
run(event);
return;
}
}
resetState();
}
private void run(KeyEvent event) {
final ActionManager actionManager = ActionManager.getInstance();
final AnAction action = actionManager.getAction(IdeActions.ACTION_SEARCH_EVERYWHERE);
if (KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_SEARCH_EVERYWHERE).length > 0) {
return;
}
final AnActionEvent anActionEvent = new AnActionEvent(event,
DataManager.getInstance().getDataContext(IdeFocusManager.findInstance().getFocusOwner()),
ActionPlaces.MAIN_MENU,
action.getTemplatePresentation(),
actionManager,
0);
action.actionPerformed(anActionEvent);
}
}, null);
}
@@ -50,6 +50,8 @@ public interface IdeActions {
@NonNls String ACTION_EDITOR_COMPLETE_STATEMENT = "EditorCompleteStatement";
@NonNls String ACTION_EDITOR_USE_SOFT_WRAPS = "EditorToggleUseSoftWraps";
@NonNls String ACTION_EDITOR_ADD_OR_REMOVE_CARET= "EditorAddOrRemoveCaret";
@NonNls String ACTION_EDITOR_CLONE_CARET_BELOW= "EditorCloneCaretBelow";
@NonNls String ACTION_EDITOR_CLONE_CARET_ABOVE= "EditorCloneCaretAbove";
@NonNls String ACTION_EDITOR_NEXT_TEMPLATE_VARIABLE = "NextTemplateVariable";
@NonNls String ACTION_EDITOR_PREVIOUS_TEMPLATE_VARIABLE = "PreviousTemplateVariable";
@@ -634,7 +634,9 @@ public final class IdeKeyEventDispatcher implements Disposable {
processor.onUpdatePassed(e, action, actionEvent);
((DataManagerImpl.MyDataContext)myContext.getDataContext()).setEventCount(IdeEventQueue.getInstance().getEventCount(), this);
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()) {
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* 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.
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.keymap.impl;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
@@ -35,6 +36,7 @@ import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.*;
@@ -92,6 +94,10 @@ public class KeymapManagerImpl extends KeymapManagerEx implements PersistentStat
}
}
load();
ModifierKeyDoubleClickHandler.getInstance().registerAction(IdeActions.ACTION_EDITOR_CLONE_CARET_ABOVE, KeyEvent.VK_CONTROL, KeyEvent.VK_UP);
ModifierKeyDoubleClickHandler.getInstance().registerAction(IdeActions.ACTION_EDITOR_CLONE_CARET_BELOW, KeyEvent.VK_CONTROL, KeyEvent.VK_DOWN);
ourKeymapManagerInitialized = true;
}
@@ -0,0 +1,210 @@
/*
* 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.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.util.Clock;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.util.containers.ConcurrentHashMap;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntIntProcedure;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* Support for keyboard shortcuts like Control-double-click or Control-double-click+A
*/
public class ModifierKeyDoubleClickHandler {
private static final ModifierKeyDoubleClickHandler INSTANCE = new ModifierKeyDoubleClickHandler();
private final ConcurrentMap<String, IdeEventQueue.EventDispatcher> myDispatchers = new ConcurrentHashMap<String, IdeEventQueue.EventDispatcher>();
private ModifierKeyDoubleClickHandler() { }
public static ModifierKeyDoubleClickHandler getInstance() {
return INSTANCE;
}
/**
* @param actionId Id of action to be triggered on modifier+modifier[+actionKey]
* @param modifierKeyCode keyCode for modifier, e.g. KeyEvent.VK_SHIFT
* @param actionKeyCode keyCode for actionKey, or -1 if action should be triggered on bare modifier double click
*/
public void registerAction(@NotNull String actionId,
int modifierKeyCode,
int actionKeyCode) {
final MyDispatcher dispatcher = new MyDispatcher(actionId, modifierKeyCode, actionKeyCode);
IdeEventQueue.EventDispatcher oldDispatcher = myDispatchers.put(actionId, dispatcher);
IdeEventQueue.getInstance().addDispatcher(dispatcher, null);
if (oldDispatcher != null) {
IdeEventQueue.getInstance().removeDispatcher(oldDispatcher);
}
}
public void unregisterAction(@NotNull String actionId) {
IdeEventQueue.EventDispatcher oldDispatcher = myDispatchers.remove(actionId);
if (oldDispatcher != null) {
IdeEventQueue.getInstance().removeDispatcher(oldDispatcher);
}
}
private static class MyDispatcher implements IdeEventQueue.EventDispatcher {
private static final TIntIntHashMap KEY_CODE_TO_MODIFIER_MAP = new TIntIntHashMap();
static {
KEY_CODE_TO_MODIFIER_MAP.put(KeyEvent.VK_ALT, InputEvent.ALT_MASK);
KEY_CODE_TO_MODIFIER_MAP.put(KeyEvent.VK_CONTROL, InputEvent.CTRL_MASK);
KEY_CODE_TO_MODIFIER_MAP.put(KeyEvent.VK_META, InputEvent.META_MASK);
KEY_CODE_TO_MODIFIER_MAP.put(KeyEvent.VK_SHIFT, InputEvent.SHIFT_MASK);
}
private final String myActionId;
private final int myModifierKeyCode;
private final int myActionKeyCode;
private final Couple<AtomicBoolean> ourPressed = Couple.of(new AtomicBoolean(false), new AtomicBoolean(false));
private final Couple<AtomicBoolean> ourReleased = Couple.of(new AtomicBoolean(false), new AtomicBoolean(false));
private final AtomicBoolean ourOtherKeyWasPressed = new AtomicBoolean(false);
private final AtomicLong ourLastTimePressed = new AtomicLong(0);
public MyDispatcher(@NotNull String actionId, int modifierKeyCode, int actionKeyCode) {
myActionId = actionId;
myModifierKeyCode = modifierKeyCode;
myActionKeyCode = actionKeyCode;
}
@Override
public boolean dispatch(AWTEvent event) {
if (event instanceof KeyEvent) {
final KeyEvent keyEvent = (KeyEvent)event;
final int keyCode = keyEvent.getKeyCode();
if (keyCode == myModifierKeyCode) {
if (hasOtherModifiers(keyEvent)) {
resetState();
return false;
}
if (ourOtherKeyWasPressed.get() && Clock.getTime() - ourLastTimePressed.get() < 500) {
resetState();
return false;
}
ourOtherKeyWasPressed.set(false);
if (ourPressed.first.get() && Clock.getTime() - ourLastTimePressed.get() > 500) {
resetState();
}
handleModifier((KeyEvent)event);
return false;
} else if (ourPressed.first.get() && ourReleased.first.get() && ourPressed.second.get() && myActionKeyCode != -1 && !isActionBound()) {
if (keyCode == myActionKeyCode) {
if (event.getID() == KeyEvent.KEY_RELEASED) {
run(keyEvent);
}
return true;
}
return false;
} else {
ourLastTimePressed.set(Clock.getTime());
ourOtherKeyWasPressed.set(true);
if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_TAB) {
ourLastTimePressed.set(0);
}
}
resetState();
}
return false;
}
private boolean hasOtherModifiers(KeyEvent keyEvent) {
final int modifiers = keyEvent.getModifiers();
return !KEY_CODE_TO_MODIFIER_MAP.forEachEntry(new TIntIntProcedure() {
@Override
public boolean execute(int keyCode, int modifierMask) {
return keyCode == myModifierKeyCode || (modifiers & modifierMask) == 0;
}
});
}
private void handleModifier(KeyEvent event) {
if (ourPressed.first.get() && Clock.getTime() - ourLastTimePressed.get() > 300) {
resetState();
return;
}
if (event.getID() == KeyEvent.KEY_PRESSED) {
if (!ourPressed.first.get()) {
resetState();
ourPressed.first.set(true);
ourLastTimePressed.set(Clock.getTime());
return;
} else {
if (ourPressed.first.get() && ourReleased.first.get()) {
ourPressed.second.set(true);
ourLastTimePressed.set(Clock.getTime());
return;
}
}
} else if (event.getID() == KeyEvent.KEY_RELEASED) {
if (ourPressed.first.get() && !ourReleased.first.get()) {
ourReleased.first.set(true);
ourLastTimePressed.set(Clock.getTime());
return;
} else if (ourPressed.first.get() && ourReleased.first.get() && ourPressed.second.get()) {
resetState();
if (myActionKeyCode == -1 && !isActionBound()) {
run(event);
}
return;
}
}
resetState();
}
private void resetState() {
ourPressed.first.set(false);
ourPressed.second.set(false);
ourReleased.first.set(false);
ourReleased.second.set(false);
}
private void run(KeyEvent event) {
final ActionManager actionManager = ActionManager.getInstance();
final AnAction action = actionManager.getAction(myActionId);
final AnActionEvent anActionEvent = new AnActionEvent(event,
DataManager.getInstance().getDataContext(IdeFocusManager.findInstance().getFocusOwner()),
ActionPlaces.MAIN_MENU,
action.getTemplatePresentation(),
actionManager,
0);
action.actionPerformed(anActionEvent);
}
private boolean isActionBound() {
return KeymapManager.getInstance().getActiveKeymap().getShortcuts(myActionId).length > 0;
}
}
}
@@ -0,0 +1,169 @@
/*
* 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.IdeEventQueue;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.util.Clock;
import com.intellij.testFramework.LightPlatformTestCase;
import javax.swing.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
public class ModifierKeyDoubleClickHandlerTest extends LightPlatformTestCase {
private static final String MY_SHIFT_SHIFT_ACTION = "ModifierKeyDoubleClickHandlerTest.action1";
private static final String MY_SHIFT_KEY_ACTION = "ModifierKeyDoubleClickHandlerTest.action2";
private static final String MY_SHIFT_SHIFT_KEY_ACTION = "ModifierKeyDoubleClickHandlerTest.action3";
public static final KeyboardShortcut SHIFT_KEY_SHORTCUT = new KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,
InputEvent.SHIFT_MASK),
null);
private final JComponent myComponent = new JPanel();
private long myCurrentTime;
private int myShiftShiftActionInvocationCount;
private int myShiftKeyActionInvocationCount;
private int myShiftShiftKeyActionInvocationCount;
@Override
public void setUp() throws Exception {
super.setUp();
Clock.setTime(0);
ActionManager.getInstance().registerAction(MY_SHIFT_SHIFT_ACTION, new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
myShiftShiftActionInvocationCount++;
}
});
ActionManager.getInstance().registerAction(MY_SHIFT_KEY_ACTION, new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
myShiftKeyActionInvocationCount++;
}
});
ActionManager.getInstance().registerAction(MY_SHIFT_SHIFT_KEY_ACTION, new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
myShiftShiftKeyActionInvocationCount++;
}
});
KeymapManager.getInstance().getActiveKeymap().addShortcut(MY_SHIFT_KEY_ACTION, SHIFT_KEY_SHORTCUT);
ModifierKeyDoubleClickHandler.getInstance().registerAction(MY_SHIFT_SHIFT_ACTION, KeyEvent.VK_SHIFT, -1);
ModifierKeyDoubleClickHandler.getInstance().registerAction(MY_SHIFT_SHIFT_KEY_ACTION, KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SPACE);
}
@Override
public void tearDown() throws Exception {
ModifierKeyDoubleClickHandler.getInstance().unregisterAction(MY_SHIFT_SHIFT_KEY_ACTION);
ModifierKeyDoubleClickHandler.getInstance().unregisterAction(MY_SHIFT_SHIFT_ACTION);
KeymapManager.getInstance().getActiveKeymap().removeShortcut(MY_SHIFT_KEY_ACTION, SHIFT_KEY_SHORTCUT);
ActionManager.getInstance().unregisterAction(MY_SHIFT_SHIFT_KEY_ACTION);
ActionManager.getInstance().unregisterAction(MY_SHIFT_KEY_ACTION);
ActionManager.getInstance().unregisterAction(MY_SHIFT_SHIFT_ACTION);
Clock.reset();
super.tearDown();
}
public void testShiftShiftSuccessfulCase() {
press();
release();
press();
assertInvocationCounts(0, 0, 0);
release();
assertInvocationCounts(0, 1, 0);
}
public void testLongSecondClick() {
press();
release();
press();
timeStep(400);
release();
assertInvocationCounts(0, 0, 0);
}
public void testShiftShiftKeySuccessfulCase() {
press();
release();
press();
key();
assertInvocationCounts(0, 0, 1);
release();
assertInvocationCounts(0, 0, 1);
}
public void testShiftKey() {
press();
key();
assertInvocationCounts(1, 0, 0);
release();
}
public void assertInvocationCounts(int shiftKeyCount, int shiftShiftCount, int shiftShiftKeyCount) {
assertEquals(shiftKeyCount, myShiftKeyActionInvocationCount);
assertEquals(shiftShiftCount, myShiftShiftActionInvocationCount);
assertEquals(shiftShiftKeyCount, myShiftShiftKeyActionInvocationCount);
}
private void press() {
IdeEventQueue.getInstance().dispatchEvent(new KeyEvent(myComponent,
KeyEvent.KEY_PRESSED,
Clock.getTime(),
InputEvent.SHIFT_MASK,
KeyEvent.VK_SHIFT,
KeyEvent.CHAR_UNDEFINED));
}
private void release() {
IdeEventQueue.getInstance().dispatchEvent(new KeyEvent(myComponent,
KeyEvent.KEY_RELEASED,
Clock.getTime(),
0,
KeyEvent.VK_SHIFT,
KeyEvent.CHAR_UNDEFINED));
}
private void key() {
IdeEventQueue.getInstance().dispatchEvent(new KeyEvent(myComponent,
KeyEvent.KEY_PRESSED,
Clock.getTime(),
InputEvent.SHIFT_MASK,
KeyEvent.VK_BACK_SPACE,
'\b'));
IdeEventQueue.getInstance().dispatchEvent(new KeyEvent(myComponent,
KeyEvent.KEY_TYPED,
Clock.getTime(),
InputEvent.SHIFT_MASK,
0,
'\b'));
IdeEventQueue.getInstance().dispatchEvent(new KeyEvent(myComponent,
KeyEvent.KEY_RELEASED,
Clock.getTime(),
InputEvent.SHIFT_MASK,
KeyEvent.VK_BACK_SPACE,
'\b'));
}
private void timeStep(long step) {
Clock.setTime(myCurrentTime += step);
}
}