javaswingdrag-and-dropjtree

How to get selected rows including child rows of a JTree


I'm working on implementing Drag & Drop functionality on a JTree component by using a TransferHandler. I've used this old thread on CodeRanch, which is pretty much the only apparent ressource that gets linked to whenever any question regarding this comes up in any forum.

I've gotten it to work well with my Data and my UI, so that leaf-Nodes can easily be dragged and dropped. The problem comes, when I try to drag Nodes that have children. The overriden boolean function canImport(TransferSupport support) returns false, because it checks the selected rows on the tree. Therefore, I then tested wether I could CTRL + Click to manually select child nodes after selecting the "root"-node of my drag operation, which works just fine and the node, including all it's children gets drag and dropped.

My question is this: to get the selected rows, I call tree.getSelectionRows() and save that value to an integer array. How do I get the child-rows into that integer array? I'm aware I could probably rewrite other parts of my TransferHandler, but since everything else works just fine, if only it would actually select the rows how I anticipate it to, I feel like this is the simpler approach.


Solution

  • So, drag and drop is ... complicated. There are lots of gotchas you need to be aware of.

    In the linked example you should beware that it's dealing with DefaultMutableTreeNodes and not TreeNodes, might not be an issue, but you might want to keep it in mind.

    The haveCompleteNode seems to be stopping you from moving a branch node if its child elements aren't selected ... which seems somewhat weird to me.

    Also, the code following it seems to prevent you from moving/copying a node to a position above the source nodes current level ... for some reason

    I'm also not sure I'd keep two copies of the selected nodes, this is me, but a defensive copy seems a little over the top (could be a particular edge case, but I'd be worried if the tree can be updated during a drag operation, but that's me).

    So, what are the lessons here?

    Any code you find on the web is going to need you to spend some time actually figuring out what it's doing. It might start out as a good fit for your needs, but at some point in time, you're going to have to pull up your sleeves and dig into to modify it for your own needs.

    The first thing you should do is get a good understanding of the Drag and Drop and Data Transfer APIs.

    The following is a modified example of the code you linked, which will allow you to move a branch node (and its children) and also allow you to move them to a position "above" their source depth.

    But again - you need to take the time to understand what the code is doing and be prepared to modify it to your needs

    import java.awt.datatransfer.DataFlavor;
    import java.awt.datatransfer.Transferable;
    import java.awt.datatransfer.UnsupportedFlavorException;
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.List;
    import javax.swing.DropMode;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.JTree;
    import javax.swing.TransferHandler;
    import static javax.swing.TransferHandler.COPY_OR_MOVE;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreeNode;
    import javax.swing.tree.TreePath;
    import javax.swing.tree.TreeSelectionModel;
    
    public class TreeDragAndDrop {
    
        private JScrollPane getContent() {
            JTree tree = new JTree();
            tree.setDragEnabled(true);
            tree.setDropMode(DropMode.ON_OR_INSERT);
            tree.setTransferHandler(new TreeTransferHandler());
            tree.getSelectionModel().setSelectionMode(
                    TreeSelectionModel.CONTIGUOUS_TREE_SELECTION);
            expandTree(tree);
            return new JScrollPane(tree);
        }
    
        private void expandTree(JTree tree) {
            DefaultMutableTreeNode root
                    = (DefaultMutableTreeNode) tree.getModel().getRoot();
            Enumeration e = root.breadthFirstEnumeration();
            while (e.hasMoreElements()) {
                DefaultMutableTreeNode node
                        = (DefaultMutableTreeNode) e.nextElement();
                if (node.isLeaf()) {
                    continue;
                }
                int row = tree.getRowForPath(new TreePath(node.getPath()));
                tree.expandRow(row);
            }
        }
    
        public static void main(String[] args) {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.add(new TreeDragAndDrop().getContent());
            f.setSize(400, 400);
            f.setLocation(200, 200);
            f.setVisible(true);
        }
    
        class TreeTransferHandler extends TransferHandler {
    
            DataFlavor nodesFlavor;
            DataFlavor[] flavors = new DataFlavor[1];
    
            public TreeTransferHandler() {
                try {
                    String mimeType = DataFlavor.javaJVMLocalObjectMimeType
                            + ";class=\""
                            + javax.swing.tree.DefaultMutableTreeNode[].class.getName()
                            + "\"";
                    nodesFlavor = new DataFlavor(mimeType);
                    flavors[0] = nodesFlavor;
                } catch (ClassNotFoundException e) {
                    System.out.println("ClassNotFound: " + e.getMessage());
                }
            }
    
            public boolean canImport(TransferHandler.TransferSupport support) {
                if (!support.isDrop()) {
                    return false;
                }
                support.setShowDropLocation(true);
                if (!support.isDataFlavorSupported(nodesFlavor)) {
                    System.out.println("Unsupported flavor");
                    return false;
                }
                // Do not allow a drop on the drag source selections.
                JTree.DropLocation dl
                        = (JTree.DropLocation) support.getDropLocation();
                JTree tree = (JTree) support.getComponent();
                int dropRow = tree.getRowForPath(dl.getPath());
                int[] selRows = tree.getSelectionRows();
                for (int i = 0; i < selRows.length; i++) {
                    if (selRows[i] == dropRow) {
                        return false;
                    }
                }
                // This seems to stop the node from been copied to a level above itself?!
    //            TreePath dest = dl.getPath();
    //            DefaultMutableTreeNode target = (DefaultMutableTreeNode) dest.getLastPathComponent();
    //            TreePath path = tree.getPathForRow(selRows[0]);
    //            DefaultMutableTreeNode firstNode = (DefaultMutableTreeNode) path.getLastPathComponent();
    //            if (firstNode.getChildCount() > 0 && target.getLevel() < firstNode.getLevel()) {
    //                return false;
    //            }
                return true;
            }
    
            protected Transferable createTransferable(JComponent c) {
                JTree tree = (JTree) c;
                TreePath[] paths = tree.getSelectionPaths();
                if (paths != null) {
                    // Make up a node array of copies for transfer and
                    // another for/of the nodes that will be removed in
                    // exportDone after a successful drop.
                    List<DefaultMutableTreeNode> copies = new ArrayList<DefaultMutableTreeNode>();
    
                    for (int i = 0; i < paths.length; i++) {
                        DefaultMutableTreeNode next = (DefaultMutableTreeNode) paths[i].getLastPathComponent();
                        System.out.println("Selected = " + next.getUserObject());
                        copies.add(next);
                    }
                    DefaultMutableTreeNode[] nodes = copies.toArray(new DefaultMutableTreeNode[copies.size()]);
                    return new NodesTransferable(nodes);
                }
                return null;
            }
    
            /**
             * Defensive copy used in createTransferable.
             */
            private DefaultMutableTreeNode copy(TreeNode node) {
                return new DefaultMutableTreeNode(node);
            }
    
            protected void exportDone(JComponent source, Transferable data, int action) {
                // Already dealt with this
            }
    
            public int getSourceActions(JComponent c) {
                return COPY_OR_MOVE;
            }
    
            public boolean importData(TransferHandler.TransferSupport support) {
                System.out.println("Import here");
                if (!canImport(support)) {
                    return false;
                }
                // Extract transfer data.
                DefaultMutableTreeNode[] nodes = null;
                try {
                    Transferable t = support.getTransferable();
                    nodes = (DefaultMutableTreeNode[]) t.getTransferData(nodesFlavor);
                } catch (UnsupportedFlavorException ufe) {
                    System.out.println("UnsupportedFlavor: " + ufe.getMessage());
                } catch (java.io.IOException ioe) {
                    System.out.println("I/O error: " + ioe.getMessage());
                }
                // Get drop location info.
                JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
                int childIndex = dl.getChildIndex();
                TreePath dest = dl.getPath();
                DefaultMutableTreeNode parent = (DefaultMutableTreeNode) dest.getLastPathComponent();
                JTree tree = (JTree) support.getComponent();
                DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
                // Configure for drop mode.
                int index = childIndex;    // DropMode.INSERT
                if (childIndex == -1) {     // DropMode.ON
                    index = parent.getChildCount();
                }
    
                // One consideration to consider is what to do if it's a different
                // source model ... and it's only a "copy" operation
                // Might want to look into that
                for (DefaultMutableTreeNode node : nodes) {
                    model.removeNodeFromParent(node);
                }
    
                // Add data to model.
                for (DefaultMutableTreeNode node : nodes) {
                    model.insertNodeInto(node, parent, index++);
                }
                return true;
            }
    
            public String toString() {
                return getClass().getName();
            }
    
            public class NodesTransferable implements Transferable {
    
                DefaultMutableTreeNode[] nodes;
    
                public NodesTransferable(DefaultMutableTreeNode[] nodes) {
                    this.nodes = nodes;
                }
    
                public Object getTransferData(DataFlavor flavor)
                        throws UnsupportedFlavorException {
                    if (!isDataFlavorSupported(flavor)) {
                        throw new UnsupportedFlavorException(flavor);
                    }
                    return nodes;
                }
    
                public DataFlavor[] getTransferDataFlavors() {
                    return flavors;
                }
    
                public boolean isDataFlavorSupported(DataFlavor flavor) {
                    return nodesFlavor.equals(flavor);
                }
            }
        }
    }
    

    As far as "code I found on the web", this isn't too bad.