package org.simantics.debug.graphical; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; /** *

* This class wraps your KeyAdapter and suppresses spurious repeated events. *

*

* Written by Brian Ziman and available * for download and comments at http://www.brianziman.com/r/post/stuff-200910092100. *

*

Rationale

*

* When running in an X11 environment, it appears as though either Java or * the windowing system stupidly generate lots of repeated key events when * you hold a key down. As it is impractical to get users to reconfigure their * systems, and Sun seems unable to fix this issue (having been reported nearly * ten years ago). This adapter is an attempt to filter out the useless events. *

*

* Many people have whined about this, but few have fixed it. Some have tried, * but their solutions are not as clean as my OCD nature requires. Here are * some examples of what this is meant to handle: *

* *

* * The goal is to generate a KeyPressed event when a key is pressed, * and to generate a KeyReleased event when a key is released. And * ONLY when a key is pressed or released. *

*

Compatibility

*

* The class is tested with Java 1.6.0 on Ubuntu 8.10, but should theoretically * work on any platform, even if that platform issues events properly anyway. * That is, this SHOULD work in a cross platform manner. If it doesn't, * please let me know and I may fix it. *

*

Usage

*

* You don't actually call any of the methods on this class, directly. When Java * thinks it is receiving an event, it will call the methods in this class, * which will, in turn, decide whether an event has actually occurred, and * call YOUR methods when you probably intended them to be called. *

*

* Replace: *

*
 * addKeyListener(new KeyAdapter() {
 *      public void keyPressed(KeyEvent e) { ... }
 *      public void keyReleased(KeyEvent e) { ... }
 * });
 * 
*

* with *

*
 * addKeyListener(new UsefulKeyAdapter(new KeyAdapter() {
 *      public void keyPressed(KeyEvent e) { ... }
 *      public void keyReleased(KeyEvent e) { ... }
 * }));
 * 
*

* Don't use this for keyTyped() events. If you are interested in these * sorts of events, the default behaviour is probably what you actually * want. *

*

* See How to Write a Key Listener on Sun's web site for more details. *

*

Algorithm:

*

This algorithm queues up keyboard events as the system reports them, * and then uses a separate thread to handle the desired events, and * disregard the spurious events.

*

Testing has indicated that there is zero delay between the key released * event and the subsequent key pressed event when that pair are generated * spuriously, and that is the case in which they are ignored. The default * tolerance is 10 ms, as it is possible that the system will not always * generate spurious key events instantly, but that is still substantially * faster than a physical keyboard is able to perform multiple physical * actions.

*

This code uses an ArrayBlockingQueue so the thread will not spin while * waiting for input. Most events are handled immediately, and no event * will be delayed more than the specified maximum age (default 10 ms). * *

License

*

* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. *

*

* This program 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 General Public License for more details. *

*

* You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. *

*

* Copyright 2009-2010 Brian Ziman *

*

* Please contact me if you find flaws in this software, or have any other * comments or suggestions. *

*

Version History

* */ public class UsefulKeyAdapter implements KeyListener, Runnable { private final KeyListener gKeyListener; // if we queue up more than 100 events, we're screwed! private final BlockingQueue gQueue = new ArrayBlockingQueue(100); private final Thread gRunner; private boolean gRunning = true; private int gMaxAge; // default 10 ms /** * Creates a KeyListener implementation that delegates event handling * to the specified KeyListener, after filtering out spurious key pressed * and key released events. In a sequence of events, events are considered * spurious if a key pressed event immediately follows a key released event, * for the same key, and within the specified maximum age. The * default maximum age is 10 ms. */ public UsefulKeyAdapter(KeyListener keyListener) { this(keyListener, 10); } /** *

The run loop processes events as they are queued up.

*

New algorithm:

*

All events get queued up in sequence.

*

* A separate thread pulls events off in a tight loop.

*

If the head event is a key released event:

* */ public void run() { while(gRunning) { try { KeyEvent ev = gQueue.take(); // block until event is available if(ev.getID() == KeyEvent.KEY_RELEASED) { KeyEvent ev2 = gQueue.poll(gMaxAge, TimeUnit.MILLISECONDS); if(!matches(ev, ev2)) { dispatchEvent(ev); dispatchEvent(ev2); // might be null, that's okay. } // otherwise suppress them both } else { dispatchEvent(ev); // no need to suppress anything else } } catch (InterruptedException ie) { // might be triggered when adapter is shut down. } } } /** * Creates a KeyListener implementation that delegates event handling * to the specified KeyListener, after filtering out spurious key pressed * and key released events. In a sequence of events, events are considered * spurious if a key pressed event immediately follows a key released event, * for the same key, and within the specified maximum age. The * default maximum age is 10 ms. */ public UsefulKeyAdapter(KeyListener keyListener, int maxAge) { this.gKeyListener = keyListener; this.gMaxAge = maxAge; final UsefulKeyAdapter adapter = this; gRunner = new Thread(this); gRunner.setName("UsefulKeyAdapter-Thread"); gRunner.setDaemon(true); gRunner.start(); } /** * Delegates the key pressed event. */ public void keyPressed(KeyEvent e) { gQueue.offer(e); } /** * Delegates the key released event. */ public void keyReleased(KeyEvent e) { gQueue.offer(e); } /** * Don't use the UsefulKeyAdapter for keyTyped events. */ public void keyTyped(KeyEvent e) { gKeyListener.keyTyped(e); } private void dispatchEvent(KeyEvent e) { if(e == null) return; // do nothing if(e.getID() == KeyEvent.KEY_PRESSED) gKeyListener.keyPressed(e); else if(e.getID() == KeyEvent.KEY_RELEASED) gKeyListener.keyReleased(e); } /** If two events represent the same key and within 10 ms */ private boolean matches(KeyEvent e1, KeyEvent e2) { if(e1 == e2) return true; if(e2 == null) return false; return (e1.getID() == KeyEvent.KEY_RELEASED) && (e2.getID() == KeyEvent.KEY_PRESSED) && e1.getKeyCode() == e2.getKeyCode() && e1.getKeyChar() == e2.getKeyChar() && e2.getWhen() <= e1.getWhen() + gMaxAge; } /** * Disable the background thread when the adapter goes out of scope. * Not guaranteed to be called, but it's polite to clean up resources. */ @Override protected void finalize() throws Throwable { gRunning = false; gRunner.interrupt(); super.finalize(); } }