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.


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.


haha said...

this is _exactly_ what i need, thanks for posting!

*downloading from svn*

valjok said...

That is incredible! You are awesome! I have discovered one sad peculiarity though.

As of Java 6, JTabbedPane allows for arbitrary components to represent the tab title, which is very convenient for the tab close (x) button. Replacing the default tab title by a custom component breaks the autonavigation. That is the line of code:

tabbedPane.setTabComponentAt(index, new JLabel("Tab " + index))

technomage said...

I don't understand what you're referring to. What effect does the new Java 6 functionality have with respect to the auto navigator? How does it break the autonavigation?

Are you simply saying that the demo does not work under Java 6?

valjok said...

The tab merely does not switch when the default title is replaced by a custom one. Peahaps, the autonavigator detects a custom component and refuses to find out it is another tab. :)

technomage said...

Browsing the component hierarchy with the Costello editor, the custom tabs don't seem to show up via the normal findComponentAt search.

Where previously the JTabbedPane was the component under the cursor, now it's a custom component instead.

technomage said...

JTabbedPane drops under Java 1.6 is now fixed in SVN.