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); } }

14 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. Bạn đang tìm siêu thị điện máy cũ ở tphcm. Bạn đang tìm nơi bán máy lạnh cũ hcm? Nếu bạn chưa tìm thấy nơi đó thì hãy đến với chúng tôi. Shop thằng ghờm chúng tôi chuyên cung cấp các loại mặt hàng điện máy cũ như bán máy giặt cũ tphcm, bán tủ lạnh cũ giá rẻ tphcm,... Nếu bạn cần hãy liên hệ thằng ghờm shop nhé. Ngoài ra để tìm hiểu những từ như atsm nghĩa là gì thì ghé vào ngôi nhà kiến thức.

    ReplyDelete
  9. 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
  10. cách làm mặt nạ tự nhiên tại nhà Hiện nay vấn đề bữa ăn giấc ngủ đang rất được quan tâm chăm sóc kĩ lưỡng. Tình trạng mất ngủ là căn bệnh phổ biến hiện nay.
    đi biển nên dùng kem chống nắng loại nàoTáo có thành phần dinh dưỡng rất phong phú, đặc biệt là các loại vi chất,sinh tố và axit hoa quả.
    phuong phap lam trang da don gian nhatTrong đậu xanh có chứa nhiều vitamin B6, Loại vitamin này sản xuất ra melatonin một loại hormone có liên quan mật thiết đến giấc ngủ.
    bí quyết giảm cân cho người khó giảm cânChuối có chứa nhiều chất vitamin B6 giúp tế bào thần kinh khoẻ mạnh. Nên ăn 2-3 quả chuối mỗi ngày sẽ tốt cho sức khoẻ và giúp bạn an thần dể ngủ.
    các món ăn chữa bệnh mất ngủCơ thể bị rối loạn đường huyết cũng thường xuyên buồn ngủ, mãnh liệt nhất là sau bữa ăn.
    cách trị sẹo thâm bằng dầu dừaCách làm rất đơn giản, mỗi tối trước khi đi ngủ bạn rữa mặt thật sạch, dùng dầu dừa thoa lên những vùng da bị sẹo do mụn.
    Cách làm tinh dầu bưởi trị rụng tócCái răng cái tóc là gốc con người, nhưng do một số nguyên nhân khiến tóc bạn bị rụng. Có thể do cơ địa của bạn không tốt, hoặc là do chế độ dinh dưỡng bạn không phù hợp, nhưng do nguyên nhân gì đi nữa thì điều này chẳng tốt chút nào.
    Cách trị nám da từ thiên nhiênCó nhiều nguyên nhân khiến da chúng ta bị nám, có thể do bẩm sinh và cũng có thể do những tác động bên ngoài từ cuộc sống.
    Cách trị sẹo lõm bằng phương pháp tự nhiênNha đam là loại thảo mộc không chỉ tốt cho da, mà còn có khả năng làm lành da nên rất hữu hiệu với những vùng da bị sẹo lõm do mụn, viêm.
    Cách làm trắng da tự nhiên cấp tốcSử dung bơ để làm đẹp rất hiệu quả, bơ giúp tái tạo và giữ ẩm, giúp da không bị khô, có tác dụng làm sáng và tăng khả năng đàn hồi của da
    các loại thực phẩm tốt cho mắtTheo các chuyên gia, trong cà chua có chứa nhiều carotenoid và lycopene giúp ngăn ngừa những tác nhân làm ảnh hưởng đến võng mạc và khu vực khác của mắt.

    ReplyDelete
  11. phim đao hạ lưu tình tvb khi hắn phát hiện ra gian thần,chàng trai đáng thương và điều đó sẽ chìm vào lãng quên phim ngọa hổ tàng long 2Cô chia sẻ: “Ban đầu tôi vốn tham gia casting một vai khác hồi đầu năm 2014 phim công công xuất cungNội dung phim nói về cuộc đời của những vị thái giám sau khi triều đại phong kiến sụp đổ hoàn toàn xem phim Naruto Shippuuden Mười hai năm trước thời điểm loạt truyện bắt đầu, quái thú Cửu Vĩ Hồ Ly tấn công làng Lá, hủy hoại phần lớn ngôi làng và cướp đi biết bao nhiêu sinh mạng phim hạnh phúc muộn màng hứa hẹn sẽ là một kiệt tác điện ảnh nữa của Ấn Độ sẽ lên sóng THVL1. xem phim online phim thả thí thiên hạcâu chuyện nói về Bạch Phong Tình là cô gái xinh đẹp và tấm lòng yêu thương bá tính hậu duệ của mặt trờido đài truyền hình KBS sản xuất. Đây là sự hợp tác đầu tiên của cô với nữ biên kịch tài năng Kim Eun-sook.
    hạt điều rang muối có tác dụng gìCòn các hạt điều nhập khẩu không có được vị ngon như vậy. lợi ích khi mua hàngDo vậy, sản phẩm luôn giữ được màu sắc sáng đẹp và hương vị tự nhiên của nguyên liệu ban đầu. hạt điều rang muối bình phướcKhoáng chất đồng có trong hạt điều cung cấp sự linh hoạt cho xương, khớp và mạch máu.

    ReplyDelete
  12. 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