
com.threerings.util.KeyboardManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nenya Show documentation
Show all versions of nenya Show documentation
Facilities for making networked multiplayer games.
The newest version!
//
// 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