Wednesday, August 30, 2006

Swing Drag Images, Improved

Performance is improved, generating the drag image once instead of repeatedly.




Source

Saturday, August 12, 2006

Drop Target Navigation, or You Drag Your Bags, Let the Doorman Get the Door

When I'm dragging a 75lb bag and a five year old, it's rather nice to have someone open the door. When I'm dragging things around in my application, it's also nice not to have to open all the doors on the way to my destination before I start dragging.

Specifically, I've got a set of forms in an instance of JTabbedPane. I know one of the tabs holds a field that defines my preferred download directory, which I happen to have open in my file browser. Drag from the file browser onto the JTabbedPane, and -- oops, I'm not on the right pane. It'd be nice to have the tabs change as I drag across them so that I can find the right destination.

Since this is system-wide behavior, I'd like to enable the behavior once at application startup and forget about it, rather than having to add listeners or subclass every instance of JTabbedPane and JTree that's going to accept a drop.

DropTargetNavigator.enableDropTargetNavigation();

In order to make this happen, we install a global listener to the default DragSource, which lets us respond to any drags initiated within the same VM. Since that provides us with the current drag location, we can scan through the currently available components using Frame.getFrames() and SwingUtilities.getDeepestComponentAt() to see if what's under the cursor needs to be navigated. If we do make a modification, a Runnable which will undo the modification is added to a queue so that we can restore the component to its original state if the drop ends up going somewhere else or being canceled.

To handle drags originating from outside the VM requires a little hacking, since there isn't any sort of global drop target listener. During a native drag, you won't see any MouseEvents. As of 1.4+, there is a MouseEvent (actually a SunDropTargetEvent) that gets processed by Component.dispatchEventImpl(), but it gets swallowed before the component passes events off to AWTEventListeners. You can't override dispatchEvent, and wouldn't want to anyway because you'd have to do it on every component. Instead, we can install a custom EventQueue which can watch for the drop target events and forward them to our drop target listener.

The navigator class has built-in support for JTabbedPanes and JTrees. Custom classes that wish to support navigation can register themselves with a simple interface that performs custom navigation and rollback. This way you can enable auto navigation on whatever tree table implementation you happen to be using.

In order to avoid unintended navigation when the mouse is dragged rapidly across a component, I've added a configurable delay to when the navigation starts. You need to hover over the same spot for roughly half a second to activate the navigation.


Tuesday, August 08, 2006

Mighty Mitochondria! Them Cells Are Animated!

Java provides effortless loading and display of animated GIFs. Just tell ImageIcon where it can find the file, then plug the ImageIcon into a JLabel or JButton. Couldn't be simpler.




Now how about putting that animated icon into a JTree as feedback for loading status.





Oops. What happened? Did we get the right icon? Why isn't it animated? Try selecting one of the tree nodes, then rapidly move the selection up and down. Watch what happens.

What happened is that no one told the JTree it needed to repaint itself after it painted the initial image. Selection changes trigger repaint messages, which is why the icons move a little bit when you move the selection around. Any animation is driven by regular repaints of the component where it is drawn. JLabels and JButtons are no exception. Take a look at the source for ImageIcon.paintIcon:

public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
if(imageObserver == null) {
g.drawImage(image, x, y, c);
} else {
g.drawImage(image, x, y, imageObserver);
}
}

When you create your ImageIcon, it doesn't have an image observer, so the component where it's being drawn is used instead (note that java.awt.Component implements ImageObserver):

public boolean imageUpdate(Image img, int infoflags,
int x, int y, int w, int h) {
int rate = -1;
if ((infoflags & (FRAMEBITS|ALLBITS)) != 0) {
rate = 0;
}
// ...
if (rate >= 0) {
repaint(rate, 0, 0, width, height);
}
return (infoflags & (ALLBITS|ABORT)) == 0;
}

So whenever the image reports that it has another frame ready (infoflags&FRAMEBITS != 0), it calls this method.

That's just fine for a label or button which only displays a single icon, but what about a tree, which might display this icon in every row? We need an image observer which can call repaint with the cell bounds of every cell that contains the animated icon. Using the tree itself as the observer could work, but we'd end up redrawing the entire tree on every frame. It's preferable to use a decorator which keeps track of which individual cells need updating.



In this case, let's use the tree cell renderer as the entry point or trigger to start or stop animation, since that's normally where a tree's icons get customized.


JTree tree = ...;
TreeCellRenderer r = new DefaultTreeCellRenderer() {
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row, boolean focused) {
Component c =
super.getTreeCellRendererComponent(tree, value, selected, expanded,
leaf, row, focused);
TreePath path = tree.getPathForRow(row);
if (isLoading(path.getLastPathComponent()))
setIcon(LOADING_ICON);
// The following line is what you'd normally use; the rest is just illustrative
//CellAnimator.animate(tree, c, path);
// Let the CellAnimator utility figure out if there's an animated icon here
ImageIcon icon = (ImageIcon)getIcon();
if (CellAnimator.isAnimated(icon)) {
CellAnimator.animate(tree, new CellAnimator.TreeUpdater(tree, row), icon);
}
else {
CellAnimator.stop(tree, row);
}
return c;
}
};
tree.setCellRenderer(r);


The CellAnimator needs to do several things. At any point if it is determined that no animation is required, the animation support is disposed for the current location.

  1. Obtain the icon in use, if any
  2. If the icon is animated, install an ImageObserver
  3. Identify the location to receive repaint notifications, and add it to the observer's list


Whenever the animated icon posts an update, the ImageObserver for that icon will walk its list of repaint locations and trigger repaints. The actual implementation takes care of a number of other details, like using weak references for UI components and remove locations if the location or component is no longer valid.

The CellAnimator provides one-line methods to enable animation on standard Swing components and is easily extensible to enable animation on custom components that use the cell renderer technique.

Here is a demo of the cell animator applied to a lazy-loading tree.


The source includes JUnit-based unit tests which use the Abbot library.

Friday, August 04, 2006

UI Testing on the Sly

One of the drawbacks of testing your UI components (you are testing them, right?) is that to properly run a test, it needs to run on a display. Unfortunately, if you want to use that display for something else (like writing code and fixing bugs), you run into problems with either the tests writing your code, or the code you're writing supplying input to your UI tests.

One good solution is to have a dedicated machine for the tests, which you can continually monitor and can easily be updated with changes from your dev machine. Since that isn't always an option, I'll present a few alternatives for several different platforms which allow you to run UI tests without having a separate machine to run them on.

OS X


VNC is a great tool for controlling remote computers of any platform. If you have fast user switching enabled, you can actually use OSXvnc to display the desktop for a user other than the one currently using the display. If you configure OSXvnc to not quit when switching to another user, you will always be able to connect to the desktop where OSXvnc was first launched.

Set up a "test" user, and set up OSXvnc to launch when that user logs in. Set up the "Startup" tab like this:

Once OSXvnc is running, you can access the "test" desktop while using your normal desktop. Run your tests in the background, peek in on them to check the results.

Linux


Run vncserver, either manually or as a daemon. This will give you an additional X display which is totally separate from your physical one. Connect to the new display with vncviewer, and you have a window onto your "test machine".


Windows


Windows isn't quite as functional when it comes to sharing a display, but you can still get some mileage out of UI testing on the same machine. There's a peculiar little mode in windows, I'll call it "service mode", where you think you have a display but you don't. This prevents java.awt.Robot from working properly, but you can still get some mileage from UI tests that don't depend on screen captures or native drag and drop. Abbot (a UI testing library) will work just fine in this mode, and does so by artificially generating events directly into the AWT event queue in order to drive a UI.

The easiest way to run tests in this manner is to install cygwin sshd. It runs as a service, so by default it doesn't have access to the desktop (there is a checkbox in the services admin control panel that indicates whether a service is allowed desktop access). Once that is installed and configured (google "cygwin sshd install"), you can use ssh to get a shell prompt within the service mode "sandbox". If you've got an ant script to run your tests, just invoke it. Abbot automatically detects whether this mode is in effect, so if you're using that library, everything works out of the box. If you only run your tests interactively through an IDE, you're out of luck, since you won't be able to interact with the IDE if you launch it in service mode.