Package com.szuppe.jakub.view

Source Code of com.szuppe.jakub.view.RepeatingReleasedEventsFixer$ReleasedAction

/**
*
*/
package com.szuppe.jakub.view;

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 href="http://bugs.sun.com/view_bug.do?bug_id=4153069">a 12 yo bug</a> in the Linux
* KeyEvent handling for keyboard repeat. Linux apparently implements repeating keypresses by repeating both the
* {@link KeyEvent#KEY_PRESSED} <b>and</b> {@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)}:
*
* <br>
*
* <pre> *
* Toolkit.getDefaultToolkit().addAWTEventListener(new {@link RepeatingReleasedEventsFixer}, AWTEvent.KEY_EVENT_MASK);
* </pre>
*
* 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.
*
* <b>Notice:</b> 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. <br>
* <h3>
* Mode of operation</h3> 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:
*
* <pre>
* * PRESSED
* -wait between key repeats-
* RELEASED
* PRESSED
* -wait between key repeats-
* RELEASED
* PRESSED
* etc.
* </pre>
*
* 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 <a href="http://www.arco.in-berlin.de/keyevent.html">http://www.arco.in-berlin.de/keyevent.html</a>
*
* @author <a href="http://endre.stolsvik.com/">Endre Stølsvik</a>
*/
public class RepeatingReleasedEventsFixer implements AWTEventListener
{
  private final Map<Integer, ReleasedAction>  _map  = new HashMap<Integer, ReleasedAction>();

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

  @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(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
  {
    /**
     *
     */
    private static final long  serialVersionUID  = -879234162173462380L;

    public RepostedKeyEvent(Component source, 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;
  }
}
TOP

Related Classes of com.szuppe.jakub.view.RepeatingReleasedEventsFixer$ReleasedAction

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.