
com.threerings.util.KeyboardManager Maven / Gradle / Ivy
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// https://github.com/threerings/nenya
//
// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.util;
import java.util.HashMap;
import java.util.Iterator;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import com.google.common.collect.Maps;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.Interval;
import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere;
import com.samskivert.swing.Controller;
import com.samskivert.swing.RuntimeAdjust;
import com.threerings.util.keybd.Keyboard;
import com.threerings.media.MediaPrefs;
import static com.threerings.NenyaLog.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 KeyEventDispatcher, AncestorListener, WindowFocusListener
{
/**
* An interface to be implemented by those that care to be notified whenever an event (either a
* key press or a key release) occurs for any key while the keyboard manager is active. We use
* this custom interface rather than the more standard {@link java.awt.event.KeyListener}
* interface so that we needn't create key pressed and released event objects each time a
* (potentially artificially-generated) event occurs.
*/
public interface KeyObserver
{
/**
* Called whenever a key event occurs for a particular key.
*/
public void handleKeyEvent (int id, int keyCode, long timestamp);
}
/**
* Constructs a keyboard manager that is initially disabled. The keyboard manager should not
* be enabled until it has been supplied with a target component and translator via
* {@link #setTarget}.
*/
public KeyboardManager ()
{
// capture low-level keyboard events via the keyboard focus manager
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
}
/**
* Resets the keyboard manager, clearing any target and key translator in use and disabling the
* keyboard manager if it is currently active.
*/
public void reset ()
{
setEnabled(false);
_target = null;
_xlate = null;
_focus = false;
}
/**
* Initializes the keyboard manager with the supplied target component and key translator and
* disables the keyboard manager if it is currently active.
*
* @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 void setTarget (JComponent target, KeyTranslator xlate)
{
setEnabled(false);
// save off references
_target = target;
_xlate = xlate;
}
/**
* Registers a key observer that will be notified of all key events while the keyboard manager
* is active.
*/
public void registerKeyObserver (KeyObserver obs)
{
_observers.add(obs);
}
/**
* Removes the supplied key observer from the list of observers to be notified of all key
* events while the keyboard manager is active.
*/
public void removeKeyObserver (KeyObserver obs)
{
_observers.remove(obs);
}
/**
* Sets whether the keyboard manager processes keyboard input.
*/
public void setEnabled (boolean enabled)
{
// report incorrect usage
if (enabled && _target == null) {
log.warning("Attempt to enable uninitialized keyboard manager!", new Exception());
return;
}
// ignore NOOPs
if (enabled == _enabled) {
return;
}
if (!enabled) {
if (Keyboard.isAvailable()) {
// restore the original key auto-repeat settings
Keyboard.setKeyRepeat(_nativeRepeat);
}
// clear out all of our key states
releaseAllKeys();
_keys.clear();
// cease listening to all of our business
if (_window != null) {
_window.removeWindowFocusListener(this);
_window = null;
}
_target.removeAncestorListener(this);
// note that we no longer have the focus
_focus = false;
} else {
// listen to ancestor events so that we can cease our business
// if we lose the focus
_target.addAncestorListener(this);
// if we're already showing, listen to window focus events,
// else we have to wait until the target is added since it
// doesn't currently have a window
if (_target.isShowing() && _window == null) {
_window = SwingUtilities.getWindowAncestor(_target);
if (_window != null) {
_window.addWindowFocusListener(this);
}
}
// assume the keyboard focus since we were just enabled
_focus = true;
if (Keyboard.isAvailable()) {
// note whether key auto-repeating was enabled
_nativeRepeat = Keyboard.isKeyRepeatEnabled();
// Disable native key auto-repeating so that we can definitively ascertain key
// pressed/released events.
// Or not, if we've discovered we don't want to.
Keyboard.setKeyRepeat(!_shouldDisableNativeRepeat);
}
}
// save off our new enabled state
_enabled = enabled;
}
/**
* 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;
}
/**
* Releases all keys and ceases any hot repeating action that may be going on.
*/
public void releaseAllKeys ()
{
long now = System.currentTimeMillis();
Iterator iter = _keys.elements();
while (iter.hasNext()) {
iter.next().release(now);
}
}
/**
* Called when the keyboard manager gains focus and should begin handling keys again if it was
* previously enabled.
*/
protected void gainedFocus ()
{
if (Keyboard.isAvailable()) {
// disable key auto-repeating
Keyboard.setKeyRepeat(false);
}
// note that we've regained the focus
_focus = true;
}
/**
* Called when the keyboard manager loses focus and should cease handling keys.
*/
protected void lostFocus ()
{
if (Keyboard.isAvailable()) {
// restore key auto-repeating
Keyboard.setKeyRepeat(_nativeRepeat);
}
// clear out all of our keyboard state
releaseAllKeys();
// note that we no longer have the focus
_focus = false;
}
// documentation inherited from interface KeyEventDispatcher
public boolean dispatchKeyEvent (KeyEvent e)
{
// bail if we're not enabled, we haven't the focus, or we're not
// showing on-screen
if (!_enabled || !_focus || !_target.isShowing()) {
// log.info("dispatchKeyEvent [enabled=" + _enabled +
// ", focus=" + _focus +
// ", showing=" + ((_target == null) ? "N/A" :
// "" + _target.isShowing()) + "].");
return false;
}
// handle key press and release events
switch (e.getID()) {
case KeyEvent.KEY_PRESSED:
return keyPressed(e);
case KeyEvent.KEY_RELEASED:
return keyReleased(e);
case KeyEvent.KEY_TYPED:
return keyTyped(e);
default:
return false;
}
}
/**
* Called when Swing notifies us that a key has been pressed while the
* keyboard manager is active.
*
* @return true to swallow the key event
*/
protected boolean keyPressed (KeyEvent e)
{
logKey("keyPressed", e);
// get the action command associated with this key
int keyCode = e.getKeyCode();
boolean hasCommand = _xlate.hasCommand(keyCode);
if (hasCommand) {
// get the info object for this key, creating one if necessary
KeyInfo info = _keys.get(keyCode);
if (info == null) {
info = new KeyInfo(keyCode);
_keys.put(keyCode, info);
}
// remember the last time this key was pressed
info.setPressTime(RunAnywhere.getWhen(e));
}
// notify any key observers of the key press
notifyObservers(KeyEvent.KEY_PRESSED, e.getKeyCode(), RunAnywhere.getWhen(e));
return hasCommand;
}
/**
* Called when Swing notifies us that a key has been typed while the
* keyboard manager is active.
*
* @return true to swallow the key event
*/
protected boolean keyTyped (KeyEvent e)
{
logKey("keyTyped", e);
// get the action command associated with this key
char keyChar = e.getKeyChar();
boolean hasCommand = _xlate.hasCommand(keyChar);
if (hasCommand) {
// Okay, we're clearly doing actions based on key typing, so we're going to need native
// keyboard repeating turned on. Oh well.
if (_shouldDisableNativeRepeat) {
_shouldDisableNativeRepeat = false;
if (Keyboard.isAvailable()) {
Keyboard.setKeyRepeat(!_shouldDisableNativeRepeat);
}
}
KeyInfo info = _chars.get(keyChar);
if (info == null) {
info = new KeyInfo(keyChar);
_chars.put(keyChar, info);
}
// remember the last time this key was pressed
info.setPressTime(RunAnywhere.getWhen(e));
}
// notify any key observers of the key press
notifyObservers(KeyEvent.KEY_TYPED, e.getKeyChar(), RunAnywhere.getWhen(e));
return hasCommand;
}
/**
* Called when Swing notifies us that a key has been released while
* the keyboard manager is active.
*
* @return true to swallow the key event
*/
protected boolean keyReleased (KeyEvent e)
{
logKey("keyReleased", e);
// get the info object for this key
KeyInfo info = _keys.get(e.getKeyCode());
if (info != null) {
// remember the last time we received a key release
info.setReleaseTime(RunAnywhere.getWhen(e));
}
// notify any key observers of the key release
notifyObservers(KeyEvent.KEY_RELEASED, e.getKeyCode(), RunAnywhere.getWhen(e));
return (info != null);
}
/**
* Notifies all registered key observers of the supplied key event. This method provides a
* thread-safe manner in which to notify the observers, which is necessary since the
* {@link KeyInfo} objects do various antics from the interval manager thread whilst we may do
* other notification from the AWT thread when normal key events are handled.
*/
protected synchronized void notifyObservers (int id, int keyCode, long timestamp)
{
_keyOp.init(id, keyCode, timestamp);
_observers.apply(_keyOp);
}
/**
* Logs the given message and key.
*/
protected void logKey (String msg, KeyEvent e)
{
if (DEBUG_EVENTS || _debugTyping.getValue()) {
int keyCode = e.getKeyCode();
log.info(msg, "key", KeyEvent.getKeyText(keyCode));
}
}
// documentation inherited from interface AncestorListener
public void ancestorAdded (AncestorEvent e)
{
gainedFocus();
if (_window == null) {
_window = SwingUtilities.getWindowAncestor(_target);
_window.addWindowFocusListener(this);
}
}
// documentation inherited from interface AncestorListener
public void ancestorMoved (AncestorEvent e)
{
// nothing for now
}
// documentation inherited from interface AncestorListener
public void ancestorRemoved (AncestorEvent e)
{
lostFocus();
if (_window != null) {
_window.removeWindowFocusListener(this);
_window = null;
}
}
// documentation inherited from interface WindowFocusListener
public void windowGainedFocus (WindowEvent e)
{
gainedFocus();
}
// documentation inherited from interface WindowFocusListener
public void windowLostFocus (WindowEvent e)
{
lostFocus();
}
protected class KeyInfo extends Interval
{
/**
* Constructs a key info object for the given key code.
*/
public KeyInfo (int keyCode)
{
super(RUN_DIRECT);
_keyCode = keyCode;
_keyText = KeyEvent.getKeyText(_keyCode);
_pressCommand = _xlate.getPressCommand(_keyCode);
_releaseCommand = _xlate.getReleaseCommand(_keyCode);
int rate = _xlate.getRepeatRate(_keyCode);
_pressDelay = (rate == 0) ? 0 : (1000L / rate);
_repeatDelay = _xlate.getRepeatDelay(_keyCode);
}
/**
* Constructs a key info object for the given character.
*/
public KeyInfo (char keyChar)
{
super(RUN_DIRECT);
_keyChar = keyChar;
_keyText = "" +_keyChar;
_pressCommand = _xlate.getPressCommand(_keyChar);
_releaseCommand = _xlate.getReleaseCommand(_keyChar);
int rate = _xlate.getRepeatRate(_keyChar);
_pressDelay = (rate == 0) ? 0 : (1000L / rate);
_repeatDelay = _xlate.getRepeatDelay(_keyChar);
}
/**
* Returns true if we're based off a character & key typed events rather than a keycode
* and key pressed/released events.
*/
public boolean isCharacterBased ()
{
return _keyCode == KeyEvent.VK_UNDEFINED;
}
/**
* Sets the last time the key was pressed.
*/
public synchronized void setPressTime (long time)
{
if (_debugTyping.getValue()) {
log.info("setPressTime",
"time", time,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled);
}
if (_lastPress == 0 && _pressCommand != null) {
// post the initial key press command
postPress(time);
}
if (!_scheduled && (_pressDelay > 0 || isCharacterBased())) {
// register an interval to post the key press command
// until the key is decidedly released
if (_repeatDelay > 0) {
schedule(_repeatDelay, _pressDelay);
} else {
schedule(_pressDelay, true);
}
_scheduled = true;
if (DEBUG_EVENTS) {
log.info("Pressing key", "key", _keyText);
}
}
_lastPress = time;
_lastRelease = time;
}
/**
* Sets the last time the key was released.
*/
public synchronized void setReleaseTime (long time)
{
release(time);
_lastRelease = time;
if (_debugTyping.getValue()) {
log.info("setReleaseTime",
"time", time,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled);
}
// handle key release events received so quickly after the key
// press event that the press/release times are exactly equal
// and, in intervalExpired(), we would therefore be unable to
// distinguish between the key being initially pressed and the
// actual true key release that's taken place.
// the only case I can think of that might result in this
// happening is if the event manager class queues up a key
// press and release event succession while other code is
// executing, and when it comes time for it to dispatch the
// events in its queue it manages to dispatch both of them to
// us really-lickety-split. one would still think at least a
// few milliseconds should pass between the press and release,
// but in any case, we arguably ought to be watching for and
// handling this case for posterity even though it would seem
// unlikely or impossible, and so, now we do, which is a good
// thing since it appears this does in fact happen, and not so
// infrequently.
if (_lastPress == _lastRelease) {
if (DEBUG_EVENTS) {
log.warning("Insta-releasing key due to equal key press/release times",
"key", _keyText);
}
release(time);
}
}
/**
* Releases the key if pressed and cancels any active key repeat interval.
*/
public synchronized void release (long timestamp)
{
if (_debugTyping.getValue()) {
log.info("release",
"time", timestamp,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled);
}
// bail if we're not currently pressed
if (_lastPress == 0) {
return;
}
if (DEBUG_EVENTS) {
log.info("Releasing key", "key", _keyText);
}
// remove the repeat interval
if (_scheduled) {
cancel();
_scheduled = false;
}
if (_releaseCommand != null) {
// post the key release command
postRelease(timestamp);
}
// clear out the last press and release timestamps
_lastPress = _lastRelease = 0;
}
@Override
public synchronized void expired ()
{
long now = System.currentTimeMillis();
long deltaPress = now - _lastPress;
long deltaRelease = now - _lastRelease;
if (_debugTyping.getValue()) {
log.info("expired",
"time", now,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled,
"deltaPress", deltaPress,
"deltaRelease", deltaRelease);
}
if (KeyboardManager.DEBUG_INTERVAL) {
log.info("Interval",
"key", _keyText, "deltaPress", deltaPress, "deltaRelease", deltaRelease);
}
// cease repeating if we're certain the key is now up, or repeat the key
// command if we're certain the key is still down
if (_lastRelease != _lastPress) {
release(now);
} else if (_lastPress != 0) {
if (!isCharacterBased()) {
if (_pressCommand != null) {
// post the key press command again
postPress(now);
}
} else {
// We're dealing with a key typed event, so we don't really know what's going
// on, so we'll pretend we released it now, and hope the native keyboard repeat
// takes care of us.
release(now);
}
}
}
/**
* Posts the press command for this key and notifies all key observers of the key press.
*/
protected void postPress (long timestamp)
{
if (_debugTyping.getValue()) {
log.info("postPress",
"time", timestamp,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled);
}
if (!isCharacterBased()) {
notifyObservers(KeyEvent.KEY_PRESSED, _keyCode, timestamp);
} else {
notifyObservers(KeyEvent.KEY_TYPED, _keyChar, timestamp);
}
Controller.postAction(_target, _pressCommand);
}
/**
* Posts the release command for this key and notifies all key observers of the key release.
*/
protected void postRelease (long timestamp)
{
if (_debugTyping.getValue()) {
log.info("postRelease",
"time", timestamp,
"this", this,
"lastPress", _lastPress,
"lastRelease", _lastRelease,
"pressCommand", _pressCommand,
"releaseCommand", _releaseCommand,
"pressDelay", _pressDelay,
"repeatDelay", _repeatDelay,
"scheduled", _scheduled);
}
notifyObservers(KeyEvent.KEY_RELEASED, _keyCode, timestamp);
Controller.postAction(_target, _releaseCommand);
}
@Override
public String toString ()
{
return "[key=" + _keyText + ", charBased=" + isCharacterBased() + "]";
}
/** True if we are a scheduled interval. */
protected boolean _scheduled = false;
/** 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, if any. */
protected int _keyCode = KeyEvent.VK_UNDEFINED;
/** The character associated with this key info object, if any. */
protected char _keyChar;
/** The milliseconds to sleep between sending repeat key commands. */
protected long _pressDelay;
/** The delay in milliseconds before auto-repeating the key press. */
protected long _repeatDelay;
}
/** An observer operation to notify observers of a key event. */
protected static class KeyObserverOp implements ObserverList.ObserverOp
{
/** Initialized the operation with its parameters. */
public void init (int id, int keyCode, long timestamp)
{
_id = id;
_keyCode = keyCode;
_timestamp = timestamp;
}
// documentation inherited from interface ObserverList.ObserverOp
public boolean apply (KeyObserver observer)
{
observer.handleKeyEvent(_id, _keyCode, _timestamp);
return true;
}
/** The key event id. */
protected int _id;
/** The key code. */
protected int _keyCode;
/** The key event timestamp. */
protected long _timestamp;
}
/** Whether to output debugging info for individual key events. */
protected static final boolean DEBUG_EVENTS = false;
/** Whether to output debugging info for interval callbacks. */
protected static final boolean DEBUG_INTERVAL = false;
/** The default repeat delay. */
protected static final long DEFAULT_REPEAT_DELAY = 50L;
/** The expected approximate milliseconds between each key
* release/press event while the key is being auto-repeated. */
protected long _repeatDelay = DEFAULT_REPEAT_DELAY;
/** A hashtable mapping key codes to {@link KeyInfo} objects. */
protected HashIntMap _keys = new HashIntMap();
/** A hashtable mapping characters to {@link KeyInfo} objects. */
protected HashMap _chars = Maps.newHashMap();
/** Whether the keyboard manager currently has the keyboard focus. */
protected boolean _focus;
/** Whether the keyboard manager is accepting keyboard input. */
protected boolean _enabled;
/** The window containing our target component whose focus events we
* care to observe, or null if we're not observing a window. */
protected Window _window;
/** The component that receives keyboard events and that we associate
* with posted controller commands. */
protected JComponent _target;
/** The translator that maps keyboard events to controller commands. */
protected KeyTranslator _xlate;
/** The list of key observers. */
protected ObserverList _observers = ObserverList.newFastUnsafe();
/** The operation used to notify observers of actual key events. */
protected KeyObserverOp _keyOp = new KeyObserverOp();
/** Whether native key auto-repeating was enabled when the keyboard manager was last enabled. */
protected boolean _nativeRepeat;
/** Whether we want to disable native key auto-repeating. If we're dealing with wacky keys that
* send only key typed events, we might need to fall back to letting that happen so things work
* right.
*/
protected boolean _shouldDisableNativeRepeat = true;
/** A debug hook that toggles excessive logging to help debug keyTyped behavior. */
protected static RuntimeAdjust.BooleanAdjust _debugTyping = new RuntimeAdjust.BooleanAdjust(
"Toggles key typed debugging", "nenya.util.keyboard",
MediaPrefs.config, false);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy