Monday, September 8, 2008

Debug Listeners on AWT/Swing components

Being a newbie to the whole Swing stuff, I often wonder what events are fired on a given component for any given gesture or event.

I wonder no more!

Copy the following (ctrl-C), then activate some package node in a project in Eclipse (e.g. the obvious "com.example") and paste (ctrl-V).

import java.awt.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.JButton;

public class DebugStatics {

public interface LineTaker {
void line(String msg);
}

public static final void main(String[] args) {
LineTaker out = new LineTaker() {
@Override
public void line(String msg) {
System.out.println(msg);
}
};
addDebugListeners(out, new JButton(), false);
}

public static void addDebugListeners(final LineTaker out, final Object component,
final boolean includeAllMoveEvents) {

// :: Find all add*Listener methods for supplied component.

List<Class<?>> listenerInterfaces = new ArrayList<Class<?>>();
List<Method> addListenerMethods = new ArrayList<Method>();
Method[] methods = component.getClass().getMethods();
for (Method method : methods) {
String name = method.getName();
if (name.startsWith("add") && name.endsWith("Listener")) {
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length == 1) {
Class<?> interfaze = parameters[0];
if (interfaze.isInterface()) {
listenerInterfaces.add(interfaze);
addListenerMethods.add(method);
}
}
}
}

// :: Make handler for "super listener"

InvocationHandler handler = new InvocationHandler() {
long _lastEvent;
String _lastMethodName;
boolean _threadFired;
InvocationHandler _handler = this;

@Override
public synchronized Object invoke(@SuppressWarnings ("unused") Object proxy, Method method, Object[] args)
throws Throwable {
_lastEvent = System.currentTimeMillis();
String methodName = method.getName();
if (includeAllMoveEvents || !(methodName.endsWith("Moved") && methodName.equals(_lastMethodName))) {
out.line("event:[" + method.getName() + "] - on - [" + method.getDeclaringClass().getName()
+ "] - with - " + Arrays.asList(args) + ".");
if (!_threadFired) {
_threadFired = true;
new Thread("debug:Event Stream Breaker") {
@Override
public void run() {
while (true) {
long millisLeft;
synchronized (_handler) {
millisLeft = 400 - (System.currentTimeMillis() - _lastEvent);
if (millisLeft < 0) {
out.line("===== event stream break.");
_threadFired = false;
_lastMethodName = null;
break;
}
}
try {
Thread.sleep(millisLeft + 2);
continue;
}
catch (InterruptedException e) {
break;
}
}
}
}.start();
}
}
_lastMethodName = methodName;
return null;
}
};

// :: Make "super listener", "implementing" all the Listener interfaces

Object superListener = Proxy.newProxyInstance(DebugStatics.class.getClassLoader(), listenerInterfaces
.toArray(new Class<?>[0]), handler);

// :: Attach "super listener" using all add*Listener methods on supplied component

for (Method method : addListenerMethods) {
try {
method.invoke(component, superListener);
out.line(" ++ add*Listener: [" + method + "].");
}
catch (Throwable e) {
out.line("Got error when trying to invoke add*Listener method:[" + method + "]." + e);
}
}
}
}
Code discussion: The LineTaker comes from the need to have some flexibility regarding where the textual stream of events should end up (e.g. to System.out.println(...) or to some logger.debug(...)). The driver (main method) is just to check out the listener adding (and possibly also to find out which types of add*Listener methods a given AWT/Swing/Whatever class has). The reason for the Thread-forking is so that you more easily can understand which events "belongs" to each actual, physical gesture: Move the mouse into a button, wait a second, then press down mouse button, wait a second, release mouse button, wait a second, move out. Now the result event stream have embedded a break on every second-wait. The thread only lives long enough to write the break, then exits. The reason for the code regarding move-events suppression is that you get an awful lot of move-events without it - this code results in only one move event being recorded in a consecutive run of the same move event.

While dumping the code into this post, it hit me that the above method actually works for any class having add*Listener(AnyInterface listener) methods. That's why the type of the component argument is Object.

There's a pretty impressive amount of events being fired on a JButton for the simple act of using the mouse to click it!