// // ThudActions.java // Thud // // Copyright (c) 2001-2007 Anthony Parker & the THUD team. // All rights reserved. See LICENSE.TXT for more information. // package net.sourceforge.btthud.ui; import net.sourceforge.btthud.engine.commands.UserCommand; import java.awt.*; import java.awt.event.*; import javax.swing.*; // FIXME: All of this stuff probably shouldn't go into one class. public class ThudActions { // FIXME: This class only exists to keep Ant from rebuilding this file. } // Action class to send a fixed command string. class SendCommandAction extends AbstractAction { protected final Thud thud; private final String command; SendCommandAction (final Thud thud, final String command) { super(command.intern()); this.thud = thud; this.command = command.intern(); } public void actionPerformed (final ActionEvent ae) { if (thud.conn == null) return; final String realCommand = getCommand(); if (realCommand == null) return; try { thud.conn.sendCommand(new UserCommand (realCommand)); } catch (Exception e) { // TODO: Seems like it'd be more friendly to report // these errors in the main window, or in a modal // dialog. Hiding things in the console is so like // 1990. System.err.println("Can't send: " + e); } } protected String getCommand () { return command; } } class StayHeadingAction extends SendCommandAction { StayHeadingAction (final Thud thud) { super(thud, "StayHeadingAction"); } protected String getCommand () { if (thud.data == null || thud.data.myUnit == null) return null; return "heading " + thud.data.myUnit.heading; } } class ReverseHeadingAction extends SendCommandAction { ReverseHeadingAction (final Thud thud) { super(thud, "ReverseHeadingAction"); } protected String getCommand () { if (thud.data == null || thud.data.myUnit == null) return null; return "heading " + (thud.data.myUnit.heading + 180) % 360; } } /** * Proxy for an Action in a specific JComponent's ActionMap. This proxy is * currently limited to just relaying actionPerformed(); a full implementation * should mirror other properties of the target Action. * * TODO: This sort of thing seems useful enough that it'd be supplied by Swing. */ class ActionRedirector extends AbstractAction { private final Object newSrc; private final Action newAction; ActionRedirector (final JComponent newSrc, final Object newAction) { this.newSrc = newSrc; this.newAction = newSrc.getActionMap().get(newAction); } public void actionPerformed (final ActionEvent ae) { ae.setSource(newSrc); newAction.actionPerformed(ae); } } /** * A KeyListener to steal numeric pad KeyEvents and reroute it directly to the * JComponent and InputMap of our choosing. This is basically needed because * the KeyStroke class doesn't encode nor support KeyEvent.getLocation() * information, and the alternatives are inadequate in terms of behavior. * * The typical usage is that a private InputMap would be defined at the top * level component of interest for numeric pad keys, which would trigger the * desired actions on that component. Then the listener would be added to all * the focusable components in the hierarchy. * * Note that to work properly, this needs to be set on all focusable components * that need to have their numeric pad input captured. * * This class could be extended to handle other types of KeyEvents. */ class NumpadKeyListener implements KeyListener { private JComponent target; private InputMap inputMap; NumpadKeyListener (final JComponent target, final InputMap inputMap) { this.target = target; this.inputMap = inputMap; } void setJComponent (final JComponent target) { this.target = target; } void setInputMap (final InputMap inputMap) { this.inputMap = inputMap; } private void processKeyEvent (final KeyEvent ke) { if (ke.getKeyLocation() != KeyEvent.KEY_LOCATION_NUMPAD) return; // We remap all the numeric pad keys to the same values, // regardless of numlock status. Technically, this is // platform/locale-specific, but this should be good enough, // especially since everything will work fine with numlock on // (which is what this class tries to map to). // // In any case, it'd be best to keep such knowledge centralized // here, rather than scattered around the code. // // Note that this still leaves a big hole at 5. // // Windows hacks: SHIFT + numeric keypad on Windows effectively // reverses NUMLOCK. There's no real way around this, except // by translating the various keypad codes back. As a side // effect, this makes NUMLOCK off act like a shift lock on all // platforms. int keyCode = ke.getKeyCode(); boolean shift = false; switch (keyCode) { case KeyEvent.VK_HOME: keyCode = KeyEvent.VK_NUMPAD7; shift = true; // Winders break; case KeyEvent.VK_UP: // Winders case KeyEvent.VK_KP_UP: keyCode = KeyEvent.VK_NUMPAD8; shift = true; // Winders break; case KeyEvent.VK_PAGE_UP: keyCode = KeyEvent.VK_NUMPAD9; shift = true; // Winders break; case KeyEvent.VK_LEFT: // Winders case KeyEvent.VK_KP_LEFT: keyCode = KeyEvent.VK_NUMPAD4; shift = true; // Winders break; // Hole at 5. Seems to map to "Begin" on Unix. case KeyEvent.VK_RIGHT: // Winders case KeyEvent.VK_KP_RIGHT: keyCode = KeyEvent.VK_NUMPAD6; shift = true; // Winders break; case KeyEvent.VK_END: keyCode = KeyEvent.VK_NUMPAD1; shift = true; // Winders break; case KeyEvent.VK_DOWN: // Winders case KeyEvent.VK_KP_DOWN: keyCode = KeyEvent.VK_NUMPAD2; shift = true; // Winders break; case KeyEvent.VK_PAGE_DOWN: keyCode = KeyEvent.VK_NUMPAD3; shift = true; // Winders break; case KeyEvent.VK_INSERT: keyCode = KeyEvent.VK_NUMPAD0; shift = true; // Winders break; // FIXME: We don't handle the typed period case yet. case KeyEvent.VK_DELETE: keyCode = KeyEvent.VK_PERIOD; shift = true; // Winders break; case KeyEvent.VK_NUMPAD0: case KeyEvent.VK_NUMPAD1: case KeyEvent.VK_NUMPAD2: case KeyEvent.VK_NUMPAD3: case KeyEvent.VK_NUMPAD4: case KeyEvent.VK_NUMPAD5: case KeyEvent.VK_NUMPAD6: case KeyEvent.VK_NUMPAD7: case KeyEvent.VK_NUMPAD8: case KeyEvent.VK_NUMPAD9: break; default: // XXX: We can't be sure the NumpadInputMap will catch // all the typable characters we miss. //System.out.println(ke.paramString()); return; } ke.consume(); // Locate action. final KeyStroke stroke = KeyStroke .getKeyStroke(keyCode, ke.getModifiers() | (shift ? Event.SHIFT_MASK : 0), (ke.getID() == KeyEvent.KEY_RELEASED)); final Object actionKey = inputMap.get(stroke); if (actionKey == null) return; final Action action = target.getActionMap().get(actionKey); if (action == null) return; // Invoke action. SwingUtilities.notifyAction(action, stroke, ke, target, ke.getModifiers()); } // // KeyListener interface. // public void keyPressed (final KeyEvent ke) { processKeyEvent(ke); } public void keyReleased (final KeyEvent ke) { processKeyEvent(ke); } public void keyTyped (final KeyEvent ke) { // Typed events are always KEY_LOCATION_UNKNOWN. This is a bit // problematic if we want to intercept numeric pad input when // NUMLOCK is activated, since it will also generate typed // numbers. // // We can use NumpadInputMap to solve this problem, although it // would be more elegant if we only had to set this listener to // do the same thing, perhaps by converting numbers from // pressed key events. However, then we'd have to reimplement // the routing logic for InputMaps. Not fun, especially as the // standard Java API evolves in the future. } } /** * An InputMap wrapper to allow us to bind numeric pad keys, without having * them generate "typed #"-style KeyStrokes in JTextComponents. */ class NumpadInputMap extends InputMap { // Various KeyStrokes we're interested in. private static final KeyStroke TYPED_0 = KeyStroke.getKeyStroke('0'); private static final KeyStroke TYPED_1 = KeyStroke.getKeyStroke('1'); private static final KeyStroke TYPED_2 = KeyStroke.getKeyStroke('2'); private static final KeyStroke TYPED_3 = KeyStroke.getKeyStroke('3'); private static final KeyStroke TYPED_4 = KeyStroke.getKeyStroke('4'); private static final KeyStroke TYPED_5 = KeyStroke.getKeyStroke('5'); private static final KeyStroke TYPED_6 = KeyStroke.getKeyStroke('6'); private static final KeyStroke TYPED_7 = KeyStroke.getKeyStroke('7'); private static final KeyStroke TYPED_8 = KeyStroke.getKeyStroke('8'); private static final KeyStroke TYPED_9 = KeyStroke.getKeyStroke('9'); // Backing InputMap. private final InputMap realMap; NumpadInputMap (final InputMap realMap) { this.realMap = realMap; } private KeyStroke translateKeyStroke (final KeyStroke keyStroke) { // Only translate unmodified number keys. if (keyStroke.getModifiers() != 0) return keyStroke; switch (keyStroke.getKeyEventType()) { case KeyEvent.KEY_PRESSED: // Treat pressed numbers like typed numbers. // TODO: It'd be nice to ignore NUMLOCK'd, too. switch (keyStroke.getKeyCode()) { case KeyEvent.VK_0: return TYPED_0; case KeyEvent.VK_1: return TYPED_1; case KeyEvent.VK_2: return TYPED_2; case KeyEvent.VK_3: return TYPED_3; case KeyEvent.VK_4: return TYPED_4; case KeyEvent.VK_5: return TYPED_5; case KeyEvent.VK_6: return TYPED_6; case KeyEvent.VK_7: return TYPED_7; case KeyEvent.VK_8: return TYPED_8; case KeyEvent.VK_9: return TYPED_9; } break; case KeyEvent.KEY_TYPED: // Drop typed numbers. // TODO: Drop other characters typable on the numeric // keypad. Those are less used, though. switch (keyStroke.getKeyChar()) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return null; } break; } return keyStroke; } // // Proxy methods. // public KeyStroke[] allKeys () { // TODO: We probably shouldn't return keys which are filtered. return realMap.allKeys(); } public void clear () { realMap.clear(); } public Object get (final KeyStroke keyStroke) { final KeyStroke translated = translateKeyStroke(keyStroke); return (translated == null) ? null : realMap.get(translated); } public InputMap getParent () { // TODO: Do we want to let a parent have a crack at it? return realMap.getParent(); } public KeyStroke[] keys () { // TODO: We probably shouldn't return keys which are filtered. return realMap.keys(); } public void put (final KeyStroke keyStroke, final Object actionMapKey) { realMap.put(keyStroke, actionMapKey); } public void remove (final KeyStroke key) { realMap.remove(key); } public void setParent (final InputMap map) { realMap.setParent(map); } public int size () { // TODO: We should subtract out the filtered keys. return realMap.size(); } }