Adding drag-and-drop support to JTree
s has been covered already in Java Tip 97: Add Drag and Drop to Your JTrees. I encourage you to read that tip first to get a good understanding of the drag-and-drop paradigm.
The code I present in this tip has the following design goals:
- Ensures a drag image displays even when the underlying platform does not do so (but lets the platform draw the image if it can)
- Draws a "cue line" under the drop target to indicate where the drop will occur
- Draws the drag image as a "ghosted" version of the
JTree
node being dragged - Automatically expands or collapses the drop target when the user mouses over the target
- Recognizes some mouse gestures (right flick and left flick), which the model could then use to shift
JTree
nodes right and left without keyboard interaction
As a bonus, I have included support for the Autoscroll
interface so that the JTree
content is automatically scrolled when the mouse nears the viewport's edge during a drag operation. This is implemented as described in Eckstein, Loy, and Wood's Java Swing, which explains the autoscrolling process adequately, so I won't elaborate here.
Arguably, these capabilities should all be part of a Swing look-and-feel implementation, but I found them easier to implement as part of a JTree
class extension. In spite of the number of goals, the actual code involved is quite minimal.
Figure 1 is an example of what you would see during a drag operation after implementing this tip -- the food folder is about to be dropped after the football node.
To emulate this JTree
behavior, you must change both the DragGestureListener
and the DropTargetListener
classes.
DragGestureListener
You add code to the DragGestureListener
's dragGestureRecognized()
method to figure out which JTree
node is being dragged, and then build a BufferedImage
to represent the node during the drag operation.
First, find out where the mouse is clicked relative to the selected tree node's bounding rectangle. You'll need this later to keep the drag image positioned at the same distance from the mouse pointer as the node is being dragged.
Point ptDragOrigin = e.getDragOrigin(); TreePath path = getPathForLocation(ptDragOrigin.x, ptDragOrigin.y); Rectangle raPath = getPathBounds(path); m_ptOffset.setLocation(ptDragOrigin.x-raPath.x, ptDragOrigin.y-raPath.y);
Now, ask the tree cell renderer (if you are using the DefaultTreeCellRenderer
, the renderer is a JLabel
) to render itself into a BufferedImage
, using a Graphics2D
graphics context set up to create a semi-transparent image. The result is a ghosted version of the original tree node that won't interfere with the ability to see the underlying JTree
nodes as they are dragged over.
// Get the tree cell renderer JLabel lbl = (JLabel) getCellRenderer().getTreeCellRendererComponent ( this, // tree path.getLastPathComponent(), // value false, // isSelected isExpanded(path), // isExpanded getModel().isLeaf(path.getLastPathComponent()), // isLeaf 0, // row false // hasFocus ); // The layout manager normally does this... lbl.setSize((int)raPath.getWidth(), (int)raPath.getHeight()); // Get a buffered image of the selection for dragging a ghost image _imgGhost = new BufferedImage ( (int)raPath.getWidth(), (int)raPath.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE ); // Get a graphics context for this image Graphics2D g2 = _imgGhost.createGraphics(); // Make the image ghostlike g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f)); // Ask the cell renderer to paint itself into the BufferedImage lbl.paint(g2);
To make the ghosted JLabel
look more flash, you also paint under the text with a GradientPaint
that fades from left to right:
// Locate the JLabel's icon so you don't paint under it Icon icon = lbl.getIcon(); int nStartOfText = (icon == null) ? 0 : icon.getIconWidth()+lbl.getIconTextGap(); // Use DST_OVER to cause under-painting to occur g2.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER, 0.5f)); // Use system colors to match the existing decor g2.setPaint(new GradientPaint(nStartOfText, 0, SystemColor.controlShadow, getWidth(), 0, new Color(255,255,255,0))); // Paint under the JLabel's text g2.fillRect(nStartOfText, 0, getWidth(), _imgGhost.getHeight()); // Finished with the graphics context now g2.dispose();
Finally, wrap the TreeNode
about to be dragged in a Transferable
class and invoke startDrag()
. Note that you still pass the drag image to the startDrag()
method because the underlying platform uses it if it can.
// Wrap the path being transferred into a Transferable object Transferable transferable = new CTransferableTreePath(path); // Remember the path being dragged (you may want to delete it later) _pathSource = path; // Pass the drag image just in case the platform IS supports it e.startDrag(null, _imgGhost, new Point(5,5), transferable, this);
DropTargetListener
All the drag-image painting happens in the DropTargetListener'
s dragOver()
method. You only need to draw the image if the platform does not already do it for you, which you find out by first calling the DragSource.isDragImageSupported()
static method.
To draw the drag image, you must do at least two things: First, repaint the real estate the drag image last occupied. Note that simply calling repaint()
won't work because it effectively delays the repainting, possibly until after you have drawn the new drag image, and therefore, erases all or part of it. You really must paint the area immediately, using the, you guessed it, paintImmediately()
method.
Second, you draw the ghost image in its new location. Note that you draw the image the same distance away from the mouse pointer as when the node was first clicked.
Graphics2D g2 = (Graphics2D) getGraphics(); if (!DragSource.isDragImageSupported()) { // Erase the last ghost image and cue line paintImmediately(_raGhost.getBounds()); // Remember where you are about to draw the new ghost image _raGhost.setRect(pt.x - _ptOffset.x, pt.y - _ptOffset.y, _imgGhost.getWidth(), _imgGhost.getHeight()); // Draw the ghost image g2.drawImage(_imgGhost, AffineTransform.getTranslateInstance(_raGhost.getX(), _raGhost.getY()), null); }
In addition, you create a ghosted cue line. A cue line is a line temporarily drawn under each prospective drop target, giving the user a better idea of which node is the current drop target.
// Get the drop target's bounding rectangle Rectangle raPath = getPathBounds(path); // Cue line bounds (2 pixels beneath the drop target) _raCueLine.setRect(0, raPath.y+(int)raPath.getHeight(), getWidth(), 2); g2.setColor(_colorCueLine); // The cue line color g2.fill(_raCueLine); // Draw the cue line
To enable automatic expanding and collapsing of tree nodes as the user's mouse hovers over them, you need to restart a timer each time the tree selection changes.
TreePath path = getClosestPathForLocation(pt.x, pt.y); if (!(path == _pathLast)) { _pathLast = path; _timerHover.restart(); }
If the timer pops, then you toggle the expanded/collapsed state of the last selected tree path. The timer is set up in the DropTargetListener
constructor. The following code kicks in after about 1,000 milliseconds (1 second) of loitering.
_timerHover = new Timer(1000, new ActionListener() { public void actionPerformed(ActionEvent e) { _nLeftRight = 0; // Reset left/right movement trend if (isRootPath(_pathLast)) return; // Do nothing if you are hovering over the root node if (isExpanded(_pathLast)) collapsePath(_pathLast); else expandPath(_pathLast); } });
Our next design goal is to recognize some mouse gestures. If the user drags the mouse sufficiently far to the right, then the dragged tree node must be inserted under rather than after the drop target tree node. Similar, but reverse, logic applies to a sufficiently long drag to the left.
Your TreeModel
must actually insert the node in the appropriate place. Our task here is to recognize the gesture and pass the hint to the model. You can do this in the dragOver()
method after you determine that the mouse pointer has actually moved.
int nDeltaLeftRight = pt.x - _ptLast.x; if ((_nLeftRight > 0 && nDeltaLeftRight < 0) || (_nLeftRight < 0 && nDeltaLeftRight > 0) ) _nLeftRight = 0; _nLeftRight += nDeltaLeftRight;
I have found that on my system a 20-pixel horizontal movement equates to a "flick." The flick gesture sends a visual signal to the user by overlaying the ghost image with an arrow pointing to either the left or right as required. (Arrow images are built by the CArrowImage
class, which saves having to ship image files with your application.)
if (_nLeftRight > 20) { g2.drawImage(_imgRight, AffineTransform.getTranslateInstance(pt.x - _ptOffset.x, pt.y - _ptOffset.y), null); _nShift = +1; } else if (_nLeftRight < -20) { g2.drawImage(_imgLeft, AffineTransform.getTranslateInstance(pt.x - _ptOffset.x, pt.y - _ptOffset.y), null); _nShift = -1; } else _nShift = 0;
Your one-stop shop for drag and drop
This tip resolves an annoying inconsistency in the cross-platform implementation of drag and drop. Users increasingly expect strong visual feedback when performing drag-and-drop operations, especially when they doubt what will happen when they release the mouse button!
The code presented here is a one-stop shop for the following drag-and-drop features:
- Ghosted drag images
- Ghosted cue lines
- Automatic node expand/collapse when hovering
- Mouse gesture recognition
There is plenty of room for enhancing this code. For example, you can graphically represent a multiple-selection drag, animated drag images, snail trails, and other eye candy; I shall leave those to readers with more time on their hands!
This story, "Java Tip 114: Add ghosted drag images to your JTrees" was originally published by JavaWorld.