Saturday, April 18, 2009

Finding Modifier State without Swing Event parameters

Java Swing calls a number of methods with either an InputEvent or an ActionEvent instance as a parameter. Each of these classes provide a getModifiers() method that can be used to find the current state of the CTRL, ALT, SHIFT, and META keys. Given a KeyEvent or MouseEvent subclass of InputEvent there are even more details available, such as the exact key or button that was pressed. However, what if some of these details are required without having an event instance available?

For example, I was looking to add a shortcut to a JTree. If the CTRL button is being held down while a node is expanded or collapsed, all child nodes should recursively do the same. The plan seemed fairly simple: Call JTree.addTreeExpansionListener(TreeExpansionListener), adding a listener that detects if the CTRL button is being held down, then recursively call expandPath or collapsePath>. Unfortunately, the methods on TreeExpansionListener (as well as TreeWillExpandListener) only provide a TreeExpansionEvent, which does not extend either InputEvent or ActionEvent and does not provide any of the above details.

I thought I could easily trap these details myself, through the use of addKeyListener and addMouseListener. Unfortunately, due to the event model in Swing, these listeners are not called until after the tree expand or collapse methods have been processed. Attempting to add these listeners to the component's parent also failed, as the events are first dispatched to the child. There is also no API method to get the current keyboard or mouse state "from anywhere" without having access to one of the event instances, short of writing some custom JNI code. Changing my requirements to modify the GUI instead of making the keyboard modifier work would also have been an undesirable possibility.

This situation is not particular to JTree. There are many other subclasses of AWTEventListener that pass subclasses of EventObject (other than subclasses of AWTEvent) that do not provide means for accessing the desired details.

I did find some possible solutions:

  • Use of a glass pane. Basically, it is set as visible and intercepts all events. Unfortunately, this requires then forwarding all events to the desired destination - including a non-trivial amount of work to determine the original destination. This would not scale well, and does not work well with a decorator-type pattern. While it may be a good feature to remember for other specific uses, this certainly is not the first solution I would recommend for this scenario.

In each of the below possible solutions, the idea is to find the event when it is available, and keep a reference to the latest someplace for later:

  1. A rather hacked solution - and another one that I do not at all recommend: Get the current EventQueue from Toolkit.getSystemEventQueue(). Then, replace it with a subclassed version using push(). In the subclassed version, override the dispatchEvent method to capture and store the state of any mouse and/or keyboard events. Note that if not done properly, this can result in serious performance and other issues.

  2. Subclass JFrame or any other component that is the target for extension. Override processEvent, or more efficiently, processKeyEvent and/or processMouseEvent. Unfortunately, this does not allow for these listeners to be added as a decorator after the instance is created.

  3. The winner: Add a listener using Toolkit.addAWTEventListener(AWTEventListener listener, long eventMask). This is my current favorite and go-forward approach, as it works well with decorators and is better-performing than #1. By passing an appropriate eventMask, this assures that the listener only receives events that it is remotely interested in. For example, in my scenario, I'm using AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK.

    The AWTEventListener added here in in effect for the entire application, and again, can result in serious performance and other issues if not done properly. Adding such a listener to store only the last keyboard- or mouse-related event should only need to be done once, and suited for reuse throughout the entire application. (Continually adding additional listeners for each use would certainly not scale well.) Look for a utility version of this functionality in an upcoming release of MarkUtils-Swing.

1 comment:

Hervé said...

And why not simplyt do the same thing that Swing developper?
As you can see in DefaultButtonModel.setPressed(boolean b) they call:

int modifiers = 0;
AWTEvent currentEvent = EventQueue.getCurrentEvent();
if (currentEvent instanceof InputEvent) {
modifiers = ((InputEvent)currentEvent).getModifiers();
} else if (currentEvent instanceof ActionEvent) {
modifiers = ((ActionEvent)currentEvent).getModifiers();
}