REQUEST: Portable key handler

Hi,

On linux, key management in games is not easy due to a bug (or is it a linux feature?): http://developer.java.sun.com/developer/bugParade/bugs/4153069.html

On linux, the keyReleased() event is fired, when the user holds a key down, even if the key is not released. This makes it difficult to determine when a key is actually released.
On Windows, the keyReleased event is sent only at the end of the keyPressed() / keyTyped() sequence, as documented.

I think it would be good to have a bit of portable code to handle both configurations. Who has already had troubles with this situation?

This apparently has to do with how linux handles repeating keys… there is already a bug filed (don’t feel like searching for the #)… This inconsistency which basically amounts to false information is a major pain in the butt…

LWJGL seems to handle keys properly :wink:

Cas :slight_smile:

Hi,

@swpalmer,
Yes, the bug numbers in Sun Database are 4153069 and 4839127. And Sun does not know how to solve it easily…

@princec
Can you confirm that LWJGL handles the sequence of events correctly, on both platforms Windows and linux (the bug is visible on linux; Windows is correct according to the doc). Press and hold a key and look at the KeyEvent generated when the typomatic key-repeat runs.

On Windows you get:
pressed
typed
pressed
typed
pressed
typed
pressed
typed
pressed
typed
released

On linux, you get:
pressed
typed
released
pressed
typed
released
pressed
typed
released
pressed
typed
released
pressed
typed
released

You can use the code from http://developer.java.sun.com/developer/bugParade/bugs/4839127.html

If you confirm that it works in LWJGL, I’ll look at the code. But I want a Java-only solution to use on a J2ME platform…

We don’t use Java events in LWJGL, and we do indeed see correct key behaviour. But you won’t see it on any devices in the near future because we’ve got no OpenGL for them yet.

Not sure why you’re worried about Linux if you’re doing J2ME work…?

Cas :slight_smile:

Hi,

Because the Sharp Zaurus is running linux ;D. And the bug occurs with Esmertec/Insignia Jeode and Sun Personal Profile JVMs.

It makes me wonder if there are people writing games on linux workstations in Java ???

Notice that even the Windows implementation is technically not reporting what is really happening… there should only be one ‘pressed’, one or more ‘typed’, then one ‘release’.

At least I can’t imagine any other reasonable sequence. I find it mind boggling that something like this remains unfixed after so long. It is so fundamental.

In LWJGL however there’s one subtle difference - the keyboard repeat rate is off. That’s a consequence of DirectInput on win32 and the disabling of the key repeat on linux (the only solution at hand). So it might not be the best solution after all, if you need the repeat rate of the OS.

  • elias

Thinking more of it, that’s not a trivial problem…
What sequence of events (and value) should you obtain if you press and hold two keys? :o

Under Windows, I see a KeyPressed and KeyTyped for both keys, at the begining of the sequence; then KeyPressed + KeyTyped for only one; and KeyReleased for both.

The key repeat mecanism does not make it simple to go up right when the user presses at the same time [^]+[>] :stuck_out_tongue:

I don’t think the repeating need do anything special for two keys… if it wants to repeat only one or none it would make no difference to me… so long as there is only ONE pressed and ONE released that happen only when the key or keys are actually pressed and released. Wether or not you get multiple key typed events is usually not relevant for games.

Perhaps ignoring the OS repeat and handling it internally with a timer is a better way to go? (When you say it is turned off for Linux in LWJGL does that affect the whole system or just the LWJGL window?) If the repeat rate can be queried from the OS then that seems like a good solution.

It’s for the whole systme unfortunately - and is only enabled again at Keyboard.destroy(). Better solutions are welcome.

  • elias

Hi,

As a start, here is some code to filter the surnumerous KeyReleased events under linux.

The ONE advantage:

  • It’s portable code using JDK 1.1.8 API.

The disadvantages:

  • It creates on thread by KeyListener it manages.
  • The filtering is based on a delay. If no more event is received during a delay, then the KeyReleased must be the last of the sequence and is sent to the KeyListener. If the delay is chosen too big, there is a lag before the last event is sent; if too short, some KeyReleased events can escape the filter…
  • The choice of the delay depends on the platform. 100 ms seems to be OK on the Zaurus, but creates problems when accessing it remotely with VNC.

TODO:

  • Define exactly what we need (in another post) for cross-platform key processing.
  • Support dumb keys like [Shift] which repeat under Windows but don’t under linux.
  • Put the delay in a system property, or at least in a parameter of the constructor…

The begining of the code:


import java.awt.event.*;
import java.awt.*;

/**
 * Test class for the KeyHandler code
 *
 * @author genepi
 * @created  May 29, 2003
 */
public class Test
{
      /**
       * @param  args the command line arguments
       */
      public static void main(String[] args)
      {
            Frame fr = new Frame();
            TextField l = new TextField("Enter keys");
            fr.add(l);
            fr.pack();
            fr.show();
            l.requestFocus();

            fr.addWindowListener(new WindowAdapter()
                  {
                        public void windowClosing(final WindowEvent evt)
                        {
                              System.exit(0);
                        }
                  });
            
            if (args.length > 0)
            {
                  l.addKeyListener(new KL());
            }
            else
            {
                  l.addKeyListener(new KeyHandler(new KL()));
            }
      }


      /**
       * Simple KeyListener that prints events received
       *
       * @author genepi
       * @created  May 29, 2003
       */
      private static class KL
            implements KeyListener
      {
            /**
             *  Description of the Method
             *
             * @param  e Description of Parameter
             */
            public void keyPressed(KeyEvent e)
            {
                  System.out.println(e);
            }


            /**
             *  Description of the Method
             *
             * @param  e Description of Parameter
             */
            public void keyReleased(KeyEvent e)
            {
                  System.out.println(e);
            }


            /**
             *  Description of the Method
             *
             * @param  e Description of Parameter
             */
            public void keyTyped(KeyEvent e)
            {
                  System.out.println(e);
            }
      }
}


/**
 * The <code>KeyHandler</code> filters <code>KeyEvent</code>
 * sequences to make linux and Windows behave in the same way.
 * 
 * On linux, the key-repeat is different from Windows:
 * <b>Character key pressed and held</b>
 * <pre>
 * Windows:
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED
 * 
 * Linux:
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED (*)
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED (*)
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED (*)
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED (*)
 * KEY_PRESSED
 * KEY_TYPED
 * KEY_RELEASED
 * </pre>
 * The KeyHandler filters for the extraneous KEY_RELEASED events received
 * on the linux platform.
 * 
 * TODO:
 * - Manage dumb keys like [Shift] or [Control]
 * - Check behavior with simultaneous keys pressed.
 * - The lag/delay of the filter must be read form a property or
 * passed as a parameter.
 *
 * This class is a façade to the real <code>KeyListener</code>.
 *
 * @author genepi
 * @created  May 29, 2003
 */
class KeyHandler
       implements KeyListener
{
      /**
       * The <code>KeyListener</code> protected by the <code>KeyHandler</code>.
       */
      private final KeyListener _listener;
      
      
      /**
       * The thread that filters the keys.
       */
      private final KeyHandlerThread _thread;
      
      
      /**
       * Create the key handler that will dispatch the events to the key listener.
       *
       * @param listener The key listener that will receive key events.
       */
      public KeyHandler(final KeyListener listener)
      {
            _listener = listener;
            // TODO: $$PME
            // The 100ms delay must be adapted to the plaform
            _thread = new KeyHandlerThread(100L);
            _thread.start();
      }
      
      
      /**
       * When the <code>KeyHandler</code> is garbage collected, the companion
       * thread is no more necessary and can stop running.
       */
      protected void finalize()
            throws Throwable
      {
            // KeyEventThread can kill itself now
            _thread.stopHandler();
      }


      /**
       * Add the key pressed event to the queue
       *
       * @param  evt The key event
       */
      public void keyPressed(final KeyEvent evt)
      {
            _thread.addEvent(evt);
      }


      /**
       * Add the key released event to the queue
       *
       * @param  evt The key event
       */
      public void keyReleased(final KeyEvent evt)
      {
            _thread.addEvent(evt);
      }


      /**
       * Add the key typed event to the queue
       *
       * @param  evt The key event
       */
      public void keyTyped(final KeyEvent evt)
      {
            _thread.addEvent(evt);
      }


      /**
       * Send a key event to the listener.
       *
       * @param evt The key event to send
       */
      private void dispatchKeyEvent(final KeyEvent evt)
      {
            switch (evt.getID())
            {
            case KeyEvent.KEY_PRESSED:
                  _listener.keyPressed(evt);
                  break;
                  
            case KeyEvent.KEY_RELEASED:
                  _listener.keyReleased(evt);
                  break;
                  
            case KeyEvent.KEY_TYPED:
                  _listener.keyTyped(evt);
                  break;
                  
            default:
                  throw new RuntimeException("Unhandled key event: " + evt);
            }
      }
      
      
      /**
       * The <code>KeyHandlerThread</code> filters the key events
       * generated by the key-repeat controller to get a platform
       * neutral sequence of events.
       * 
       * @author genepi
       * @date May 30, 2003
       */
      private class KeyHandlerThread
            extends Thread
      {
            /**
             * The queue of key events.
             */
            private final FlexibleQueue _events = new FlexibleQueue();
            
            
            /**
             * Handle events while this variable is <code>true</code>
             */
            private volatile boolean _handleKeys;
            
            
            /**
             * The delay we have to wait to accept a <code>KeyReleased</code>
             * event.
             */
            final private long _delay;
            
            
            /**
             * Constructor of the KeyHandler thread.
             * 
             * @param delay The delay while no new event must be received before
             * we accept a <code>KeyReleased</code> event.
             */
            KeyHandlerThread(final long delay)
            {
                  super("KeyHandlerThread");
                  setDaemon(true);
                  _delay = delay;
                  _handleKeys = true;
            }
            
            
            /**
             * The key event queue processing loop.
             */
            public void run()
            {
                  while (_handleKeys)
                  {
                        synchronized (this)
                        {
                              try
                              {
                                    // Sleep only if no more events to process
                                    if (_events.size() == 0)
                                    {
                                          wait();
                                    }
                              }
                              catch (InterruptedException ie)
                              {
                                    // A new key should be available in the queue...
                              }
                              
                              if (_events.size() > 0)
                              {
                                    // Time to wake up: a key has been detected
                                    KeyEvent event = (KeyEvent) _events.removeElement();
                                    
                                    if (event.getID() == KeyEvent.KEY_RELEASED)
                                    {
                                          // We have to wait for next key to determine what to do
                                          try
                                          {
                                                wait(_delay);
                                          }
                                          catch (InterruptedException ie2)
                                          {
                                                // Could be a new key in the queue or delay expired.
                                          }
                                          
                                          // There is another key waiting in the queue
                                          if (_events.size() > 0)
                                          {
                                                KeyEvent event2 = (KeyEvent) _events.removeElement();
                                                // If it's a KeyPressed on the same key...
                                                if (event2.getID() == KeyEvent.KEY_PRESSED && event.getKeyChar() == event2.getKeyChar())
                                                {
                                                      // We can drop the KeyReleased event
                                                      dispatchKeyEvent(event2);
                                                }
                                                else
                                                {
                                                      // Both events (KeyReleased + other) must be re-sent
                                                      dispatchKeyEvent(event);
                                                      dispatchKeyEvent(event2);
                                                }
                                          }
                                          else
                                          {
                                                // No new event after the KeyReleased (we are there
                                                // because the delay expired), we send the KeyReleased
                                                dispatchKeyEvent(event);
                                          }
                                    }
                                    else
                                    {
                                          // Non filtered event
                                          dispatchKeyEvent(event);
                                    }
                              }
                        }
                  }
            }
            
            
            /**
             * Change the condition in the thread loop: the thread dies 
             */
            void stopHandler()
            {
                  _handleKeys = false;
            }
            
            
            /**
             * Add a new key event in the thread queue for processing.
             * 
             * @param evt The key event
             */
            void addEvent(final KeyEvent evt)
            {
                  synchronized (this)
                  {
                        _events.addElement(evt);
                        notifyAll();
                  }
            }
      }
}

The end of the code:



/**
 * A flexible queue backed with an array, that resizes if needed.
 * This implementation is synchronized.
 *
 * Let capacity = _content.length.
 * Let _endIndex = (_end - 1 + capacity) % capacity
 *
 * _content[_start] is the start
 * _content[_endIndex] is the end
 *
 * if _start <= _end,
 *      _content[_start .. _endIndex] contains the Objects in the order they were
 *      inserted, and _size = _end - _start.
 * if _start > _end,
 *       _content[_start .. _content.length - 1, 0 .. _endIndex] contains the Objects in
 *      the order they were inserted, and _size = _end - _start + capacity.
 *
 * Adapted from http://www.cs.toronto.edu/~jepson/148/general/exercises/src/RubberBandQueue/RubberBandQueue.java
 *
 * @author genepi
 * @created  May 29, 2003
 */
class FlexibleQueue
{
      /**
       * The initial number of elements this queue can hold,
       */
      private final int _initialSize;

      /**
       * The increment.
       */
      private final int _increment;

      /**
       * The index of the start of the queue.
       */
      private int _start = 0;

      /**
       * The index of the end of the queue.
       */
      private int _end = 0;

      /**
       * The number of elements in me.
       */
      private int _size = 0;

      /**
       * The items stored in _content[_start .. _end-1],
       * with wraparound.
       */
      private Object[] _content;


      /**
       * Constructor
       */
      public FlexibleQueue()
      {
            this(10, 5);
      }


      /**
       * Constructor with parameters
       *
       * @param size The initial size
       * @param increment The size increment when the queue is full
       */
      public FlexibleQueue(final int size, final int increment)
      {
            _content = new Object[size];
            _initialSize = size;
            _increment = increment;
            _start = 0;
            _end = 0;
            _size = 0;
      }


      /**
       * Add the given element to this queue.
       *
       * @param object the element to be added.
       * @exception  QueueFullException the queue cannot be expanded
       *             this only occurs when the machine is nearly out
       *             of memory.
       */
      public void addElement(final Object object)
      {
            synchronized (_content)
            {
                  if (_size == _content.length)
                  {
                        expandFullQueue();
                  }
      
                  _content[_end] = object;
                  _end = (_end + 1) % _content.length;
                  _size += 1;
            }
      }


      /**
       * Dequeue the first object in the queue.
       * Precondition: The queue cannot be empty.
       *
       * @return  the first object in the queue.
       */
      public Object removeElement()
      {
            synchronized (_content)
            {
                  Object result = _content[_start];
                  _start = (_start + 1) % _content.length;
                  _size -= 1;
                  return result;
            }
      }


      /**
       * Reead the first object in the queue.
       * Precondition: The queue cannot be empty.
       *
       * @return  the first object in the queue.
       */
      public Object readElement()
      {
            synchronized (_content)
            {
                  return _content[_start];
            }
      }


      /**
       * Get the size of the queue (the number of elements queued)
       *
       * @return  the size of the queue.
       */
      public int size()
      {
            synchronized (_content)
            {
                  return _size;
            }
      }


      /**
       * Returns the current capacity of the queue.
       *
       * @return  the capacity of the queue.
       */
      public int capacity()
      {
            synchronized (_content)
            {
                  return _content.length;
            }
      }


      /**
       * Resets the queue to be empty and of minimal capacity.
       */
      public void reset()
      {
            synchronized (_content)
            {
                  _content = new Object[_initialSize];
                  _start = 0;
                  _end = 0;
                  _size = 0;
            }
      }


      /**
       * Returns a string representation of this queue.
       *
       * @return  a string representation of this queue.
       */
      public String toString()
      {
            final StringBuffer buf = new StringBuffer();
            buf.append(getClass().getName());
            buf.append("[size=");
            buf.append(_size);
            buf.append(", content={");
            
            if (_start > _end || _size == _content.length)
            {
                  for (int i = _start, size = _content.length; i < size; i++)
                  {
                        buf.append(_content[i]);
                        if (i != _size - 1)
                        {
                              buf.append(", ");
                        }
                  }
                  for (int i = 0; i < _end; i++)
                  {
                        buf.append(_content[i]);
                        if (i != _end - 1)
                        {
                              buf.append(", ");
                        }
                  }
            }
            else
            {
                  for (int i = _start; i < _end; i++)
                  {
                        buf.append(_content[i]);
                        if (i != _end - 1)
                        {
                              buf.append(", ");
                        }
                  }
            }
            
            buf.append("}]@");
            buf.append(hashCode());
            
            return buf.toString();
      }



      /**
       * Make the _content array _increment items larger.
       * Precondition: The queue must currently be full!
       */
      private void expandFullQueue()
      {
            // New array for copying queue into.
            final Object[] tmpContent;
            tmpContent = new Object[_content.length + _increment];

            // Copy _content to new array, using wrap around.
            if (_start > _end || _size == _content.length)
            {
                  System.arraycopy(_content, _start, tmpContent, 0, _content.length - _start);
                  System.arraycopy(_content, 0, tmpContent, _content.length - _start, _end);
            }
            else
            {
                  System.arraycopy(_content, _start, tmpContent, 0, _end - _start + 1);
            }

            _start = 0;
            _end = _content.length;
            // one past the last element in the queue.

            // Finally, replace old _content array with the new one.
            _content = tmpContent;
      }
}

Jinput should cover this, if you don’t mind polling the keybaord that is

HTH

Endolf