JTree的拖曳

可以实现JTree的自拖曳和递归拖曳,以及一个右键增删改的升级版本

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);
	}
}

升级版本如下:


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);
	}
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值