lunes, 18 de enero de 2010

Problem with keyboard event handling in java, both with swing and swt

I will try to explain, with my bad english, a recent problem that I'm having with keyboard event handling in java using both swing and swt GUI libraries. Basically the problem is that there is not a way, both in swing and swt, of knowing the status of the keyboard keys. Basically the problem is that, while the user is still pressing the key (and never release it), keyrelease events are being throwed (!) and for that it is impossible to get the real keyrelease event. This problem only manifest in programs that need to use advance keyboard controlling; think for example in a piano simulator in which the user uses the keyboard keys as if it where a piano's keys. When the user presses a key a note turn on, and it is turned off only when the user releases it. The user can hold the key an arbitrary amount of time without release it and the note must be turned on in all that time.

I will present the problem with two simple GUI snippets, one for swing and other for swt, and I will compile and execute them both in windows and linux, using sun jdk version 1.6_13. The snippets are a window that listens to keypressed and keyreleased events and prints a message when the event notification is received. In all the tests, the user will press a key and will keep it pressed for some time (more than a second) and then release it. So the correct output for this "use case" would be only two messages "keyPressed" and
"keyReleased" in the output console. The only toolkit and OS combination that worked as spected was swt+linux. In the rest of the combinations, several "keyPressed" and
"keyReleased" are repeated while the user is keeping the key presssed.











SWT snippetSWING snippet

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class KeyPressTest extends org.eclipse.swt.widgets.Dialog {
    
    private Shell dialogShell;
    
    public KeyPressTest(Shell parent, int style) {
        super(parent, style);
    }
    
    public void open() {
        try {
            Shell parent = getParent();
            dialogShell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
            dialogShell.addKeyListener(new KeyListener() {
                @Override
                public void keyReleased(KeyEvent arg0) {
                    System.out.println("keyReleased");
                }
                @Override
                public void keyPressed(KeyEvent arg0) {
                    System.out.println("keyPressed");
                }
            });
            dialogShell.setLayout(new FormLayout());
            dialogShell.layout();
            dialogShell.pack();
            dialogShell.setLocation(getParent().toDisplay(100, 100));
            dialogShell.open();
            Display display = dialogShell.getDisplay();
            while (!dialogShell.isDisposed()) {
                if (!display.readAndDispatch())
                display.sleep();
            }
            } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        try {
            Display display = Display.getDefault();
            Shell shell = new Shell(display);
            KeyPressTest inst = new KeyPressTest(shell, SWT.NULL);
            inst.open();
            } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class KeyPressTest extends javax.swing.JFrame {
    public KeyPressTest() {
        super();
        initGUI();
    }
    private void initGUI() {
        try {
            setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            this.addKeyListener(new KeyListener() {
                @Override
                public void keyTyped(KeyEvent e) {
                }
                @Override
                public void keyReleased(KeyEvent e) {
                    System.out.println("keyReleased");
                }
                @Override
                public void keyPressed(KeyEvent e) {
                    System.out.println("keyPressed");
                }
            });
            pack();
            setSize(400, 300);
            } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                KeyPressTest inst = new KeyPressTest();
                inst.setLocationRelativeTo(null);
                inst.setVisible(true);
            }
        });
    }
}


And the results, for swing and swt in combination with windows and linux are:















windowslinux
swing

keyPressed
keyReleased
keyPressed
keyReleased
keyPressed
keyReleased
....
keyPressed
keyReleased

keyPressed
keyReleased
keyPressed
keyReleased
keyPressed
keyReleased
....
keyPressed
keyReleased
SWT

keyPressed
keyReleased
keyPressed
keyReleased
keyPressed
keyReleased
....
keyPressed
keyReleased

keyPressed
keyPressed
keyPressed
keyPressed
keyPressed
...
keyPressed
keyPressed
keyReleased


As I understand, THIS IS A BUG, because the behaviour does not apply the documentation. For example, this is the more relevant paragraph about keypressed and keyreleased events of swing (java.awt.event.KeyEvent javadoc):

"Key pressed" and "key released" events are lower-level and depend on the platform and keyboard layout. They are generated whenever a key is pressed or released, and are the only way to find out about keys that don't generate character input (e.g., action keys, modifier keys, etc.).

and this is not true because keyreleased events are generated without a key being released

Note that the only different situation is swt in linux. Imho it is still incorrect (because several keypressed events are fired but only one actually happens) but in this case we can we can know when the key is really releaed. In the other scenarios it is impossible.

IMHO, a cause of this problem coud be that swt and swing designers have modeled the keyboard event system thinking on text and not in other keyboard usage. In the case of swt that heavily relies on underlying plafform (gtk in linux, winapi in windows), I think the event stuff is a responsability of the uderlying platform and it is there where we note the defferencies.

So my question for java developers reading this is if there exists a way of being correctly notified when a keyrelease event occurs using swing or swt. Other question could be if there exists other library for java which handles this correctly because now, I havent figure out how to do this simple task in java ;(.

If you are a java programmer and want a workaround, I think the only one is the following. Since, while the key is pressed the keypressed event is fired regularly, the way we have to know that a key is released is when keypressed event is not firing anymore for that key. Note that this will be incorrect (because it is not really a keyrelease event) and can be expensive to implement because it because it require, for each key, a listener thread that triggers the fake keyrelease event when it detects no more keypressed. My proposal is the following keylistener (KeyListener2 inner class):


package org.sgx.javatests.keypress.swingAll;

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class SwingSollutionAllClasses {

/**
* this class is responsible for maintaining a key status map
* @author sebastian
*
*/
static class InputManager implements KeyListener {
//sgurin : þe last keypressed and keyreleased events registered for each key
Map lastKeyPressedEvents = new HashMap();
Map lastKeyReleasedEvents = new HashMap();
//one for each ascii character.
public boolean[] key_state_up = new boolean[256]; //true if pressed
public boolean[] key_state_down = new boolean[256]; //true if not pressed
//a string used as a buffer by widgets or other text input controls
private String keyCache = "";
public InputManager(){
for (int i = 0; i < key_state_up.length; i++) {
key_state_up[i]=true;
key_state_down[i]=false;
}
}
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if( code >= 0 && code < 256 ) {
key_state_down[code] = true;
key_state_up[code] = false;
lastKeyPressedEvents.put(code, e);
}
}
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
if( code >= 0 && code < 256 ) {
key_state_up[code] = true;
key_state_down[code] = false;
lastKeyReleasedEvents.put(code, e);
}
}
public void keyTyped(KeyEvent e) {
keyCache += e.getKeyChar();
}
public boolean isKeyDown( int key ) {
return key_state_down[key];
}
public boolean isKeyUp( int key ) {
return key_state_up[key];
}
}

/**
* this class is a new KeyListener adapter that corrects the default keyreleased
* notification policy (repeatedly notifies keypressed and keyreleased events
* when a key is pressed and not released for a while).
*
* note that this is a heavy listener, use only at special cases (when you need different behaviour for keyreleased event handling)
* it uses an InputManager that mantains the key status and starts a thread that checks each key status and notify changes
*
* @author sgurin
*
*/
static abstract class KeyListener2 implements KeyListener {

KeyChecker checkerThread ;
public KeyListener2(Component target) {
super();
checkerThread=new KeyChecker(this, target);
checkerThread.start();
}
public void destroy() {
checkerThread.stopChecking();
}

static class KeyChecker extends Thread implements KeyListener {
private static final long SLEEP_AMOUNT =50;
InputManager iman;
Component target;
volatile private boolean stoped;
public boolean[] last_key_state_up=null, last_key_state_down=null;
KeyListener2 listener;

public KeyChecker(KeyListener2 listener, Component target){
iman = new InputManager();
this.target = target;
this.listener = listener;
target.addKeyListener(iman);
target.addKeyListener(this);
}

@Override
public void run() {
last_key_state_down=cloneArray(iman.key_state_down);
last_key_state_up = cloneArray(iman.key_state_up);
while(!stoped) {
try {
Thread.sleep(SLEEP_AMOUNT);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < last_key_state_up.length; i++) {
if(last_key_state_up[i] && iman.key_state_down[i])
listener.keyPressed(iman.lastKeyPressedEvents.get(i));
if(last_key_state_down[i] && iman.key_state_up[i])
listener.keyReleased(iman.lastKeyReleasedEvents.get(i));
}
last_key_state_down=cloneArray(iman.key_state_down);
last_key_state_up = cloneArray(iman.key_state_up);
}
}
private boolean[] cloneArray(boolean[] a) {
if(a==null)
return null;
boolean [] r = new boolean[a.length];
for (int i = 0; i < r.length; i++)
r[i]=a[i];
return r;
}
public void stopChecking() {
stoped=true;
}
@Override
public void keyPressed(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
listener.keyTyped(e);
}
}
}

/**
* swing test
*/
static class KeyPressTest extends javax.swing.JFrame {
public KeyPressTest() {
super();
initGUI();
}
private void initGUI() {
try {
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
new KeyListener2(this) {
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased");
}
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed");
}
};
pack();
setSize(400, 300);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
KeyPressTest inst = new KeyPressTest();
inst.setLocationRelativeTo(null);
inst.setVisible(true);
}
});
}
}
public static void main(String[] args) {
KeyPressTest.main(args);
}
}



Related information I found:
http://stackoverflow.com/questions/1736828/how-to-stop-repeated-keypressed-keyreleased-events-in-swing
http://groups.google.com/group/comp.lang.java.gui/browse_thread/thread/e215f9729154511e
http://gpsnippets.blogspot.com/2008/03/keyboard-input-polling-system-in-java.html

7 comentarios:

Anónimo dijo...

Nice dispatch and this enter helped me alot in my college assignement. Thank you seeking your information.

Anónimo dijo...

Brim over I to but I contemplate the list inform should have more info then it has.

Anónimo dijo...
Este comentario ha sido eliminado por un administrador del blog.
Anónimo dijo...
Este comentario ha sido eliminado por un administrador del blog.
Anónimo dijo...
Este comentario ha sido eliminado por un administrador del blog.
Anónimo dijo...

It is time to become reasonable. It is time to come in itself.

Anónimo dijo...

Just wondering if eBay makes it possible for you to market [url=http://www.ticketchoice.com.au]concert tickets[/url] on the net? Do you know if you will find any restrictions depending on what country you're in?

My parents have just called me and asked if i could "get rid" of their two tickets to some concert as they wont have the ability to make it due to one more family event.

Besides asking close friends etc, i thought ebay would be an excellent place to market them.

But whats ebay's policy on marketing tickets? Ive heard alot about it around the news but ive forgotten what happened.

and if it matters, the concert is inside of this coming month

Thanks ahead of time for the advice.