]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.debug.graphical/src/org/simantics/debug/graphical/UsefulKeyAdapter.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.debug.graphical / src / org / simantics / debug / graphical / UsefulKeyAdapter.java
diff --git a/bundles/org.simantics.debug.graphical/src/org/simantics/debug/graphical/UsefulKeyAdapter.java b/bundles/org.simantics.debug.graphical/src/org/simantics/debug/graphical/UsefulKeyAdapter.java
new file mode 100644 (file)
index 0000000..39944ea
--- /dev/null
@@ -0,0 +1,248 @@
+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;
+
+/**
+ * <p>
+ * This class wraps your KeyAdapter and suppresses spurious repeated events.
+ * </p>
+ * <p>
+ * Written by <a href="http://www.brianziman.com/">Brian Ziman</a> and available
+ * for download and comments at <a href="http://www.brianziman.com/r/post/stuff-200910092100">http://www.brianziman.com/r/post/stuff-200910092100</a>.
+ * </p>
+ * <h1>Rationale</h1>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * 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:
+ * </p>
+ * <ul>
+ * <li><a href="http://bugs.sun.com/view_bug.do?bug_id=4153069">http://bugs.sun.com/view_bug.do?bug_id=4153069</a></li>
+ * <li><a href="http://forums.sun.com/thread.jspa?threadID=5382946">http://forums.sun.com/thread.jspa?threadID=5382946</a></li>
+ * <li><a href="http://stackoverflow.com/questions/1457071/how-to-know-when-a-user-has-really-released-a-key-in-java">http://stackoverflow.com/questions/1457071/how-to-know-when-a-user-has-really-released-a-key-in-java</a></li>
+ * </ul>
+ * <p>
+ *
+ * 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.
+ * </p>
+ * <h1>Compatibility</h1>
+ * <p>
+ * 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.
+ * </p>
+ * <h1>Usage</h1>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * Replace:
+ * </p>
+ * <pre>
+ * addKeyListener(new KeyAdapter() {
+ *      public void keyPressed(KeyEvent e) { ... }
+ *      public void keyReleased(KeyEvent e) { ... }
+ * });
+ * </pre>
+ * <p>
+ * with
+ * </p>
+ * <pre>
+ * addKeyListener(new UsefulKeyAdapter(new KeyAdapter() {
+ *      public void keyPressed(KeyEvent e) { ... }
+ *      public void keyReleased(KeyEvent e) { ... }
+ * }));
+ * </pre>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * See <a href="http://java.sun.com/docs/books/tutorial/uiswing/events/keylistener.html">How to Write a Key Listener</a> on Sun's web site for more details.
+ * </p>
+ * <h1>Algorithm:</h1>
+ * <p>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.</p>
+ * <p>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.</p>
+ * <p>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).
+ * </ul>
+ * <h1>License</h1>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
+ * </p>
+ * <p>
+ * Copyright 2009-2010 <a href="http://www.brianziman.com/">Brian Ziman</a>
+ * </p>
+ * <p>
+ * Please contact me if you find flaws in this software, or have any other
+ * comments or suggestions.
+ * </p>
+ * <h1>Version History</h1>
+ * <ul>
+ * <li>1/4/2010 - Reworked algorithm to eliminate spinning by using java.util.concurrent framework, and to improve reliability.
+ * <li>10/9/2009 - Initial Revision</li>
+ * </ul>
+ */
+public class UsefulKeyAdapter implements KeyListener, Runnable {
+
+    private final KeyListener gKeyListener;
+
+    // if we queue up more than 100 events, we're screwed!
+    private final BlockingQueue<KeyEvent> gQueue = new ArrayBlockingQueue<KeyEvent>(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);
+    }
+
+    /**
+     * <p>The run loop processes events as they are queued up.</p>
+     * <p>New algorithm:</p>
+     * <p>All events get queued up in sequence.</p>
+     * <p>
+     * A separate thread pulls events off in a tight loop.</p>
+     * <p>If the head event is a key released event:</p>
+     * <ul><li>pull another event (waiting up to 10 ms for one to appear)</li>
+     *     <li>if the next event is a key pressed, suppress both,</li>
+     *     <li>otherwise dispatch both.</li>
+     * </ul>
+     */
+    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();
+    }
+
+}