f9d9a5897843fb2d76d9ccf193ac66ef412ba213
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / keymap / KeymapUtil.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.keymap;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.openapi.actionSystem.*;
20 import com.intellij.openapi.application.Application;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.util.Disposer;
23 import com.intellij.openapi.util.InvalidDataException;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.registry.Registry;
26 import com.intellij.openapi.util.registry.RegistryValue;
27 import com.intellij.openapi.util.registry.RegistryValueListener;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.util.containers.ContainerUtil;
30 import org.intellij.lang.annotations.JdkConstants;
31 import org.jetbrains.annotations.NonNls;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import javax.swing.*;
36 import java.awt.*;
37 import java.awt.event.*;
38 import java.util.HashSet;
39 import java.util.Set;
40 import java.util.StringTokenizer;
41
42 public class KeymapUtil {
43
44   @NonNls private static final String CANCEL_KEY_TEXT = "Cancel";
45   @NonNls private static final String BREAK_KEY_TEXT = "Break";
46   @NonNls private static final String SHIFT = "shift";
47   @NonNls private static final String CONTROL = "control";
48   @NonNls private static final String CTRL = "ctrl";
49   @NonNls private static final String META = "meta";
50   @NonNls private static final String ALT = "alt";
51   @NonNls private static final String ALT_GRAPH = "altGraph";
52   @NonNls private static final String DOUBLE_CLICK = "doubleClick";
53
54   private static final Set<Integer> ourTooltipKeys = new HashSet<>();
55   private static final Set<Integer> ourOtherTooltipKeys = new HashSet<>();
56   private static RegistryValue ourTooltipKeysProperty;
57
58   private KeymapUtil() {
59   }
60
61   public static String getShortcutText(@NotNull Shortcut shortcut) {
62     String s = "";
63
64     if (shortcut instanceof KeyboardShortcut) {
65       KeyboardShortcut keyboardShortcut = (KeyboardShortcut)shortcut;
66
67       String acceleratorText = getKeystrokeText(keyboardShortcut.getFirstKeyStroke());
68       if (!acceleratorText.isEmpty()) {
69         s = acceleratorText;
70       }
71
72       acceleratorText = getKeystrokeText(keyboardShortcut.getSecondKeyStroke());
73       if (!acceleratorText.isEmpty()) {
74         s += ", " + acceleratorText;
75       }
76     }
77     else if (shortcut instanceof MouseShortcut) {
78       s = getMouseShortcutText((MouseShortcut)shortcut);
79     }
80     else if (shortcut instanceof KeyboardModifierGestureShortcut) {
81       final KeyboardModifierGestureShortcut gestureShortcut = (KeyboardModifierGestureShortcut)shortcut;
82       s = gestureShortcut.getType() == KeyboardGestureAction.ModifierType.dblClick ? "Press, release and hold " : "Hold ";
83       s += getKeystrokeText(gestureShortcut.getStroke());
84     }
85     else {
86       throw new IllegalArgumentException("unknown shortcut class: " + shortcut.getClass().getCanonicalName());
87     }
88     return s;
89   }
90
91   public static Icon getShortcutIcon(Shortcut shortcut) {
92     if (shortcut instanceof KeyboardShortcut) {
93       return AllIcons.General.KeyboardShortcut;
94     }
95     else if (shortcut instanceof MouseShortcut) {
96       return AllIcons.General.MouseShortcut;
97     }
98     else {
99       throw new IllegalArgumentException("unknown shortcut class: " + shortcut);
100     }
101   }
102
103   public static String getMouseShortcutText(@NotNull MouseShortcut shortcut) {
104     if (shortcut instanceof PressureShortcut) return shortcut.toString();
105     return getMouseShortcutText(shortcut.getButton(), shortcut.getModifiers(), shortcut.getClickCount());
106   }
107
108   /**
109    * @param button        target mouse button
110    * @param modifiers     modifiers used within the target click
111    * @param clickCount    target clicks count
112    * @return string representation of passed mouse shortcut.
113    */
114   public static String getMouseShortcutText(int button, @JdkConstants.InputEventMask int modifiers, int clickCount) {
115     String resource;
116     if (button == MouseShortcut.BUTTON_WHEEL_UP) {
117       resource = "mouse.wheel.rotate.up.shortcut.text";
118     }
119     else if (button == MouseShortcut.BUTTON_WHEEL_DOWN) {
120       resource = "mouse.wheel.rotate.down.shortcut.text";
121     }
122     else if (clickCount < 2) {
123       resource = "mouse.click.shortcut.text";
124     }
125     else if (clickCount < 3) {
126       resource = "mouse.double.click.shortcut.text";
127     } else {
128       throw new IllegalStateException("unknown clickCount: " + clickCount);
129     }
130     return KeyMapBundle.message(resource, getModifiersText(mapNewModifiers(modifiers)), button);
131   }
132
133   @JdkConstants.InputEventMask
134   private static int mapNewModifiers(@JdkConstants.InputEventMask int modifiers) {
135     if ((modifiers & InputEvent.SHIFT_DOWN_MASK) != 0) {
136       modifiers |= InputEvent.SHIFT_MASK;
137     }
138     if ((modifiers & InputEvent.ALT_DOWN_MASK) != 0) {
139       modifiers |= InputEvent.ALT_MASK;
140     }
141     if ((modifiers & InputEvent.ALT_GRAPH_DOWN_MASK) != 0) {
142       modifiers |= InputEvent.ALT_GRAPH_MASK;
143     }
144     if ((modifiers & InputEvent.CTRL_DOWN_MASK) != 0) {
145       modifiers |= InputEvent.CTRL_MASK;
146     }
147     if ((modifiers & InputEvent.META_DOWN_MASK) != 0) {
148       modifiers |= InputEvent.META_MASK;
149     }
150
151     return modifiers;
152   }
153
154   public static String getKeystrokeText(KeyStroke accelerator) {
155     if (accelerator == null) return "";
156     if (SystemInfo.isMac) {
157       return MacKeymapUtil.getKeyStrokeText(accelerator);
158     }
159     String acceleratorText = "";
160     int modifiers = accelerator.getModifiers();
161     if (modifiers > 0) {
162       acceleratorText = getModifiersText(modifiers);
163     }
164
165     final int code = accelerator.getKeyCode();
166     String keyText = SystemInfo.isMac ? MacKeymapUtil.getKeyText(code) : KeyEvent.getKeyText(code);
167     // [vova] this is dirty fix for bug #35092
168     if(CANCEL_KEY_TEXT.equals(keyText)){
169       keyText = BREAK_KEY_TEXT;
170     }
171
172     acceleratorText += keyText;
173     return acceleratorText.trim();
174   }
175
176   private static String getModifiersText(@JdkConstants.InputEventMask int modifiers) {
177     if (SystemInfo.isMac) {
178       //try {
179       //  Class appleLaf = Class.forName(APPLE_LAF_AQUA_LOOK_AND_FEEL_CLASS_NAME);
180       //  Method getModifiers = appleLaf.getMethod(GET_KEY_MODIFIERS_TEXT_METHOD, int.class, boolean.class);
181       //  return (String)getModifiers.invoke(appleLaf, modifiers, Boolean.FALSE);
182       //}
183       //catch (Exception e) {
184       //  if (SystemInfo.isMacOSLeopard) {
185       //    return getKeyModifiersTextForMacOSLeopard(modifiers);
186       //  }
187       //
188       //  // OK do nothing here.
189       //}
190       return MacKeymapUtil.getModifiersText(modifiers);
191     }
192
193     final String keyModifiersText = KeyEvent.getKeyModifiersText(modifiers);
194     if (keyModifiersText.isEmpty()) {
195       return keyModifiersText;
196     }
197     else {
198       return keyModifiersText + "+";
199     }
200   }
201
202   @NotNull
203   public static ShortcutSet getActiveKeymapShortcuts(@Nullable String actionId) {
204     Application application = ApplicationManager.getApplication();
205     KeymapManager keymapManager = application == null ? null : application.getComponent(KeymapManager.class);
206     if (keymapManager == null || actionId == null) {
207       return new CustomShortcutSet(Shortcut.EMPTY_ARRAY);
208     }
209     return new CustomShortcutSet(keymapManager.getActiveKeymap().getShortcuts(actionId));
210   }
211
212   @NotNull
213   public static String getFirstKeyboardShortcutText(@NotNull String actionId) {
214     Shortcut[] shortcuts = getActiveKeymapShortcuts(actionId).getShortcuts();
215     KeyboardShortcut shortcut = ContainerUtil.findInstance(shortcuts, KeyboardShortcut.class);
216     return shortcut == null? "" : getShortcutText(shortcut);
217   }
218
219   @NotNull
220   public static String getFirstKeyboardShortcutText(@NotNull AnAction action) {
221     return getFirstKeyboardShortcutText(action.getShortcutSet());
222   }
223
224   @NotNull
225   public static String getFirstKeyboardShortcutText(@NotNull ShortcutSet set) {
226     Shortcut[] shortcuts = set.getShortcuts();
227     KeyboardShortcut shortcut = ContainerUtil.findInstance(shortcuts, KeyboardShortcut.class);
228     return shortcut == null ? "" : getShortcutText(shortcut);
229   }
230
231   @NotNull
232   public static String getPreferredShortcutText(@NotNull Shortcut[] shortcuts) {
233     KeyboardShortcut shortcut = ContainerUtil.findInstance(shortcuts, KeyboardShortcut.class);
234     return shortcut != null ? getShortcutText(shortcut) :
235            shortcuts.length > 0 ? getShortcutText(shortcuts[0]) : "";
236   }
237
238   public static String getShortcutsText(Shortcut[] shortcuts) {
239     if (shortcuts.length == 0) {
240       return "";
241     }
242     StringBuilder buffer = new StringBuilder();
243     for (int i = 0; i < shortcuts.length; i++) {
244       Shortcut shortcut = shortcuts[i];
245       if (i > 0) {
246         buffer.append(' ');
247       }
248       buffer.append(getShortcutText(shortcut));
249     }
250     return buffer.toString();
251   }
252
253   /**
254    * Factory method. It parses passed string and creates <code>MouseShortcut</code>.
255    *
256    * @param keystrokeString       target keystroke
257    * @return                      shortcut for the given keystroke
258    * @throws InvalidDataException if <code>keystrokeString</code> doesn't represent valid <code>MouseShortcut</code>.
259    */
260   public static MouseShortcut parseMouseShortcut(String keystrokeString) throws InvalidDataException {
261     if (Registry.is("ide.mac.forceTouch") && keystrokeString.startsWith("Force touch")) {
262       return new PressureShortcut(2);
263     }
264
265     int button = -1;
266     int modifiers = 0;
267     int clickCount = 1;
268     for (StringTokenizer tokenizer = new StringTokenizer(keystrokeString); tokenizer.hasMoreTokens();) {
269       String token = tokenizer.nextToken();
270       if (SHIFT.equals(token)) {
271         modifiers |= InputEvent.SHIFT_DOWN_MASK;
272       }
273       else if (CONTROL.equals(token) || CTRL.equals(token)) {
274         modifiers |= InputEvent.CTRL_DOWN_MASK;
275       }
276       else if (META.equals(token)) {
277         modifiers |= InputEvent.META_DOWN_MASK;
278       }
279       else if (ALT.equals(token)) {
280         modifiers |= InputEvent.ALT_DOWN_MASK;
281       }
282       else if (ALT_GRAPH.equals(token)) {
283         modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK;
284       }
285       else if (token.startsWith("button") && token.length() > 6) {
286         try {
287           button = Integer.parseInt(token.substring(6));
288         }
289         catch (NumberFormatException e) {
290           throw new InvalidDataException("unparseable token: " + token);
291         }
292       }
293       else if (DOUBLE_CLICK.equals(token)) {
294         clickCount = 2;
295       }
296       else {
297         throw new InvalidDataException("unknown token: " + token);
298       }
299     }
300     return new MouseShortcut(button, modifiers, clickCount);
301   }
302
303   /**
304    * @return string representation of passed mouse shortcut. This method should
305    *         be used only for serializing of the <code>MouseShortcut</code>
306    */
307   public static String getMouseShortcutString(MouseShortcut shortcut) {
308     if (Registry.is("ide.mac.forceTouch") && shortcut instanceof PressureShortcut) {
309       return "Force touch";
310     }
311
312     StringBuilder buffer = new StringBuilder();
313
314     // modifiers
315     int modifiers = shortcut.getModifiers();
316     if ((InputEvent.SHIFT_DOWN_MASK & modifiers) > 0) {
317       buffer.append(SHIFT);
318       buffer.append(' ');
319     }
320     if ((InputEvent.CTRL_DOWN_MASK & modifiers) > 0) {
321       buffer.append(CONTROL);
322       buffer.append(' ');
323     }
324     if ((InputEvent.META_DOWN_MASK & modifiers) > 0) {
325       buffer.append(META);
326       buffer.append(' ');
327     }
328     if ((InputEvent.ALT_DOWN_MASK & modifiers) > 0) {
329       buffer.append(ALT);
330       buffer.append(' ');
331     }
332     if ((InputEvent.ALT_GRAPH_DOWN_MASK & modifiers) > 0) {
333       buffer.append(ALT_GRAPH);
334       buffer.append(' ');
335     }
336
337     // button
338     buffer.append("button").append(shortcut.getButton()).append(' ');
339
340     if (shortcut.getClickCount() > 1) {
341       buffer.append(DOUBLE_CLICK);
342     }
343     return buffer.toString().trim(); // trim trailing space (if any)
344   }
345
346   public static String getKeyModifiersTextForMacOSLeopard(@JdkConstants.InputEventMask int modifiers) {
347     StringBuilder buf = new StringBuilder();
348       if ((modifiers & InputEvent.META_MASK) != 0) {
349           buf.append("\u2318");
350       }
351       if ((modifiers & InputEvent.CTRL_MASK) != 0) {
352           buf.append(Toolkit.getProperty("AWT.control", "Ctrl"));
353       }
354       if ((modifiers & InputEvent.ALT_MASK) != 0) {
355           buf.append("\u2325");
356       }
357       if ((modifiers & InputEvent.SHIFT_MASK) != 0) {
358           buf.append(Toolkit.getProperty("AWT.shift", "Shift"));
359       }
360       if ((modifiers & InputEvent.ALT_GRAPH_MASK) != 0) {
361           buf.append(Toolkit.getProperty("AWT.altGraph", "Alt Graph"));
362       }
363       if ((modifiers & InputEvent.BUTTON1_MASK) != 0) {
364           buf.append(Toolkit.getProperty("AWT.button1", "Button1"));
365       }
366       return buf.toString();
367   }
368
369   public static boolean isTooltipRequest(KeyEvent keyEvent) {
370     if (ourTooltipKeysProperty == null) {
371       ourTooltipKeysProperty = Registry.get("ide.forcedShowTooltip");
372       ourTooltipKeysProperty.addListener(new RegistryValueListener.Adapter() {
373         @Override
374         public void afterValueChanged(RegistryValue value) {
375           updateTooltipRequestKey(value);
376         }
377       }, Disposer.get("ui"));
378
379       updateTooltipRequestKey(ourTooltipKeysProperty);
380     }
381
382     if (keyEvent.getID() != KeyEvent.KEY_PRESSED) return false;
383
384     for (Integer each : ourTooltipKeys) {
385       if ((keyEvent.getModifiers() & each.intValue()) == 0) return false;
386     }
387
388     for (Integer each : ourOtherTooltipKeys) {
389       if ((keyEvent.getModifiers() & each.intValue()) > 0) return false;
390     }
391
392     final int code = keyEvent.getKeyCode();
393
394     return code == KeyEvent.VK_META || code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_ALT;
395   }
396
397   private static void updateTooltipRequestKey(RegistryValue value) {
398     final String text = value.asString();
399
400     ourTooltipKeys.clear();
401     ourOtherTooltipKeys.clear();
402
403     processKey(text.contains("meta"), InputEvent.META_MASK);
404     processKey(text.contains("control") || text.contains("ctrl"), InputEvent.CTRL_MASK);
405     processKey(text.contains("shift"), InputEvent.SHIFT_MASK);
406     processKey(text.contains("alt"), InputEvent.ALT_MASK);
407
408   }
409
410   private static void processKey(boolean condition, int value) {
411     if (condition) {
412       ourTooltipKeys.add(value);
413     } else {
414       ourOtherTooltipKeys.add(value);
415     }
416   }
417
418   public static boolean isEmacsKeymap() {
419     return isEmacsKeymap(KeymapManager.getInstance().getActiveKeymap());
420   }
421
422   public static boolean isEmacsKeymap(@Nullable Keymap keymap) {
423     for (; keymap != null; keymap = keymap.getParent()) {
424       if ("Emacs".equalsIgnoreCase(keymap.getName())) {
425         return true;
426       }
427     }
428     return false;
429   }
430
431   @Nullable
432   public static KeyStroke getKeyStroke(@NotNull final ShortcutSet shortcutSet) {
433     final Shortcut[] shortcuts = shortcutSet.getShortcuts();
434     if (shortcuts.length == 0 || !(shortcuts[0] instanceof KeyboardShortcut)) return null;
435     final KeyboardShortcut shortcut = (KeyboardShortcut)shortcuts[0];
436     if (shortcut.getSecondKeyStroke() != null) {
437       return null;
438     }
439     return shortcut.getFirstKeyStroke();
440   }
441
442   @NotNull
443   public static String createTooltipText(@Nullable String name, @NotNull AnAction action) {
444     String toolTipText = name == null ? "" : name;
445     while (StringUtil.endsWithChar(toolTipText, '.')) {
446       toolTipText = toolTipText.substring(0, toolTipText.length() - 1);
447     }
448     String shortcutsText = getFirstKeyboardShortcutText(action);
449     if (!shortcutsText.isEmpty()) {
450       toolTipText += " (" + shortcutsText + ")";
451     }
452     return toolTipText;
453   }
454
455   /**
456    * Checks that one of the mouse shortcuts assigned to the provided action has the same modifiers as provided
457    */
458   public static boolean matchActionMouseShortcutsModifiers(final Keymap activeKeymap,
459                                                            @JdkConstants.InputEventMask int modifiers,
460                                                            final String actionId) {
461     final MouseShortcut syntheticShortcut = new MouseShortcut(MouseEvent.BUTTON1, modifiers, 1);
462     for (Shortcut shortcut : activeKeymap.getShortcuts(actionId)) {
463       if (shortcut instanceof MouseShortcut) {
464         final MouseShortcut mouseShortcut = (MouseShortcut)shortcut;
465         if (mouseShortcut.getModifiers() == syntheticShortcut.getModifiers()) {
466           return true;
467         }
468       }
469     }
470     return false;
471   }
472
473   /**
474    * Creates shortcut corresponding to a single-click event
475    */
476   public static MouseShortcut createMouseShortcut(@NotNull MouseEvent e) {
477     int button = MouseShortcut.getButton(e);
478     int modifiers = e.getModifiersEx();
479     if (button == MouseEvent.NOBUTTON && e.getID() == MouseEvent.MOUSE_DRAGGED) {
480       // mouse drag events don't have button field set due to some reason
481       if ((modifiers & InputEvent.BUTTON1_DOWN_MASK) != 0) {
482         button = MouseEvent.BUTTON1;
483       }
484       else if ((modifiers & InputEvent.BUTTON2_DOWN_MASK) != 0) {
485         button = MouseEvent.BUTTON2;
486       }
487     }
488     return new MouseShortcut(button, modifiers, 1);
489   }
490
491   /**
492    * @param component    target component to reassign previously mapped action (if any)
493    * @param oldKeyStroke previously mapped keystroke (e.g. standard one that you want to use in some different way)
494    * @param newKeyStroke new keystroke to be assigned. <code>null</code> value means 'just unregister previously mapped action'
495    * @param condition    one of
496    *                     <ul>
497    *                     <li>JComponent.WHEN_FOCUSED,</li>
498    *                     <li>JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT</li>
499    *                     <li>JComponent.WHEN_IN_FOCUSED_WINDOW</li>
500    *                     <li>JComponent.UNDEFINED_CONDITION</li>
501    *                     </ul>
502    * @return <code>true</code> if the action is reassigned successfully
503    */
504   public static boolean reassignAction(@NotNull JComponent component,
505                                        @NotNull KeyStroke oldKeyStroke,
506                                        @Nullable KeyStroke newKeyStroke,
507                                        int condition) {
508     return reassignAction(component, oldKeyStroke, newKeyStroke, condition, true);
509   }
510   /**
511    * @param component    target component to reassign previously mapped action (if any)
512    * @param oldKeyStroke previously mapped keystroke (e.g. standard one that you want to use in some different way)
513    * @param newKeyStroke new keystroke to be assigned. <code>null</code> value means 'just unregister previously mapped action'
514    * @param condition    one of
515    *                     <ul>
516    *                     <li>JComponent.WHEN_FOCUSED,</li>
517    *                     <li>JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT</li>
518    *                     <li>JComponent.WHEN_IN_FOCUSED_WINDOW</li>
519    *                     <li>JComponent.UNDEFINED_CONDITION</li>
520    *                     </ul>
521    * @param muteOldKeystroke if <code>true</code> old keystroke wouldn't work anymore
522    * @return <code>true</code> if the action is reassigned successfully
523    */
524   public static boolean reassignAction(@NotNull JComponent component,
525                                        @NotNull KeyStroke oldKeyStroke,
526                                        @Nullable KeyStroke newKeyStroke,
527                                        int condition, boolean muteOldKeystroke) {
528     ActionListener action = component.getActionForKeyStroke(oldKeyStroke);
529     if (action == null) return false;
530     if (newKeyStroke != null) {
531       component.registerKeyboardAction(action, newKeyStroke, condition);
532     }
533     if (muteOldKeystroke) {
534       component.registerKeyboardAction(new RedispatchEventAction(component), oldKeyStroke, condition);
535     }
536     return true;
537   }
538
539   private static final class RedispatchEventAction extends AbstractAction {
540     private final Component myComponent;
541
542     public RedispatchEventAction(Component component) {
543       myComponent = component;
544     }
545
546     @Override
547     public void actionPerformed(ActionEvent e) {
548       AWTEvent event = EventQueue.getCurrentEvent();
549       if (event instanceof KeyEvent && event.getSource() == myComponent) {
550         Container parent = myComponent.getParent();
551         if (parent != null) {
552           KeyEvent keyEvent = (KeyEvent)event;
553           parent.dispatchEvent(new KeyEvent(parent, event.getID(), ((KeyEvent)event).getWhen(), keyEvent.getModifiers(), keyEvent.getKeyCode(), keyEvent.getKeyChar(), keyEvent
554             .getKeyLocation()));
555         }
556       }
557     }
558   }
559 }