Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8129830

JTree drag/drop on lower half of last child of container incorrect

    XMLWordPrintable

Details

    • Bug
    • Resolution: Fixed
    • P3
    • 9
    • 8u45, 9
    • client-libs
    • b74
    • generic

    Backports

      Description

        FULL PRODUCT VERSION :
        java version "1.8.0_05"
        Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
        Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

        ADDITIONAL OS VERSION INFORMATION :
        Linux aluminum.lebanon.cd-adapco.com 3.10.0-229.4.2.el7.x86_64 #1 SMP Wed May 13 10:06:09 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

        A DESCRIPTION OF THE PROBLEM :
        The DropLocation created for a point is not correct when dropped on the lower part of the last child of a container. It always indicates a sibling of the container following the container.

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        Start with a tree with the following hierarchy:

        A
          - X
          - Y
        B
        C

        Enable drag/drop and set DropMode to INSERT or ON_OR_INSERT.

        Drag C to a point between Y and B.

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        Dragging C to a point between Y and B with the mouse of the lower half of Y with DropMode.INSERT or the lower third with DropMode.ON_OR_INSERT should indicate that dropping will move C to be the last child of A:

        A
          - X
          - Y
          - C
        B

        Dragging C to a point between Y and B with the mouse over the top half (or third) of B should indicate that dropping will move C above B but still remain a child of A:

        A
          - X
          - Y
        C
        B
        ACTUAL -
        C always ends up as a sibling of A even if over Y.

        REPRODUCIBILITY :
        This bug can be reproduced always.

        ---------- BEGIN SOURCE ----------
        package dnd;

        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 static javax.swing.TransferHandler.MOVE;
        import javax.swing.tree.DefaultMutableTreeNode;
        import javax.swing.tree.DefaultTreeModel;
        import javax.swing.tree.TreeModel;
        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(getTreeModel());
                tree.setRootVisible(false);
                tree.setDragEnabled(true);
                tree.setDropMode(DropMode.INSERT);
                tree.setTransferHandler(new TreeTransferHandler());
                tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
                expandTree(tree);
                return new JScrollPane(tree);
            }

            protected static TreeModel getTreeModel() {
                DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
                DefaultMutableTreeNode a;

                a = new DefaultMutableTreeNode("A");
                root.add(a);
                a.add(new DefaultMutableTreeNode("X"));
                a.add(new DefaultMutableTreeNode("Y"));

                root.add(new DefaultMutableTreeNode("B"));
                root.add(new DefaultMutableTreeNode("C"));
                return new DefaultTreeModel(root);
            }

            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.setLocationRelativeTo(null);
                f.setVisible(true);
            }
        }

        class TreeTransferHandler extends TransferHandler {
            DataFlavor nodesFlavor;
            DataFlavor[] flavors = new DataFlavor[1];
            DefaultMutableTreeNode[] nodesToRemove;

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

            @Override
            public boolean canImport(TransferHandler.TransferSupport support) {
                if (!support.isDrop()) {
                    return false;
                }
                support.setShowDropLocation(true);
                if (!support.isDataFlavorSupported(nodesFlavor)) {
                    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;
                    }
                }
                // Do not allow MOVE-action drops if a non-leaf node is
                // selected unless all of its children are also selected.
                int action = support.getDropAction();
                if (action == MOVE) {
                    return haveCompleteNode(tree);
                }
                // Do not allow a non-leaf node to be copied to a level
                // which is less than its source level.
                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;
            }

            private boolean haveCompleteNode(JTree tree) {
                int[] selRows = tree.getSelectionRows();
                TreePath path = tree.getPathForRow(selRows[0]);
                DefaultMutableTreeNode first = (DefaultMutableTreeNode) path.getLastPathComponent();
                int childCount = first.getChildCount();
                // first has children and no children are selected.
                if (childCount > 0 && selRows.length == 1) {
                    return false;
                }
                // first may have children.
                for (int i = 1; i < selRows.length; i++) {
                    path = tree.getPathForRow(selRows[i]);
                    DefaultMutableTreeNode next = (DefaultMutableTreeNode) path.getLastPathComponent();
                    if (first.isNodeChild(next)) {
                        // Found a child of first.
                        if (childCount > selRows.length - 1) {
                            // Not all children of first are selected.
                            return false;
                        }
                    }
                }
                return true;
            }

            @Override
            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<>();
                    List<DefaultMutableTreeNode> toRemove = new ArrayList<>();
                    DefaultMutableTreeNode node = (DefaultMutableTreeNode) paths[0].getLastPathComponent();
                    DefaultMutableTreeNode copy = copy(node);
                    copies.add(copy);
                    toRemove.add(node);
                    for (int i = 1; i < paths.length; i++) {
                        DefaultMutableTreeNode next = (DefaultMutableTreeNode) paths[i]
                                .getLastPathComponent();
                        // Do not allow higher level nodes to be added to list.
                        if (next.getLevel() < node.getLevel()) {
                            break;
                        } else if (next.getLevel() > node.getLevel()) { // child node
                            copy.add(copy(next));
                            // node already contains child
                        } else { // sibling
                            copies.add(copy(next));
                            toRemove.add(next);
                        }
                    }
                    DefaultMutableTreeNode[] nodes = copies
                            .toArray(new DefaultMutableTreeNode[copies.size()]);
                    nodesToRemove = toRemove.toArray(new DefaultMutableTreeNode[toRemove.size()]);
                    return new NodesTransferable(nodes);
                }
                return null;
            }

            /**
             * Defensive copy used in createTransferable.
             */
            private DefaultMutableTreeNode copy(TreeNode node) {
                return new DefaultMutableTreeNode(node);
            }

            @Override
            protected void exportDone(JComponent source, Transferable data, int action) {
                if ((action & MOVE) == MOVE) {
                    JTree tree = (JTree) source;
                    DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
                    // Remove nodes saved in nodesToRemove in createTransferable.
                    for (DefaultMutableTreeNode nodesToRemove1 : nodesToRemove) {
                        model.removeNodeFromParent(nodesToRemove1);
                    }
                }
            }

            @Override
            public int getSourceActions(JComponent c) {
                return COPY_OR_MOVE;
            }

            @Override
            public boolean importData(TransferHandler.TransferSupport support) {
                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();
                }
                // Add data to model.
                for (DefaultMutableTreeNode node : nodes) {
                    model.insertNodeInto(node, parent, index++);
                }
                return true;
            }

            @Override
            public String toString() {
                return getClass().getName();
            }

            public class NodesTransferable implements Transferable {
                DefaultMutableTreeNode[] nodes;

                public NodesTransferable(DefaultMutableTreeNode[] nodes) {
                    this.nodes = nodes;
                }

                @Override
                public Object getTransferData(DataFlavor flavor)
                        throws UnsupportedFlavorException {
                    if (!isDataFlavorSupported(flavor)) {
                        throw new UnsupportedFlavorException(flavor);
                    }
                    return nodes;
                }

                @Override
                public DataFlavor[] getTransferDataFlavors() {
                    return flavors;
                }

                @Override
                public boolean isDataFlavorSupported(DataFlavor flavor) {
                    return nodesFlavor.equals(flavor);
                }
            }
        }
        ---------- END SOURCE ----------

        CUSTOMER SUBMITTED WORKAROUND :
        Workarounds investigated:
        * Create a custom UI subclass of BasicTreeUI and override isDopLine and getDropLineRect to account for the erroneous drop location. All other places would also need to make the same correction for the erroneous drop location. This is not very nice, since you'd have to always use the same UI or override all possible UIs.
        * Override JTree.dropLocationForPoint(Point p) to create a correct DropLocation but it is package visible rather than protected so it is not possible.
        * Change the DropLocation values after it is created to correct the erroneous values. This is not possible because there is no setter for the path and child index, nor is DropLocation public.

        Basically, there is no viable workaround.

        Attachments

          Issue Links

            Activity

              People

                ssadetsky Semyon Sadetsky (Inactive)
                webbuggrp Webbug Group
                Votes:
                0 Vote for this issue
                Watchers:
                4 Start watching this issue

                Dates

                  Created:
                  Updated:
                  Resolved: