Slipstream-Mod-Manager/src/main/java/net/vhati/modmanager/ui/tree/TreeTransferHandler.java

268 lines
9.1 KiB
Java

package net.vhati.modmanager.ui.tree;
import java.awt.Cursor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DragSource;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* A handler to enable drag-and-drop within a JTree.
*
* When dropped, copies of highlighted nodes will be made via clone() and
* inserted at the drop location, then the originals will be removed.
*
* Dragging onto a space between nodes will insert at that location.
* Dragging onto a node that allows children will insert into it.
* Dragging onto a node that doesn't allow children will insert after it.
*
* The TreeModel must be DefaultTreeModel (or a subclass).
* All nodes must be DefaultMutableTreeNode (or a subclass) and properly
* implement Cloneable.
* Set the Jtree's DropMode to ON_OR_INSERT.
* The root node must be hidden, to prevent it from being dragged.
* The tree's selection model may be set to single or multiple.
*/
public class TreeTransferHandler extends TransferHandler {
private DataFlavor localTreePathFlavor = null;
private JTree tree = null;
public TreeTransferHandler( JTree tree ) {
super();
this.tree = tree;
try {
localTreePathFlavor = new DataFlavor( DataFlavor.javaJVMLocalObjectMimeType + ";class=\""+ TreePath[].class.getName() +"\"" );
}
catch ( ClassNotFoundException e ) {
//log.error( e );
}
}
@Override
protected Transferable createTransferable( JComponent c ) {
assert ( c == tree );
TreePath[] highlightedPaths = tree.getSelectionPaths();
Map<Integer,List<TreePath>> pathsByLengthMap = new TreeMap<Integer,List<TreePath>>();
for ( TreePath path : highlightedPaths ) {
if ( path.getPath().length == 1 ) continue; // Omit root node (shouldn't drag it anyway).
Integer pathLength = new Integer( path.getPath().length );
if ( !pathsByLengthMap.containsKey( pathLength ) ) {
pathsByLengthMap.put( pathLength, new ArrayList<TreePath>() );
}
pathsByLengthMap.get( pathLength ).add( path );
}
// For each length (shortest-first), iterate its paths.
// For each of those paths, search longer lengths' lists,
// removing any paths that are descendants of those short ancestor nodes.
List<Integer> lengthsList = new ArrayList<Integer>( pathsByLengthMap.keySet() );
for ( int i=0; i < lengthsList.size(); i++ ) {
for ( TreePath ancestorPath : pathsByLengthMap.get( lengthsList.get( i ) ) ) {
for ( int j=i+1; j < lengthsList.size(); j++ ) {
List<TreePath> childPaths = pathsByLengthMap.get( lengthsList.get( j ) );
for ( Iterator<TreePath> childIt = childPaths.iterator(); childIt.hasNext(); ) {
TreePath childPath = childIt.next();
if ( ancestorPath.isDescendant( childPath ) ) {
childIt.remove();
}
}
}
}
}
List<TreePath> uniquePathList = new ArrayList<TreePath>();
for ( List<TreePath> paths : pathsByLengthMap.values() ) {
uniquePathList.addAll( paths );
}
TreePath[] uniquePathsArray = uniquePathList.toArray( new TreePath[uniquePathList.size()] );
return new TreePathTransferrable( uniquePathsArray );
}
@Override
public boolean canImport( TransferHandler.TransferSupport ts ) {
boolean b = ( ts.getComponent() == tree && ts.isDrop() && ts.isDataFlavorSupported(localTreePathFlavor) );
tree.setCursor( b ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop );
return b;
}
@Override
public int getSourceActions( JComponent comp ) {
return TransferHandler.MOVE;
}
@Override
@SuppressWarnings("Unchecked")
public boolean importData( TransferHandler.TransferSupport ts ) {
if ( !canImport(ts) ) return false;
JTree dstTree = (JTree)ts.getComponent();
DefaultTreeModel dstTreeModel = (DefaultTreeModel)dstTree.getModel();
JTree.DropLocation dl = (JTree.DropLocation)ts.getDropLocation();
TreePath dropPath = dl.getPath(); // Dest parent node, or null.
int dropIndex = dl.getChildIndex(); // Insertion child index in the dest parent node,
// or -1 if dropped onto a group.
dstTree.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
if ( dropPath == null ) return false;
MutableTreeNode dropParentNode = (MutableTreeNode)dropPath.getLastPathComponent();
// When dropping onto a non-group node, insert into the position after it instead.
if ( !dropParentNode.getAllowsChildren() ) {
MutableTreeNode prevParentNode = dropParentNode;
dropPath = dropPath.getParentPath();
dropParentNode = (MutableTreeNode)dropPath.getLastPathComponent();
dropIndex = dropParentNode.getIndex( prevParentNode ) + 1;
}
try {
TreePath[] draggedPaths = (TreePath[])ts.getTransferable().getTransferData( localTreePathFlavor );
// Bail if the dropPath was among those dragged.
boolean badDrop = false;
for ( TreePath path : draggedPaths ) {
if ( path.equals( dropPath ) ) {
badDrop = true;
break;
}
}
if ( !badDrop && dropParentNode.getAllowsChildren() ) {
for ( TreePath path : draggedPaths ) {
// Copy the dragged node and any children.
DefaultMutableTreeNode srcNode = (DefaultMutableTreeNode)path.getLastPathComponent();
MutableTreeNode newNode = (MutableTreeNode)cloneNodes( srcNode );
if ( dropIndex != -1 ) {
// Insert.
dropParentNode.insert( newNode, dropIndex );
dstTreeModel.nodesWereInserted( dropParentNode, new int[]{dropIndex} );
dropIndex++; // Next insertion will be after this node.
}
else {
// Add to the end.
int addIndex = dropParentNode.getChildCount();
dropParentNode.insert( newNode, addIndex );
dstTreeModel.nodesWereInserted( dropParentNode, new int[]{addIndex} );
if ( !dstTree.isExpanded( dropPath ) ) dstTree.expandPath( dropPath );
}
}
return true;
}
}
catch ( Exception e ) {
// UnsupportedFlavorException: if Transferable.getTransferData() fails.
// IOException: if Transferable.getTransferData() fails.
// IllegalStateException: if insert/add fails because dropPath's node doesn't allow children.
//log.error( e );
}
return false;
}
@Override
protected void exportDone( JComponent source, Transferable data, int action ) {
if ( action == TransferHandler.MOVE || action == TransferHandler.NONE ) {
tree.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
}
JTree srcTree = (JTree)source;
DefaultTreeModel srcTreeModel = (DefaultTreeModel)srcTree.getModel();
if ( action == TransferHandler.MOVE ) {
// Remove original dragged rows now that the move completed.
try {
TreePath[] draggedPaths = (TreePath[])data.getTransferData( localTreePathFlavor );
for ( TreePath path : draggedPaths ) {
MutableTreeNode doomedNode = (MutableTreeNode)path.getLastPathComponent();
TreeNode parentNode = doomedNode.getParent();
int doomedIndex = parentNode.getIndex( doomedNode );
doomedNode.removeFromParent();
srcTreeModel.nodesWereRemoved( parentNode, new int[]{doomedIndex}, new Object[]{doomedNode} );
}
}
catch ( Exception e ) {
//log.error( e );
}
}
}
/**
* Recursively clones a node and its descendants.
*
* The clone() methods will generally do a shallow copy, sharing
* userObjects.
*
* Sidenote: The parameter couldn't just be MutableTreeNode, because that
* doesn't offer the clone() method. And blindly using reflection to
* invoke it wouldn't be pretty. Conceivably, a settable factory could be
* designed to copy specific custom classes (using constructors instead
* of clone(). But that'd be overkill.
*/
@SuppressWarnings("Unchecked")
protected MutableTreeNode cloneNodes( DefaultMutableTreeNode srcNode ) {
MutableTreeNode resultNode = (MutableTreeNode)srcNode.clone();
Enumeration enumer = srcNode.children();
while ( enumer.hasMoreElements() ) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement();
int addIndex = resultNode.getChildCount();
resultNode.insert( cloneNodes( (DefaultMutableTreeNode)childNode ), addIndex );
}
return resultNode;
}
/**
* Drag and drop TreePath data, constructed with a raw object
* from a drag source, to be transformed into a flavor
* suitable for the drop target.
*/
private class TreePathTransferrable implements Transferable {
private TreePath[] data;
public TreePathTransferrable( TreePath[] data ) {
this.data = data;
}
@Override
public Object getTransferData( DataFlavor flavor ) {
if ( flavor.equals( localTreePathFlavor ) ) {
return data;
}
return null;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] {localTreePathFlavor};
}
@Override
public boolean isDataFlavorSupported( DataFlavor flavor ) {
return flavor.equals( localTreePathFlavor );
}
}
}