/* 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();
}
}
}