001: /*
002: * KeyEventTranslator.java - Hides some warts of AWT event API
003: * :tabSize=8:indentSize=8:noTabs=false:
004: * :folding=explicit:collapseFolds=1:
005: *
006: * Copyright (C) 2003, 2005 Slava Pestov
007: *
008: * This program is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU General Public License
010: * as published by the Free Software Foundation; either version 2
011: * of the License, or any later version.
012: *
013: * This program is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with this program; if not, write to the Free Software
020: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
021: */
022:
023: package org.gjt.sp.jedit.gui;
024:
025: //{{{ Imports
026: import java.awt.event.*;
027: import java.util.*;
028: import org.gjt.sp.jedit.*;
029: import org.gjt.sp.util.Log;
030: import org.gjt.sp.util.StandardUtilities;
031:
032: //}}}
033:
034: /**
035: * In conjunction with the <code>KeyEventWorkaround</code>, hides some
036: * warts in the AWT key event API.
037: *
038: * @author Slava Pestov
039: * @version $Id: KeyEventTranslator.java 11176 2007-12-01 08:29:08Z k_satoda $
040: */
041: public class KeyEventTranslator {
042: //{{{ addTranslation() method
043: /**
044: * Adds a keyboard translation.
045: * @param key1 Translate this key
046: * @param key2 Into this key
047: * @since jEdit 4.2pre3
048: */
049: public static void addTranslation(Key key1, Key key2) {
050: transMap.put(key1, key2);
051: } //}}}
052:
053: //{{{ translateKeyEvent() method
054:
055: protected static KeyEvent lastKeyPressEvent;
056:
057: protected static boolean lastKeyPressAccepted;
058:
059: /**
060: * Pass this an event from {@link
061: * KeyEventWorkaround#processKeyEvent(java.awt.event.KeyEvent)}.
062: * @param evt the KeyEvent to translate
063: * @since jEdit 4.2pre3
064: */
065: public static Key translateKeyEvent(KeyEvent evt) {
066: Key key = translateKeyEvent2(evt);
067:
068: if (key != null) {
069: if (key.isPhantom()) {
070: key = null;
071: }
072: }
073:
074: return key;
075: }
076:
077: /**
078: * Pass this an event from {@link
079: * KeyEventWorkaround#processKeyEvent(java.awt.event.KeyEvent)}.
080: * @param evt the KeyEvent to translate
081: * @since jEdit 4.2pre3
082: */
083: public static Key translateKeyEvent2(KeyEvent evt) {
084: if (Options.SIMPLIFIED_KEY_HANDLING) { // This is still experimental code.
085:
086: /**
087: A summary of Java key handling intricacies:
088: (1) No "key pressed" events are generated for umlaut keys and for "combined characters" (key for diacritic mark + key for base character), only "key typed" and "key released" events are generated for them
089: (2) The "key typed" event for Ctrl+J is indistinguishable from the "key typed" event for Ctrl+Return (in both cases: keycode=0, keychar=0xa) (in Java 1.5 under linux, but not in Java 1.6)
090: (3) If a key is pressed longer, not only additional "key typed" events but also additional "key released", "key pressed" events are generated.
091: (4) There are no proper key events generated for dead key + space (like '^' + ' ' resulting in '^') in Java 1.5 under linux. In Java 1.6, this bug is fixed.
092:
093: For (2), we could simply ignore "key typed" events (as (3) allows us to do so). But then we would loose umlaut keys and combined characters (due to (1)).
094: For (1), we could simply ignore "key pressed" events. But then we would suffer from (2).
095:
096: Thus, we need to distinguish for (2) at the "key pressed" event state, however fire the internal key events only at the "key typed" stage.
097: This makes it necessary to store information about the last "key pressed" event (to be able to distinguish).
098:
099: */
100: char keyChar = evt.getKeyChar();
101: int keyCode = evt.getKeyCode();
102: int modifiers = evt.getModifiers();
103: boolean usecooked = !evt.isActionKey();
104:
105: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): 1: keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+", usecooked="+usecooked+", event="+evt+".");
106:
107: /*
108: Workaround against the bug of jdk1.5, that Ctrl+A has keyChar 0x1 instead of keyChar 0x41:
109:
110: This bug may be related to http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6320676
111: */
112: if ((modifiers & InputEvent.CTRL_MASK) != 0) {
113: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+": 1.");
114: if (keyChar < 0x20) {
115: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+": 1.1.");
116: if (keyChar != keyCode) { // specifically: if the real Escape, Backspace, Delete, Tab, Enter key was pressed, then this is false
117: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+": 1.1.1");
118: keyChar += 0x40;
119:
120: if ((keyChar >= 'A') && (keyChar <= 'Z')) { // if they are uppercase letters
121: keyChar += 0x20; // make them lowercase letters
122: }
123: // usecooked = false;
124:
125: }
126: }
127:
128: if (keyChar == '\\') { // for compatibility with traditional jEdit installations (Shortcuts are called "C+BACK_SLASH" instead of "C+\")
129: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+": 1.1.1: backslash.");
130: keyChar = 0;
131: keyCode = KeyEvent.VK_BACK_SLASH;
132: }
133:
134: // Log.log(Log.DEBUG,"KeyEventTranslator","translateKeyEvent(): 2: keyChar="+((int) keyChar)+",keyCode="+keyCode+",modifiers="+modifiers+", event="+evt+".");
135: }
136:
137: /**
138: These keys are hidden "control keys". That is, they are used as special function key (instead of representing a character to be input), but
139: Java delivers them with a valid keyChar. We intentionally ignore this keyChar.
140: (However, not ignoring the keyChar would be an easy way to enter "escape" or "delete" characters into the edited text document, but this is not what we want.)
141: */
142: switch (keyChar) {
143: case 0x1b: // case KeyEvent.VK_ESCAPE:
144: case 0x08: // case KeyEvent.VK_BACK_SPACE:
145: case 0x7f: // case KeyEvent.VK_DELETE:
146: case 0x09: // case KeyEvent.VK_TAB:
147: case 0x0a: // case KeyEvent.VK_ENTER:
148: case KeyEvent.CHAR_UNDEFINED:
149: usecooked = false;
150: keyChar = 0;
151: }
152:
153: boolean accept = false;
154: boolean acceptAsPhantom = false;
155: if (true) {
156: switch (evt.getID()) {
157: case KeyEvent.KEY_PRESSED:
158: accept = !usecooked;
159: acceptAsPhantom = !accept;
160: lastKeyPressAccepted = accept;
161: lastKeyPressEvent = evt;
162: break;
163: case KeyEvent.KEY_TYPED:
164: if (lastKeyPressAccepted
165: && (lastKeyPressEvent != null)
166: && (lastKeyPressEvent.getKeyChar() == evt
167: .getKeyChar())) {
168: // Do not emit internal key event twice.
169: // This works around the case where "Ctrl+J" and "Ctrl+Return" are indistinguishable in that "Ctrl+Return" is handled at the "key pressed" stage where "Ctrl+J" is handled at the "key typed" stage.
170: } else {
171: accept = usecooked;
172: acceptAsPhantom = !accept;
173: }
174: break;
175: default:
176: }
177: } else {
178: /*
179: This attempt does work for the "Ctrl+Enter"-Problem, but this does work neither for umlauts nor for combined synthetic keys (like characters with diacritic marks).
180: The reason is that java 1.5.0_06 (on Linux) does not deliver "key pressed" events for those keys, only "key typed" events.
181: */
182: /*
183: We ignore all the "key typed" events, as key repeat is already synthetically generated by synthetic "key pressed" "key released" events.
184: "key typed" events have less information.
185:
186: This is highly experimental, as this relies on the JVM to generate these synthetic "key released", "key pressed" events.
187: */
188: switch (evt.getID()) {
189: case KeyEvent.KEY_PRESSED:
190: accept = true;
191: if (usecooked) { // This destroys information, but this is what the rest of jEdit is used to :-(
192: keyCode = 0;
193: }
194: break;
195: default:
196: }
197: }
198:
199: Key returnValue = null;
200:
201: if (accept || acceptAsPhantom) {
202: if (!accept && acceptAsPhantom) {
203: if (keyChar != 0) {
204: keyCode = 0;
205: }
206: }
207:
208: returnValue = new Key(modifiersToString(modifiers),
209: keyCode, keyChar);
210:
211: if (!accept && acceptAsPhantom) {
212: if (keyChar != 0) {
213: }
214: returnValue.setIsPhantom(true);
215: }
216: }
217:
218: return returnValue;
219: } else {
220: int modifiers = evt.getModifiers();
221: Key returnValue;
222:
223: switch (evt.getID()) {
224: case KeyEvent.KEY_PRESSED:
225: int keyCode = evt.getKeyCode();
226: if ((keyCode >= KeyEvent.VK_0 && keyCode <= KeyEvent.VK_9)
227: || (keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z)) {
228: if (Debug.ALTERNATIVE_DISPATCHER)
229: return null;
230: else {
231: returnValue = new Key(
232: modifiersToString(modifiers), '\0',
233: Character.toLowerCase((char) keyCode));
234: }
235: } else {
236: if (keyCode == KeyEvent.VK_TAB) {
237: evt.consume();
238: returnValue = new Key(
239: modifiersToString(modifiers), keyCode,
240: '\0');
241: } else if (keyCode == KeyEvent.VK_SPACE) {
242: // for SPACE or S+SPACE we pass the
243: // key typed since international
244: // keyboards sometimes produce a
245: // KEY_PRESSED SPACE but not a
246: // KEY_TYPED SPACE, eg if you have to
247: // do a "<space> to insert ".
248: if ((modifiers & ~InputEvent.SHIFT_MASK) == 0)
249: returnValue = null;
250: else {
251: returnValue = new Key(
252: modifiersToString(modifiers), 0,
253: ' ');
254: }
255: } else {
256: returnValue = new Key(
257: modifiersToString(modifiers), keyCode,
258: '\0');
259: }
260: }
261: break;
262: case KeyEvent.KEY_TYPED:
263: char ch = evt.getKeyChar();
264:
265: if (KeyEventWorkaround.isMacControl(evt))
266: ch |= 0x60;
267:
268: switch (ch) {
269: case '\n':
270: case '\t':
271: case '\b':
272: return null;
273: case ' ':
274: if ((modifiers & ~InputEvent.SHIFT_MASK) != 0)
275: return null;
276: }
277:
278: int ignoreMods;
279: if (Debug.ALT_KEY_PRESSED_DISABLED) {
280: /* on MacOS, A+ can be user input */
281: ignoreMods = InputEvent.SHIFT_MASK
282: | InputEvent.ALT_GRAPH_MASK
283: | InputEvent.ALT_MASK;
284: } else {
285: /* on MacOS, A+ can be user input */
286: ignoreMods = InputEvent.SHIFT_MASK
287: | InputEvent.ALT_GRAPH_MASK;
288: }
289:
290: if ((modifiers & InputEvent.ALT_GRAPH_MASK) == 0
291: && evt.getWhen()
292: - KeyEventWorkaround.lastKeyTime < 750L
293: && (KeyEventWorkaround.modifiers & ~ignoreMods) != 0) {
294: if (Debug.ALTERNATIVE_DISPATCHER) {
295: returnValue = new Key(
296: modifiersToString(modifiers), 0, ch);
297: } else
298: return null;
299: } else {
300: if (ch == ' ') {
301: returnValue = new Key(
302: modifiersToString(modifiers), 0, ch);
303: } else
304: returnValue = new Key(null, 0, ch);
305: }
306: break;
307: default:
308: return null;
309: }
310:
311: /* I guess translated events do not have the 'evt' field set
312: so consuming won't work. I don't think this is a problem as
313: nothing uses translation anyway */
314: Key trans = transMap.get(returnValue);
315: if (trans == null)
316: return returnValue;
317: else
318: return trans;
319: }
320: } //}}}
321:
322: //{{{ parseKey() method
323: /**
324: * Converts a string to a keystroke. The string should be of the
325: * form <i>modifiers</i>+<i>shortcut</i> where <i>modifiers</i>
326: * is any combination of A for Alt, C for Control, S for Shift
327: * or M for Meta, and <i>shortcut</i> is either a single character,
328: * or a keycode name from the <code>KeyEvent</code> class, without
329: * the <code>VK_</code> prefix.
330: * @param keyStroke A string description of the key stroke
331: * @since jEdit 4.2pre3
332: */
333: public static Key parseKey(String keyStroke) {
334: if (keyStroke == null)
335: return null;
336: int modifiers = 0;
337: String key;
338: int endOfModifiers = keyStroke.indexOf('+');
339: if (endOfModifiers <= 0) // not found or found at first
340: {
341: key = keyStroke;
342: } else {
343: for (int i = 0; i < endOfModifiers; i++) {
344: switch (Character.toUpperCase(keyStroke.charAt(i))) {
345: case 'A':
346: modifiers |= a;
347: break;
348: case 'C':
349: modifiers |= c;
350: break;
351: case 'M':
352: modifiers |= m;
353: break;
354: case 'S':
355: modifiers |= s;
356: break;
357: }
358: }
359: key = keyStroke.substring(endOfModifiers + 1);
360: }
361: if (key.length() == 1) {
362: return new Key(modifiersToString(modifiers), 0, key
363: .charAt(0));
364: } else if (key.length() == 0) {
365: Log.log(Log.ERROR, KeyEventTranslator.class,
366: "Invalid key stroke: " + keyStroke);
367: return null;
368: } else if (key.equals("SPACE")) {
369: return new Key(modifiersToString(modifiers), 0, ' ');
370: } else {
371: int ch;
372:
373: try {
374: ch = KeyEvent.class.getField("VK_".concat(key)).getInt(
375: null);
376: } catch (Exception e) {
377: Log.log(Log.ERROR, KeyEventTranslator.class,
378: "Invalid key stroke: " + keyStroke);
379: return null;
380: }
381:
382: return new Key(modifiersToString(modifiers), ch, '\0');
383: }
384: } //}}}
385:
386: //{{{ setModifierMapping() method
387: /**
388: * Changes the mapping between symbolic modifier key names
389: * (<code>C</code>, <code>A</code>, <code>M</code>, <code>S</code>) and
390: * Java modifier flags.
391: *
392: * You can map more than one Java modifier to a symobolic modifier, for
393: * example :
394: * <p><code><pre>
395: * setModifierMapping(
396: * InputEvent.CTRL_MASK,
397: * InputEvent.ALT_MASK | InputEvent.META_MASK,
398: * 0,
399: * InputEvent.SHIFT_MASK);
400: *<pre></code></p>
401: *
402: * You cannot map a Java modifer to more than one symbolic modifier.
403: *
404: * @param c The modifier(s) to map the <code>C</code> modifier to
405: * @param a The modifier(s) to map the <code>A</code> modifier to
406: * @param m The modifier(s) to map the <code>M</code> modifier to
407: * @param s The modifier(s) to map the <code>S</code> modifier to
408: *
409: * @since jEdit 4.2pre3
410: */
411: public static void setModifierMapping(int c, int a, int m, int s) {
412:
413: int duplicateMapping = (c & a) | (c & m) | (c & s) | (a & m)
414: | (a & s) | (m & s);
415:
416: if ((duplicateMapping & InputEvent.CTRL_MASK) != 0) {
417: throw new IllegalArgumentException(
418: "CTRL is mapped to more than one modifier");
419: }
420: if ((duplicateMapping & InputEvent.ALT_MASK) != 0) {
421: throw new IllegalArgumentException(
422: "ALT is mapped to more than one modifier");
423: }
424: if ((duplicateMapping & InputEvent.META_MASK) != 0) {
425: throw new IllegalArgumentException(
426: "META is mapped to more than one modifier");
427: }
428: if ((duplicateMapping & InputEvent.SHIFT_MASK) != 0) {
429: throw new IllegalArgumentException(
430: "SHIFT is mapped to more than one modifier");
431: }
432:
433: KeyEventTranslator.c = c;
434: KeyEventTranslator.a = a;
435: KeyEventTranslator.m = m;
436: KeyEventTranslator.s = s;
437: } //}}}
438:
439: //{{{ getSymbolicModifierName() method
440: /**
441: * Returns a the symbolic modifier name for the specified Java modifier
442: * flag.
443: *
444: * @param mod A modifier constant from <code>InputEvent</code>
445: *
446: * @since jEdit 4.2pre3
447: */
448: public static char getSymbolicModifierName(int mod) {
449: if ((mod & c) != 0)
450: return 'C';
451: else if ((mod & a) != 0)
452: return 'A';
453: else if ((mod & m) != 0)
454: return 'M';
455: else if ((mod & s) != 0)
456: return 'S';
457: else
458: return '\0';
459: } //}}}
460:
461: //{{{ modifiersToString() method
462: private static final int[] MODS = { InputEvent.CTRL_MASK,
463: InputEvent.ALT_MASK, InputEvent.META_MASK,
464: InputEvent.SHIFT_MASK };
465:
466: public static String modifiersToString(int mods) {
467: StringBuffer buf = null;
468:
469: for (int i = 0; i < MODS.length; i++) {
470: if ((mods & MODS[i]) != 0)
471: buf = lazyAppend(buf, getSymbolicModifierName(MODS[i]));
472: }
473:
474: if (buf == null)
475: return null;
476: else
477: return buf.toString();
478: } //}}}
479:
480: //{{{ getModifierString() method
481: /**
482: * Returns a string containing symbolic modifier names set in the
483: * specified event.
484: *
485: * @param evt The event
486: *
487: * @since jEdit 4.2pre3
488: */
489: public static String getModifierString(InputEvent evt) {
490: StringBuilder buf = new StringBuilder();
491: if (evt.isControlDown())
492: buf.append(getSymbolicModifierName(InputEvent.CTRL_MASK));
493: if (evt.isAltDown())
494: buf.append(getSymbolicModifierName(InputEvent.ALT_MASK));
495: if (evt.isMetaDown())
496: buf.append(getSymbolicModifierName(InputEvent.META_MASK));
497: if (evt.isShiftDown())
498: buf.append(getSymbolicModifierName(InputEvent.SHIFT_MASK));
499: return buf.length() == 0 ? null : buf.toString();
500: } //}}}
501:
502: static int c, a, m, s;
503:
504: //{{{ Private members
505: /** This map is a pool of Key. */
506: private static final Map<Key, Key> transMap = new HashMap<Key, Key>();
507:
508: private static StringBuffer lazyAppend(StringBuffer buf, char ch) {
509: if (buf == null)
510: buf = new StringBuffer();
511: if (buf.indexOf(String.valueOf(ch)) == -1)
512: buf.append(ch);
513: return buf;
514: } //}}}
515:
516: static {
517: if (OperatingSystem.isMacOS()) {
518: setModifierMapping(InputEvent.META_MASK, /* == C+ */
519: InputEvent.CTRL_MASK, /* == A+ */
520: /* M+ discarded by key event workaround! */
521: InputEvent.ALT_MASK, /* == M+ */
522: InputEvent.SHIFT_MASK /* == S+ */);
523: } else {
524: setModifierMapping(InputEvent.CTRL_MASK,
525: InputEvent.ALT_MASK, InputEvent.META_MASK,
526: InputEvent.SHIFT_MASK);
527: }
528: } //}}}
529:
530: //{{{ Key class
531: public static class Key {
532: public final String modifiers;
533: public final int key;
534: public final char input;
535:
536: private final int hashCode;
537: /**
538: Wether this Key event applies to all jEdit windows (and not only a specific jEdit GUI component).
539: */
540: protected boolean isFromGlobalContext;
541:
542: /**
543: Wether this Key event is a phantom key event. A phantom key event is a kind of duplicate key event which
544: should not - due to its nature of being a duplicate - generate any action on data.
545: However, phantom key events may be necessary to notify the rest of the GUI that the key event, if it was not a phantom key event but a real key event,
546: would generate any action and thus would be consumed.
547: */
548: protected boolean isPhantom;
549:
550: public Key(String modifiers, int key, char input) {
551: this .modifiers = modifiers;
552: this .key = key;
553: this .input = input;
554: hashCode = key + input;
555: }
556:
557: public int hashCode() {
558: return hashCode;
559: }
560:
561: public boolean equals(Object o) {
562: if (o instanceof Key) {
563: Key k = (Key) o;
564: if (StandardUtilities.objectsEqual(modifiers,
565: k.modifiers)
566: && key == k.key && input == k.input) {
567: return true;
568: }
569: }
570:
571: return false;
572: }
573:
574: public String toString() {
575: return (modifiers == null ? "" : modifiers) + '<'
576: + Integer.toString(key, 16) + ','
577: + Integer.toString(input, 16) + '>';
578: }
579:
580: public void setIsFromGlobalContext(boolean to) {
581: isFromGlobalContext = to;
582: }
583:
584: public boolean isFromGlobalContext() {
585: return isFromGlobalContext;
586: }
587:
588: public void setIsPhantom(boolean to) {
589: isPhantom = to;
590: }
591:
592: public boolean isPhantom() {
593: return isPhantom;
594: }
595: } //}}}
596: }
|