Events to detect change in physical keyboard key state
I would like my application to respond in one way to a keyboard key being pressed, and then another way to the key being released. That is, I want my application to be able to reflect the actual physical state of the keyboard key.
The obvious use of the (several) Java API's for keyboard events rapidly fires repeated KEY_PRESSED and KEY_RELEASED events if the key is held down for any time. This unfortunately obscures the actual state of the physical key (how do you know which was thelast KEY_RELEASED?)
Is it possible to turn off the key repeat feature, just for component, say?
This would seem to be an obvious thing to do, but I can't find sample code anywhere. The code I find in the Sun Java API manuals all shows the above problem with key repetition.
I would be also pleased to know that the solution of this problem is well known, on some FAQ or something, but I have looked through several FAQ's and searched this formum and have found nothing that helps. Also, I have had the misfortune of having to build messy and non-robust solutions for the equivalent problem in other development environments. I hope it is already there somewhere in Java and I'm just missing it.
Thanks!
# 1
I think KeyboardFocusManager can help you.
For example you can add new KeyEventDispatcher and dosable any key processing:
KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
focusManager.addKeyEventDispatcher(new KeyEventDispatcher() {
public boolean dispatchKeyEvent(KeyEvent e) {
return true; // any pressed key will b ignored.
// return false; // all works correct
}
});
# 2
Are you looking for something like this? It will print out the last key pressed/released.
import java.awt.event.*;
import javax.swing.*;
public class KeyTest extends JPanel implements KeyListener {
int m_keyCode = -1;
public static void main(String[] args) {
Runnable doRun = new Runnable() {
public void run() { new KeyTest(); }
};
SwingUtilities.invokeLater( doRun );
}
public KeyTest() {
JPanel panel = new JPanel();
panel.setFocusable( true );
panel.requestFocusInWindow();
panel.addKeyListener( this );
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add( panel );
f.setSize(200, 100);
f.setLocation(200, 200);
f.setVisible(true);
}
public void keyPressed( KeyEvent e ) {
int keyCode = e.getKeyCode();
if ( keyCode != m_keyCode ) {
m_keyCode = keyCode;
System.out.println( e.toString() );
}
}
public void keyReleased( KeyEvent e ) {
m_keyCode = -1;
System.out.println( e.toString() );
}
public void keyTyped( KeyEvent e ) {}
}
# 3
> rapidly fires repeated KEY_PRESSED and KEY_RELEASED events if the key is held down for any time.
Not in Windows. In Windows you get multple keyPressed events while the key is held down and a single keyReleased event when the key is released. Is this a non Windows problem.
Maybe you will have better luck using [url http://java.sun.com/docs/books/tutorial/uiswing/misc/keybinding.html]Key Bindings[/url].
# 4
camickr,
You identified the source of the problem and the confusion. The Java API does not specify that the order or number of KEY_PRESSED and KEY_RELEASED events should be consistent across platforms. And guess what...it isn't!
This problem crosses all of the numerous Java event API's, including Key Bindings. It seems a programmer's only recourse is one of several hacks that involve examining the time stamps of the events. I see no official endorsement of any of the hacks I've seen, and my guess is, they are not robust.
I think the problem stems from the existence of two different paradigms for use of the keys:
1) typing characters
(key down and key repetition are important--key up is not)
2) generic control buttons
(key down and key up are important, repetition is not)
The API behaves as if the alphanumeric keys are only used for typing, and the other keys are only control keys.
Of course, it could be and should be easy to write code for either paradigm.
The problem has been reported numerous times. The same chaos, finger-pointing, and denial is repeated over and over, just like the key events on Linux/Unix. For example:
Direct access to the keyboard?
http://forum.java.sun.com/thread.jspa?forumID=406&threadID=698156
"real" keyreleased events?
http://forum.java.sun.com/thread.jspa?forumID=257&threadID=461740
KeyEvent-bug - workaround?
http://forum.java.sun.com/thread.jspa?forumID=31&threadID=568557
java Robot
http://forum.java.sun.com/thread.jspa?forumID=31&threadID=728032
KeyListener on unix
http://forum.java.sun.com/thread.jspa?forumID=54&threadID=790206
This link proposes a work-around that I might be able to use, but haven't tried yet:
http://wiki.java.net/bin/view/Games/HowToDoKeyboardInput
The original poster suggested turning X key repetition off
Runtime.exec("xset r off")
A reply to the original poster says
"If two subsequent KEY_RELEASED and KEY_PRESSED events have the same
timestamp, they are caused by autorepeat"
Several bug reports exist on this point. Many are closed as "the API doesn't specify that the behaviour should be consistent across platforms, therefore this is not a bug". E.g.
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5011907
Here's a bug report that's still open, from 1998!
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4153069
Of course, it is a serious problem that it is hard for a programmer to write robust code that does such an obvious thing.
Now, how do we get somebody at Sun listen?
# 5
JayDS,
Thanks, this made the point clear.
I ran your example on Windows and on Linux.
On Windows it prints out one event when I press a key down, and another when I release the key. On Linux, if I hold the key down for a very short time, it prints out repeated events:
java KeyTest
java.awt.event.KeyEvent[KEY_PRESSED,keyCode=65,keyText=A,keyChar='a',keyLocation=KEY_LOCATION_STANDARD] on javax.swing.JPanel[,0,0,192x73,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
java.awt.event.KeyEvent[KEY_RELEASED,keyCode=65,keyText=A,keyChar='a',keyLocation=KEY_LOCATION_STANDARD] on javax.swing.JPanel[,0,0,192x73,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
java.awt.event.KeyEvent[KEY_PRESSED,keyCode=65,keyText=A,keyChar='a',keyLocation=KEY_LOCATION_STANDARD] on javax.swing.JPanel[,0,0,192x73,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
java.awt.event.KeyEvent[KEY_RELEASED,keyCode=65,keyText=A,keyChar='a',keyLocation=KEY_LOCATION_STANDARD] on javax.swing.JPanel[,0,0,192x73,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
...
And that is the problem. One would expect this simple Java code to do the same thing on any platform, but it doesn't. It's a booby-trap.
# 6
Follow-up to the idea I found posted:
"If two subsequent KEY_RELEASED and KEY_PRESSED events have the same
timestamp, they are caused by autorepeat"
This sounds good. But unfortunately the repeated KEY_RELEASED and KEY_PRESSED caused by key autorepeat on my system (Linux, java 1.5.0_09) have timestamps that increase with each repetition.
So this idea is useless.
# 7
> "If two subsequent KEY_RELEASED and KEY_PRESSED
> events have the same
> timestamp, they are caused by autorepeat"
>
> This sounds good. But unfortunately the repeated
> KEY_RELEASED and KEY_PRESSED caused by key autorepeat
> on my system (Linux, java 1.5.0_09) have
> timestamps that increase with each repetition.
I think this means only that the pair of pressed/release events have the
same timestamp, not that every such pair has the same timestamp.
That is, if you have the sequence PRESS,RELEASE,PRESS,RELEASE,
the first two would have the same timestamp, and then the second two
would have the same timestamp, but different than the first. Regardless,
I found that also to not be the case; the PRESS and RELEASE did not have
the same timestamp even between pairs.
That said, this seems to work on both Windows and Linux, with the caveat
that releases may lag.
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
public class LinuxKeyTest extends JPanel implements KeyListener {
int delay = 750; // Must be > than your 500ms start delay.
int delayRepeat = 75; // Must be greater than keyboard repeat interval.
Map timers = new HashMap();
int m_keyCode = -1;
public static void main( String[] args ) {
Runnable doRun = new Runnable() {
public void run() {
new LinuxKeyTest();
}
};
SwingUtilities.invokeLater( doRun );
}
public LinuxKeyTest() {
JPanel panel = new JPanel();
panel.setFocusable( true );
panel.requestFocusInWindow();
panel.addKeyListener( this );
JFrame f = new JFrame();
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
f.getContentPane().add( panel );
f.setSize( 200, 100 );
f.setLocation( 200, 200 );
f.setVisible( true );
}
public void keyPressed( KeyEvent e ) {
int keyCode = e.getKeyCode();
m_keyCode = keyCode;
Integer timerKey = new Integer( keyCode );
Timer timer = (Timer) timers.get( timerKey );
if ( timer == null ) {
KeyTimeout keyTimeout = new KeyTimeout( timerKey, e.getWhen() );
timer = new Timer( delay, keyTimeout );
timers.put( timerKey, timer );
timer.start();
pressed( keyCode );
} else {
( (KeyTimeout) timer.getActionListeners()[0] ).setPressed( e.getWhen() );
timer.restart();
}
}
public void keyReleased( KeyEvent e ) {
int keyCode = e.getKeyCode();
Integer timerKey = new Integer( keyCode );
Timer timer = (Timer) timers.get( timerKey );
if ( timer != null ) {
( (KeyTimeout) timer.getActionListeners()[0] ).setReleased( e.getWhen() );
timer.setInitialDelay( delayRepeat );
m_keyCode = ( m_keyCode == keyCode ? -1 : keyCode );
} else if ( m_keyCode != keyCode ) {
released( keyCode );
}
}
public void keyTyped( KeyEvent e ) {
}
public void pressed( int keyCode ) {
System.out.println( keyCode + " pressed" );
}
public void released( int keyCode ) {
System.out.println( keyCode + " released" );
}
public class KeyTimeout implements ActionListener {
private Integer timerKey;
private int keyCode;
private long firstPressed;
private long pressed;
private long released;
public KeyTimeout( Integer key, long time ) {
this.timerKey = key;
this.keyCode = timerKey.intValue();
this.firstPressed = time;
this.pressed = time;
this.released = time;
}
public void actionPerformed( ActionEvent e ) {
if ( released != firstPressed && released != pressed ) {
released( keyCode );
stop();
} else if ( m_keyCode != keyCode ) {
if ( m_keyCode == -1 ) {
released( keyCode );
}
stop();
}
}
public void stop() {
Timer timer = (Timer) timers.get( timerKey );
timer.stop();
timers.remove( timerKey );
}
public void setPressed( long time ) {
this.pressed = time;
}
public void setReleased( long time ) {
this.released = time;
}
}
}
# 8
JayDS,
This code does indeed work on my Linux system, as well as on Windows Vista.
It excellently illustrates the problem in using the existing Java libraries to write robust, cross-platform code that respnds simply to a key being pressed.
JayDS, this is meant as no sort of personal rebuke: you are plainly a good programmer. My target is the Java libraries.
We have here over a hundred lines of code, involving:
* two magic numbers, which have to be tuned to the target platform
* an assumption about the (unstandardized) way in which key repeat events are fired
* timers, which have nothing to do conceptually with the task at hand.
This is a hack: the programmer first wrote something on Windows that seemed adequate. When it failed on Linux, the programmer diagnosed the different behaviour, and wrote code to accommodate that behaviour.
The code is not robust. For instance, is there anything in the specs that says that key auto-repeat events are fired at a constant rate? I think there is not. If they were to accelerate, for example, the code would fail.
I do not know of any way to write robust, cross-platform Java code to respond simply to a key press. I think there is no way. (Please, somebody show me I'm wrong!)
This is a long-term shortfall of the Java libraries. It should have been fixed years ago. It should be fixed straightaway.
I would expect a very simple API, with something like a key listerner interface, and a means of registering the listener. The listener might have two methods, one for key press and one for key release, or else one method with two arguments. It should be plainly stated in the documentation that on all platforms the press method will be fired exactly once when the key is pressed, and the release method exactly once when the key is released.
Comments?
(further criticism: I got a compiler warning:
javac -Xlint:unchecked LinuxKeyTest.java
LinuxKeyTest.java:45: warning: [unchecked] unchecked call to put(K,V) as a member of the raw type java.util.Map
timers.put( timerKey, timer );
This was new to me. I think it's easy to fix though.)
