Welcome to the Java Programming Forums


The professional, friendly Java community. 21,500 members and growing!


The Java Programming Forums are a community of Java programmers from all around the World. Our members have a wide range of skills and they all have one thing in common: A passion to learn and code Java. We invite beginner Java programmers right through to Java professionals to post here and share your knowledge. Become a part of the community, help others, expand your knowledge of Java and enjoy talking with like minded people. Registration is quick and best of all free. We look forward to meeting you.


>> REGISTER NOW TO START POSTING


Members have full access to the forums. Advertisements are removed for registered users.

Results 1 to 5 of 5

Thread: Drag and Drop in JTrees

  1. #1
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Drag and Drop in JTrees

    Preface
    I know there are other tutorials about Drag and Drop with JTrees, but I felt that they were for the most part inadequate. The generally covered specific sections really well, but would go back to using "decrepit" methods for other sections, and didn't really give the user a full view of how it should be designed. So, hopefully this is a working compilation of all the examples I found. This has been implemented using JDK 6. Note: You must have JDK 6 (possible 7 when it gets released will work, too) because I make use of the TransferSupport class, a new class in SE 6. Also, note that this is a very long tutorial, as drag and drop is a pain in the you-know-what. This tutorial is aimed at people who have some knowledge of programming Swing GUI programming (mostly the API stuff) and have a good amount of knowledge programming in Java.

    Specifications
    Here's what you can do with this implementation of drag and drop:

    1. Drag/drop multiple nodes at the same time (even only take parts of selections, but you'll have to implement that yourself).
    2. Define how nodes get added/ don't get added.
    3. Define how different actions can be treated.
    4. Define copy or move of tree nodes.
    5. Much much more (some how, this makes me sound like a salesman)

    I chose to have most of the information stored inside of the code (in the form of comments and the actual code). Before each class, I also gave a brief description of what was required of each component.

    JTree extension
    In my implementation, I extended JTree so I can support some basic selection tracking in the tree. This isn't that big of a deal when there's only one node to keep track of, but for multiple selected nodes it's the easiest method. So, what we need in our DnDJtree is:

    1. Implement TreeSelectionListener so we can keep track of selections.
    2. Turn on drag and drop. All JComponents have the interface to handle drag and drop, but not all sub-classes implement the necessary components to make it work.
    3. Setup a transfer handler. The transfer handler is a fairly intricate class that allows Java to transfer objects either to the system or to your program. It's used in both copy/paste and drag and drop. For our application, we'll only be focusing on the drag and drop portion of handling, but realize that the same infrastructure (and in fact, a lot of the same code) can be reused for copy/paste.
    4. (optional) Setting the drop mode. I left the default drop mode which only allows you to drop onto the node, but you can change this to allow your DnDJTree to allow you to "insert" nodes into a location. Note that if you do choose this path, you will have to modify the JTreeTransferHandler because at the moment it's designed only to handle dropping onto a node.


    The DnDJTree class:

    import java.util.ArrayList;
     
    import javax.swing.JTree;
    import javax.swing.event.TreeSelectionEvent;
    import javax.swing.event.TreeSelectionListener;
    import javax.swing.tree.TreeNode;
    import javax.swing.tree.TreePath;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     * 
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDJTree extends JTree implements TreeSelectionListener
    {
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = -4260543969175732269L;
    	/**
    	 * Used to keep track of selected nodes. This list is automatically updated.
    	 */
    	protected ArrayList<TreePath> selected;
     
    	/**
    	 * Constructs a DnDJTree with root as the main node.
    	 * 
    	 * @param root
    	 */
    	public DnDJTree(TreeNode root)
    	{
    		super(root);
    		// turn on the JComponent dnd interface
    		this.setDragEnabled(true);
    		// setup our transfer handler
    		this.setTransferHandler(new JTreeTransferHandler(this));
    		// setup tracking of selected nodes
    		this.selected = new ArrayList<TreePath>();
    		this.addTreeSelectionListener(this);
    	}
     
    	/**
    	 * @return the list of selected nodes
    	 */
    	@SuppressWarnings("unchecked")
    	public ArrayList<TreePath> getSelection()
    	{
    		return (ArrayList<TreePath>) (this.selected).clone();
    	}
     
    	/**
    	 * keeps the list of selected nodes up to date.
    	 * 
    	 * @param e
    	 **/
    	@Override
    	public void valueChanged(TreeSelectionEvent e)
    	{
    		// should contain all the nodes who's state (selected/non selected)
    		// changed
    		TreePath[] selection = e.getPaths();
    		for (int i = 0; i < selection.length; i++)
    		{
    			// for each node who's state changed, either add or remove it form
    			// the selected nodes
    			if (e.isAddedPath(selection[i]))
    			{
    				// node was selected
    				this.selected.add(selection[i]);
    			}
    			else
    			{
    				// node was de-selected
    				this.selected.remove(selection[i]);
    			}
    		}
    	}
    }

    Transfer handler

    From item 3 above, we added a transfer handler for our JTree. However, JTree's default transfer handler rejects all drops. Obviously, we need to change that. So, for our transfer handler we will need:

    1. A method to check if data import is valid.
    2. A method to add nodes to the appropriate drop location, and to remove nodes that were moved rather than copied.

    For moving around nodes in a JTree it's best to do so with the tree model because it should handle the event generation/dispatch, as well as update the changes for us. I'm using the DefaultTreeModel because it works quite well.

    Please note that because of the way I implemented tracking of selected nodes, it's possible that the nodes being dropped won't be in the same "order" as they were previously. ex:

    root
    -child1
    -child2
    -child3

    (drop child2 and child3 into child1)

    root
    -child1
    --child3
    --child2

    This is because the current implementation is based on the order they were queued into the selected queue. To change this, you need to either change the way selected nodes are queued or modify the importData method to take into account the current "order" of selected nodes.

    JTreeTransferHandler class:

    import java.awt.datatransfer.Transferable;
    import java.awt.datatransfer.UnsupportedFlavorException;
    import java.io.IOException;
    import java.util.ArrayList;
     
    import javax.swing.*;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreePath;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     * 
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class JTreeTransferHandler extends TransferHandler
    {
    	/**
    	 * Using tree models allows us to add/remove nodes from a tree and pass the
    	 * appropriate messages.
    	 */
    	protected DefaultTreeModel tree;
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = -6851440217837011463L;
     
    	/**
    	 * Creates a JTreeTransferHandler to handle a certain tree. Note that this
    	 * constructor does NOT set this transfer handler to be that tree's transfer
    	 * handler, you must still add it manually.
    	 * 
    	 * @param tree
    	 */
    	public JTreeTransferHandler(DnDJTree tree)
    	{
    		super();
    		this.tree = (DefaultTreeModel) tree.getModel();
    	}
     
    	/**
    	 * 
    	 * @param c
    	 * @return
    	 */
    	@Override
    	public int getSourceActions(JComponent c)
    	{
    		return TransferHandler.COPY_OR_MOVE;
    	}
     
    	/**
    	 * 
    	 * @param c
    	 * @return null if no nodes were selected, or this transfer handler was not
    	 *         added to a DnDJTree. I don't think it's possible because of the
    	 *         constructor layout, but one more layer of safety doesn't matter.
    	 */
    	@Override
    	protected Transferable createTransferable(JComponent c)
    	{
    		if (c instanceof DnDJTree)
    		{
    			return new DnDTreeList(((DnDJTree) c).getSelection());
    		}
    		else
    		{
    			return null;
    		}
    	}
     
    	/**
    	 * @param c
    	 * @param t
    	 * @param action
    	 */
    	@Override
    	protected void exportDone(JComponent c, Transferable t, int action)
    	{
    		if (action == TransferHandler.MOVE)
    		{
    			// we need to remove items imported from the appropriate source.
    			try
    			{
    				// get back the list of items that were transfered
    				ArrayList<TreePath> list = ((DnDTreeList) t
    						.getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes();
    				for (int i = 0; i < list.size(); i++)
    				{
    					// remove them
    					this.tree.removeNodeFromParent((DnDNode) list.get(i).getLastPathComponent());
    				}
    			}
    			catch (UnsupportedFlavorException exception)
    			{
    				// for debugging purposes (and to make the compiler happy). In
    				// theory, this shouldn't be reached.
    				exception.printStackTrace();
    			}
    			catch (IOException exception)
    			{
    				// for debugging purposes (and to make the compiler happy). In
    				// theory, this shouldn't be reached.
    				exception.printStackTrace();
    			}
    		}
    	}
     
    	/**
    	 * 
    	 * @param supp
    	 * @return
    	 */
    	@Override
    	public boolean canImport(TransferSupport supp)
    	{
    		// Setup so we can always see what it is we are dropping onto.
    		supp.setShowDropLocation(true);
    		if (supp.isDataFlavorSupported(DnDTreeList.DnDTreeList_FLAVOR))
    		{
    			// at the moment, only allow us to import list of DnDNodes
     
    			// Fetch the drop path
    			TreePath dropPath = ((JTree.DropLocation) supp.getDropLocation()).getPath();
    			if (dropPath == null)
    			{
    				// Debugging a few anomalies with dropPath being null. In the
    				// future hopefully this will get removed.
    				System.out.println("Drop path somehow came out null");
    				return false;
    			}
    			// Determine whether we accept the location
    			if (dropPath.getLastPathComponent() instanceof DnDNode)
    			{
    				// only allow us to drop onto a DnDNode
    				try
    				{
    					// using the node-defined checker, see if that node will
    					// accept
    					// every selected node as a child.
    					DnDNode parent = (DnDNode) dropPath.getLastPathComponent();
    					ArrayList<TreePath> list = ((DnDTreeList) supp.getTransferable()
    							.getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes();
    					for (int i = 0; i < list.size(); i++)
    					{
    						if (parent.getAddIndex((DnDNode) list.get(i).getLastPathComponent()) < 0)
    						{
    							return false;
    						}
    					}
     
    					return true;
    				}
    				catch (UnsupportedFlavorException exception)
    				{
    					// Don't allow dropping of other data types. As of right
    					// now,
    					// only DnDNode_FLAVOR and DnDTreeList_FLAVOR are supported.
    					exception.printStackTrace();
    				}
    				catch (IOException exception)
    				{
    					// to make the compiler happy.
    					exception.printStackTrace();
    				}
    			}
    		}
    		// something prevented this import from going forward
    		return false;
    	}
     
    	/**
    	 * 
    	 * @param supp
    	 * @return
    	 */
    	@Override
    	public boolean importData(TransferSupport supp)
    	{
    		if (this.canImport(supp))
    		{
     
    			try
    			{
    				// Fetch the data to transfer
    				Transferable t = supp.getTransferable();
    				ArrayList<TreePath> list = ((DnDTreeList) t
    						.getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes();
    				// Fetch the drop location
    				TreePath loc = ((javax.swing.JTree.DropLocation) supp.getDropLocation()).getPath();
    				// Insert the data at this location
    				for (int i = 0; i < list.size(); i++)
    				{
    					this.tree.insertNodeInto((DnDNode) list.get(i).getLastPathComponent(),
    							(DnDNode) loc.getLastPathComponent(), ((DnDNode) loc
    									.getLastPathComponent()).getAddIndex((DnDNode) list.get(i)
    									.getLastPathComponent()));
    				}
    				// success!
    				return true;
    			}
    			catch (UnsupportedFlavorException e)
    			{
    				// In theory, this shouldn't be reached because we already
    				// checked to make sure imports were valid.
    				e.printStackTrace();
    			}
    			catch (IOException e)
    			{
    				// In theory, this shouldn't be reached because we already
    				// checked to make sure imports were valid.
    				e.printStackTrace();
    			}
    		}
    		// import isn't allowed at this time.
    		return false;
    	}
    }

    DnDNode

    The standard DefaultMutableTreeNode doesn't implement the Transferable interface, vital to use Java's framework for transfering data. Fortunately, it's not too hard to implement the interface. It's also very important that DnDNode be serializable. The default drag and drop framework requires that everything be serializable (including all the data inside). This was a common problem I ran into when I tried this on a custom object I created that wasn't serializable. The beauty of the Serializable interface, though, is that there are no methods you have to implement (makes me wonder why all objects aren't serializable).

    Because the data I had was highly dependent on where it was in the tree (particularly what children it had) I designed this node to function so you could implement sub-classes and they would still function correctly.

    DnDNode class:

    package tree;
     
    import java.awt.datatransfer.*;
    import java.io.IOException;
    import java.io.Serializable;
     
    import javax.swing.tree.DefaultMutableTreeNode;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     * 
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDNode extends DefaultMutableTreeNode implements Transferable, Serializable
    {
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 4816704492774592665L;
     
    	/**
    	 * data flavor used to get back a DnDNode from data transfer
    	 */
    	public static final DataFlavor DnDNode_FLAVOR = new DataFlavor(DnDNode.class,
    			"Drag and drop Node");
     
    	/**
    	 * list of all flavors that this DnDNode can be transfered as
    	 */
    	protected static DataFlavor[] flavors = { DnDNode.DnDNode_FLAVOR };
     
    	public DnDNode()
    	{
    		super();
    	}
     
    	/**
    	 * 
    	 * Constructs
    	 * 
    	 * @param data
    	 */
    	public DnDNode(Serializable data)
    	{
    		super(data);
    	}
     
    	/**
    	 * Determines if we can add a certain node as a child of this node.
    	 * 
    	 * @param node
    	 * @return
    	 */
    	public boolean canAdd(DnDNode node)
    	{
    		if (node != null)
    		{
    			if (!this.equals(node.getParent()))
    			{
    				if ((!this.equals(node)))
    				{
    					return true;
    				}
    			}
    		}
    		return false;
    	}
     
    	/**
    	 * Gets the index node should be inserted at to maintain sorted order. Also
    	 * performs checking to see if that node can be added to this node. By
    	 * default, DnDNode adds children at the end.
    	 * 
    	 * @param node
    	 * @return the index to add at, or -1 if node can not be added
    	 */
    	public int getAddIndex(DnDNode node)
    	{
    		if (!this.canAdd(node))
    		{
    			return -1;
    		}
    		return this.getChildCount();
    	}
     
    	/**
    	 * Checks this node for equality with another node. To be equal, this node
    	 * and all of it's children must be equal. Note that the parent/ancestors do
    	 * not need to match at all.
    	 * 
    	 * @param o
    	 * @return
    	 */
    	@Override
    	public boolean equals(Object o)
    	{
    		if (o == null)
    		{
    			return false;
    		}
    		else if (!(o instanceof DnDNode))
    		{
    			return false;
    		}
    		else if (!this.equalsNode((DnDNode) o))
    		{
    			return false;
    		}
    		else if (this.getChildCount() != ((DnDNode) o).getChildCount())
    		{
    			return false;
    		}
    		{
    			// compare all children
    			for (int i = 0; i < this.getChildCount(); i++)
    			{
    				if (!this.getChildAt(i).equals(((DnDNode) o).getChildAt(i)))
    				{
    					return false;
    				}
    			}
    			// they are equal!
    			return true;
    		}
    	}
     
    	/**
    	 * Compares if this node is equal to another node. In this method, children
    	 * and ancestors are not taken into concideration.
    	 * 
    	 * @param node
    	 * @return
    	 */
    	public boolean equalsNode(DnDNode node)
    	{
    		if (node != null)
    		{
    			if (this.getAllowsChildren() == node.getAllowsChildren())
    			{
    				if (this.getUserObject() != null)
    				{
    					if (this.getUserObject().equals(node.getUserObject()))
    					{
    						return true;
    					}
    				}
    				else
    				{
    					if (node.getUserObject() == null)
    					{
    						return true;
    					}
    				}
    			}
    		}
    		return false;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 * @throws UnsupportedFlavorException
    	 * @throws IOException
    	 **/
    	@Override
    	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
    	{
    		if (this.isDataFlavorSupported(flavor))
    		{
    			return this;
    		}
    		else
    		{
    			throw new UnsupportedFlavorException(flavor);
    		}
    	}
     
    	/**
    	 * @return
    	 **/
    	@Override
    	public DataFlavor[] getTransferDataFlavors()
    	{
    		return DnDNode.flavors;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 **/
    	@Override
    	public boolean isDataFlavorSupported(DataFlavor flavor)
    	{
    		DataFlavor[] flavs = this.getTransferDataFlavors();
    		for (int i = 0; i < flavs.length; i++)
    		{
    			if (flavs[i].equals(flavor))
    			{
    				return true;
    			}
    		}
    		return false;
    	}
     
    	/**
    	 * @param temp
    	 * @return
    	 */
    	public int indexOfNode(DnDNode node)
    	{
    		if (node == null)
    		{
    			throw new NullPointerException();
    		}
    		else
    		{
    			for (int i = 0; i < this.getChildCount(); i++)
    			{
    				if (this.getChildAt(i).equals(node))
    				{
    					return i;
    				}
    			}
    			return -1;
    		}
    	}
    }

    DnD nodes list

    Unfortunately, I didn't find any standard collection classes that implemented transferable, but that's ok because our own implementation is very simple. Here's what we need:

    1. Some sort of list to store nodes to transfer. i chose an ArrayList because I figured that this list will be accessed randomly a lot, but won't really be changed once it has been created.
    2. Implementation of the Transferable interface. The implementation will look an awful lot like the DnDNodes.

    DnDTreeList class:

    import java.awt.datatransfer.*;
    import java.io.IOException;
    import java.io.Serializable;
    import java.util.ArrayList;
     
    import javax.swing.tree.TreePath;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     * 
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDTreeList implements Transferable, Serializable
    {
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 1270874212613332692L;
    	/**
    	 * Data flavor that allows a DnDTreeList to be extracted from a transferable
    	 * object
    	 */
    	public final static DataFlavor DnDTreeList_FLAVOR = new DataFlavor(DnDTreeList.class,
    			"Drag and drop list");
    	/**
    	 * List of flavors this DnDTreeList can be retrieved as. Currently only
    	 * supports DnDTreeList_FLAVOR
    	 */
    	protected static DataFlavor[] flavors = { DnDTreeList.DnDTreeList_FLAVOR };
     
    	/**
    	 * Nodes to transfer
    	 */
    	protected ArrayList<TreePath> nodes;
     
    	/**
    	 * @param selection
    	 */
    	public DnDTreeList(ArrayList<TreePath> nodes)
    	{
    		this.nodes = nodes;
    	}
     
    	public ArrayList<TreePath> getNodes()
    	{
    		return this.nodes;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 * @throws UnsupportedFlavorException
    	 * @throws IOException
    	 **/
    	@Override
    	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
    	{
    		if (this.isDataFlavorSupported(flavor))
    		{
    			return this;
    		}
    		else
    		{
    			throw new UnsupportedFlavorException(flavor);
    		}
    	}
     
    	/**
    	 * @return
    	 **/
    	@Override
    	public DataFlavor[] getTransferDataFlavors()
    	{
    		// TODO Auto-generated method stub
    		return DnDTreeList.flavors;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 **/
    	@Override
    	public boolean isDataFlavorSupported(DataFlavor flavor)
    	{
    		DataFlavor[] flavs = this.getTransferDataFlavors();
    		for (int i = 0; i < flavs.length; i++)
    		{
    			if (flavs[i].equals(flavor))
    			{
    				return true;
    			}
    		}
    		return false;
    	}
     
    }

    Conclusion

    That's pretty much all there is to implementing a "simple" drag and drop for JTrees that allows multiple node trasfers. If you want, here's a small test application that sets up a DnDJTree with some nodes.

    public class DnDJTreeApp
    {
    	public static void main(String[] args)
    	{
    		DnDNode root = new DnDNode("root");
    		DnDNode child = new DnDNode("parent 1");
    		root.add(child);
    		child = new DnDNode("parent 2");
    		root.add(child);
    		child = new DnDNode("parent 3");
    		child.add(new DnDNode("child 1"));
    		child.add(new DnDNode("child 2"));
    		root.add(child);
    		DnDJTree tree = new DnDJTree(root);
    		JFrame frame = new JFrame("Drag and drop JTrees");
    		frame.getContentPane().add(tree);
    		frame.setSize(600, 400);
    		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    		frame.setVisible(true);
    	}
    }

    Happy coding

    edit: made some minor changes to DnDNode so it supports null user objects correctly now.
    Last edited by helloworld922; January 24th, 2010 at 12:55 AM.

  2. The Following 3 Users Say Thank You to helloworld922 For This Useful Post:

    copeg (January 24th, 2010), Den3107 (March 20th, 2014), JavaPF (January 24th, 2010)


  3. #2
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Re: Drag and Drop in JTrees

    I found out that the data can't actually be imported until a message is sent to allow data import (hidden away in the Swing source code). This isn't a problem with internal data transfer because your application can access the data directly, but it does make it difficult if you want to transfer data across different applications. Unfortunately, there's not really a good way to over-ride this without re-writing large chunks of the drag and drop support. I am currently working on a second version of this code to avoid this issue while maintaining functionality, as well as add extra functionality (like triggering events, support for inserting nodes, and support structure for copy/paste).

  4. #3
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Re: Drag and Drop in JTrees

    Here's an updated version of the guide. Note that I added several external functions, bloating the code by at least two times. Hopefully everything works now.

    Preface
    I know there are other tutorials about Drag and Drop with JTrees, but I felt that they were for the most part inadequate. The generally covered specific sections really well, but would go back to using "decrepit" methods for other sections, and didn't really give the user a full view of how it should be designed. So, hopefully this is a working compilation of all the examples I found. This has been implemented using JDK 6. Note: You must have JDK 6 (possible 7 when it gets released will work, too) because I make use of the TransferSupport class, a new class in SE 6. Also, note that this is a very long tutorial, as drag and drop is a pain in the you-know-what. This tutorial is aimed at people who have some knowledge of programming Swing GUI programming (mostly the API stuff) and have a good amount of knowledge programming in Java.

    Specifications
    Here's what you can do with this implementation of drag and drop:

    1. Drag/drop multiple nodes at the same time.
    2. Define how nodes get added/ don't get added. *changed
    3. Define how different actions can be treated. *changed
    4. Define copy or move of tree nodes. *changed
    5. Interface for cut/copy/paste implemented *new
    6. A basic undo system implemented *new
    7. Much much more (some how, this makes me sound like a salesman)

    I chose to have most of the information stored inside of the code (in the form of comments and the actual code). Before each class, I also gave a brief description of what was required of each component.

    JTree extension
    In my implementation, I extended JTree so I can support some basic selection tracking in the tree. This isn't that big of a deal when there's only one node to keep track of, but for multiple selected nodes it's the easiest method. So, what we need in our DnDJtree is:

    1. Use the default JTree selection methods to get the selection. Method was modified to only get the "top" selections, ie. if a parent and one or more of it's children are selected, only the parent is removed as being selected (decreases data transfer and potential data duplication)
    2. Turn on drag and drop. All JComponents have the interface to handle drag and drop, but not all sub-classes implement the necessary components to make it work.
    3. Setup a transfer handler. The transfer handler is a fairly intricate class that allows Java to transfer objects either to the system or to your program. It's used in both copy/paste and drag and drop. For our application, we'll only be focusing on the drag and drop portion of handling, but realize that the same infrastructure (and in fact, a lot of the same code) can be reused for copy/paste.
    4. Setting the drop mode. I left the default drop mode which only allows you to drop onto the node, but you can change this to allow your DnDJTree to allow you to "insert" nodes into a location. This version of code supports the insertion of nodes, but you can turn this off by changing the drop mode.
    5. Setup my own TreeModel. See the section about DnDTreeModel, but for the most part it's almost the same as DefaultTreeModel.
    6. Setup a basic undo/redo model. It uses a pseudo-stack and keeps track of individual add/remove events (TreeEvent). Performing multiple actions with one undo/redo trigger is kept track with a timer (anything that happens within 100ms of the first event that can be undone is added to that list).
    7. Setup a source where different events can be triggered (cut, copy, paste, undo/redo). Note that I chose to leave this part to be implemented externally so the DnDJTree and be plugged into any application you want.

    The DnDJTree class:

    package tree;
     
    import java.awt.AWTEvent;
    import java.awt.Rectangle;
    import java.awt.event.*;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.util.ArrayList;
     
    import javax.swing.*;
    import javax.swing.event.TreeModelEvent;
    import javax.swing.event.TreeModelListener;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreePath;
     
    import actions.*;
    import events.UndoEventCap;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDJTree extends JTree implements MouseListener, PropertyChangeListener,
    		TreeModelListener, ActionListener
    {
    	private static final long serialVersionUID = -4260543969175732269L;
    	protected Node<AWTEvent> undoLoc;
     
    	private boolean undoStack;
    	private boolean doingUndo;
     
    	/**
    	 * Constructs a DnDJTree with root as the main node.
    	 * 
    	 * @param root
    	 */
    	public DnDJTree(DnDNode root)
    	{
    		super();
    		this.setModel(new DnDTreeModel(root));
    		// turn on the JComponent dnd interface
    		this.setDragEnabled(true);
    		// setup our transfer handler
    		this.setTransferHandler(new JTreeTransferHandler(this));
    		this.setDropMode(DropMode.ON_OR_INSERT);
    		this.setScrollsOnExpand(true);
    		// this.addTreeSelectionListener(this);
    		this.addMouseListener(this);
    		this.getModel().addTreeModelListener(this);
    		this.undoLoc = new Node<AWTEvent>(null);
    	}
     
    	/**
    	 * 
    	 * @param e
    	 */
    	protected void addUndo(AWTEvent e)
    	{
    		this.undoLoc.linkNext(new Node<AWTEvent>(e));
    		this.undoLoc = this.undoLoc.next;
    	}
     
    	/**
    	 * Only returns the top level selection<br>
    	 * ex. if a child and it's parent are selected, only it's parent is returned
    	 * in the list.
    	 * 
    	 * @return an array of TreePath objects indicating the selected nodes, or
    	 *         null if nothing is currently selected
    	 */
    	@Override
    	public TreePath[] getSelectionPaths()
    	{
    		// get all selected paths
    		TreePath[] temp = super.getSelectionPaths();
    		if (temp != null)
    		{
    			ArrayList<TreePath> list = new ArrayList<TreePath>();
    			for (int i = 0; i < temp.length; i++)
    			{
    				// determine if a node can be added
    				boolean canAdd = true;
    				for (int j = 0; j < list.size(); j++)
    				{
    					if (temp[i].isDescendant(list.get(j)))
    					{
    						// child was a descendant of another selected node,
    						// disallow add
    						canAdd = false;
    						break;
    					}
    				}
    				if (canAdd)
    				{
    					list.add(temp[i]);
    				}
    			}
    			return list.toArray(new TreePath[list.size()]);
    		}
    		else
    		{
    			// no paths selected
    			return null;
    		}
    	}
     
    	/**
    	 * Implemented a check to make sure that it is possible to de-select all
    	 * nodes. If this component is added as a mouse listener of another
    	 * component, that componenet can trigger a deselect of all nodes.
    	 * <p>
    	 * This method also allows for de-select if a blank spot inside this tree is
    	 * selected. Note that using the expand/contract button next to the label
    	 * will not cause a de-select.
    	 * <p>
    	 * if the given mouse event was from a popup trigger, was not BUTTON1, or
    	 * shift/control were pressed, a deselect is not triggered.
    	 * 
    	 * @param e
    	 **/
    	@Override
    	public void mouseClicked(MouseEvent e)
    	{
    		if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1 && !e.isShiftDown() && !e
    				.isControlDown())
    		{
    			if (e.getSource() != this)
    			{
    				// source was elsewhere, deselect
    				this.clearSelection();
    			}
    			else
    			{
    				// get the potential selection bounds
    				Rectangle bounds = this.getRowBounds(this.getClosestRowForLocation(e.getX(), e
    						.getY()));
    				if (!bounds.contains(e.getPoint()))
    				{
    					// need to check to see if the expand box was clicked
    					Rectangle check = new Rectangle(bounds.x - 15, bounds.y, 9, bounds.height);
    					if (!check.contains(e.getPoint()))
    					{
    						this.clearSelection();
    					}
    				}
    			}
    		}
    	}
     
    	/**
    	 * @param e
    	 **/
    	@Override
    	public void mouseEntered(MouseEvent e)
    	{}
     
    	/**
    	 * @param e
    	 **/
    	@Override
    	public void mouseExited(MouseEvent e)
    	{}
     
    	/**
    	 * @param e
    	 **/
    	@Override
    	public void mousePressed(MouseEvent e)
    	{}
     
    	/**
    	 * @param e
    	 **/
    	@Override
    	public void mouseReleased(MouseEvent e)
    	{}
     
    	public void performRedo()
    	{
    		if (this.undoLoc.next != null)
    		{
    			this.undoLoc = this.undoLoc.next;
    			if (this.undoLoc.data instanceof UndoEventCap)
    			{
    				// should be start cap. else, diagnostic output
    				if (((UndoEventCap) this.undoLoc.data).isStart())
    				{
    					this.doingUndo = true;
    					this.undoLoc = this.undoLoc.next;
    					while (!(this.undoLoc.data instanceof UndoEventCap))
    					{
    						if (this.undoLoc.data instanceof TreeEvent)
    						{
    							// perform the action
    							if (this.undoLoc.data instanceof TreeEvent)
    							{
    								this.performTreeEvent(((TreeEvent) this.undoLoc.data));
    							}
    						}
    						this.undoLoc = this.undoLoc.next;
    					}
    					this.doingUndo = false;
    				}
    				else
    				{
    					System.out.println("undo stack problems");
    				}
    			}
    		}
    	}
     
    	public void performUndo()
    	{
    		DefaultTreeModel model = (DefaultTreeModel) this.getModel();
    		if (this.undoLoc.data instanceof UndoEventCap)
    		{
    			// should be end cap. else, diagnostic output
    			if (!((UndoEventCap) this.undoLoc.data).isStart())
    			{
    				this.doingUndo = true;
    				this.undoLoc = this.undoLoc.prev;
    				while (!(this.undoLoc.data instanceof UndoEventCap))
    				{
    					if (this.undoLoc.data instanceof TreeEvent)
    					{
    						// perform inverse
    						// System.out.println(((AddRemoveEvent)
    						// this.undoLoc.data).invert());
    						this.performTreeEvent(((TreeEvent) this.undoLoc.data).invert());
    					}
    					this.undoLoc = this.undoLoc.prev;
    				}
    				// move to previous
    				if (this.undoLoc.prev != null)
    				{
    					this.undoLoc = this.undoLoc.prev;
    				}
    				this.doingUndo = false;
    			}
    			else
    			{
    				System.out.println("undo stack problems");
    			}
    		}
    	}
     
    	public void performTreeEvent(TreeEvent e)
    	{
    		DefaultTreeModel model = (DefaultTreeModel) this.getModel();
    		if (e.isAdd())
    		{
    			model.insertNodeInto(e.getNode(), e.getDestination(), e.getIndex());
    		}
    		else
    		{
    			model.removeNodeFromParent(e.getNode());
    		}
    	}
     
    	/**
    	 * @param evt
    	 */
    	@Override
    	public void propertyChange(PropertyChangeEvent evt)
    	{
    		if (evt.getPropertyName().equals(DeleteAction.DO_DELETE))
    		{
    			// perform delete
    			DefaultTreeModel model = (DefaultTreeModel) this.getModel();
    			TreePath[] selection = this.getSelectionPaths();
    			if (selection != null)
    			{
    				// something is selected, delete it
    				for (int i = 0; i < selection.length; i++)
    				{
    					if (((DnDNode) selection[i].getLastPathComponent()).getLevel() > 1)
    					{
    						// TODO send out action to partially remove node
    						model.removeNodeFromParent((DnDNode) selection[i].getLastPathComponent());
    					}
    				}
    			}
    		}
    		else if (evt.getPropertyName().equals(UndoAction.DO_UNDO))
    		{
    			this.performUndo();
    		}
    		else if (evt.getPropertyName().equals(RedoAction.DO_REDO))
    		{
    			this.performRedo();
    		}
    		else
    		{
    			System.out.println(evt.getPropertyName());
    		}
    	}
     
    	/**
    	 * @param e
    	 */
    	@Override
    	public void treeNodesChanged(TreeModelEvent e)
    	{
    		// TODO Auto-generated method stub
    		System.out.println("nodes changed");
    	}
     
    	/**
    	 * @param e
    	 */
    	@Override
    	public void treeNodesInserted(TreeModelEvent e)
    	{
    		// TODO Auto-generated method stub
    		if (!this.doingUndo)
    		{
    			this.checkUndoStatus();
    			System.out.println("inserted");
    			int index = e.getChildIndices()[0];
    			DnDNode parent = (DnDNode) e.getTreePath().getLastPathComponent();
    			this.addUndo(new TreeEvent(this, true, parent, (DnDNode) e.getChildren()[0], index));
    		}
    	}
     
    	/**
    	 * @param e
    	 */
    	@Override
    	public void treeNodesRemoved(TreeModelEvent e)
    	{
    		// TODO Auto-generated method stub
    		if (!this.doingUndo)
    		{
    			this.checkUndoStatus();
    			System.out.println("removed");
    			int index = e.getChildIndices()[0];
    			DnDNode parent = (DnDNode) e.getTreePath().getLastPathComponent();
    			this.addUndo(new TreeEvent(this, false, parent, (DnDNode) e.getChildren()[0], index));
    		}
    	}
     
    	/**
    	 * @param e
    	 */
    	@Override
    	public void treeStructureChanged(TreeModelEvent e)
    	{
    		// TODO Auto-generated method stub
    		System.out.println("structure changed");
    	}
     
    	/**
    	 * @param e
    	 */
    	protected void checkUndoStatus()
    	{
    		if (!this.undoStack)
    		{
    			this.undoStack = true;
    			this.addUndo(new UndoEventCap(this, true));
    			Timer timer = new Timer(100, this);
    			timer.setRepeats(false);
    			timer.setActionCommand("update");
    			timer.start();
    		}
    	}
     
    	/**
    	 * @param e
    	 */
    	@Override
    	public void actionPerformed(ActionEvent e)
    	{
    		if (e.getActionCommand().equals("update") && this.undoStack)
    		{
    			this.undoStack = false;
    			this.addUndo(new UndoEventCap(this, false));
    		}
    	}
    }

    Transfer handler

    From item 3 above, we added a transfer handler for our JTree. However, JTree's default transfer handler rejects all drops. Obviously, we need to change that. So, for our transfer handler we will need:

    1. A method to check if data import is valid.
    2. A method to add nodes to the appropriate drop location, and to remove nodes that were moved rather than copied.

    For moving around nodes in a JTree it's best to do so with the tree model because it should handle the event generation/dispatch, as well as update the changes for us. I'm using my own DnDTreeModel because the default one has trouble removing the correct node (it uses an equals() comparison, but what is really necessary is an absolute object equality).

    Children will now be moved in the "correct order".
    ex.

    root
    - child1
    - child2
    - child3

    move child2 and child3 above child1:

    root
    - child2
    - child3
    - child1

    JTreeTransferHandler class:

    package tree;
     
    import java.awt.datatransfer.*;
    import java.io.IOException;
    import java.util.ArrayList;
     
    import javax.swing.*;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreePath;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class JTreeTransferHandler extends TransferHandler
    {
    	protected DnDJTree tree;
    	private static final long serialVersionUID = -6851440217837011463L;
     
    	/**
    	 * Creates a JTreeTransferHandler to handle a certain tree. Note that this
    	 * constructor does NOT set this transfer handler to be that tree's transfer
    	 * handler, you must still add it manually.
    	 * 
    	 * @param tree
    	 */
    	public JTreeTransferHandler(DnDJTree tree)
    	{
    		super();
    		this.tree = tree;
    	}
     
    	/**
    	 * @param supp
    	 * @return
    	 */
    	@Override
    	public boolean canImport(TransferSupport supp)
    	{
    		if (supp.isDataFlavorSupported(DnDTreeList.DnDTreeList_FLAVOR))
    		{
    			DnDNode[] destPaths = null;
    			// get the destination paths
    			if (supp.isDrop())
    			{
    				TreePath dropPath = ((JTree.DropLocation) supp.getDropLocation()).getPath();
    				if (dropPath == null)
    				{
    					// debugging a few anomalies with dropPath being null.
    					System.out.println("Drop path somehow came out null");
    					return false;
    				}
    				if (dropPath.getLastPathComponent() instanceof DnDNode)
    				{
    					destPaths = new DnDNode[1];
    					destPaths[0] = (DnDNode) dropPath.getLastPathComponent();
    				}
    			}
    			else
    			{
    				// cut/copy, get all selected paths as potential drop paths
    				TreePath[] paths = this.tree.getSelectionPaths();
    				if (paths == null)
    				{
    					// possibility no nodes were selected, do nothing
    					return false;
    				}
    				destPaths = new DnDNode[paths.length];
    				for (int i = 0; i < paths.length; i++)
    				{
    					destPaths[i] = (DnDNode) paths[i].getLastPathComponent();
    				}
    			}
    			for (int i = 0; i < destPaths.length; i++)
    			{
    				// check all destinations accept all nodes being transfered
    				DataFlavor[] incomingFlavors = supp.getDataFlavors();
    				for (int j = 1; j < incomingFlavors.length; j++)
    				{
    					if (!destPaths[i].canImport(incomingFlavors[j]))
    					{
    						// found one unsupported import, invalidate whole import
    						return false;
    					}
    				}
    			}
    			return true;
    		}
    		return false;
    	}
     
    	/**
    	 * @param c
    	 * @return null if no nodes were selected, or this transfer handler was not
    	 *         added to a DnDJTree. I don't think it's possible because of the
    	 *         constructor layout, but one more layer of safety doesn't matter.
    	 */
    	@Override
    	protected Transferable createTransferable(JComponent c)
    	{
    		if (c instanceof DnDJTree)
    		{
    			((DnDJTree) c).setSelectionPaths(((DnDJTree) c).getSelectionPaths());
    			return new DnDTreeList(((DnDJTree) c).getSelectionPaths());
    		}
    		else
    		{
    			return null;
    		}
    	}
     
    	/**
    	 * @param c
    	 * @param t
    	 * @param action
    	 */
    	@Override
    	protected void exportDone(JComponent c, Transferable t, int action)
    	{
    		if (action == TransferHandler.MOVE)
    		{
    			// we need to remove items imported from the appropriate source.
    			try
    			{
    				// get back the list of items that were transfered
    				ArrayList<TreePath> list = ((DnDTreeList) t
    						.getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes();
    				for (int i = 0; i < list.size(); i++)
    				{
    					// get the source
    					DnDNode sourceNode = (DnDNode) list.get(i).getLastPathComponent();
    					DefaultTreeModel model = (DefaultTreeModel) this.tree.getModel();
    					model.removeNodeFromParent(sourceNode);
    				}
    			}
    			catch (UnsupportedFlavorException exception)
    			{
    				// for debugging purposes (and to make the compiler happy). In
    				// theory, this shouldn't be reached.
    				exception.printStackTrace();
    			}
    			catch (IOException exception)
    			{
    				// for debugging purposes (and to make the compiler happy). In
    				// theory, this shouldn't be reached.
    				exception.printStackTrace();
    			}
    		}
    	}
     
    	/**
    	 * @param c
    	 * @return
    	 */
    	@Override
    	public int getSourceActions(JComponent c)
    	{
    		return TransferHandler.COPY_OR_MOVE;
    	}
     
    	/**
    	 * 
    	 * @param supp
    	 * @return
    	 */
    	@Override
    	public boolean importData(TransferSupport supp)
    	{
    		if (this.canImport(supp))
    		{
    			try
    			{
    				// Fetch the data to transfer
    				Transferable t = supp.getTransferable();
    				ArrayList<TreePath> list;
     
    				list = ((DnDTreeList) t.getTransferData(DnDTreeList.DnDTreeList_FLAVOR)).getNodes();
     
    				TreePath[] destPaths;
    				DefaultTreeModel model = (DefaultTreeModel) this.tree.getModel();
    				if (supp.isDrop())
    				{
    					// the destination path is the location
    					destPaths = new TreePath[1];
    					destPaths[0] = ((javax.swing.JTree.DropLocation) supp.getDropLocation())
    							.getPath();
    				}
    				else
    				{
    					// pasted, destination is all selected nodes
    					destPaths = this.tree.getSelectionPaths();
    				}
    				// create add events
    				for (int i = 0; i < destPaths.length; i++)
    				{
    					// process each destination
    					DnDNode destNode = (DnDNode) destPaths[i].getLastPathComponent();
    					for (int j = 0; j < list.size(); j++)
    					{
    						// process each node to transfer
    						int destIndex = -1;
    						DnDNode sourceNode = (DnDNode) list.get(j).getLastPathComponent();
    						// case where we moved the node somewhere inside of the
    						// same node
    						boolean specialMove = false;
    						if (supp.isDrop())
    						{
    							// chance to drop to a determined location
    							destIndex = ((JTree.DropLocation) supp.getDropLocation())
    									.getChildIndex();
    						}
    						if (destIndex == -1)
    						{
    							// use the default drop location
    							destIndex = destNode.getAddIndex(sourceNode);
    						}
    						else
    						{
    							// update index for a determined location in case of
    							// any shift
    							destIndex += j;
    						}
    						model.insertNodeInto(sourceNode, destNode, destIndex);
    					}
    				}
    				return true;
    			}
    			catch (UnsupportedFlavorException exception)
    			{
    				// TODO Auto-generated catch block
    				exception.printStackTrace();
    			}
    			catch (IOException exception)
    			{
    				// TODO Auto-generated catch block
    				exception.printStackTrace();
    			}
    		}
    		// import isn't allowed at this time.
    		return false;
    	}
    }

    DnDNode

    The standard DefaultMutableTreeNode doesn't implement the Transferable interface, vital to use Java's framework for transfering data. Fortunately, it's not too hard to implement the interface. It's also very important that DnDNode be serializable. The default drag and drop framework requires that everything be serializable (including all the data inside). This was a common problem I ran into when I tried this on a custom object I created that wasn't serializable. The beauty of the Serializable interface, though, is that there are no methods you have to implement (makes me wonder why all objects aren't serializable).

    Because the data I had was highly dependent on where it was in the tree (particularly what children it had) I designed this node to function so you could implement sub-classes and they would still function correctly.

    DnDNode class:

    package tree;
     
    import java.awt.datatransfer.*;
    import java.io.IOException;
    import java.io.Serializable;
     
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.MutableTreeNode;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDNode extends DefaultMutableTreeNode implements Transferable, Serializable,
    		Cloneable
    {
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 4816704492774592665L;
     
    	/**
    	 * data flavor used to get back a DnDNode from data transfer
    	 */
    	public static final DataFlavor DnDNode_FLAVOR = new DataFlavor(DnDNode.class,
    			"Drag and drop Node");
     
    	/**
    	 * list of all flavors that this DnDNode can be transfered as
    	 */
    	protected static DataFlavor[] flavors = { DnDNode.DnDNode_FLAVOR };
     
    	public DnDNode()
    	{
    		super();
    	}
     
    	/**
    	 * Constructs
    	 * 
    	 * @param data
    	 */
    	public DnDNode(Serializable data)
    	{
    		super(data);
    	}
     
    	/**
    	 * Determines if we can add a certain node as a child of this node.
    	 * 
    	 * @param node
    	 * @return
    	 */
    	public boolean canAdd(DnDNode node)
    	{
    		if (node != null)
    		{
    			// if (!this.equals(node.getParent()))
    			// {
    			if ((!this.equals(node)))
    			{
    				return true;
    			}
    			// }
    		}
    		return false;
    	}
     
    	/**
    	 * @param dataFlavor
    	 * @return
    	 */
    	public boolean canImport(DataFlavor flavor)
    	{
    		return this.isDataFlavorSupported(flavor);
    	}
     
    	/**
    	 * Dummy clone. Just returns this
    	 * 
    	 * @return
    	 */
    	@Override
    	public Object clone()
    	{
    		DnDNode node = this.cloneNode();
    		for (int i = 0; i < this.getChildCount(); i++)
    		{
    			node.add((MutableTreeNode) ((DnDNode) this.getChildAt(i)).clone());
    		}
     
    		return node;
    	}
     
    	/**
    	 * 
    	 * @return
    	 */
    	public DnDNode cloneNode()
    	{
    		DnDNode node = new DnDNode((Serializable) this.userObject);
    		node.setAllowsChildren(this.getAllowsChildren());
    		return node;
    	}
     
    	/**
    	 * Checks this node for equality with another node. To be equal, this node
    	 * and all of it's children must be equal. Note that the parent/ancestors do
    	 * not need to match at all.
    	 * 
    	 * @param o
    	 * @return
    	 */
    	@Override
    	public boolean equals(Object o)
    	{
    		if (o == null)
    		{
    			return false;
    		}
    		else if (!(o instanceof DnDNode))
    		{
    			return false;
    		}
    		else if (!this.equalsNode((DnDNode) o))
    		{
    			return false;
    		}
    		else if (this.getChildCount() != ((DnDNode) o).getChildCount())
    		{
    			return false;
    		}
    		// compare all children
    		for (int i = 0; i < this.getChildCount(); i++)
    		{
    			if (!this.getChildAt(i).equals(((DnDNode) o).getChildAt(i)))
    			{
    				return false;
    			}
    		}
    		// they are equal!
    		return true;
    	}
     
    	/**
    	 * Compares if this node is equal to another node. In this method, children
    	 * and ancestors are not taken into concideration.
    	 * 
    	 * @param node
    	 * @return
    	 */
    	public boolean equalsNode(DnDNode node)
    	{
    		if (node != null)
    		{
    			if (this.getAllowsChildren() == node.getAllowsChildren())
    			{
    				if (this.getUserObject() != null)
    				{
    					if (this.getUserObject().equals(node.getUserObject()))
    					{
    						return true;
    					}
    				}
    				else
    				{
    					if (node.getUserObject() == null)
    					{
    						return true;
    					}
    				}
    			}
    		}
    		return false;
    	}
     
    	/**
    	 * Gets the index node should be inserted at to maintain sorted order. Also
    	 * performs checking to see if that node can be added to this node. By
    	 * default, DnDNode adds children at the end.
    	 * 
    	 * @param node
    	 * @return the index to add at, or -1 if node can not be added
    	 */
    	public int getAddIndex(DnDNode node)
    	{
    		if (!this.canAdd(node))
    		{
    			return -1;
    		}
    		return this.getChildCount();
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 * @throws UnsupportedFlavorException
    	 * @throws IOException
    	 **/
    	@Override
    	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
    	{
    		if (this.canImport(flavor))
    		{
    			return this;
    		}
    		else
    		{
    			throw new UnsupportedFlavorException(flavor);
    		}
    	}
     
    	/**
    	 * @return
    	 **/
    	@Override
    	public DataFlavor[] getTransferDataFlavors()
    	{
    		return DnDNode.flavors;
    	}
     
    	/**
    	 * @param temp
    	 * @return
    	 */
    	public int indexOfNode(DnDNode node)
    	{
    		if (node == null)
    		{
    			throw new NullPointerException();
    		}
    		else
    		{
    			for (int i = 0; i < this.getChildCount(); i++)
    			{
    				if (this.getChildAt(i).equals(node))
    				{
    					return i;
    				}
    			}
    			return -1;
    		}
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 **/
    	@Override
    	public boolean isDataFlavorSupported(DataFlavor flavor)
    	{
    		DataFlavor[] flavs = this.getTransferDataFlavors();
    		for (int i = 0; i < flavs.length; i++)
    		{
    			if (flavs[i].equals(flavor))
    			{
    				return true;
    			}
    		}
    		return false;
    	}
    }

    DnD nodes list

    Unfortunately, I didn't find any standard collection classes that implemented transferable, but that's ok because our own implementation is very simple. Here's what we need:

    1. Some sort of list to store nodes to transfer. i chose an ArrayList because I figured that this list will be accessed randomly a lot, but won't really be changed once it has been created.
    2. Implementation of the Transferable interface. The implementation will look an awful lot like the DnDNodes.

    DnDTreeList class:

    package tree;
     
    import java.awt.datatransfer.*;
    import java.io.IOException;
    import java.io.Serializable;
    import java.util.ArrayList;
     
    import javax.swing.tree.TreePath;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     * 
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDTreeList implements Transferable, Serializable
    {
    	/**
    	 * Data flavor that allows a DnDTreeList to be extracted from a transferable
    	 * object
    	 */
    	public final static DataFlavor DnDTreeList_FLAVOR = new DataFlavor(DnDTreeList.class,
    			"Drag and drop list");
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 1270874212613332692L;
    	/**
    	 * List of flavors this DnDTreeList can be retrieved as. Currently only
    	 * supports DnDTreeList_FLAVOR
    	 */
    	protected ArrayList<DataFlavor> flavors;
    	// protected DataFlavor[] flavors = { DnDTreeList.DnDTreeList_FLAVOR };
     
    	/**
    	 * Nodes to transfer
    	 */
    	protected ArrayList<TreePath> nodes;
     
    	/**
    	 * @param selection
    	 */
    	public DnDTreeList(ArrayList<TreePath> nodes)
    	{
    		this.finishBuild(nodes);
    	}
     
    	/**
    	 * @param selectionPaths
    	 */
    	public DnDTreeList(TreePath[] nodes)
    	{
    		ArrayList<TreePath> n = new ArrayList<TreePath>(nodes.length);
    		for (int i = 0; i < nodes.length; i++)
    		{
    			n.add(nodes[i]);
    		}
    		this.finishBuild(n);
    	}
     
    	/**
    	 * Called from contructors to finish building this object once data has been
    	 * put into the correct form.
    	 * 
    	 * @param nodes
    	 */
    	private void finishBuild(ArrayList<TreePath> nodes)
    	{
    		this.nodes = nodes;
    		this.flavors = new ArrayList<DataFlavor>();
    		this.flavors.add(DnDTreeList.DnDTreeList_FLAVOR);
    		for (int i = 0; i < nodes.size(); i++)
    		{
    			// add a list of all flavors of selected nodes
    			DataFlavor[] temp = ((DnDNode) nodes.get(i).getLastPathComponent())
    					.getTransferDataFlavors();
    			for (int j = 0; j < temp.length; j++)
    			{
    				if (!this.flavors.contains(temp[j]))
    				{
    					this.flavors.add(temp[j]);
    				}
    			}
    		}
    	}
     
    	public ArrayList<TreePath> getNodes()
    	{
    		return this.nodes;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 * @throws UnsupportedFlavorException
    	 * @throws IOException
    	 **/
    	@Override
    	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
    	{
    		if (this.isDataFlavorSupported(flavor))
    		{
    			return this;
    		}
    		else
    		{
    			throw new UnsupportedFlavorException(flavor);
    		}
    	}
     
    	/**
    	 * @return
    	 **/
    	@Override
    	public DataFlavor[] getTransferDataFlavors()
    	{
    		// TODO Auto-generated method stub
    		DataFlavor[] flavs = new DataFlavor[this.flavors.size()];
    		this.flavors.toArray(flavs);
    		return flavs;
    	}
     
    	/**
    	 * @param flavor
    	 * @return
    	 **/
    	@Override
    	public boolean isDataFlavorSupported(DataFlavor flavor)
    	{
    		DataFlavor[] flavs = this.getTransferDataFlavors();
    		for (int i = 0; i < flavs.length; i++)
    		{
    			if (flavs[i].equals(flavor))
    			{
    				return true;
    			}
    		}
    		return false;
    	}
    }

    DnDTreeModel

    This class is almist identical to the DefaultTreeModel except I changed the removeNodeFromParent method to use an absolute comparison to find the correct node to remove. This is necessary because of several problems that arise with insert and copying if the comparison is not absolute (==)

    package tree;
     
    import javax.swing.tree.*;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DnDTreeModel extends DefaultTreeModel
    {
     
    	/**
    	 * @param root
    	 */
    	public DnDTreeModel(TreeNode root)
    	{
    		super(root);
    	}
     
    	public DnDTreeModel(TreeNode root, boolean asksAllowsChildren)
    	{
    		super(root, asksAllowsChildren);
    	}
     
    	/**
    	 * Removes the specified node. Note that the comparison is made absolutely,
    	 * ie. must be the exact same object not just equal.
    	 * 
    	 * @param node
    	 */
    	@Override
    	public void removeNodeFromParent(MutableTreeNode node)
    	{
    		// get back the index of the node
    		DnDNode parent = (DnDNode) node.getParent();
    		int sourceIndex = 0;
    		for (sourceIndex = 0; sourceIndex < parent.getChildCount() && parent
    				.getChildAt(sourceIndex) != node; sourceIndex++)
    		{}
    		// time to perform the removal
    		parent.remove(sourceIndex);
    		// need a custom remove event because we manually removed
    		// the correct node
    		int[] childIndices = new int[1];
    		childIndices[0] = sourceIndex;
    		Object[] removedChildren = new Object[1];
    		removedChildren[0] = node;
    		this.nodesWereRemoved(parent, childIndices, removedChildren);
    	}
    }

    Actions and Events

    Actions are used to interface with the DnDJTree externally, and events are used to keep track of different actions to be undone/redone. Because all of these classes are kind of small, I clumped them all into one section. Cut/Copy/Paste are available by setting the TreeModel's action to some object (button, hotkey, etc.) and the putting it into the action map for the DnDJTree.

    ActionMap map = tree.getActionMap();
    map.put(TransferHandler.getCutAction().getValue(Action.NAME), TransferHandler.getCutAction());

    TreeEvent

    Used to keep track of undo/redo actions. Currently only support add/remove of a single node.

    package tree;
     
    import java.awt.AWTEvent;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class TreeEvent extends AWTEvent
    {
    	/**
    	 * True if this event is for an add event, false otherwise
    	 */
    	boolean isAdd;
    	/**
    	 * The node to add/remove from
    	 */
    	protected DnDNode destination;
    	/**
    	 * The node to be added/removed, or the parent of the node that was moved
    	 * from
    	 */
    	protected DnDNode node;
    	/**
    	 * The index to add/remove node to
    	 */
    	protected int index;
     
    	/**
    	 * Creates an event that adds/removes items from the tree at the specified
    	 * node and index.
    	 * 
    	 * @param source
    	 * @param add
    	 * @param destination
    	 * @param node
    	 * @param index
    	 */
    	public TreeEvent(Object source, boolean isAdd, DnDNode destination, DnDNode node, int index)
    	{
    		super(source, AWTEvent.RESERVED_ID_MAX + 1);
    		this.destination = destination;
    		this.node = node;
    		this.index = index;
    		this.isAdd = isAdd;
    	}
     
    	public TreeEvent invert()
    	{
    		return new TreeEvent(this.source, !this.isAdd, this.destination, this.node, this.index);
     
    	}
     
    	/**
    	 * @return the destination
    	 */
    	public DnDNode getDestination()
    	{
    		return this.destination;
    	}
     
    	/**
    	 * @param destination
    	 *            the destination to set
    	 */
    	public void setDestination(DnDNode destination)
    	{
    		this.destination = destination;
    	}
     
    	/**
    	 * @return the node
    	 */
    	public DnDNode getNode()
    	{
    		return this.node;
    	}
     
    	/**
    	 * @param node
    	 *            the node to set
    	 */
    	public void setNode(DnDNode node)
    	{
    		this.node = node;
    	}
     
    	public boolean isAdd()
    	{
    		return this.isAdd;
    	}
     
    	/**
    	 * @return the index
    	 */
    	public int getIndex()
    	{
    		return this.index;
    	}
     
    	/**
    	 * @param index
    	 *            the index to set
    	 */
    	public void setIndex(int index)
    	{
    		this.index = index;
    	}
     
    	@Override
    	public String toString()
    	{
    		return "Add remove " + this.destination + " " + this.node + " " + this.index + " " + this.isAdd;
    	}
    }

    Node

    A simple node used for the undo/redo tracker.

    package tree;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class Node<E>
    {
    	public Node<E> prev;
    	public Node<E> next;
    	public E data;
     
    	public Node(E data)
    	{
    		this.data = data;
    		this.prev = null;
    		this.next = null;
    	}
     
    	public void linkNext(Node<E> node)
    	{
    		if (node == null)
    		{
    			// unlink next, return
    			this.unlinkNext();
    			return;
    		}
    		if (node.prev != null)
    		{
    			// need to unlink previous node from node
    			node.unlinkPrev();
    		}
    		if (this.next != null)
    		{
    			// need to unlink next node from this
    			this.unlinkNext();
    		}
    		this.next = node;
    		node.prev = this;
    	}
     
    	public void linkPrev(Node<E> node)
    	{
    		if (node == null)
    		{
    			this.unlinkPrev();
    			return;
    		}
    		if (node.next != null)
    		{
    			// need to unlink next node from node
    			node.unlinkNext();
    		}
    		if (this.prev != null)
    		{
    			// need to unlink prev from this
    			this.unlinkPrev();
    		}
    		this.prev = node;
    		node.next = this;
    	}
     
    	public void unlinkNext()
    	{
    		this.next.prev = null;
    		this.next = null;
    	}
     
    	public void unlinkPrev()
    	{
    		this.prev.next = null;
    		this.prev = null;
    	}
    }

    Delete

    To allow the user to delete nodes, create a new Delete Action, and associate with some button or hotkey. Then add your DnDJTree as a PropertyChangeListener for that event.

    package actions;
     
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
     
    import javax.swing.*;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class DeleteAction extends AbstractAction
    {
    	// public final static Icon ICON = new ImageIcon("resources/find.gif");
    	public final static String DO_DELETE = "delete";
     
    	public DeleteAction(String text)
    	{
    		super(text);
    		// super(text, FindAction.ICON);
    		this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("DELETE"));
    		this.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_DELETE);
    	}
     
    	public void actionPerformed(ActionEvent e)
    	{
    		this.firePropertyChange(DeleteAction.DO_DELETE, null, null);
    	}
    }

    Undo

    To allow the user to undo, create a new Undo Action, and associate with some button or hotkey. Then add your DnDJTree as a PropertyChangeListener for that event.

    package actions;
     
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
     
    import javax.swing.*;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class UndoAction extends AbstractAction
    {
    	public final static Icon ICON = new ImageIcon("resources/undo.gif");
    	public static final String DO_UNDO = "undo";
     
    	public UndoAction(String text)
    	{
    		super(text, UndoAction.ICON);
    		this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control Z"));
    		this.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Z);
    	}
     
    	public void actionPerformed(ActionEvent e)
    	{
    		this.firePropertyChange(UndoAction.DO_UNDO, null, null);
    	}
     
    }

    Redo

    To allow the user to redo, create a new Redo Action, and associate with some button or hotkey. Then add your DnDJTree as a PropertyChangeListener for that event.

    package actions;
     
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
     
    import javax.swing.*;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class RedoAction extends AbstractAction
    {
    	public final static Icon ICON = new ImageIcon("resources/redo.gif");
    	public static final String DO_REDO = "redo";
     
    	public RedoAction(String text)
    	{
    		super(text, RedoAction.ICON);
    		this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control Y"));
    		this.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Y);
    	}
     
    	public void actionPerformed(ActionEvent e)
    	{
    		this.firePropertyChange(RedoAction.DO_REDO, null, null);
    	}
    }

    Undo Event cap

    Used to keep track of which events are to be undone/redone together.

    package events;
     
    import java.awt.AWTEvent;
     
    /**
     * @author helloworld922
     *         <p>
     * @version 1.0
     *          <p>
     *          copyright 2010 <br>
     *          You are welcome to use/modify this code for any purposes you want so
     *          long as credit is given to me.
     */
    public class UndoEventCap extends AWTEvent
    {
    	private boolean start;
     
    	public UndoEventCap(Object source, boolean start)
    	{
    		super(source, AWTEvent.RESERVED_ID_MAX + 2);
    		this.start = start;
    	}
     
    	public boolean isStart()
    	{
    		return this.start;
    	}
     
    	@Override
    	public String toString()
    	{
    		return "UndoEventCap " + this.start;
    	}
    }

    Conclusion

    That's pretty much all there is to implementing a drag and drop for JTrees that allows multiple node trasfers. If you want, here's a small test application that sets up a DnDJTree with some nodes. Note: this code doesn't implement any of the extra features I had above (undo, redo, cut/copy/paste). However, all of the framework is there and all you need to do is follow the above instructions to create an interface between your application and the DnDJTree to utilize the features.

    public class DnDJTreeApp
    {
    	public static void main(String[] args)
    	{
    		DnDNode root = new DnDNode("root");
    		DnDNode child = new DnDNode("parent 1");
    		root.add(child);
    		child = new DnDNode("parent 2");
    		root.add(child);
    		child = new DnDNode("parent 3");
    		child.add(new DnDNode("child 1"));
    		child.add(new DnDNode("child 2"));
    		root.add(child);
    		DnDJTree tree = new DnDJTree(root);
    		JFrame frame = new JFrame("Drag and drop JTrees");
    		frame.getContentPane().add(tree);
    		frame.setSize(600, 400);
    		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    		frame.setVisible(true);
    	}
    }

    Happy coding

    edit: missing UndoEventCap class. added.
    Last edited by helloworld922; February 20th, 2010 at 03:00 PM.

  5. The Following 2 Users Say Thank You to helloworld922 For This Useful Post:

    Chotis (January 24th, 2014), Den3107 (March 20th, 2014)

  6. #4
    Junior Member
    Join Date
    Mar 2014
    Posts
    2
    Thanks
    2
    Thanked 0 Times in 0 Posts

    Default Re: Drag and Drop in JTrees

    I must say that your work is great, though we do live in a world with people that want everything to be as close to perfect as possible.
    Guess you can count me as one of them.
    Basically there's one feature I couldn't ass myself and I would really appreciate if you'd give me a solution to it (or some one else).
    The problem is: It doesn't support the drop mode "ON_OR_INSERT" (or well, it doesn't make the JTree behave any different), which makes organizing impossible.

    In every attempt I've made myself I ended up not being able to put nodes inside other nodes, though I can place nodes in already existing 'maps' and organize the nodes.

    Once again, I love your work and I really appreciate it and WILL use it, though if you'd be able to make this one fix, I'd be only happier.

    With dear regards,
    Dennis

  7. #5
    Junior Member
    Join Date
    Mar 2014
    Posts
    2
    Thanks
    2
    Thanked 0 Times in 0 Posts

    Default Re: Drag and Drop in JTrees

    Quote Originally Posted by Den3107 View Post
    I must say that your work is great, though we do live in a world with people that want everything to be as close to perfect as possible.
    Guess you can count me as one of them.
    Basically there's one feature I couldn't ass myself and I would really appreciate if you'd give me a solution to it (or some one else).
    The problem is: It doesn't support the drop mode "ON_OR_INSERT" (or well, it doesn't make the JTree behave any different), which makes organizing impossible.

    In every attempt I've made myself I ended up not being able to put nodes inside other nodes, though I can place nodes in already existing 'maps' and organize the nodes.

    Once again, I love your work and I really appreciate it and WILL use it, though if you'd be able to make this one fix, I'd be only happier.

    With dear regards,
    Dennis
    Actually, I have managed to fix the problem myself (so far without any bugs).
    Though the solution is quite absolute, as it uses the JTree from de GUI (the DnDJTree object would be public which a DnDNode then requests for it's drop location location)
    Is there any way to make this more abstract? So basically make it possible for the DnDNode object to request the DnDJTree it is inside of?

    With dear regards,
    Dennis

Similar Threads

  1. Drag and Drop in JTrees
    By helloworld922 in forum AWT / Java Swing
    Replies: 2
    Last Post: January 19th, 2010, 10:51 PM
  2. Struts drop down
    By kalees in forum Web Frameworks
    Replies: 1
    Last Post: January 14th, 2010, 11:56 PM
  3. 'MouseUp' Drag and Drop Events
    By copeg in forum AWT / Java Swing
    Replies: 0
    Last Post: November 10th, 2009, 06:21 PM