I've refined stolsvik hack to prevent repeating of KEY_PRESSED and KEY_TYPED events as well, with this refinement it works correctly under Win7 (should work everywhere as it truly watches out for KEY_PRESSED/KEY_TYPED/KEY_RELEASED events).
Cheers!
Jakub
package com.example;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.swing.Timer;
/**
* This {@link AWTEventListener} tries to work around for KEY_PRESSED / KEY_TYPED/ KEY_RELEASED repeaters.
*
* If you wish to obtain only one pressed / typed / released, no repeatings (i.e., when the button is hold for a long time).
* Use new RepeatingKeyEventsFixer().install() as a first line in main() method.
*
* Based on xxx
* Which was done by Endre Stølsvik and inspired by xxx (hyperlinks stipped out due to stackoverflow policies)
*
* Refined by Jakub Gemrot not only to fix KEY_RELEASED events but also KEY_PRESSED and KEY_TYPED repeatings. Tested under Win7.
*
* If you wish to test the class, just uncomment all System.out.println(...)s.
*
* @author Endre Stølsvik
* @author Jakub Gemrot
*/
public class RepeatingKeyEventsFixer implements AWTEventListener {
public static final int RELEASED_LAG_MILLIS = 5;
private static boolean assertEDT() {
if (!EventQueue.isDispatchThread()) {
throw new AssertionError("Not EDT, but [" + Thread.currentThread() + "].");
}
return true;
}
private Map<Integer, ReleasedAction> _releasedMap = new HashMap<Integer, ReleasedAction>();
private Set<Integer> _pressed = new HashSet<Integer>();
private Set<Character> _typed = new HashSet<Character>();
public void install() {
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
}
public void remove() {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
}
@Override
public void eventDispatched(AWTEvent event) {
assert event instanceof KeyEvent : "Shall only listen to KeyEvents, so no other events shall come here";
assert assertEDT(); // REMEMBER THAT THIS IS SINGLE THREADED, so no need
// for synch.
// ?: Is this one of our synthetic RELEASED events?
if (event instanceof Reposted) {
//System.out.println("REPOSTED: " + ((KeyEvent)event).getKeyChar());
// -> Yes, so we shalln't process it again.
return;
}
final KeyEvent keyEvent = (KeyEvent) event;
// ?: Is this already consumed?
// (Note how events are passed on to all AWTEventListeners even though a
// previous one consumed it)
if (keyEvent.isConsumed()) {
return;
}
// ?: KEY_TYPED event? (We're only interested in KEY_PRESSED and
// KEY_RELEASED).
if (event.getID() == KeyEvent.KEY_TYPED) {
if (_typed.contains(keyEvent.getKeyChar())) {
// we're being retyped -> prevent!
//System.out.println("TYPED: " + keyEvent.getKeyChar() + " (CONSUMED)");
keyEvent.consume();
} else {
// -> Yes, TYPED, for a first time
//System.out.println("TYPED: " + keyEvent.getKeyChar());
_typed.add(keyEvent.getKeyChar());
}
return;
}
// ?: Is this RELEASED? (the problem we're trying to fix!)
if (keyEvent.getID() == KeyEvent.KEY_RELEASED) {
// -> Yes, so stick in wait
/*
* Really just wait until "immediately", as the point is that the
* subsequent PRESSED shall already have been posted on the event
* queue, and shall thus be the direct next event no matter which
* events are posted afterwards. The code with the ReleasedAction
* handles if the Timer thread actually fires the action due to
* lags, by cancelling the action itself upon the PRESSED.
*/
final Timer timer = new Timer(RELEASED_LAG_MILLIS, null);
ReleasedAction action = new ReleasedAction(keyEvent, timer);
timer.addActionListener(action);
timer.start();
ReleasedAction oldAction = (ReleasedAction)_releasedMap.put(Integer.valueOf(keyEvent.getKeyCode()), action);
if (oldAction != null) oldAction.cancel();
// Consume the original
keyEvent.consume();
//System.out.println("RELEASED: " + keyEvent.getKeyChar() + " (CONSUMED)");
return;
}
if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {
if (_pressed.contains(keyEvent.getKeyCode())) {
// we're still being pressed
//System.out.println("PRESSED: " + keyEvent.getKeyChar() + " (CONSUMED)");
keyEvent.consume();
} else {
// Remember that this is single threaded (EDT), so we can't have
// races.
ReleasedAction action = (ReleasedAction) _releasedMap.get(keyEvent.getKeyCode());
// ?: Do we have a corresponding RELEASED waiting?
if (action != null) {
// -> Yes, so dump it
action.cancel();
}
_pressed.add(keyEvent.getKeyCode());
//System.out.println("PRESSED: " + keyEvent.getKeyChar());
}
return;
}
throw new AssertionError("All IDs should be covered.");
}
/**
* The ActionListener that posts the RELEASED {@link RepostedKeyEvent} if
* the {@link Timer} times out (and hence the repeat-action was over).
*/
protected class ReleasedAction implements ActionListener {
private final KeyEvent _originalKeyEvent;
private Timer _timer;
ReleasedAction(KeyEvent originalReleased, Timer timer) {
_timer = timer;
_originalKeyEvent = originalReleased;
}
void cancel() {
assert assertEDT();
_timer.stop();
_timer = null;
_releasedMap.remove(Integer.valueOf(_originalKeyEvent.getKeyCode()));
}
@Override
public void actionPerformed(@SuppressWarnings("unused") ActionEvent e) {
assert assertEDT();
// ?: Are we already cancelled?
// (Judging by Timer and TimerQueue code, we can theoretically be
// raced to be posted onto EDT by TimerQueue,
// due to some lag, unfair scheduling)
if (_timer == null) {
// -> Yes, so don't post the new RELEASED event.
return;
}
//System.out.println("REPOST RELEASE: " + _originalKeyEvent.getKeyChar());
// Stop Timer and clean.
cancel();
// Creating new KeyEvent (we've consumed the original).
KeyEvent newEvent = new RepostedKeyEvent(
(Component) _originalKeyEvent.getSource(),
_originalKeyEvent.getID(), _originalKeyEvent.getWhen(),
_originalKeyEvent.getModifiers(), _originalKeyEvent
.getKeyCode(), _originalKeyEvent.getKeyChar(),
_originalKeyEvent.getKeyLocation());
// Posting to EventQueue.
_pressed.remove(_originalKeyEvent.getKeyCode());
_typed.remove(_originalKeyEvent.getKeyChar());
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(newEvent);
}
}
/**
* Marker interface that denotes that the {@link KeyEvent} in question is
* reposted from some {@link AWTEventListener}, including this. It denotes
* that the event shall not be "hack processed" by this class again. (The
* problem is that it is not possible to state
* "inject this event from this point in the pipeline" - one have to inject
* it to the event queue directly, thus it will come through this
* {@link AWTEventListener} too.
*/
public interface Reposted {
// marker
}
/**
* Dead simple extension of {@link KeyEvent} that implements
* {@link Reposted}.
*/
public static class RepostedKeyEvent extends KeyEvent implements Reposted {
public RepostedKeyEvent(@SuppressWarnings("hiding") Component source,
@SuppressWarnings("hiding") int id, long when, int modifiers,
int keyCode, char keyChar, int keyLocation) {
super(source, id, when, modifiers, keyCode, keyChar, keyLocation);
}
}
}