How to know when a user has really released a key in Java?
Asked Answered
O

10

18

(Edited for clarity)

I want to detect when a user presses and releases a key in Java Swing, ignoring the keyboard auto repeat feature. I also would like a pure Java approach the works on Linux, Mac OS and Windows.

Requirements:

  1. When the user presses some key I want to know what key is that;
  2. When the user releases some key, I want to know what key is that;
  3. I want to ignore the system auto repeat options: I want to receive just one keypress event for each key press and just one key release event for each key release;
  4. If possible, I would use items 1 to 3 to know if the user is holding more than one key at a time (i.e, she hits 'a' and without releasing it, she hits "Enter").

The problem I'm facing in Java is that under Linux, when the user holds some key, there are many keyPress and keyRelease events being fired (because of the keyboard repeat feature).

I've tried some approaches with no success:

  1. Get the last time a key event occurred - in Linux, they seem to be zero for key repeat, however, in Mac OS they are not;
  2. Consider an event only if the current keyCode is different from the last one - this way the user can't hit twice the same key in a row;

Here is the basic (non working) part of code:

import java.awt.event.KeyListener;

public class Example implements KeyListener {

public void keyTyped(KeyEvent e) {
}

public void keyPressed(KeyEvent e) {
    System.out.println("KeyPressed: "+e.getKeyCode()+", ts="+e.getWhen());
}

public void keyReleased(KeyEvent e) {
    System.out.println("KeyReleased: "+e.getKeyCode()+", ts="+e.getWhen());
}

}

When a user holds a key (i.e, 'p') the system shows:

KeyPressed:  80, ts=1253637271673
KeyReleased: 80, ts=1253637271923
KeyPressed:  80, ts=1253637271923
KeyReleased: 80, ts=1253637271956
KeyPressed:  80, ts=1253637271956
KeyReleased: 80, ts=1253637271990
KeyPressed:  80, ts=1253637271990
KeyReleased: 80, ts=1253637272023
KeyPressed:  80, ts=1253637272023
...

At least under Linux, the JVM keeps resending all the key events when a key is being hold. To make things more difficult, on my system (Kubuntu 9.04 Core 2 Duo) the timestamps keep changing. The JVM sends a key new release and new key press with the same timestamp. This makes it hard to know when a key is really released.

Any ideas?

Thanks

Ovarian answered 21/9, 2009 at 21:48 Comment(2)
Do you really get multiple keyReleased events due to auto-repeat? For me only keyPressed gets repeated, not keyReleased. I have used this quite a few times in game applets with no problems.Fdic
Have you tried in KDE? I've tested it under Debian Squeeze and under Kubuntu 9.04. It seems that X (or KDE) really sends many events when you hold down some keys (but not all of them - Shift, Ctrl and Alt don't repeat). This can be seen by running "xev" on the console.Ovarian
E
5

This could be problematic. I can't remember for sure (it's been a long time), but it's likely the repeating-key feature (which is handled by the underlying operating system, not Java) isn't providing enough information for the JVM developer to distinguish those additional key events from the 'real' one. (I worked on this in the OS/2 AWT back in 1.1.x by the way).

From the javadoc for KeyEvent:

"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.). The key being pressed or released is indicated by the getKeyCode method, which returns a virtual key code.

As I recall from doing this in OS/2 (which at the time still had only the 2-event up/down flavor of keyboard handling like older versions of Windows, not the 3-event up/down/char flavor you get in more modern versions), I didn't report KeyReleased events any differently if the key was just being held down and the events auto-generated; but I suspect OS/2 didn't even report that information to me (can't remember for sure). We used the Windows reference JVM from Sun as our guide for developing our AWT - so I suspect if it were possible to report this information there, I'd have at least seen it on their end.

Eisele answered 22/9, 2009 at 14:17 Comment(0)
D
4

This question is duplicated here.

In that question, a link to the Sun bug parade is given, where some workaround is suggested.

I've made a hack implemented as an AWTEventListener that can be installed at the start of the application.

Basically, observe that the time between the RELEASED and the subsequent PRESSED is small - actually, it is 0 millis. Thus, you can use that as a measure: Hold the RELEASED for some time, and if a new PRESSED comes right after, then swallow the RELEASED and just handle the PRESSED (And thus you will get the same logic as on Windows, which obviously is the correct way). However, watch for the wrap-over from one millisecond to the next (I've seen this happen) - so use at least 1 ms to check. To account for lags and whatnots, some 20-30 milliseconds probably won't hurt.

Deliberation answered 4/5, 2010 at 9:57 Comment(0)
E
3

I've refined stolsvik hack to prevent repeating of KEY_PRESSED and KEY_TYPED events as well, with this refinement it works correctly under Win7 (should work everywhere as it truly watches out for KEY_PRESSED/KEY_TYPED/KEY_RELEASED events).

Cheers! Jakub

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.HashSet;
import java.util.Map;
import java.util.Set;

import javax.swing.Timer;

/**
 * This {@link AWTEventListener} tries to work around for KEY_PRESSED / KEY_TYPED/     KEY_RELEASED repeaters.
 * 
 * If you wish to obtain only one pressed / typed / released, no repeatings (i.e., when the button is hold for a long time).
 * Use new RepeatingKeyEventsFixer().install() as a first line in main() method.
 * 
 * Based on xxx
 * Which was done by Endre Stølsvik and inspired by xxx (hyperlinks stipped out due to stackoverflow policies)
 * 
 * Refined by Jakub Gemrot not only to fix KEY_RELEASED events but also KEY_PRESSED and KEY_TYPED repeatings. Tested under Win7.
 * 
 * If you wish to test the class, just uncomment all System.out.println(...)s.
 * 
 * @author Endre Stølsvik
 * @author Jakub Gemrot
 */
public class RepeatingKeyEventsFixer implements AWTEventListener {

 public static final int RELEASED_LAG_MILLIS = 5;

 private static boolean assertEDT() {
  if (!EventQueue.isDispatchThread()) {
   throw new AssertionError("Not EDT, but [" + Thread.currentThread() + "].");
  }
  return true;
 }

 private Map<Integer, ReleasedAction> _releasedMap = new HashMap<Integer, ReleasedAction>();
 private Set<Integer> _pressed = new HashSet<Integer>();
 private Set<Character> _typed = new HashSet<Character>();

 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) {
   //System.out.println("REPOSTED: " + ((KeyEvent)event).getKeyChar());
   // -> Yes, so we shalln't process it again.
   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;
  }

  // ?: KEY_TYPED event? (We're only interested in KEY_PRESSED and
  // KEY_RELEASED).
  if (event.getID() == KeyEvent.KEY_TYPED) {
   if (_typed.contains(keyEvent.getKeyChar())) {
    // we're being retyped -> prevent!
    //System.out.println("TYPED: " + keyEvent.getKeyChar() + " (CONSUMED)");
    keyEvent.consume();  
   } else {
    // -> Yes, TYPED, for a first time
    //System.out.println("TYPED: " + keyEvent.getKeyChar());
    _typed.add(keyEvent.getKeyChar());
   }
   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(RELEASED_LAG_MILLIS, null);
   ReleasedAction action = new ReleasedAction(keyEvent, timer);
   timer.addActionListener(action);
   timer.start();

   ReleasedAction oldAction = (ReleasedAction)_releasedMap.put(Integer.valueOf(keyEvent.getKeyCode()), action);
   if (oldAction != null) oldAction.cancel();

   // Consume the original
   keyEvent.consume();
   //System.out.println("RELEASED: " + keyEvent.getKeyChar() + " (CONSUMED)");
   return;
  }

  if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {

   if (_pressed.contains(keyEvent.getKeyCode())) {
    // we're still being pressed
    //System.out.println("PRESSED: " + keyEvent.getKeyChar() + " (CONSUMED)"); 
    keyEvent.consume();
   } else {   
    // Remember that this is single threaded (EDT), so we can't have
    // races.
    ReleasedAction action = (ReleasedAction) _releasedMap.get(keyEvent.getKeyCode());
    // ?: Do we have a corresponding RELEASED waiting?
    if (action != null) {
     // -> Yes, so dump it
     action.cancel();

    }
    _pressed.add(keyEvent.getKeyCode());
    //System.out.println("PRESSED: " + keyEvent.getKeyChar());    
   }

   return;
  }

  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).
  */
 protected 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;
   _releasedMap.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;
   }
   //System.out.println("REPOST RELEASE: " + _originalKeyEvent.getKeyChar());
   // 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.
   _pressed.remove(_originalKeyEvent.getKeyCode());
   _typed.remove(_originalKeyEvent.getKeyChar());
   Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(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);
  }
 }

}
Elsi answered 13/1, 2011 at 10:48 Comment(1)
Please see: #8948738 - a low rep user was unable to comment on this answer. Would you care to have a look?Stimulus
F
2

I have found a solution to this problem without relying on timing (which according to some users is not necessarily consistent 100% of the time) but instead by issuing extra key presses to override the key repeat.

To see what I mean, try holding a key and then hitting another mid-stream. The repeat will stop. It seems that, on my system at least, the key hits issued by Robot also have this effect.

For an example implementation, tested in Windows 7 & Ubuntu, see:

http://elionline.co.uk/blog/2012/07/12/ignore-key-repeats-in-java-swing-independently-of-platform/

Also, thanks to Endre Stolsvik's solution for showing me how to do a global event listener! Appreciated.

Fillagree answered 17/7, 2012 at 22:14 Comment(3)
Thanks. Sounds like a great solution. Any way to get your code besides trying to copy and paste from the syntax highlighter? It copies all kinds of garbage white space that is giving me errors in Eclipse.Carrera
It was a bad whitespace character, but I did a find replace and it works. Thanks.Carrera
Sorry about that, Ralph. There is actually a version under source control on sourceforge somewhere, I'll try to remember to link to it on the blog... Glad to hear it worked, you're the first to try it other than myself, afaik :).Fillagree
E
1

Save the timestamp of the event (arg0.when()) in keyReleased. If the next keyPressed event is for the same key and has the same timestamp, it is an autorepeat.

If you hold down multiple keys, X11 only autorepeats the last key pressed. So, if you hold down 'a' and 'd' you'll see something like:

a down
a up
a down
d down
d up
d down
d up
a up
Evangelize answered 22/9, 2009 at 14:34 Comment(0)
B
1

I've found a solution that does without waiting in case you have something like a game loop going. The idea is storing the release events. Then you can check against them both inside the game loop and inside the key pressed handler. By "(un)register a key" I mean the extracted true press/release events that should be processed by the application. Take care of synchronization when doing the following!

  • on release events: store the event per key; otherwise do nothing!
  • on press events: if there is no stored release event, this is a new press -> register it; if there is a stored event within 5 ms, this is an auto-repeat -> remove its release event; otherwise we have a stored release event that hasn't been cleared by the game loop, yet -> (fast user) do as you like, e.g. unregister-register
  • in your loop: check the stored release events and treat those that are older than 5 ms as true releases; unregister them; handle all registered keys
Baldachin answered 5/2, 2011 at 22:48 Comment(0)
H
0

Well, you said that it is possible that the time between key events in case of key repeat be non-negative. Even so, it is likely very short. You could then threshold this time to some very small value, and everything equal or lower than it be considered a key repeat.

Hicks answered 21/9, 2009 at 21:54 Comment(1)
I didn't want to rely on specific timings to do this, because of portability. Anyway, I will test again to see if it will be possible to isolate a real keyRelease event from the fake ones by measuring the time between events.Ovarian
V
0

You might want to use the action map of the component you are interested in. Here's an example that deals with a specific key (SPACE BAR) but I'm sure that if you read the documentation you may be able to modify it to handle generic key presses and releases.

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;

import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

public class Main {
    public static void main(String[] args) {
        JFrame f = new JFrame("Test");
        JPanel c = new JPanel();

        c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
                KeyStroke.getKeyStroke("SPACE"), "pressed");
        c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
                KeyStroke.getKeyStroke("released SPACE"), "released");
        c.getActionMap().put("pressed", new Action() {
            public void addPropertyChangeListener(
                    PropertyChangeListener listener) {
            }

            public Object getValue(String key) {
                return null;
            }

            public boolean isEnabled() {
                return true;
            }

            public void putValue(String key, Object value) {
            }

            public void removePropertyChangeListener(
                    PropertyChangeListener listener) {
            }

            public void setEnabled(boolean b) {
            }

            public void actionPerformed(ActionEvent e) {
                System.out.println("Pressed space at "+System.nanoTime());
            }
        });
        c.getActionMap().put("released", new Action() {
            public void addPropertyChangeListener(
                    PropertyChangeListener listener) {
            }

            public Object getValue(String key) {
                return null;
            }

            public boolean isEnabled() {
                return true;
            }

            public void putValue(String key, Object value) {
            }

            public void removePropertyChangeListener(
                    PropertyChangeListener listener) {
            }

            public void setEnabled(boolean b) {
            }

            public void actionPerformed(ActionEvent e) {
                System.out.println("Released space at "+System.nanoTime());
            }
        });
        c.setPreferredSize(new Dimension(200,200));


        f.getContentPane().add(c);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.pack();
        f.setVisible(true);
    }
}
Vendee answered 22/9, 2009 at 17:8 Comment(1)
Thanks for the attempt. But here I get the same results: lots of repetitions, but this time different time stamps for each code. Released space at 7623492422327 Pressed space at 7623492546365 Released space at 7623525621728 Pressed space at 7623525754217 Released space at 7623559107407 Pressed space at 7623559228023 Released space at 7623591715040 Pressed space at 7623591835237Ovarian
C
0

This approach stores key presses in a HashMap, resetting them when the key is released. Most of the code is courtesy of Elist in this post.

import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class KeyboardInput2 {
    private static HashMap<Integer, Boolean> pressed = new HashMap<Integer, Boolean>();
    public static boolean isPressed(int key) {
        synchronized (KeyboardInput2.class) {
            return pressed.get(key);
        }
    }

    public static void allPressed() {
        final Set<Integer> templist = pressed.keySet();
        if (templist.size() > 0) {
            System.out.println("Key(s) logged: ");
        }
        for (int key : templist) {
            System.out.println(KeyEvent.getKeyText(key));
        }
    }

    public static void main(String[] args) {
        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {

            @Override
            public boolean dispatchKeyEvent(KeyEvent ke) {
                synchronized (KeyboardInput2.class) {
                    switch (ke.getID()) {
                        case KeyEvent.KEY_PRESSED:
                            pressed.put(ke.getKeyCode(), true);
                            break;
                        case KeyEvent.KEY_RELEASED:
                            pressed.remove(ke.getKeyCode());
                            break;
                        }
                        return false;
                }
            }
        });
    }
}

You can use the HashMap to check if a certain key is pressed, or call KeyboardInput2.allPressed() to print every pressed key.

Cavern answered 20/10, 2013 at 17:16 Comment(0)
M
0

What am I not getting about all the elaborate but questionable suggestions? The solution is so simple! (Overlooked the key part of OP's question: "under Linux, when the user holds some key, there are many keyPress and keyRelease events being fired")

In your keyPress event, check if the keyCode is already in a Set<Integer>. If it is, it must be an autorepeat event. If it is not, put it in and digest it. In your keyRelease event, blindly remove the keyCode from the Set - assuming that OP's statement about many keyRelease events is false. On Windows, I only get several keyPresses, but only one keyRelease.

To abstract this a little, you could create a wrapper that can carry KeyEvents, MouseEvents, and MouseWheelEvents and has a flag that already says that the keyPress is just an autorepeat.

Mendel answered 1/11, 2014 at 12:59 Comment(2)
I'm getting both keyPress and KeyRelease multiple times when I press and hold a key.Ovarian
Oh, ok, I should have read your post more closely. Now I get the elaborate timing approaches. You would have to check if a key that got released within the last 10ms just got pressed again, because that is certainly beyond the ability of the user. You would have to add each key release incl. its time to a Set and check (and clean up) that Set on keypress. Seems simple enough.Mendel

© 2022 - 2024 — McMap. All rights reserved.