Merge branch 'db/javac-ast'
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / KeyStrokeAdapter.java
1 /*
2  * Copyright 2000-2015 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.ui;
17
18 import com.intellij.Patches;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.intellij.openapi.util.registry.Registry;
22 import com.intellij.util.containers.HashMap;
23
24 import javax.swing.*;
25 import java.awt.event.InputEvent;
26 import java.awt.event.KeyEvent;
27 import java.awt.event.KeyListener;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.Method;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.StringTokenizer;
33
34 /**
35  * @author Sergey.Malenkov
36  */
37 public class KeyStrokeAdapter implements KeyListener {
38   private static final Logger LOG = Logger.getInstance(KeyStrokeAdapter.class);
39
40   @Override
41   public void keyTyped(KeyEvent event) {
42     keyTyped(event, getKeyStroke(event, false));
43   }
44
45   protected boolean keyTyped(KeyStroke stroke) {
46     return false;
47   }
48
49   private void keyTyped(KeyEvent event, KeyStroke stroke) {
50     if (stroke != null && keyTyped(stroke)) {
51       event.consume();
52     }
53   }
54
55   @Override
56   public void keyPressed(KeyEvent event) {
57     keyPressed(event, getKeyStroke(event, true));
58     keyPressed(event, getKeyStroke(event, false));
59   }
60
61   protected boolean keyPressed(KeyStroke stroke) {
62     return false;
63   }
64
65   private void keyPressed(KeyEvent event, KeyStroke stroke) {
66     if (stroke != null && keyPressed(stroke)) {
67       event.consume();
68     }
69   }
70
71   @Override
72   public void keyReleased(KeyEvent event) {
73     keyReleased(event, getKeyStroke(event, true));
74     keyReleased(event, getKeyStroke(event, false));
75   }
76
77   protected boolean keyReleased(KeyStroke stroke) {
78     return false;
79   }
80
81   private void keyReleased(KeyEvent event, KeyStroke stroke) {
82     if (stroke != null && keyReleased(stroke)) {
83       event.consume();
84     }
85   }
86
87   /**
88    * @param event the specified key event to process
89    * @return a key stroke or {@code null} if it is not applicable
90    * @see KeyStroke#getKeyStrokeForEvent(KeyEvent)
91    */
92   public static KeyStroke getDefaultKeyStroke(KeyEvent event) {
93     if (event == null || event.isConsumed()) return null;
94     // On Windows and Mac it is preferable to use normal key code here
95     boolean extendedKeyCodeFirst = !SystemInfo.isWindows && !SystemInfo.isMac && event.getModifiers() == 0;
96     KeyStroke stroke = getKeyStroke(event, extendedKeyCodeFirst);
97     return stroke != null ? stroke : getKeyStroke(event, !extendedKeyCodeFirst);
98   }
99
100   /**
101    * @param event    the specified key event to process
102    * @param extended {@code true} if extended key code should be used
103    * @return a key stroke or {@code null} if it is not applicable
104    * @see JComponent#processKeyBindings(KeyEvent, boolean)
105    */
106   public static KeyStroke getKeyStroke(KeyEvent event, boolean extended) {
107     if (event != null && !event.isConsumed()) {
108       int id = event.getID();
109       if (id == KeyEvent.KEY_TYPED) {
110         return extended ? null : getKeyStroke(event.getKeyChar(), 0);
111       }
112       boolean released = id == KeyEvent.KEY_RELEASED;
113       if (released || id == KeyEvent.KEY_PRESSED) {
114         int code = event.getKeyCode();
115         if (extended) {
116           if (Registry.is("actionSystem.extendedKeyCode.disabled")) {
117             return null;
118           }
119           code = getExtendedKeyCode(event);
120           if (code == event.getKeyCode()) {
121             return null;
122           }
123         }
124         return getKeyStroke(code, event.getModifiers(), released);
125       }
126     }
127     return null;
128   }
129
130   /**
131    * @param ch        the specified key character
132    * @param modifiers the modifier mask from the event
133    * @return a key stroke or {@code null} if {@code ch} is undefined
134    */
135   private static KeyStroke getKeyStroke(char ch, int modifiers) {
136     return KeyEvent.CHAR_UNDEFINED == ch ? null : KeyStroke.getKeyStroke(Character.valueOf(ch), modifiers);
137   }
138
139   /**
140    * @param code      the numeric code for a keyboard key
141    * @param modifiers the modifier mask from the event
142    * @param released  {@code true} if the key stroke should represent a key release
143    * @return a key stroke or {@code null} if {@code code} is undefined
144    */
145   private static KeyStroke getKeyStroke(int code, int modifiers, boolean released) {
146     return KeyEvent.VK_UNDEFINED == code ? null : KeyStroke.getKeyStroke(code, modifiers, released);
147   }
148
149   // TODO: HACK because of Java7 required:
150   // replace later with event.getExtendedKeyCode()
151   private static int getExtendedKeyCode(KeyEvent event) {
152     //noinspection ConstantConditions
153     assert Patches.USE_REFLECTION_TO_ACCESS_JDK7;
154     try {
155       Method method = KeyEvent.class.getMethod("getExtendedKeyCode");
156       if (!method.isAccessible()) {
157         method.setAccessible(true);
158       }
159       return (Integer)method.invoke(event);
160     }
161     catch (Exception exception) {
162       return event.getKeyCode();
163     }
164   }
165
166   /**
167    * Parses a string and returns the corresponding key stroke.
168    * The string must have the following syntax:
169    * <pre>
170    *    &lt;modifiers&gt;* (&lt;typedID&gt; | &lt;pressedReleasedID&gt;)
171    * </pre>where<pre>
172    *    modifiers := shift | ctrl | control | meta | alt | altGr | altGraph
173    *    typedID := typed &lt;char&gt;
174    *    pressedReleasedID := (pressed | released) key
175    * </pre>
176    * If {@code typed}, {@code pressed} or {@code released} is not specified, {@code pressed} is assumed.
177    * The {@code char} is a string of length 1 giving Unicode character.
178    * The {@code key} is a virtual key name or an integer that represents a key code.
179    * Note that the virtual key name is a second part of a name
180    * of the corresponding field defined in the {@link KeyEvent} class.
181    * <p/>
182    * This method has two differences from the {@link KeyStroke#getKeyStroke(String)} method.
183    * First, it does not throw an exception if the specified string cannot be parsed.
184    * Second, it supports an integer representation of a key code
185    * if the corresponding virtual key is not specified in the {@link KeyEvent} class.
186    * <p/>
187    * This method returns {@code null}
188    * if the specified string is {@code null} or if it cannot be parsed.
189    * The error message is logged without throwing an exception.
190    *
191    * @param string the specified string to parse as described above
192    * @return a key stroke string represented by the specified string
193    */
194   public static KeyStroke getKeyStroke(String string) {
195     if (string != null) {
196       StringTokenizer st = new StringTokenizer(string, " ");
197
198       int modifiers = 0;
199       boolean typed = false;
200       boolean pressed = false;
201       boolean released = false;
202
203       int count = st.countTokens();
204       for (int i = 1; i <= count; i++) {
205         String token = st.nextToken();
206         if (typed) {
207           if (st.hasMoreTokens()) {
208             LOG.error("key stroke declaration has more tokens: " + st.nextToken());
209             return null;
210           }
211           if (token.length() != 1) {
212             LOG.error("unexpected key stroke character: " + token);
213             return null;
214           }
215           return getKeyStroke(token.charAt(0), modifiers);
216         }
217         String tokenLowerCase = token.toLowerCase(Locale.ENGLISH);
218         if (pressed || released || i == count) {
219           if (st.hasMoreTokens()) {
220             LOG.error("key stroke declaration has more tokens: " + st.nextToken());
221             return null;
222           }
223           Integer code = LazyVirtualKeys.myNameToCode.get(tokenLowerCase);
224           if (code == null) {
225             try {
226               code = Integer.decode(token);
227             }
228             catch (NumberFormatException exception) {
229               LOG.error("unexpected key stroke code: " + token);
230               return null;
231             }
232           }
233           return getKeyStroke(code, modifiers, released);
234         }
235         if (tokenLowerCase.equals("typed")) {
236           typed = true;
237         }
238         else if (tokenLowerCase.equals("pressed")) {
239           pressed = true;
240         }
241         else if (tokenLowerCase.equals("released")) {
242           released = true;
243         }
244         else {
245           Integer mask = LazyModifiers.mapNameToMask.get(tokenLowerCase);
246           if (mask == null) {
247             LOG.error("unexpected key stroke modifier: " + token);
248             return null;
249           }
250           modifiers |= mask;
251         }
252       }
253       LOG.error("key stroke declaration is not completed");
254     }
255     return null;
256   }
257
258   /**
259    * Returns a string that represents the specified key stroke.
260    * The result of this method can be passed as a parameter
261    * to the {@link #getKeyStroke(String)} method to produce
262    * a key stroke equal to the specified key stroke.
263    * <p/>
264    * This method has two differences from the {@link KeyStroke#toString()} method.
265    * First, the resulting string does not contain the "pressed" keyword.
266    * Second, the resulting string contains a hexadecimal integer instead of {@code null}
267    * if the corresponding virtual key is not specified in the {@link KeyEvent} class.
268    * <p/>
269    * This method returns {@code null}
270    * if the specified key stroke is {@code null} or
271    * if its {@code keyCode} and {@code keyChar} both are undefined.
272    *
273    * @param stroke the specified key stroke to process
274    * @return a string representation of the specified key stroke
275    */
276   public static String toString(KeyStroke stroke) {
277     if (stroke != null) {
278       StringBuilder sb = new StringBuilder();
279
280       int modifiers = stroke.getModifiers();
281       append(sb, "shift", modifiers, InputEvent.SHIFT_DOWN_MASK);
282       append(sb, "ctrl", modifiers, InputEvent.CTRL_DOWN_MASK);
283       append(sb, "meta", modifiers, InputEvent.META_DOWN_MASK);
284       append(sb, "alt", modifiers, InputEvent.ALT_DOWN_MASK);
285       append(sb, "altGraph", modifiers, InputEvent.ALT_GRAPH_DOWN_MASK);
286       append(sb, "button1", modifiers, InputEvent.BUTTON1_DOWN_MASK);
287       append(sb, "button2", modifiers, InputEvent.BUTTON2_DOWN_MASK);
288       append(sb, "button3", modifiers, InputEvent.BUTTON3_DOWN_MASK);
289
290       int code = stroke.getKeyCode();
291       if (code != KeyEvent.VK_UNDEFINED) {
292         append(sb, "released", stroke.isOnKeyRelease());
293         String name = LazyVirtualKeys.myCodeToName.get(code);
294         if (name == null) {
295           sb.append('#');
296           name = Integer.toHexString(code);
297         }
298         return sb.append(name).toString();
299       }
300       char ch = stroke.getKeyChar();
301       if (ch != KeyEvent.CHAR_UNDEFINED) {
302         append(sb, "typed", true);
303         return sb.append(ch).toString();
304       }
305       LOG.error("undefined key stroke");
306     }
307     return null;
308   }
309
310   private static void append(StringBuilder sb, String name, int modifiers, int mask) {
311     append(sb, name, (modifiers & mask) != 0);
312   }
313
314   private static void append(StringBuilder sb, String name, boolean set) {
315     if (set) sb.append(name).append(' ');
316   }
317
318   private static final class LazyModifiers {
319     private static final Map<String, Integer> mapNameToMask = new HashMap<>();
320
321     static {
322       mapNameToMask.put("shift", InputEvent.SHIFT_DOWN_MASK | InputEvent.SHIFT_MASK);
323       mapNameToMask.put("ctrl", InputEvent.CTRL_DOWN_MASK | InputEvent.CTRL_MASK); // duplicate
324       mapNameToMask.put("control", InputEvent.CTRL_DOWN_MASK | InputEvent.CTRL_MASK);
325       mapNameToMask.put("meta", InputEvent.META_DOWN_MASK | InputEvent.META_MASK);
326       mapNameToMask.put("alt", InputEvent.ALT_DOWN_MASK | InputEvent.ALT_MASK);
327       mapNameToMask.put("altgr", InputEvent.ALT_GRAPH_DOWN_MASK | InputEvent.ALT_GRAPH_MASK); // duplicate
328       mapNameToMask.put("altgraph", InputEvent.ALT_GRAPH_DOWN_MASK | InputEvent.ALT_GRAPH_MASK);
329       mapNameToMask.put("button1", InputEvent.BUTTON1_DOWN_MASK);
330       mapNameToMask.put("button2", InputEvent.BUTTON2_DOWN_MASK);
331       mapNameToMask.put("button3", InputEvent.BUTTON3_DOWN_MASK);
332     }
333   }
334
335   private static final class LazyVirtualKeys {
336     private static final Map<String, Integer> myNameToCode = new HashMap<>();
337     private static final Map<Integer, String> myCodeToName = new HashMap<>();
338
339     static {
340       try {
341         for (Field field : KeyEvent.class.getFields()) {
342           String name = field.getName();
343           if (name.startsWith("VK_")) {
344             name = name.substring(3).toLowerCase(Locale.ENGLISH);
345             int code = field.getInt(KeyEvent.class);
346             myNameToCode.put(name, code);
347             myCodeToName.put(code, name);
348           }
349         }
350       }
351       catch (Exception exception) {
352         LOG.error(exception);
353       }
354     }
355   }
356 }