/*
* $Id$
*
* Copyright (C) 2003-2014 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.command.system;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jnode.driver.console.Console;
import org.jnode.driver.console.KeyEventBindings;
import org.jnode.driver.console.TextConsole;
import org.jnode.driver.console.VirtualKey;
import org.jnode.driver.console.textscreen.ConsoleKeyEventBindings;
import org.jnode.driver.console.textscreen.KeyboardReaderAction;
import org.jnode.shell.AbstractCommand;
import org.jnode.shell.CommandLine.Token;
import org.jnode.shell.ShellUtils;
import org.jnode.shell.syntax.Argument;
import org.jnode.shell.syntax.CommandSyntaxException;
import org.jnode.shell.syntax.EnumArgument;
import org.jnode.shell.syntax.FlagArgument;
/**
* This command allows the user to examine and change JNode's key bindings.
*
* @author crawley@jnode.org
*/
public class BindKeysCommand extends AbstractCommand {
private static final String help_reset = "reset the binding to the default values";
private static final String help_add = "add bindings";
private static final String help_remove = "remove bindings";
private static final String help_action = "a keyboard reader action";
private static final String help_vkspec = "a virtual key specification";
private static final String help_char = "a character";
private static final String help_super = "Display or change the keyboard bindings";
private static final String err_not_text = "The current console is not a TextConsole";
private static final String err_no_bind = "There are no binding for action '%s'%n";
private static final String fmt_update = "Updated the current console's key bindings for action '%s'%n";
private static final String str_reset = "Reset the current console's key bindings";
private static final String str_not_bound = "not bound";
private static final String ex_inv_char = "invalid character";
private static final String ex_unknown_vkey = "'%s' is an unknown virtual key name";
private static final String ex_unknown_mod = "'%s' is an unknown modifier";
private static final Map<String, Integer> VK_NAME_MAP =
new HashMap<String, Integer>();
private static final Map<Integer, String> VK_MAP =
new HashMap<Integer, String>();
private static final Map<String, Integer> MODIFIER_NAME_MAP =
new HashMap<String, Integer>();
private static final Map<Integer, String> MODIFIER_MAP =
new HashMap<Integer, String>();
static {
// This is the best way I can think of to enumerate all of the VK_ codes
// defined by the KeyEvent class.
for (Field field : KeyEvent.class.getFields()) {
if (Modifier.isStatic(field.getModifiers()) &&
field.getName().startsWith("VK_")) {
try {
Integer vk = (Integer) field.get(null);
String name = constCase(KeyEvent.getKeyText(vk));
VK_NAME_MAP.put(name, vk);
VK_MAP.put(vk, name);
} catch (IllegalAccessException ex) {
// This cannot happen. But if it does we'll just ignore
// the virtual key constant that caused it.
}
}
}
// Now do the same for the modifiers. Note that we map the names to the 'new'
// modifier mask values; see KeyEvent javadoc ...
initModifier("AWT.shift", "Shift", KeyEvent.SHIFT_DOWN_MASK);
initModifier("AWT.control", "Ctrl", KeyEvent.CTRL_DOWN_MASK);
initModifier("AWT.alt", "Alt", KeyEvent.ALT_DOWN_MASK);
initModifier("AWT.meta", "Meta", KeyEvent.META_DOWN_MASK);
initModifier("AWT.button1", "Button 1", KeyEvent.BUTTON1_DOWN_MASK);
initModifier("AWT.button2", "Button 2", KeyEvent.BUTTON2_DOWN_MASK);
initModifier("AWT.button3", "Button 3", KeyEvent.BUTTON3_DOWN_MASK);
initModifier("AWT.altGraph", "Alt Graph", KeyEvent.ALT_GRAPH_DOWN_MASK);
}
private static void initModifier(String propName, String dflt, int modifier) {
String name = constCase(Toolkit.getProperty(propName, dflt));
MODIFIER_NAME_MAP.put(name, modifier);
MODIFIER_MAP.put(modifier, name);
}
private static final String[] ASCII_NAMES = new String[] {
"NUL", "SOH", "STC", "ETX", "EOT", "ENQ", "ACK", "BEL",
"BS", "HT", "NL", "VT", "FF", "CR", "SO", "SI",
"DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB",
"CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", "SP"
};
private static class ActionArgument extends EnumArgument<KeyboardReaderAction> {
public ActionArgument(String label, int flags, String description) {
super(label, flags, KeyboardReaderAction.class, description);
}
@Override
protected String argumentKind() {
return "keyboard reader action";
}
}
private static class VirtualKeyArgument extends Argument<VirtualKey> {
protected VirtualKeyArgument(String label, int flags, String description) {
super(label, flags, new VirtualKey[0], description);
}
@Override
protected String argumentKind() {
return "VK spec";
}
@Override
protected VirtualKey doAccept(Token value, int flags) throws CommandSyntaxException {
String str = value.text;
String[] parts = str.split("\\+");
int modifiers = 0;
for (int i = 0; i < parts.length - 1; i++) {
Integer m = MODIFIER_NAME_MAP.get(constCase(parts[i]));
if (m == null) {
throw new CommandSyntaxException(String.format(ex_unknown_mod, parts[i]));
}
modifiers |= m;
}
Integer vk = VK_NAME_MAP.get(constCase(parts[parts.length - 1]));
if (vk == null) {
throw new CommandSyntaxException(String.format(ex_unknown_vkey, parts[parts.length - 1]));
}
return new VirtualKey(vk, modifiers);
}
}
private static class CharacterArgument extends Argument<Character> {
protected CharacterArgument(String label, int flags, String description) {
super(label, flags, new Character[0], description);
}
@Override
protected String argumentKind() {
return "character";
}
@Override
protected Character doAccept(Token value, int flags) throws CommandSyntaxException {
String str = value.text;
String upper = str.toUpperCase();
for (int i = 0; i < ASCII_NAMES.length; i++) {
if (ASCII_NAMES[i].equals(upper)) {
return (char) i;
}
}
if (upper.equals("DEL")) {
return '\177';
}
if (str.length() == 3 && str.charAt(0) == '\'' && str.charAt(2) == '\'') {
return str.charAt(1);
}
if (str.startsWith("0x") || str.startsWith("0X")) {
int ch = Integer.parseInt(str.substring(2), 16);
return (char) ch;
}
throw new CommandSyntaxException(ex_inv_char);
}
}
private final FlagArgument argReset;
private final FlagArgument argAdd;
private final FlagArgument argRemove;
private final ActionArgument argAction;
private final VirtualKeyArgument argVkSpec;
private final CharacterArgument argCharacter;
private PrintWriter out;
private PrintWriter err;
public BindKeysCommand() {
super(help_super);
argReset = new FlagArgument("reset", Argument.OPTIONAL, help_reset);
argAdd = new FlagArgument("add", Argument.OPTIONAL, help_add);
argRemove = new FlagArgument("remove", Argument.OPTIONAL, help_remove);
argAction = new ActionArgument("action", Argument.OPTIONAL, help_action);
argVkSpec = new VirtualKeyArgument("vkSpec", Argument.OPTIONAL | Argument.MULTIPLE, help_vkspec);
argCharacter = new CharacterArgument("character", Argument.OPTIONAL | Argument.MULTIPLE, help_char);
registerArguments(argReset, argAdd, argRemove, argAction, argVkSpec, argCharacter);
}
private static String constCase(String keyText) {
StringBuilder sb = new StringBuilder(keyText);
int len = keyText.length();
for (int i = 0; i < len; i++) {
char ch = sb.charAt(i);
if (ch == ' ') {
sb.setCharAt(i, '_');
} else {
sb.setCharAt(i, Character.toUpperCase(ch));
}
}
return sb.toString();
}
@Override
public void execute() throws Exception {
out = getOutput().getPrintWriter();
err = getError().getPrintWriter();
Console console = ShellUtils.getCurrentShell().getConsole();
if (!(console instanceof TextConsole)) {
err.println(err_not_text);
}
TextConsole textConsole = (TextConsole) console;
if (argReset.isSet()) {
resetBindings(textConsole);
} else if (argAdd.isSet()) {
addBindings(textConsole);
} else if (argRemove.isSet()) {
removeBindings(textConsole);
} else {
displayBindings(textConsole);
}
}
/**
* Remove bindings for an action.
*
* @param console the console whose bindings are to be changed
*/
private void removeBindings(TextConsole console) {
ConsoleKeyEventBindings bindings = console.getKeyEventBindings();
// This throws an unchecked exception if the action is not supplied. It signals
// a bug in the command syntax and should be allowed to propagate to the shell.
KeyboardReaderAction action = argAction.getValue();
if (argVkSpec.isSet() || argCharacter.isSet()) {
// If virtual key names were supplied, remove only those bindings.
if (argVkSpec.isSet()) {
for (VirtualKey vk : argVkSpec.getValues()) {
bindings.unsetVKAction(vk);
}
}
if (argCharacter.isSet()) {
for (char ch : argCharacter.getValues()) {
bindings.unsetCharAction(ch);
}
}
} else {
// Otherwise remove all bindings for the action.
int count = 0;
List<Character> chars = buildCharMap(bindings).get(action);
if (chars != null) {
for (char ch : chars) {
bindings.unsetCharAction(ch);
count++;
}
}
List<VirtualKey> vks = buildVKMap(bindings).get(action);
if (vks != null) {
for (VirtualKey vk : vks) {
bindings.unsetVKAction(vk);
count++;
}
}
if (count == 0) {
err.format(err_no_bind, action);
exit(1);
}
}
console.setKeyEventBindings(bindings);
out.format(fmt_update, action);
}
private void addBindings(TextConsole console) {
ConsoleKeyEventBindings bindings = console.getKeyEventBindings();
// This throws an unchecked exception if the action is not supplied. It signals
// a bug in the command syntax and should be allowed to propagate to the shell.
KeyboardReaderAction action = argAction.getValue();
int count = 0;
if (argVkSpec.isSet()) {
for (VirtualKey vk : argVkSpec.getValues()) {
bindings.setVKAction(vk, action);
count++;
}
}
if (argCharacter.isSet()) {
for (char ch : argCharacter.getValues()) {
bindings.setCharAction(ch, action);
count++;
}
}
if (count > 0) {
console.setKeyEventBindings(bindings);
out.format(fmt_update, action);
}
}
private void resetBindings(TextConsole console) {
console.setKeyEventBindings(ConsoleKeyEventBindings.createDefault());
out.println(str_reset);
}
private void displayBindings(TextConsole console) {
ConsoleKeyEventBindings bindings = console.getKeyEventBindings();
Map<KeyboardReaderAction, List<Character>> charMap = buildCharMap(bindings);
Map<KeyboardReaderAction, List<VirtualKey>> vkMap = buildVKMap(bindings);
for (KeyboardReaderAction action : KeyboardReaderAction.values()) {
List<Character> chars = charMap.get(action);
List<VirtualKey> vks = vkMap.get(action);
if (chars == null && vks == null &&
(action == KeyboardReaderAction.getDefaultCharAction() ||
action == KeyboardReaderAction.getDefaultVKAction())) {
continue;
}
StringBuilder sb = new StringBuilder(40);
sb.append(describe(action)).append(" : ");
if (chars == null && vks == null) {
sb.append(str_not_bound);
} else {
boolean first = true;
if (chars != null) {
for (char ch : chars) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(describe(ch));
}
}
if (vks != null) {
for (VirtualKey vk : vks) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(describe(vk));
}
}
}
out.println(sb);
}
}
/**
* Build a map from actions to the virtual keys that map to them.
* @param bindings
* @return the map
*/
private Map<KeyboardReaderAction, List<VirtualKey>> buildVKMap(
KeyEventBindings<KeyboardReaderAction> bindings) {
VirtualKey[] boundKeys = bindings.getBoundVKs();
Map<KeyboardReaderAction, List<VirtualKey>> vkMap =
new HashMap<KeyboardReaderAction, List<VirtualKey>>();
for (VirtualKey vk : boundKeys) {
KeyboardReaderAction action = bindings.getVKAction(vk);
List<VirtualKey> list = vkMap.get(action);
if (list == null) {
list = new ArrayList<VirtualKey>();
vkMap.put(action, list);
}
list.add(vk);
}
return vkMap;
}
/**
* Build a map from actions to the characters that map to them.
* @param bindings
* @return the map.
*/
private Map<KeyboardReaderAction, List<Character>> buildCharMap(
KeyEventBindings<KeyboardReaderAction> bindings) {
char[] boundChars = bindings.getBoundChars();
Map<KeyboardReaderAction, List<Character>> charMap =
new HashMap<KeyboardReaderAction, List<Character>>();
for (char ch : boundChars) {
KeyboardReaderAction action = bindings.getCharAction(ch);
List<Character> list = charMap.get(action);
if (list == null) {
list = new ArrayList<Character>();
charMap.put(action, list);
}
list.add(ch);
}
return charMap;
}
private String describe(KeyboardReaderAction action) {
return action.toString();
}
private String describe(char ch) {
StringBuilder sb = new StringBuilder();
if (ch < 0x1f) {
sb.append(ASCII_NAMES[ch]);
} else if (ch == ' ') {
sb.append("SP");
} else if (ch == '\177') {
sb.append("DEL");
} else if (ch < '\177') {
sb.append('\'').append(ch).append('\'');
} else {
sb.append('\'');
sb.append(ch);
sb.append("' (0x");
sb.append(Integer.toHexString(ch));
sb.append(')');
}
return sb.toString();
}
private String describe(VirtualKey vk) {
int modifiers = vk.getModifiers();
StringBuilder sb = new StringBuilder();
// We don't use the KeyEvent.getKeyModifierText method because it
// expects the 'old' mask values and conflates some of the masks.
if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.SHIFT_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.CTRL_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.ALT_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.ALT_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.META_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.META_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.BUTTON1_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.BUTTON1_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.BUTTON2_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.BUTTON2_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.BUTTON3_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.BUTTON3_DOWN_MASK)).append('+');
}
if ((modifiers & KeyEvent.ALT_GRAPH_DOWN_MASK) != 0) {
sb.append(MODIFIER_MAP.get(KeyEvent.ALT_GRAPH_DOWN_MASK)).append('+');
}
sb.append("VK_");
sb.append(VK_MAP.get(vk.getVKCode()));
return sb.toString();
}
}