Wednesday, May 5, 2010

Linux Java Repeats RELEASED KeyEvents

In Java on Linux, there is a 12 year old bug in handling of keyboard auto-repeat. As on Windows, both KEY_PRESSED and KEY_TYPED repeats, but on Linux, also KEY_RELEASED repeats, while on Windows the released-event comes only when the user releases the key.

This class can be installed as an AWTEventListener, and will seemingly fix this. Note that the class can be installed on both Windows and Linux - it won't affect already correct behavior.

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.Map;

import javax.swing.Timer;

/**
 * This {@link AWTEventListener} tries to work around a 12 yo
 * bug in the Linux KeyEvent handling for keyboard repeat. Linux apparently implements repeating keypresses by
 * repeating both the {@link KeyEvent#KEY_PRESSED} and {@link KeyEvent#KEY_RELEASED}, while on Windows, one only
 * gets repeating PRESSES, and then a final RELEASE when the key is released. The Windows way is obviously much more
 * useful, as one then can easily distinguish between a user holding a key pressed, and a user hammering away on the
 * key.
 * 
 * This class is an {@link AWTEventListener} that should be installed as the application's first ever
 * {@link AWTEventListener} using the following code, but it is simpler to invoke {@link #install() install(new
 * instance)}:
 * 
 * 
 * Toolkit.getDefaultToolkit().addAWTEventListener(new {@link RepeatingReleasedEventsFixer}, AWTEvent.KEY_EVENT_MASK);
 * 
* * Remember to remove it and any other installed {@link AWTEventListener} if your application have some "reboot" * functionality that can potentially install it again - or else you'll end up with multiple instances, which isn't too * hot. * * Notice: Read up on the {@link Reposted} interface if you have other AWTEventListeners that resends KeyEvents * (as this one does) - or else we'll get the event back. *

Mode of operation

* The class makes use of the fact that the subsequent PRESSED event comes right after the RELEASED event - one thus * have a sequence like this: * *
 * PRESSED 
 * -wait between key repeats-
 * RELEASED
 * PRESSED 
 * -wait between key repeats-
 * RELEASED
 * PRESSED
 * etc.
 * 
* * A timer is started when receiving a RELEASED event, and if a PRESSED comes soon afterwards, the RELEASED is dropped * (consumed) - while if the timer times out, the event is reposted and thus becomes the final, wanted RELEASED that * denotes that the key actually was released. * * Inspired by http://www.arco.in-berlin.de/keyevent.html * * @author Endre Stølsvik */ public class RepeatingReleasedEventsFixer implements AWTEventListener { private final Map _map = new HashMap(); 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) { // -> Yes, so we shalln't process it again. return; } // ?: KEY_TYPED event? (We're only interested in KEY_PRESSED and KEY_RELEASED). if (event.getID() == KeyEvent.KEY_TYPED) { // -> Yes, TYPED, don't process. 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; } // ?: 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(2, null); ReleasedAction action = new ReleasedAction(keyEvent, timer); timer.addActionListener(action); timer.start(); _map.put(Integer.valueOf(keyEvent.getKeyCode()), action); // Consume the original keyEvent.consume(); } else if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { // Remember that this is single threaded (EDT), so we can't have races. ReleasedAction action = _map.remove(Integer.valueOf(keyEvent.getKeyCode())); // ?: Do we have a corresponding RELEASED waiting? if (action != null) { // -> Yes, so dump it action.cancel(); } // System.out.println("PRESSED: [" + keyEvent + "]"); } else { 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). */ private 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; _map.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; } // 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. Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(newEvent); // System.out.println("Posted synthetic RELEASED [" + 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); } } private static boolean assertEDT() { if (!EventQueue.isDispatchThread()) { throw new AssertionError("Not EDT, but [" + Thread.currentThread() + "]."); } return true; } } package com.example; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.WindowConstants; /** * Tester for {@link RepeatingReleasedEventsFixer}. * * @author Endre Stølsvik */ public class XRepeatingReleasedEventsFixer { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { startGUI(); } }); } private static void startGUI() { new RepeatingReleasedEventsFixer().install(); JFrame frame = new JFrame("TestFrame"); JPanel main = new JPanel(new FlowLayout()); JButton listenedButton = new JButton("Have KeyListener"); main.add(listenedButton); listenedButton.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { System.out.println("keyPressed: [" + e + "]."); } @Override public void keyTyped(KeyEvent e) { System.out.println("keyTyped: [" + e + "]."); } @Override public void keyReleased(KeyEvent e) { System.out.println("keyReleased: [" + e + "]."); } }); main.add(new JButton("No Listeners")); main.add(new JLabel("Try arrows, Ctrl, and chars,")); main.add(new JLabel("as well as multiple at once.")); frame.add(main); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setSize(260, 140); frame.setVisible(true); } }

11 comments:

  1. Thank you very much for this wonderful fix! It is a great drop in fix for a problem that could have cost me a whole evening!

    I made a small change though, I changed install() to a static that instantiates and installs the fix if it hasn't been installed yet.


    private static RepeatingReleasedEventsFixer installed;

    public static void install() {
    if (installed == null) {
    installed = new RepeatingReleasedEventsFixer();
    Toolkit.getDefaultToolkit().addAWTEventListener(installed, AWTEvent.KEY_EVENT_MASK);
    }
    }

    public static void remove() {
    if (installed != null) {
    Toolkit.getDefaultToolkit().removeAWTEventListener(installed);
    }
    }
    I guess one could turn it into a proper Singleton by adding a private constructor to it.

    Some questions though:

    1) Why did you use HashMap not TreeMap? Wouldn't a hashmap be slower due to calculating the hashes?

    2) Why did you create the Map without specifying the type? E.g. I changed it to Map I wonder what you think about that.

    Thanks again, Barend Scholtus

    ReplyDelete
  2. Sorry I meant Map in my second question.

    ReplyDelete
  3. AND the remove() method needs to set installed=null; *sigh* sorry.

    ReplyDelete
  4. Thanks a lot Emre. You just saved me a lot of work too.

    Although I really think you should use types in the Map declaration:

    private final Map<Integer,ReleasedAction> _map = new HashMap<Integer,ReleasedAction>();
    instead of

    private final Map _map = new HashMap();

    Else it won't compile in my end. (Maybe it just gets filtered out because of html-escaping?)

    Anyway with the above fix it works as specified and it would have taken me a great deal of time to figure it out for myself and it probably wouldn't have been just as sophisticated :)
    So thanks again.

    Sune

    ReplyDelete
  5. Thanks this really saved me alot of time!

    ReplyDelete
  6. Hi, I implemented this piece of code in my engine (even if I had to modify some things to make it work), does it bother if I quote you on the comments of my source? Because, well, you're the maker after all, I just made some modification but the original fix is yours :)

    ReplyDelete
  7. @Antonio: I would love to credited! What modifications did you need to do?
    @Sune: Yes, it is typed, it is only your browser that kills the angles. Or, more correctly, me that haven't escaped them. Should do that sometime soon now, really. Hmm.

    ReplyDelete
    Replies
    1. Hi Endre Stølsvik, Sorry for the VERY late response, but in the end, I cancelled the project :(, but I'm working on other things :)
      I don't think I'm able to explain modifications I did with a post, it could be simplier for me to show you, but I don't know if I can post the entire source code.

      Delete
  8. Có thể bạn chưa biết về : Tin tức về Chăm sóc vùng kín
    Khám phá nhiều điều bất ngờ và thú vị tại : Tin tức về Kem làm hồng vùng kín
    Vấn đề nhủ hoa bị thâm đen đã không còn đáng lo ngại.Đã có : Tin tức về Kem làm hồng nhủ hoa
    Tin tức về Kem làm trắng vùng kín
    Chuyên mục chăm sóc sức khoẻ phòng the xin giới thiệu : Tin tức về Kem trị thâm vùng kín
    HOT HOT HOT : Tin tức về Se khít âm đạo
    xem thêm : Tin tức về kem tẩy lông vùng kín
    làm đẹp : Tin tức về Trang điểm mặt
    Hàng hiệu giá rẻ : Tin tức về Kem bb cream
    kem cc cream là gì ? Tin tức về Kem cc cream
    Để có đôi môi quyến rủ và gợi cảm thì vào xem : Tin tức về Son dưỡng môi
    Vấn đề tẩy trang đã có : Tin tức về Nước hoa hồng
    xem thêm : Tin tức về Mỹ phẩm cho nam

    ReplyDelete
  9. Nhiều du khách có nhu cầumua đồ cũ tại nhật bản để tiết kiệm được nhiều chi phí mua sắm lời chúc ngày 8 3 hay ý nghĩa dành cho người mẹ, người cô, người chị. Bên cạnh đó những lời chúc ngày 8 tháng 3 hài hước luôn là điều thú vị dành cho nhiều chị em phụ nữ. Tuy nhiên, những món quà tặng luôn quan trọng. Vậy 8-3 tặng quà gì cho người yêutin nhắn 8/3 hay cho người yêu.
    Quý khách có nhu cầu ship hàng từ trung quốc về tphcm hãy liên hệ ngay đến với chúng tôi. Chúng tôi nhận mua hàng sỉ từ trung quốcmua hàng ship hàng từ mỹ để quý khách có thể kinh doang những mặt hàng mà mình ưa thích. Fan page của chúng tôi quý khách có thể truy cập và liện hệ trực tiếp tại: Vận chuyển hàng Trung Quốc facebook - mua hàng trên amazon ship về việt nam

    ReplyDelete
  10. Thanks for sharing.
    Nếu cần chành xe phú quốc thì a/c liên hệ bên mình nhé.
    Hotline: 0903751981

    ReplyDelete