/*
JDragTree.java / Frost
Copyright (C) 2003 Frost Project <jtcfrost.sourceforge.net>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package thaw.gui;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemColor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
public class JDragTree extends JTree implements DragGestureListener, DragSourceListener {
private static final long serialVersionUID = 1L;
// DropTargetListener interface object...
private class CDropTargetListener implements DropTargetListener
{
// Fields...
private TreePath _pathLast = null;
private final Rectangle2D _raCueLine = new Rectangle2D.Float();
private Rectangle2D _raGhost = new Rectangle2D.Float();
private Color _colorCueLine;
private Point _ptLast = new Point();
private javax.swing.Timer _timerHover;
private int _nLeftRight = 0; // Cumulative left/right mouse movement
// Constructor...
public CDropTargetListener()
{
_colorCueLine = new Color(
SystemColor.controlShadow.getRed(),
SystemColor.controlShadow.getGreen(),
SystemColor.controlShadow.getBlue(),
64
);
// Set up a hover timer, so that a node will be automatically expanded or collapsed
// if the user lingers on it for more than a short time
_timerHover = new javax.swing.Timer(1000, new ActionListener()
{
public void actionPerformed(final ActionEvent e)
{
_nLeftRight = 0; // Reset left/right movement trend
if (isRootPath(_pathLast))
return; // Do nothing if we are hovering over the root node
if (JDragTree.this.isExpanded(_pathLast))
collapsePath(_pathLast);
else
expandPath(_pathLast);
}
});
_timerHover.setRepeats(false); // Set timer to one-shot mode
}
// DropTargetListener interface
public void dragEnter(final DropTargetDragEvent e)
{
if (!isDragAcceptable(e))
e.rejectDrag();
else
e.acceptDrag(e.getDropAction());
}
public void dragExit(final DropTargetEvent e)
{
if (!DragSource.isDragImageSupported())
{
JDragTree.this.repaint(_raGhost.getBounds());
}
}
/**
* This is where the ghost image is drawn
*/
public void dragOver(final DropTargetDragEvent e)
{
if( (e==null) || (_raGhost == null) || (_ptLast == null) ||
(_ptOffset == null) || (_imgGhost == null) || (_raCueLine == null) )
return;
// Even if the mouse is not moving, this method is still invoked 10 times per second
final Point pt = e.getLocation();
if(pt==null)
return;
if (pt.equals(_ptLast))
return;
// Try to determine whether the user is flicking the cursor right or left
final int nDeltaLeftRight = pt.x - _ptLast.x;
if ( ((_nLeftRight > 0) && (nDeltaLeftRight < 0)) || ((_nLeftRight < 0) && (nDeltaLeftRight > 0)) )
_nLeftRight = 0;
_nLeftRight += nDeltaLeftRight;
_ptLast = pt;
final Graphics2D g2 = (Graphics2D) getGraphics();
if( g2 == null )
return;
// If a drag image is not supported by the platform, then draw my own drag image
if (!DragSource.isDragImageSupported())
{
JDragTree.this.paintImmediately(_raGhost.getBounds()); // Rub out the last ghost image and cue line
// And remember where we are about to draw the new ghost image
_raGhost.setRect(pt.x - _ptOffset.x, pt.y - _ptOffset.y, _imgGhost.getWidth(), _imgGhost.getHeight());
g2.drawImage(_imgGhost, AffineTransform.getTranslateInstance(_raGhost.getX(), _raGhost.getY()), null);
}
else // Just rub out the last cue line
JDragTree.this.paintImmediately(_raCueLine.getBounds());
final TreePath path = getClosestPathForLocation(pt.x, pt.y);
if (!(path == _pathLast))
{
_nLeftRight = 0; // We've moved up or down, so reset left/right movement trend
_pathLast = path;
_timerHover.restart();
}
// In any case draw (over the ghost image if necessary) a cue line indicating where a drop will occur
final Rectangle raPath = getPathBounds(path);
_raCueLine.setRect(0, raPath.y+(int)raPath.getHeight(), getWidth(), 2);
g2.setColor(_colorCueLine);
g2.fill(_raCueLine);
// And include the cue line in the area to be rubbed out next time
_raGhost = _raGhost.createUnion(_raCueLine);
// Do this if you want to prohibit dropping onto the drag source
if (path.equals(_pathSource))
e.rejectDrag();
else
e.acceptDrag(e.getDropAction());
}
public void dropActionChanged(final DropTargetDragEvent e)
{
if (!isDragAcceptable(e))
e.rejectDrag();
else
e.acceptDrag(e.getDropAction());
}
public void drop(final DropTargetDropEvent e)
{
_timerHover.stop(); // Prevent hover timer from doing an unwanted expandPath or collapsePath
if (!isDropAcceptable(e))
{
e.rejectDrop();
return;
}
e.acceptDrop(e.getDropAction());
final Transferable transferable = e.getTransferable();
final DataFlavor[] flavors = transferable.getTransferDataFlavors();
for (int i = 0; i < flavors.length; i++ )
{
final DataFlavor flavor = flavors[i];
if (flavor.isMimeTypeEqual(DataFlavor.javaJVMLocalObjectMimeType))
{
try
{
final Point pt = e.getLocation();
final TreePath pathTarget = getClosestPathForLocation(pt.x, pt.y);
final TreePath pathSource = (TreePath) transferable.getTransferData(flavor);
if( (pathTarget == null) || (pathSource == null) )
{
e.dropComplete(false);
return;
}
final MutableTreeNode sourceNode = (MutableTreeNode)pathSource.getLastPathComponent();
final MutableTreeNode oldParent = (MutableTreeNode)sourceNode.getParent();
final MutableTreeNode targetNode = (MutableTreeNode)pathTarget.getLastPathComponent();
final MutableTreeNode newParent = (MutableTreeNode)targetNode.getParent();
if( !sourceNode.isLeaf() && (targetNode.getParent() == sourceNode) )
{
// trying to drag a folder into its own childs
e.dropComplete(false);
return;
}
final DefaultTreeModel model = (DefaultTreeModel)getModel();
final TreePath pathNewChild = null;
if( targetNode.isLeaf() || JDragTree.this.isCollapsed(pathTarget) )
{
// collapsed tree node or leaf
// dropped on a leaf, insert into leaf's parent AFTER leaf
int idx = newParent.getIndex(targetNode);
if( idx < 0 )
{
JDragTree.logger.warning("child not found in parent!!!");
e.dropComplete(false);
return;
}
else
{
idx++; // insert AFTER targetNode
// remove node from oldParent ...
final Object[] removedChilds = { sourceNode };
final int[] childIndices = { oldParent.getIndex(sourceNode) };
sourceNode.removeFromParent();
model.nodesWereRemoved( oldParent, childIndices, removedChilds );
// ... and insert into newParent
if( idx >= newParent.getChildCount() )
{
//newParent.add( sourceNode );
newParent.insert(sourceNode, newParent.getChildCount());
final int insertedIndex[] = { newParent.getChildCount()-1 };
model.nodesWereInserted( newParent, insertedIndex );
}
else
{
newParent.insert(sourceNode, idx);
final int insertedIndex[] = { idx };
model.nodesWereInserted( newParent, insertedIndex );
}
}
}
else
{
// expanded node, insert UNDER the node (before first child)
// remove node from oldParent ...
final Object[] removedChilds = { sourceNode };
final int[] childIndices = { oldParent.getIndex(sourceNode) };
sourceNode.removeFromParent();
model.nodesWereRemoved( oldParent, childIndices, removedChilds );
// ... and add to newParent
targetNode.insert( sourceNode, 0 );
final int insertedIndex[] = { 0 };
model.nodesWereInserted( targetNode, insertedIndex );
}
if (pathNewChild != null)
setSelectionPath(pathNewChild); // Mark this as the selected path in the tree
break; // No need to check remaining flavors
}
catch (final UnsupportedFlavorException ufe)
{
JDragTree.logger.log(Level.SEVERE, "Exception thrown in drop(DropTargetDropEvent e)", ufe);
e.dropComplete(false);
return;
}
catch (final IOException ioe)
{
JDragTree.logger.log(Level.SEVERE, "Exception thrown in drop(DropTargetDropEvent e)", ioe);
e.dropComplete(false);
return;
}
}
}
e.dropComplete(true);
}
// Helpers...
public boolean isDragAcceptable(final DropTargetDragEvent e)
{
// Only accept COPY or MOVE gestures (ie LINK is not supported)
if ((e.getDropAction() & DnDConstants.ACTION_MOVE) == 0)
return false;
// Only accept this particular flavor
if (!e.isDataFlavorSupported(JDragTree.TREEPATH_FLAVOR))
return false;
// Do this if you want to prohibit dropping onto the drag source...
final Point pt = e.getLocation();
final TreePath path = getClosestPathForLocation(pt.x, pt.y);
if((path == null) || path.equals(_pathSource))
return false;
return true;
}
public boolean isDropAcceptable(final DropTargetDropEvent e)
{
// Only accept COPY or MOVE gestures (ie LINK is not supported)
if ((e.getDropAction() & DnDConstants.ACTION_MOVE) == 0)
return false;
// Only accept this particular flavor
if (!e.isDataFlavorSupported(JDragTree.TREEPATH_FLAVOR))
return false;
// Do this if you want to prohibit dropping onto the drag source...
final Point pt = e.getLocation();
final TreePath path = getClosestPathForLocation(pt.x, pt.y);
if( (path == null) || path.equals(_pathSource))
return false;
return true;
}
}
/**
* This represents a TreePath (a node in a JTree) that can be transferred between a drag source and a drop target.
*/
private class CTransferableTreePath implements Transferable
{
private TreePath _path;
/**
* Constructs a transferrable tree path object for the specified path.
*/
public CTransferableTreePath(final TreePath path)
{
_path = path;
}
// Transferable interface methods...
public DataFlavor[] getTransferDataFlavors()
{
return _flavors;
}
public boolean isDataFlavorSupported(final DataFlavor flavor)
{
return java.util.Arrays.asList(_flavors).contains(flavor);
}
public synchronized Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException
{
if (flavor.isMimeTypeEqual(JDragTree.TREEPATH_FLAVOR.getMimeType()))
return _path;
else
throw new UnsupportedFlavorException(flavor);
}
}
private static final Logger logger = Logger.getLogger(JDragTree.class.getName());
private TreePath _pathSource; // The path being dragged
private BufferedImage _imgGhost; // The 'drag image'
private final Point _ptOffset = new Point(); // Where, in the drag image, the mouse was clicked
// The type of DnD object being dragged...
public final static DataFlavor TREEPATH_FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, "TreePath");
private final DataFlavor[] _flavors = { JDragTree.TREEPATH_FLAVOR };
private DragSource dragSource = null;
private DragGestureRecognizer dgRecognizer = null;
/**
* @param root
*/
public JDragTree(final TreeNode root) {
super(root);
initialize();
}
/**
* @param root
*/
public JDragTree(final TreeModel model) {
super(model);
initialize();
}
private void initialize() {
// install drag n drop support
dragSource = DragSource.getDefaultDragSource();
dgRecognizer = dragSource.createDefaultDragGestureRecognizer(this,
DnDConstants.ACTION_MOVE,
this);
// don't act on right mouse button
dgRecognizer.setSourceActions(dgRecognizer.getSourceActions() & ~InputEvent.BUTTON3_MASK & ~InputEvent.BUTTON2_MASK);
new DropTarget(this, new CDropTargetListener());
}
/**
* @param path
* @return
*/
private boolean isRootPath(final TreePath path)
{
return isRootVisible() && (getRowForPath(path) == 0);
}
// DragSourceListener interface methods
public void dragDropEnd(final DragSourceDropEvent e)
{
this.repaint();
}
public void dragEnter(final DragSourceDragEvent e) {}
public void dragExit(final DragSourceEvent e) {}
// DragGestureListener interface method
public void dragGestureRecognized(final DragGestureEvent e)
{
//we should make sure we aren't in edit mode
final InputEvent ievent=e.getTriggerEvent();
if( ievent instanceof MouseEvent )
{
//even though I tell dgRecognizer to ignore the the right mouse button,
// it thinks the RMB starts a drag event...argh
if( (((MouseEvent)ievent).getModifiers() & InputEvent.BUTTON3_MASK) != 0 )
return;
}
// begin dnd
final Point ptDragOrigin = e.getDragOrigin();
final TreePath path = getPathForLocation(ptDragOrigin.x, ptDragOrigin.y);
if (path == null)
return;
if (isRootPath(path))
return; // Ignore user trying to drag the root node
// Work out the offset of the drag point from the TreePath bounding rectangle origin
final Rectangle raPath = getPathBounds(path);
_ptOffset.setLocation(ptDragOrigin.x-raPath.x, ptDragOrigin.y-raPath.y);
// Get the cell renderer (which is a JLabel) for the path being dragged
final JLabel lbl = (JLabel) getCellRenderer().getTreeCellRendererComponent
(
this, // tree
path.getLastPathComponent(), // value
false, // isSelected (dont want a colored background)
this.isExpanded(path), // isExpanded
getModel().isLeaf(path.getLastPathComponent()), // isLeaf
0, // row (not important for rendering)
false // hasFocus (dont want a focus rectangle)
);
lbl.setSize((int)raPath.getWidth(), (int)raPath.getHeight()); // <-- The layout manager would normally do this
// 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);
final Graphics2D g2 = _imgGhost.createGraphics();
// Ask the cell renderer to paint itself into the BufferedImage
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f)); // Make the image ghostlike
lbl.paint(g2);
// Now paint a gradient UNDER the ghosted JLabel text (but not under the icon if any)
// Note: this will need tweaking if your icon is not positioned to the left of the text
final Icon icon = lbl.getIcon();
final int nStartOfText = (icon == null) ? 0 : icon.getIconWidth()+lbl.getIconTextGap();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER, 0.5f)); // Make the gradient ghostlike
g2.setPaint(new GradientPaint(nStartOfText, 0, SystemColor.controlShadow,
getWidth(), 0, new Color(255,255,255,0)));
g2.fillRect(nStartOfText, 0, getWidth(), _imgGhost.getHeight());
g2.dispose();
setSelectionPath(path); // Select this path in the tree
// Wrap the path being transferred into a Transferable object
final Transferable transferable = new CTransferableTreePath(path);
// Remember the path being dragged (because if it is being moved, we will have to delete it later)
_pathSource = path;
// We pass our drag image just in case it IS supported by the platform
e.startDrag(null, _imgGhost, new Point(5,5), transferable, this);
}
public void dragOver(final DragSourceDragEvent e) {}
public void dropActionChanged(final DragSourceDragEvent e) {}
}