Tuesday, June 27, 2006

Dead Simple Drags

Here is a demo of usage of the Drag & Drop Library, which provides D&D support on top of Java's basic D&D in much the way Xt provided toolkit functionality on top of X11.





Here is the code to add drag handling to a JLabel:
final JLabel label = new JLabel("Drag Me");
label.setBorder(BorderFactory.createEmptyBorder(4,4,4,4));
new DragHandler(label, DnDConstants.ACTION_COPY) {
protected Transferable getTransferable(DragGestureEvent e) {
return new StringSelection(label.getText());
}
protected Icon getDragIcon(DragGestureEvent e, Point offset) {
return new ComponentIcon(label, true);
}
};


The only thing absolutely required by the API is getTransferable. However, overloading getDragIcon will automatically give you a nice ghosted drag image, regardless of whether your platform supports one natively (the ComponentIcon used to render the JLabel is just a simple class which renders its given component as an icon).

Drag and Drop has been in Java since version 1.2 when a functional but very complex set of interfaces was introduced. Compare the previous DragHandler interface for dragging with the following interfaces, which must be implemented carefully to get consistent results (note also the conspicuous absence of any methods which explicitly indicate the data to be dragged):

// DragGestureListener
void dragGestureRecognized(DragGestureEvent);
// DragSourceListener
void dragEnter(DragSourceDragEvent);
void dragOver(DragSourceDragEvent);
void dropActionChanged(DragSourceDragEvent);
void dragExit(DragSourceEvent);
void dragDropEnd(DragSourceDropEvent);
//DragSourceMotionListener
void dragMouseMoved(DragSourceDragEvent);
Not a trivial thing to get right, especially without an in-depth understanding of how the system works. Not something I'd expect of every developer. Unfortunately, there is no abstract implementation to provide a base level of functionality or useful example (I'm sure everyone has at least looked at Rockhopper or JavaWorld for functional examples). These examples provide working implementations, but not one that makes extension or reuse clear and simple. Sun has tried to make D&D simpler by introducing the TransferHandler and setting up default drag and drop handlers on Swing components. I consider the TransferHandler a mistaken attempt to merge two operations that just happened to have a little functionality in common. Copy and drag both need to produce a Transferable, and Paste and drop both need to absorb a Transferable, but the edit actions operate in a sufficiently different manner that merging the two doesn't really buy you much in practice.

Copy/paste actions are fairly trivial to construct in the first place, and the clipboard transfer is a few lines of boilerplate.

Drag and drop, on the other hand, is a lot more lines of boilerplate, some of which can introduce subtle differences in behavior if you don't get it just right.

Back to the library. An example on a JTree is a bit more involved, but only because you now need to choose which part of the JTree is going to be dragged. For illustrative purposes, I've disabled dragging of non-leaf nodes, and enabled dragging of the entire tree if you drag outside of any rows. I futz with the ComponentIcon to draw either the whole tree or just one row, as needed. You could conceivably construct a drag from a multiple selection by painting the whole tree but adding an appropriate clipping mask when painting the icon.

final JTree tree = new JTree();
// Turn off selection of rows by dragging
tree.setDragEnabled(true);
// Turn off built-in swing drag handling
tree.setTransferHandler(null);
new DragHandler(tree, DnDConstants.ACTION_COPY) {
protected boolean canDrag(DragGestureEvent e) {
Point where = e.getDragOrigin();
int row = tree.getRowForLocation(where.x, where.y);
if (row != -1) {
TreePath path = tree.getPathForRow(row);
return tree.getModel().isLeaf(path.getLastPathComponent());
}
return true;
}
protected Transferable getTransferable(DragGestureEvent e) {
Point where = e.getDragOrigin();
final int row = tree.getRowForLocation(where.x, where.y);
if (row == -1) {
return new StringSelection("full tree");
}
Object value = tree.getPathForRow(row).getLastPathComponent();
return new StringSelection(String.valueOf(value));
}
protected Icon getDragIcon(DragGestureEvent e, Point offset) {
Point where = e.getDragOrigin();
final int row = tree.getRowForLocation(where.x, where.y);
if (row != -1) {
Rectangle r = tree.getRowBounds(row);
offset.setLocation(r.x, r.y);
}
return new ComponentIcon(tree, true) {
public void paintIcon(Component c, Graphics g, int x, int y) {
g = g.create();
if (row != -1) {
Rectangle r = tree.getRowBounds(row);
g.translate(-r.x, -r.y);
g.setClip(new Rectangle(x+r.x, y+r.y, r.width, r.height));
super.paintIcon(c, g, x, y);
}
else {
super.paintIcon(c, g, x, y);
}
g.dispose();
}
};
}
};

Again, this implementation doesn't have to worry about any of the mechanics of drag and drop. The implementation decides whether an item can be dragged, what the dragged item looks like, and what is the appropriate Transferable. This API hides all the boilerplate, but you can still get access to the basic Java D&D API if you need to, if for some reason you need to augment or override dragOver or dragGestureRecognized.

Next installment will demonstrate the DropHandler, which facilitates decorating your drop target as well as accepting or rejecting incoming data.

Source.

5 comments:

Anonymous said...

Downloadable source does not work.
Java 1.4.2_09 on WinXP + Mac OSX 10.3

technomage said...

Could you be more specific? I can download the jar, unjar it, do "javac *.java" and "java -cp . DeadSimpleDrag", on winxp and osx 10.4.

Rockhopper said...

Nice work.

Drag and Drop in Java has always been a horrible mess. After my JavaOne presentation in 1999 the "architect" came up to me and asked how I got some stuff to work. It looks like you're doing the same; the toolkit is a pain but you're finding ways to get around them. Keep it up.

Gene De Lisa
http://www.rockhoppertech.com/blogs

Kevin said...

You should probably mention somewhere that DragHandler is part of Java Native Access. Took me a while to figure out exactly what you were referring to.

Timothy Wall said...

@Kevin DragHandler is included in the source jar. It's actually a precursor to the one provided in JNA, and uses only what's available in J2SE.