/* KeyCode_FileBased.java * Component: ProperJavaRDP * * Revision: $Revision: 1.4 $ * Author: $Author: telliott $ * Date: $Date: 2005/09/27 14:15:39 $ * * Copyright (c) 2005 Propero Limited * * Purpose: Read and supply keymapping information * from a file * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * * (See gpl.txt for details of the GNU General Public License.) * */ package org.jopenray.rdp.keymapping; import java.awt.event.KeyEvent; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.Hashtable; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.Vector; import org.apache.log4j.Logger; import org.jopenray.rdp.Input; import org.jopenray.rdp.Options; public class KeyCodeFileBased { private Hashtable keysCurrentlyDown = new Hashtable(); private KeyEvent lastKeyEvent = null; private boolean lastEventMatched = false; protected static Logger logger = Logger.getLogger(Input.class); public static final int SCANCODE_EXTENDED = 0x80; public static final int DOWN = 1; public static final int UP = 0; public static final int QUIETUP = 2; public static final int QUIETDOWN = 3; private int mapCode = -1; private boolean altQuiet = false; public boolean useLockingKeyState = true; public boolean capsLockDown = false; Vector keyMap = new Vector(); private void updateCapsLock(KeyEvent e) { } public KeyCodeFileBased(InputStream fstream) throws KeyMapException { readMapFile(fstream); } /** * Constructor for a keymap generated from a specified file, formatted in * the manner of a file generated by the writeToFile method * * @param keyMapFile * File containing keymap data */ public KeyCodeFileBased(String keyMapFile) throws KeyMapException { // logger.info("String called keycode reader"); int lineNum = 0; // current line number being parsed String line = ""; // contents of line being parsed boolean mapCodeSet = false; FileInputStream fstream; try { fstream = new FileInputStream(keyMapFile); readMapFile(fstream); } catch (FileNotFoundException e) { throw new KeyMapException("KeyMap file not found: " + keyMapFile); } } /** * Read in a keymap definition file and add mappings to internal keymap * * @param fstream * Stream connected to keymap file * @throws KeyMapException */ public void readMapFile(InputStream fstream) throws KeyMapException { // logger.info("Stream-based keycode reader"); int lineNum = 0; // current line number being parsed String line = ""; // contents of line being parsed if (fstream == null) throw new KeyMapException("Could not find specified keymap file"); boolean mapCodeSet = false; try { DataInputStream in = new DataInputStream(fstream); if (in == null) logger.warn("in == null"); while (in.available() != 0) { lineNum++; line = in.readLine(); char fc = 0x0; if ((line != null) && (line.length() > 0)) fc = line.charAt(0); // ignore blank and commented lines if ((line != null) && (line.length() > 0) && (fc != '#') && (fc != 'c')) { keyMap.add(new MapDef(line)); // parse line into a MapDef // object and add to list } else if (fc == 'c') { StringTokenizer st = new StringTokenizer(line); String s = st.nextToken(); s = st.nextToken(); mapCode = Integer.decode(s).intValue(); mapCodeSet = true; } } // Add a set of mappings for alphabet characters with ctrl and alt // pressed Vector newMap = new Vector(); Iterator i = keyMap.iterator(); while (i.hasNext()) { MapDef current = (MapDef) i.next(); if (current.isCharacterDef() && !(current.isAltDown() || current.isCtrlDown() || current.isShiftDown() || current .isCapslockOn())) { int code = getCodeFromAlphaChar(current.getKeyChar()); if (code > -1) { newMap.add(new MapDef(code, 0, current.getScancode(), true, false, false, false)); newMap.add(new MapDef(code, 0, current.getScancode(), false, false, true, false)); } } } // Commit added mapping definitions keyMap.addAll(newMap); in.close(); } catch (IOException e) { throw new KeyMapException("File input error: " + e.getMessage()); } catch (NumberFormatException nfEx) { throw new KeyMapException("" + nfEx.getMessage() + " is not numeric at line " + lineNum); } catch (NoSuchElementException nseEx) { throw new KeyMapException( "Not enough parameters in definition at line " + lineNum); } catch (KeyMapException kmEx) { throw new KeyMapException("Error parsing keymap file: " + kmEx.getMessage() + " at line " + lineNum); } catch (Exception e) { logger.error(e.getClass().getName() + ": " + e.getMessage()); e.printStackTrace(); throw new KeyMapException(e.getClass().getName() + ": " + e.getMessage()); } if (!mapCodeSet) throw new KeyMapException("No map identifier found in file"); } /** * Given an alphanumeric character, return an AWT keycode * * @param keyChar * Alphanumeric character * @return AWT keycode representing input character, -1 if character not * alphanumeric */ private int getCodeFromAlphaChar(char keyChar) { if (('a' <= keyChar) && (keyChar <= 'z')) { return KeyEvent.VK_A + keyChar - 'a'; } if (('A' <= keyChar) && (keyChar <= 'Z')) { return KeyEvent.VK_A + keyChar - 'A'; } return -1; } /** * Get the RDP code specifying the key map in use * * @return ID for current key map */ public int getMapCode() { return mapCode; } /** * Construct a list of changes to key states in order to correctly send the * key action jointly defined by the supplied key event and mapping * definition. * * @param e * Key event received by Java (defining current state) * @param theDef * Key mapping to define desired keypress on server end */ public String stateChanges(KeyEvent e, MapDef theDef) { String changes = ""; final int SHIFT = 0; final int CTRL = 1; final int ALT = 2; final int CAPSLOCK = 3; int BEFORE = 0; int AFTER = 1; boolean[][] state = new boolean[4][2]; state[SHIFT][BEFORE] = e.isShiftDown(); state[SHIFT][AFTER] = theDef.isShiftDown(); state[CTRL][BEFORE] = e.isControlDown() || e.isAltGraphDown(); state[CTRL][AFTER] = theDef.isCtrlDown(); state[ALT][BEFORE] = e.isAltDown() || e.isAltGraphDown(); state[ALT][AFTER] = theDef.isAltDown(); updateCapsLock(e); state[CAPSLOCK][BEFORE] = capsLockDown; state[CAPSLOCK][AFTER] = theDef.isCapslockOn(); if (e.getID() == KeyEvent.KEY_RELEASED) { AFTER = 0; BEFORE = 1; } if ((e == null) || (theDef == null) || (!theDef.isCharacterDef())) return ""; String up = "" + ((char) UP); String down = "" + ((char) DOWN); String quietup = up; String quietdown = down; quietup = "" + ((char) QUIETUP); quietdown = "" + ((char) QUIETDOWN); if (state[SHIFT][BEFORE] != state[SHIFT][AFTER]) { if (state[SHIFT][BEFORE]) changes += ((char) 0x2a) + up; else changes += ((char) 0x2a) + down; } if (state[CTRL][BEFORE] != state[CTRL][AFTER]) { if (state[CTRL][BEFORE]) changes += ((char) 0x1d) + up; else changes += ((char) 0x1d) + down; } if (Options.altkey_quiet) { if (state[ALT][BEFORE] != state[ALT][AFTER]) { if (state[ALT][BEFORE]) changes += (char) 0x38 + quietup + ((char) 0x38) + quietdown + ((char) 0x38) + up; else { if (e.getID() == KeyEvent.KEY_RELEASED) { altQuiet = true; changes += ((char) 0x38) + quietdown; } else { altQuiet = false; changes += ((char) 0x38) + down; } } } else if (state[ALT][AFTER] && altQuiet) { altQuiet = false; changes += (char) 0x38 + quietup + ((char) 0x38) + quietdown + ((char) 0x38) + up + ((char) 0x38) + down; } } else { if (state[ALT][BEFORE] != state[ALT][AFTER]) { if (state[ALT][BEFORE]) changes += ((char) 0x38) + up; else changes += ((char) 0x38) + down; } } if (state[CAPSLOCK][BEFORE] != state[CAPSLOCK][AFTER]) { changes += ((char) 0x3a) + down + ((char) 0x3a) + up; } return changes; } /** * Output key map definitions to a file as a series of single line text * descriptions * * @param filename * File in which to store definitions */ public void writeToFile(String filename) { try { FileOutputStream out = new FileOutputStream(filename); PrintStream p = new PrintStream(out); Iterator i = keyMap.iterator(); while (i.hasNext()) { ((MapDef) i.next()).writeToStream(p); } p.close(); } catch (Exception e) { System.err.println("Error writing to file: " + e.getMessage()); } } /** * Retrieve the scancode corresponding to the supplied character as defined * within this object. Also update the mod array to hold any modifier keys * that are required to send alongside it. * * @param c * Character to obtain scancode for * @param mod * List of modifiers to be updated by method * @return Scancode of supplied key */ public boolean hasScancode(char c) { if (c == KeyEvent.CHAR_UNDEFINED) return false; Iterator i = keyMap.iterator(); MapDef best = null; while (i.hasNext()) { MapDef current = (MapDef) i.next(); if (current.appliesTo(c)) { best = current; } } return (best != null); } /** * Retrieve the scancode corresponding to the supplied character as defined * within this object. Also update the mod array to hold any modifier keys * that are required to send alongside it. * * @param c * Character to obtain scancode for * @param mod * List of modifiers to be updated by method * @return Scancode of supplied key */ public int charToScancode(char c, String[] mod) { Iterator i = keyMap.iterator(); int smallestDist = -1; MapDef best = null; while (i.hasNext()) { MapDef current = (MapDef) i.next(); if (current.appliesTo(c)) { best = current; } } if (best != null) { if (best.isShiftDown()) mod[0] = "SHIFT"; else if (best.isCtrlDown() && best.isAltDown()) mod[0] = "ALTGR"; else mod[0] = "NONE"; return best.getScancode(); } else return -1; } /** * Return a mapping definition associated with the supplied key event from * within the list stored in this object. * * @param e * Key event to retrieve a definition for * @return Mapping definition for supplied keypress */ public MapDef getDef(KeyEvent e) { if (e.getID() == KeyEvent.KEY_RELEASED) { MapDef def = (MapDef) keysCurrentlyDown.get(new Integer(e .getKeyCode())); registerKeyEvent(e, def); if (e.getID() == KeyEvent.KEY_RELEASED) logger.debug("Released: " + e.getKeyCode() + " returned scancode: " + ((def != null) ? "" + def.getScancode() : "null")); return def; } updateCapsLock(e); Iterator i = keyMap.iterator(); int smallestDist = -1; MapDef best = null; boolean noScanCode = !hasScancode(e.getKeyChar()); while (i.hasNext()) { MapDef current = (MapDef) i.next(); boolean applies; if ((e.getID() == KeyEvent.KEY_PRESSED)) { applies = current.appliesToPressed(e); } else if ((!lastEventMatched) && (e.getID() == KeyEvent.KEY_TYPED)) { applies = current.appliesToTyped(e, capsLockDown); } else applies = false; if (applies) { int d = current.modifierDistance(e, capsLockDown); if ((smallestDist == -1) || (d < smallestDist)) { smallestDist = d; best = current; } } } if (e.getID() == KeyEvent.KEY_PRESSED) logger.info("Pressed: " + e.getKeyCode() + " returned scancode: " + ((best != null) ? "" + best.getScancode() : "null")); if (e.getID() == KeyEvent.KEY_TYPED) logger.info("Typed: " + e.getKeyChar() + " returned scancode: " + ((best != null) ? "" + best.getScancode() : "null")); registerKeyEvent(e, best); return best; } /** * Return a scancode for the supplied key event, from within the mapping * definitions stored in this object. * * @param e * Key event for which to determine a scancode * @return Scancode for the supplied keypress, according to current mappings */ public int getScancode(KeyEvent e) { String[] mod = { "" }; // System.out.println("KeyCode_FileBased.getScancode():" + e); MapDef d = getDef(e); int scancode = -1; if (d != null) { scancode = d.getScancode(); } // System.out.println("KeyCode_FileBased.getScancode():" + // e+" returned:"+scancode); return scancode; } private void registerKeyEvent(KeyEvent e, MapDef m) { if (e.getID() == KeyEvent.KEY_RELEASED) { keysCurrentlyDown.remove(new Integer(e.getKeyCode())); if ((!Options.caps_sends_up_and_down) && (e.getKeyCode() == KeyEvent.VK_CAPS_LOCK)) { logger.debug("Turning CAPSLOCK off - key release"); capsLockDown = false; } lastEventMatched = false; } if (e.getID() == KeyEvent.KEY_PRESSED) { lastKeyEvent = e; if (m != null) lastEventMatched = true; else lastEventMatched = false; if ((Options.caps_sends_up_and_down) && (e.getKeyCode() == KeyEvent.VK_CAPS_LOCK)) { logger.debug("Toggling CAPSLOCK"); capsLockDown = !capsLockDown; } else if (e.getKeyCode() == KeyEvent.VK_CAPS_LOCK) { logger.debug("Turning CAPSLOCK on - key press"); capsLockDown = true; } } if (lastKeyEvent != null && m != null && !keysCurrentlyDown.containsKey(new Integer(lastKeyEvent .getKeyCode()))) { keysCurrentlyDown.put(new Integer(lastKeyEvent.getKeyCode()), m); lastKeyEvent = null; } } /** * Construct a list of keystrokes needed to reproduce an AWT key event via * RDP * * @param e * Keyboard event to reproduce * @return List of character pairs representing scancodes and key actions to * send to server */ public String getKeyStrokes(KeyEvent e) { String codes = ""; MapDef d = getDef(e); if (d == null) return ""; codes = stateChanges(e, d); String type = ""; if (e.getID() == KeyEvent.KEY_RELEASED) { if ((!Options.caps_sends_up_and_down) && (e.getKeyCode() == KeyEvent.VK_CAPS_LOCK)) { logger.debug("Sending CAPSLOCK toggle"); codes = "" + ((char) 0x3a) + ((char) DOWN) + ((char) 0x3a) + ((char) UP) + codes; } else { type = "" + ((char) UP); codes = ((char) d.getScancode()) + type + codes; } } else { if ((!Options.caps_sends_up_and_down) && (e.getKeyCode() == KeyEvent.VK_CAPS_LOCK)) { logger.debug("Sending CAPSLOCK toggle"); codes += "" + ((char) 0x3a) + ((char) DOWN) + ((char) 0x3a) + ((char) UP); } else { type = "" + ((char) DOWN); codes += ((char) d.getScancode()) + type; } } return codes; } public void dump() { for (int i = 0; i < this.keyMap.size(); i++) { MapDef d = (MapDef) this.keyMap.get(i); d.dump(); } } }