diff --git a/src/java/com/threerings/util/KeyTranslator.java b/src/java/com/threerings/util/KeyTranslator.java
new file mode 100644
index 000000000..a5c7a891f
--- /dev/null
+++ b/src/java/com/threerings/util/KeyTranslator.java
@@ -0,0 +1,34 @@
+//
+// $Id: KeyTranslator.java,v 1.1 2001/12/12 16:55:06 shaper Exp $
+
+package com.threerings.yohoho.puzzle.util;
+
+/**
+ * The key translator interface provides a means whereby the keyboard
+ * manager can map a key code to the logical {@link
+ * com.samskivert.swing.Controller} action command that it represents.
+ */
+public interface KeyTranslator
+{
+ /**
+ * Returns whether there is an action command for the key
+ * corresponding to the given keycode. The translator may have a
+ * mapping for an action command for a key press or a key release of
+ * the key, or both.
+ */
+ public boolean hasCommand (int keyCode);
+
+ /**
+ * Returns the action command string associated with a key press of
+ * the key corresponding to the given key code, or null
+ * if there is no associated command.
+ */
+ public String getPressCommand (int keyCode);
+
+ /**
+ * Returns the action command string associated with a key release of
+ * the key corresponding to the given key code, or null
+ * if there is no associated command.
+ */
+ public String getReleaseCommand (int keyCode);
+}
diff --git a/src/java/com/threerings/util/KeyTranslatorImpl.java b/src/java/com/threerings/util/KeyTranslatorImpl.java
new file mode 100644
index 000000000..2b77cf774
--- /dev/null
+++ b/src/java/com/threerings/util/KeyTranslatorImpl.java
@@ -0,0 +1,55 @@
+//
+// $Id: KeyTranslatorImpl.java,v 1.1 2001/12/12 16:55:06 shaper Exp $
+
+package com.threerings.yohoho.puzzle.util;
+
+import com.samskivert.util.HashIntMap;
+
+/**
+ * A simple implementation of the {@link KeyTranslator} interface that
+ * provides facilities for mapping key codes to action command strings for
+ * use by the {@link KeyboardManager}.
+ */
+public class KeyTranslatorImpl implements KeyTranslator
+{
+ /**
+ * Adds a mapping from key press to action command string.
+ */
+ public void addPressCommand (int keyCode, String command)
+ {
+ _press.put(keyCode, command);
+ }
+
+ /**
+ * Adds a mapping from key release to action command string.
+ */
+ public void addReleaseCommand (int keyCode, String command)
+ {
+ _release.put(keyCode, command);
+ }
+
+ // documentation inherited
+ public boolean hasCommand (int keyCode)
+ {
+ return (_press.get(keyCode) != null ||
+ _release.get(keyCode) != null);
+ }
+
+ // documentation inherited
+ public String getPressCommand (int keyCode)
+ {
+ return (String)_press.get(keyCode);
+ }
+
+ // documentation inherited
+ public String getReleaseCommand (int keyCode)
+ {
+ return (String)_release.get(keyCode);
+ }
+
+ /** The mapping for key presses from key codes to action commands. */
+ protected HashIntMap _press = new HashIntMap();
+
+ /** The mapping for key releases from key codes to action commands. */
+ protected HashIntMap _release = new HashIntMap();
+}
diff --git a/src/java/com/threerings/util/KeyboardManager.java b/src/java/com/threerings/util/KeyboardManager.java
new file mode 100644
index 000000000..4dc029d27
--- /dev/null
+++ b/src/java/com/threerings/util/KeyboardManager.java
@@ -0,0 +1,318 @@
+ //
+// $Id: KeyboardManager.java,v 1.1 2001/12/12 16:55:06 shaper Exp $
+
+package com.threerings.yohoho.puzzle.util;
+
+import java.awt.Component;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.Iterator;
+
+import com.samskivert.swing.Controller;
+import com.samskivert.util.HashIntMap;
+import com.samskivert.util.Interval;
+import com.samskivert.util.IntervalManager;
+
+import com.threerings.yohoho.Log;
+
+/**
+ * The keyboard manager observes keyboard actions on a particular
+ * component and posts commands associated with the key presses to the
+ * {@link Controller} hierarchy. It allows specifying the key repeat
+ * rate, and will begin repeating a key immediately after it is held down
+ * rather than depending on the system-specific key repeat delay/rate.
+ */
+public class KeyboardManager implements KeyListener
+{
+ /**
+ * Constructs a keyboard manager.
+ *
+ * @param target the component whose keyboard events are to be observed.
+ * @param xlate the key translator used to map keyboard events to
+ * controller action commands.
+ */
+ public KeyboardManager (Component target, KeyTranslator xlate)
+ {
+ // save off references
+ _target = target;
+ _xlate = xlate;
+
+ // listen to key events
+ target.addKeyListener(this);
+
+ // listen to focus events so that we can cease repeating if we
+ // lose focus
+ target.addFocusListener(new FocusAdapter() {
+ public void focusLost (FocusEvent e) {
+ releaseAllKeys();
+ }
+ });
+ }
+
+ /**
+ * Sets the expected delay in milliseconds between each key
+ * press/release event the keyboard manager should expect to receive
+ * while a key is repeating.
+ */
+ public void setRepeatDelay (long delay)
+ {
+ _repeatDelay = delay;
+ }
+
+ /**
+ * Sets the delay in milliseconds between each repeat key action
+ * command posted by the keyboard manager while a key is down.
+ */
+ public void setPressDelay (long delay)
+ {
+ _pressDelay = delay;
+ }
+
+ /**
+ * Releases all keys and ceases any hot repeating action that may be
+ * going on.
+ */
+ public void releaseAllKeys ()
+ {
+ Iterator iter = _keys.elements();
+ while (iter.hasNext()) {
+ ((KeyInfo)iter.next()).release();
+ }
+ }
+
+ // documentation inherited
+ public void keyPressed (KeyEvent e)
+ {
+ // logKey("keyPressed", e);
+
+ // get the action command associated with this key
+ int keyCode = e.getKeyCode();
+ if (_xlate.hasCommand(keyCode)) {
+ // get the info object for this key, creating one if necessary
+ KeyInfo info = (KeyInfo)_keys.get(keyCode);
+ if (info == null) {
+ info = new KeyInfo(keyCode);
+ _keys.put(keyCode, info);
+ }
+
+ // remember the last time this key was pressed
+ info.setPressTime(System.currentTimeMillis());
+ }
+ }
+
+ // documentation inherited
+ public void keyReleased (KeyEvent e)
+ {
+ // logKey("keyReleased", e);
+
+ // get the info object for this key
+ KeyInfo info = (KeyInfo)_keys.get(e.getKeyCode());
+ if (info == null) {
+ // Log.warning("Received key released event for a key that " +
+ // "seems not to have been previously pressed " +
+ // "[e=" + e + "].");
+ return;
+ }
+
+ // remember the last time we received a key release
+ info.setReleaseTime(System.currentTimeMillis());
+ }
+
+ // documentation inherited
+ public void keyTyped (KeyEvent e)
+ {
+ // logKey("keyTyped", e);
+ }
+
+ /**
+ * Logs the given message and key.
+ */
+ protected void logKey (String msg, KeyEvent e)
+ {
+ int keyCode = e.getKeyCode();
+ Log.info(msg + " [key=" + KeyEvent.getKeyText(keyCode) + "].");
+ }
+
+ protected class KeyInfo implements Interval
+ {
+ /**
+ * Constructs a key info object for the given key code.
+ */
+ public KeyInfo (int keyCode)
+ {
+ _keyCode = keyCode;
+ _keyText = KeyEvent.getKeyText(_keyCode);
+ _pressCommand = _xlate.getPressCommand(_keyCode);
+ _releaseCommand = _xlate.getReleaseCommand(_keyCode);
+ }
+
+ /**
+ * Sets the last time the key was pressed.
+ */
+ public synchronized void setPressTime (long time)
+ {
+ _lastPress = time;
+ _lastRelease = time;
+
+ if (_iid == -1) {
+ // register an interval to post the command associated
+ // with the key press until the key is decidedly released
+ _iid = IntervalManager.register(this, _pressDelay, null, true);
+
+ // post the initial key press command if applicable
+ if (_pressCommand != null) {
+ Controller.postAction(_target, _pressCommand);
+ }
+ }
+ }
+
+ /**
+ * Sets the last time the key was released.
+ */
+ public synchronized void setReleaseTime (long time)
+ {
+ _lastRelease = time;
+ }
+
+ /**
+ * Releases the key if pressed and cancels any active key repeat
+ * interval.
+ */
+ public synchronized void release ()
+ {
+ Log.info("Releasing key [key=" + _keyText + "].");
+
+ // remove the sub-interval, if any
+ if (_siid != -1) {
+ IntervalManager.remove(_siid);
+ _siid = -1;
+ }
+
+ // remove the repeat interval
+ if (_iid != -1) {
+ IntervalManager.remove(_iid);
+ _iid = -1;
+ }
+
+ // post the key release command if applicable
+ if (_releaseCommand != null) {
+ Controller.postAction(_target, _releaseCommand);
+ }
+ }
+
+ // documentation inherited
+ public synchronized void intervalExpired (int id, Object arg)
+ {
+ long now = System.currentTimeMillis();
+ long deltaRelease = now - _lastRelease;
+
+// Log.info("intervalExpired [id=" + id +
+// ", key=" + _keyText +
+// ", deltaPress=" + (now - _lastPress) +
+// ", deltaRelease=" + deltaRelease + "].");
+
+ if (id == _iid) {
+ // handle a normal interval where we either (a) create a
+ // sub-interval if we can't yet determine definitively
+ // whether the key is still down, (b) cease repeating if
+ // we're certain the key is now up, or (c) repeat the key
+ // command if we're certain the key is still down
+ if (_lastRelease != _lastPress) {
+ if (deltaRelease < _repeatDelay) {
+ // register a one-shot sub-interval to
+ // definitively check whether the key was released
+ Log.info("Registering sub-interval to check key " +
+ "release.");
+ long delay = _repeatDelay - deltaRelease;
+ _siid = IntervalManager.register(
+ this, delay, null, false);
+
+ } else {
+ // we know the key was released, so cease repeating
+ release();
+ }
+
+ } else if (_pressCommand != null) {
+ // post the key press command again
+ Log.info("Repeating command [cmd=" + _pressCommand + "].");
+ Controller.postAction(_target, _pressCommand);
+ }
+
+ } else if (id == _siid) {
+ // clear out the non-recurring sub-interval identifier
+ _siid = -1;
+
+ // provide the last word on whether the key was released
+ if ((_lastRelease != _lastPress) &&
+ deltaRelease >= _repeatDelay) {
+ release();
+
+ } else if (_pressCommand != null) {
+ // post the key command again
+ Log.info("Repeating command [cmd=" + _pressCommand + "].");
+ Controller.postAction(_target, _pressCommand);
+ }
+ }
+ }
+
+ /**
+ * Returns a string representation of the key info object.
+ */
+ public String toString ()
+ {
+ return "[key=" + _keyText + "]";
+ }
+
+ /** The unique interval identifier for the sub-interval used to
+ * handle the case where the main interval wakes up to repeat the
+ * currently pressed key and the last key release event was
+ * received more recently than the expected repeat delay. */
+ protected int _siid = -1;
+
+ /** The unique interval identifier for the key repeat interval. */
+ protected int _iid = -1;
+
+ /** The last time a key released event was received for this key. */
+ protected long _lastRelease;
+
+ /** The last time a key pressed event was received for this key. */
+ protected long _lastPress;
+
+ /** The press action command associated with this key. */
+ protected String _pressCommand;
+
+ /** The release action command associated with this key. */
+ protected String _releaseCommand;
+
+ /** A text representation of this key. */
+ protected String _keyText;
+
+ /** The key code associated with this key info object. */
+ protected int _keyCode;
+ }
+
+ /** The default repeat delay. */
+ protected static final long DEFAULT_REPEAT_DELAY = 50L;
+
+ /** The default key press delay. */
+ protected static final long DEFAULT_PRESS_DELAY = 200L;
+
+ /** The expected approximate milliseconds between each key
+ * release/press event while the key is being auto-repeated. */
+ protected long _repeatDelay = DEFAULT_REPEAT_DELAY;
+
+ /** The milliseconds to sleep between sending repeat key commands. */
+ protected long _pressDelay = DEFAULT_PRESS_DELAY;
+
+ /** A hashtable mapping key codes to {@link KeyInfo} objects. */
+ protected HashIntMap _keys = new HashIntMap();
+
+ /** The component that receives keyboard events and that we associate
+ * with posted controller commands. */
+ protected Component _target;
+
+ /** The translator that maps keyboard events to controller commands. */
+ protected KeyTranslator _xlate;
+}