/******************************************************************************* * Copyright (c) 2012 Google, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Google, Inc. - initial API and implementation *******************************************************************************/ package com.windowtester.runtime.swt.internal.text; import java.lang.reflect.*; import java.awt.event.*; import java.util.*; import java.io.*; import javax.swing.KeyStroke; import abbot.Log; import abbot.Platform; /** Provides read of local-specific mappings for virtual keycode-based KeyStrokes to characters and vice versa. The map format is a properties file with each line containing an entry of the form<br> <code>VKNAME.MOD=VALUE</code><br> The VKNAME is the String suffix of the KeyEvent VK_ keycode. MOD is the integer value of the current modifier mask (assumes only a single modifier has any effect on key output, interesting values are considered to be 0, 1, 2, 8). VALUE is the char value of the KEY_TYPED keyChar corresponding to the VK_ keycode and modifiers, as an integer value. */ public class KeyStrokeMap implements KeyStrokeMapProvider { /** Map of Characters to virtual keycode-based KeyStrokes. */ private static Map keycodes = getKeyStrokeMap(); /** Map of Characters to the default keycode-based KeyStrokes */ private static Map defaultKeycodes = getDefaultKeyStrokeMap(); /** Map of keycode-based KeyStrokes to Characters. */ private static Map chars = getCharacterMap(); /** Map of Characters to accent keys */ private static Map accentKeys = getAccentCharMap(); /** Return the keycode-based KeyStroke corresponding to the given * character, as best we can guess it, or null if we don't know how to * generate it. */ public static KeyStroke getKeyStroke(char ch) { return (KeyStroke)keycodes.get(new Character(ch)); } /** * Return the accent key associated with the char, else return 0 * @param ch * @return 1 - circumflex ^ * 2 - umlaut * 3 - grave accent ` * 4 - acute accent */ public static int getAccentKey(char ch){ String o = (String)accentKeys.get(new Character(ch)); return o != null ? Integer.parseInt(o) : 0; } /** Given a keycode-based KeyStroke, return the equivalent character. * Defined properly for US keyboards only. Please contribute your own. * @return KeyEvent.VK_UNDEFINED if the result is unknown. */ public static char getChar(KeyStroke ks) { Character ch = (Character)chars.get(ks); if (ch == null) { // Try again, but strip all modifiers but shift int mask = ks.getModifiers() & ~KeyEvent.SHIFT_MASK; ks = KeyStroke.getKeyStroke(ks.getKeyCode(), mask); ch = (Character)chars.get(ks); if (ch == null) return KeyEvent.CHAR_UNDEFINED; } return ch.charValue(); } private static KeyStrokeMapProvider generator = null; /** If available, provide a dedicated class to provide mappings between * keystrokes and generated characters. */ private static KeyStrokeMapProvider getGenerator() { if (generator == null) { try { String gname = System.getProperty("abbot.keystroke_map_generator", "com.windowtester.runtime.swt.internal.text.KeyStrokeMap"); // "abbot.tester.KeyStrokeMap"); if (gname != null) { generator = (KeyStrokeMapProvider) Class.forName(gname).newInstance(); } } catch(Exception e) { Log.warn(e); } } return generator; } private static Map getCharacterMap() { KeyStrokeMapProvider generator = getGenerator(); Map m = generator != null ? generator.loadCharacterMap() : null; return m != null ? m : generateCharacterMappings(); } private static Map getAccentCharMap(){ KeyStrokeMapProvider generator = getGenerator(); Map m = generator != null ? generator.loadAccentKeyMap() : null; return m != null ? m : null; } /** Generate a map from characters to virtual keycode-based KeyStrokes. */ private static Map generateCharacterMappings() { Log.debug("Generating default character mappings"); Map map = new HashMap(); Iterator iter = keycodes.keySet().iterator(); while (iter.hasNext()) { Object key = iter.next(); map.put(keycodes.get(key), key); } return map; } private static Map getKeyStrokeMap() { KeyStrokeMapProvider generator = getGenerator(); Map m = generator != null ? generator.loadKeyStrokeMap() : null; return m != null ? m : generateKeyStrokeMappings(false); } /** * Generate the mapping between characters and key codes. This is * invoked exactly once per VM invocation. * We don't have complete coverage, so if you use this fallback map in AWT * mode some events may be missing that would otherwise be generated in * robot mode. */ private static Map generateKeyStrokeMappings(boolean english) { Log.debug("Generating default keystroke mappings"); // character, keycode, modifiers int shift = InputEvent.SHIFT_MASK; //int alt = InputEvent.ALT_MASK; //int altg = InputEvent.ALT_GRAPH_MASK; int ctrl = InputEvent.CTRL_MASK; //int meta = InputEvent.META_MASK; // These are assumed to be standard across all keyboards (?) int[][] universalMappings = { { '', KeyEvent.VK_ESCAPE, 0 }, // No escape sequence exists { '\b', KeyEvent.VK_BACK_SPACE, 0 }, { '', KeyEvent.VK_DELETE, 0 }, // None for this one either { '\n', KeyEvent.VK_ENTER, 0 }, { '\r', KeyEvent.VK_ENTER, 0 }, }; // Add to these as needed; note that this is based on a US keyboard // mapping, and will likely fail for others. int[][] mappings = { { ' ', KeyEvent.VK_SPACE, 0, }, { '\t', KeyEvent.VK_TAB, 0, }, { '~', KeyEvent.VK_BACK_QUOTE, shift, }, { '`', KeyEvent.VK_BACK_QUOTE, 0, }, { '!', KeyEvent.VK_1, shift, }, { '@', KeyEvent.VK_2, shift, }, { '#', KeyEvent.VK_3, shift, }, { '$', KeyEvent.VK_4, shift, }, { '%', KeyEvent.VK_5, shift, }, { '^', KeyEvent.VK_6, shift, }, { '&', KeyEvent.VK_7, shift, }, { '*', KeyEvent.VK_8, shift, }, { '(', KeyEvent.VK_9, shift, }, { ')', KeyEvent.VK_0, shift, }, { '-', KeyEvent.VK_MINUS, 0, }, { '_', KeyEvent.VK_MINUS, shift, }, { '=', KeyEvent.VK_EQUALS, 0, }, { '+', KeyEvent.VK_EQUALS, shift, }, { '[', KeyEvent.VK_OPEN_BRACKET, 0, }, { '{', KeyEvent.VK_OPEN_BRACKET, shift, }, // NOTE: The following does NOT produce a left brace //{ '{', KeyEvent.VK_BRACELEFT, 0, }, { ']', KeyEvent.VK_CLOSE_BRACKET, 0, }, { '}', KeyEvent.VK_CLOSE_BRACKET, shift, }, { '|', KeyEvent.VK_BACK_SLASH, shift, }, { ';', KeyEvent.VK_SEMICOLON, 0, }, { ':', KeyEvent.VK_SEMICOLON, shift, }, { ',', KeyEvent.VK_COMMA, 0, }, { '<', KeyEvent.VK_COMMA, shift, }, { '.', KeyEvent.VK_PERIOD, 0, }, { '>', KeyEvent.VK_PERIOD, shift, }, { '/', KeyEvent.VK_SLASH, 0, }, { '?', KeyEvent.VK_SLASH, shift, }, { '\\', KeyEvent.VK_BACK_SLASH, 0, }, { '|', KeyEvent.VK_BACK_SLASH, shift, }, { '\'', KeyEvent.VK_QUOTE, 0, }, { '"', KeyEvent.VK_QUOTE, shift, }, }; HashMap map = new HashMap(); // Universal mappings for (int i=0;i < universalMappings.length;i++) { int[] entry = universalMappings[i]; KeyStroke stroke = KeyStroke.getKeyStroke(entry[1], entry[2]); map.put(new Character((char)entry[0]), stroke); } // If the locale is not en_US/GB, provide only a very basic map and // rely on key_typed events instead if (!english){ Locale locale = Locale.getDefault(); if (!Locale.US.equals(locale) && !Locale.UK.equals(locale)) { Log.debug("Not US: " + locale); return map; } } // Basic symbol/punctuation mappings for (int i=0;i < mappings.length;i++) { int[] entry = mappings[i]; KeyStroke stroke = KeyStroke.getKeyStroke(entry[1], entry[2]); map.put(new Character((char)entry[0]), stroke); } // Lowercase for (int i='a';i <= 'z';i++) { KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_A + i - 'a', 0); map.put(new Character((char)i), stroke); // control characters stroke = KeyStroke.getKeyStroke(KeyEvent.VK_A + i - 'a', ctrl); Character key = new Character((char)(i - 'a' + 1)); // Make sure we don't overwrite something already there if (map.get(key) == null) { map.put(key, stroke); } } // Capitals for (int i='A';i <= 'Z';i++) { KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_A + i - 'A', shift); map.put(new Character((char)i), stroke); } // digits for (int i='0';i <= '9';i++) { KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_0 + i - '0', 0); map.put(new Character((char)i), stroke); } return map; } private static Map characterMap = null; private static Map keyStrokeMap = null; private static Map accentMap = null; private static boolean loaded = false; private static InputStream findMap() { String[] names = getMapNames(); for (int i=0;i < names.length;i++) { Log.debug("Trying " + names[i]); String name = getFilename(names[i]); InputStream is = KeyStrokeMapProvider.class. getResourceAsStream("keymaps/" + name); if (is != null) return is; } // do we want to return a default map? InputStream is = KeyStrokeMapProvider.class. getResourceAsStream("keymaps/default.map"); return is; } private synchronized void loadMaps() { if (loaded) return; Properties props = new Properties(); Map cmap = null; Map kmap = null; Map amap = null; try { InputStream is = findMap(); if (is == null) { Log.debug("No appropriate map file found"); loaded = true; return; } props.load(is); Iterator iter = props.keySet().iterator(); cmap = new HashMap(); kmap = new HashMap(); amap = new HashMap(); while (iter.hasNext()) { String key = (String)iter.next(); Log.debug("Property " + key + "=" + props.getProperty(key)); try { String codeName = key.substring(0, key.indexOf(".")); String modString = key.substring(key.indexOf(".")+1); int mask; String accentCode = null; if (modString.length() == 1) mask = Integer.parseInt(modString, 16); else{ mask = Integer.parseInt(modString.substring(1), 16); accentCode = modString.substring(0, 1); } int value = Integer.parseInt(props.getProperty(key), 16); Character ch = new Character((char)value); Field field = KeyEvent.class.getField("VK_" + codeName); int code = field.getInt(null); KeyStroke ks = KeyStroke.getKeyStroke(code, mask); // May be more than one KeyStroke mapping to a given key // character; prefer no mask or shift mask over any other // masks. KeyStroke existing = (KeyStroke)kmap.get(ch); if (existing == null || ((existing.getModifiers() != 0 && existing.getModifiers() != KeyEvent.SHIFT_MASK) || (mask == 0 && (existing.getModifiers() != 0 || ks.toString().length() < existing.toString().length())))) { Log.debug("Installing " + ks + " for '" + ch + "'"); kmap.put(ch, ks); } // check if it is an accent char, if so add to map if (accentCode != null){ amap.put(ch,accentCode); } cmap.put(ks, ch); } catch(NumberFormatException e) { // ignore invalid entries } catch(Exception e) { Log.warn(e); } } } catch(IOException io) { } Log.debug("Successfully loaded character/keystroke map"); characterMap = cmap; keyStrokeMap = kmap; accentMap = amap; loaded = true; } /** Load a map for the current locale to translate a character into a corresponding virtual keycode-based KeyStroke. */ public Map loadCharacterMap() { loadMaps(); return characterMap; } /** Load a map for the current locale to translate a virtual keycode into a character-based KeyStroke. */ public Map loadKeyStrokeMap() { loadMaps(); return keyStrokeMap; } public Map loadAccentKeyMap(){ loadMaps(); return accentMap; } // private static Map defaultCharacterMap = null; private static Map defaultKeyStrokeMap = null; private static boolean defaultMapLoaded = false; /** * get keystroke from default map - english */ public static KeyStroke getDefaultKeyStroke(char ch) { return (KeyStroke)defaultKeycodes.get(new Character(ch)); } private static Map getDefaultKeyStrokeMap() { KeyStrokeMapProvider generator = getGenerator(); Map m = generator != null ? generator.loadDefaultKeyStrokeMap() : null; return m != null ? m : generateKeyStrokeMappings(true); } /** Load a map for the default locale to translate a virtual keycode into a character-based KeyStroke. */ public Map loadDefaultKeyStrokeMap() { loadDefaultMaps(); return defaultKeyStrokeMap; } private synchronized void loadDefaultMaps() { if (defaultMapLoaded) return; Properties props = new Properties(); Map cmap = null; Map kmap = null; try { InputStream is = KeyStrokeMapProvider.class. getResourceAsStream("keymaps/default.map"); if (is == null) { Log.debug("No appropriate map file found"); loaded = true; return; } props.load(is); Iterator iter = props.keySet().iterator(); cmap = new HashMap(); kmap = new HashMap(); while (iter.hasNext()) { String key = (String)iter.next(); Log.debug("Property " + key + "=" + props.getProperty(key)); try { String codeName = key.substring(0, key.indexOf(".")); int mask = Integer. parseInt(key.substring(key.indexOf(".")+1), 16); int value = Integer.parseInt(props.getProperty(key), 16); Character ch = new Character((char)value); Field field = KeyEvent.class.getField("VK_" + codeName); int code = field.getInt(null); KeyStroke ks = KeyStroke.getKeyStroke(code, mask); // May be more than one KeyStroke mapping to a given key // character; prefer no mask or shift mask over any other // masks. KeyStroke existing = (KeyStroke)kmap.get(ch); if (existing == null || ((existing.getModifiers() != 0 && existing.getModifiers() != KeyEvent.SHIFT_MASK) || (mask == 0 && (existing.getModifiers() != 0 || ks.toString().length() < existing.toString().length())))) { Log.debug("Installing " + ks + " for '" + ch + "'"); kmap.put(ch, ks); } cmap.put(ks, ch); } catch(NumberFormatException e) { // ignore invalid entries } catch(Exception e) { Log.warn(e); } } } catch(IOException io) { } Log.debug("Successfully loaded character/keystroke map"); // defaultCharacterMap = cmap; defaultKeyStrokeMap = kmap; defaultMapLoaded = true; } /** Convert a String containing a unique identifier for the map into a * unique filename. */ protected static String getFilename(String base) { //return Integer.toHexString(base.hashCode()) + ".map"; return base + ".map"; } protected static String[] getMapNames() { return getMapStrings(false); } protected static String[] getMapDescriptions() { return getMapStrings(true); } /** Return the keystroke map filenames that should be available for this * locale/OS/VM version/architecture. Assume most changes across locale, * then OS, then VM version, then os version/architecture. */ private static String[] getMapStrings(boolean desc) { ArrayList list = new ArrayList(); // for testing, set locale to german // Locale locale = new Locale("de","DE"); // Locale locale = new Locale("nl","BE"); // Locale locale = new Locale("en","US"); // Locale locale = new Locale("fr","FR"); //Locale locale = new Locale("sv","SE"); Locale locale = Locale.getDefault(); String name = locale.toString(); if (desc) name = "locale=" + name; list.add(0, name); String os = "-" + getOSType(); if (desc) os = " (os=" + System.getProperty("os.name") + ", " + System.getProperty("os.version") + ")"; name += os; list.add(0, name); /* String vm = System.getProperty("java.version"); name += " vm=" + vm; list.add(0, name); String version = System.getProperty("os.version"); name += " version=" + version; list.add(0, name); String arch = System.getProperty("os.arch"); name += " arch=" + arch; list.add(0, name); */ return (String[])list.toArray(new String[list.size()]); } private static String getOSType() { return Platform.isMacintosh() ? "mac" : (Platform.isWindows() ? "w32" : "x11"); } /** Return currently available locales. */ public static void main(String[] args) { /* Locale[] available = Locale.getAvailableLocales(); System.out.println("Available Locales"); for (int i=0;i < available.length;i++) { System.out.print(available[i].toString()); System.out.print(" "); } */ Locale locale = Locale.getDefault(); System.out.println(locale.getDisplayLanguage().toString()); findMap(); System.exit(1); } }