Wednesday, June 28, 2006

Fancy Drops

Just run the demo.



Again, a very simple method override to enable drop target functionality. Paint a marquee on the full list if it's empty, otherwise mark a space at the end of the list. Most of the code is just futzing with the data model on a drop or moving/sizing the decoration.

JList list = ...;
DataFlavor[] acceptableFlavors = { DataFlavor.stringFlavor };
new DropHandler(list, DnDConstants.ACTION_COPY, acceptableFlavors) {
private Marquee marquee;
/** Always drop at the end of the list. */
protected void drop(DropTargetDropEvent e, int action) throws UnsupportedFlavorException, IOException {
final List data = new ArrayList();
for (int i=0;i < list.getModel().getSize();i++) {
data.add(list.getModel().getElementAt(i));
}
data.add(e.getTransferable().getTransferData(DataFlavor.stringFlavor));
list.setModel(new AbstractListModel() {
public int getSize() {
return data.size();
}
public Object getElementAt(int index) {
return data.get(index);
}
});
}
protected void paintDropTarget(DropTargetEvent e, int action, Point location) {
if (action != DnDConstants.ACTION_NONE && location != null) {
if (marquee == null) {
marquee = new Marquee(list);
}
int count = list.getModel().getSize();
if (count == 0) {
Dimension size = list.getSize();
marquee.setDecorationBounds(new Rectangle(0, 0, size.width, size.height));
}
else {
Rectangle r = list.getCellBounds(count-1, count-1);
r.y += r.height;
marquee.setDecorationBounds(r);
}
}
else if (marquee != null) {
marquee.dispose();
marquee = null;
}
}
};
Compare that API with the raw D&D APIs (which are still available to override in DropHandler, by the way):

// DropHandler
void drop(DropTargetDropEvent e, int action);
// DropTargetListener
void dragEnter(DropTargetDragEvent);
void dragOver(DropTargetDragEvent);
void dropActionChanged(DropTargetDragEvent);
void dropExit(DropTargetEvent);
void drop(DropTargetDropEvent);
Most of the complexity comes in figuring out the appropriate sequence of calling back methods on the DropTargetEvents, which communicates whether a drag is acceptable and if so, which action is actually to be accepted. This could be clarified a great deal in the Javadoc APIs, or by a thorough example, but ultimately it's boilerplate code that rarely, if ever, needs changes to its behavior.

Back to the example. The JTree drop handler code is more complex, but only to determine what should happen when dropping on any given location. Non-leaf nodes accept drops, but leaf nodes refuse drops, and the spaces between leaf nodes accept drops.

Some DropHandler features:
  • Automatically get the copy action when it's the only one allowed
  • Automatically disable user actions which are not allowed
  • Some parts of the drop target allow drops, some don't
  • Paint any drop target indication you please, regardless of the target component (no subclassing of components)
Usually, in order to get non-trivial drop target painting, you have to override one of the target component's paint methods and invoke paintImmediately within the drop handler. That's avoided by use of a decorator, which uses the existing component hierarchy and layout to paint over the target component. The Marquee class uses a dashed stroke to draw a rectangle. A timer regularly increases the phase and triggers a repaint, which gives the "marching ants" effect.

Unfortunately, you still have to make the component implement Autoscrolls, although you could probably call the methods yourself from within the drop handler. It'd be nice if the D&D code which calls into the Autoscrolls interface instead looked for a client property. That functionality could probably be implemented in the DropHandler.

Next step: hook this up to the list and tree animators.

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.

Monday, June 26, 2006

Decorator Update

This fixes some display glitches that showed up on X11-based systems. The decorators are also properly clipped (or not) if they extend beyond the target component bounds. This showed up with the marquee, which would get clipped by a scroll pane that was not an ancestor.




Source

Thursday, June 15, 2006

Navigating Large Spaces

This example demonstrates one method of providing a navigational element for traversing a large, scrolled component. Like Apple's Expose, it's entirely out of the way until you need it.




This implementation combines several effects. First, a ComponentIcon class renders any given target component as an icon. Second, a ScaledIcon is used to resize the ComponentIcon to fit within the desired space. The Panner composes the two icons and paints a border, indicates the visible sub-rectangle, and responds to navigational input (mouse events). Finally the PannerHandler handles showing, hiding, and resizing the panner in response to user input.

ComponentIcon
This is pretty simple. The main thing to watch out for is that you need to turn off double buffering on the target component, or the rendering ends up having odd side effects on the rest of the display. The underlying component's "isShowing" method must return true, otherwise Swing will short-circuit its painting (there are other methods for getting a component to paint which is not currently in the visible hierarchy).

ScaledIcon
This class takes any other icon, scales and translates the Graphics object so that the delegate icon fits within its allotted size, then delegates to the other Icon to paint. Aspect ratio is preserved by default, but that can be turned off if you want to stretch to fit the available space.

Panner
This is a lightweight component with optional transparency (so that you can optionally position it over the panned component and have the underlying component show through). It passes through the "preserveAspectRatio" property to the ScaledIcon.

PannerHandler
This class takes care of the details of deciding where and when to show the panner. It also sets the size appropriately (the example uses a percent value against the size of the panned component, and a fixed offset from the UL corner). Essentially this is the class responsible for configuring all the other classes and hooking them together. It installs a ComponentListener to update the panner size and position whenever the panned component is resized or moves (i.e. is scrolled).

It's possible to attach the panner itself to the panned component (so you get a thumbnail in a corner of the pane) or have it appear in its own window. The panner size can span the entire visible viewport or some fraction thereof. You can also indicate which corner of the visible viewport to use as anchor if the panner is smaller than the visible viewport.