Java Tip 114: Add ghosted drag images to your JTrees

What to do when your platform doesn't support drag images

Adding drag-and-drop support to JTrees 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.

Figure 1. Screenshot of CTree drag image in action

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!

Andrew J. Armstrong holds a degree in applied science (computing science) and has been a contract programmer since 1987, working mainly with enterprise servers (apparently the new name for mainframes) in such areas as systems programming, network problem determination, C++ programming, and more recently, Java programming. When not glued to a keyboard, he enjoys raising his two small children in sunny Sydney, Australia.

This story, "Java Tip 114: Add ghosted drag images to your JTrees" was originally published by JavaWorld.

Copyright © 2001 IDG Communications, Inc.

InfoWorld Technology of the Year Awards 2023. Now open for entries!