8129830: JTree drag/drop on lower half of last child of container incorrect.
authorssadetsky
Tue, 30 Jun 2015 18:26:38 +0300
changeset 31657 6b5f36a9a3c0
parent 31656 786638581dba
child 31658 3c25afc38b47
8129830: JTree drag/drop on lower half of last child of container incorrect. Reviewed-by: alexsch, azvegint
jdk/src/java.desktop/share/classes/javax/swing/JTree.java
jdk/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicTreeUI.java
jdk/test/javax/swing/JTree/DnD/LastNodeLowerHalfDrop.java
--- a/jdk/src/java.desktop/share/classes/javax/swing/JTree.java	Tue Jun 30 11:04:58 2015 +0300
+++ b/jdk/src/java.desktop/share/classes/javax/swing/JTree.java	Tue Jun 30 18:26:38 2015 +0300
@@ -1365,6 +1365,13 @@
 
                     child = getPathForRow(index);
                     parent = child.getParentPath();
+                    TreePath prev = getPathForRow(row).getParentPath();
+                    if (prev != null && !prev.equals(parent)) {
+                        location = new DropLocation(p, prev,
+                              model.getChildCount(prev.getLastPathComponent()));
+                        break;
+                    }
+
                 } else {
                     assert checkOn;
                     location = new DropLocation(p, getPathForRow(row), -1);
--- a/jdk/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicTreeUI.java	Tue Jun 30 11:04:58 2015 +0300
+++ b/jdk/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicTreeUI.java	Tue Jun 30 18:26:38 2015 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -1497,8 +1497,16 @@
                 rect.x = xRect.x;
                 rect.width = xRect.width;
             } else {
-                rect = tree.getPathBounds(path.pathByAddingChild(
-                    model.getChild(path.getLastPathComponent(), index)));
+                if (index >= model.getChildCount(path.getLastPathComponent())) {
+                    rect = tree.getPathBounds(path.pathByAddingChild(
+                            model.getChild(path.getLastPathComponent(),
+                                    index - 1)));
+                    rect.y = rect.y + rect.height;
+                } else {
+                    rect = tree.getPathBounds(path.pathByAddingChild(
+                            model.getChild(path.getLastPathComponent(),
+                                    index)));
+                }
             }
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/swing/JTree/DnD/LastNodeLowerHalfDrop.java	Tue Jun 30 18:26:38 2015 +0300
@@ -0,0 +1,384 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+   @bug 8129830
+   @summary JTree drag/drop on lower half of last child of container incorrect
+   @author Semyon Sadetsky
+  */
+
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.event.InputEvent;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import javax.swing.*;
+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 LastNodeLowerHalfDrop {
+
+    private static DefaultMutableTreeNode b1;
+    private static DefaultMutableTreeNode b2;
+    private static DefaultMutableTreeNode c;
+    private static JTree jTree;
+    private static DefaultMutableTreeNode a;
+    private static DefaultMutableTreeNode b;
+    private static DefaultMutableTreeNode a1;
+    private static Point dragPoint;
+    private static Point dropPoint;
+    private static JFrame f;
+    private static DefaultMutableTreeNode c1;
+    private static DefaultMutableTreeNode root;
+
+
+    public static void main(String[] args) throws Exception {
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                f = new JFrame();
+                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                f.add(new LastNodeLowerHalfDrop().getContent());
+                f.setSize(400, 400);
+                f.setLocationRelativeTo(null);
+                f.setVisible(true);
+            }
+        });
+        testCase(b2, a1, +0.4f);
+        if (!"b2".equals(jTree.getModel().
+                getChild(a, a.getChildCount() - 1).toString())) {
+            throw new RuntimeException("b1 was not inserted in the last position in a");
+        }
+        testCase(c1, c, -0.4f);
+        if (!"c1".equals(jTree.getModel().getChild(root, 2).toString())) {
+            throw new RuntimeException("c1 was not inserted beetween c and b nodes");
+        }
+
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                f.dispose();
+            }
+        });
+        System.out.printf("ok");
+    }
+
+    static void testCase(DefaultMutableTreeNode drag,
+                         DefaultMutableTreeNode drop, float shift) throws Exception {
+        Robot robot = new Robot();
+        robot.waitForIdle();
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                Rectangle rectDrag =
+                        jTree.getPathBounds(new TreePath(drag.getPath()));
+                dragPoint = new Point((int)rectDrag.getCenterX(),
+                        (int) rectDrag.getCenterY());
+                SwingUtilities.convertPointToScreen(dragPoint, jTree);
+                Rectangle rectDrop =
+                        jTree.getPathBounds(new TreePath(drop.getPath()));
+                dropPoint = new Point(rectDrop.x + 5,
+                        (int) (rectDrop.getCenterY() + shift * rectDrop.height));
+                SwingUtilities.convertPointToScreen(dropPoint, jTree);
+            }
+        });
+
+        robot.mouseMove(dragPoint.x, dragPoint.y);
+        robot.mousePress(InputEvent.BUTTON1_MASK);
+        robot.delay(400);
+        robot.mouseMove(dropPoint.x, dropPoint.y);
+        robot.delay(400);
+        robot.mouseRelease(InputEvent.BUTTON1_MASK);
+
+        robot.waitForIdle();
+    }
+
+    private JScrollPane getContent() {
+        jTree = new JTree(getTreeModel());
+        jTree.setRootVisible(false);
+        jTree.setDragEnabled(true);
+        jTree.setDropMode(DropMode.INSERT);
+        jTree.setTransferHandler(new TreeTransferHandler());
+        jTree.getSelectionModel().setSelectionMode(
+                TreeSelectionModel.SINGLE_TREE_SELECTION);
+        expandTree(jTree);
+        return new JScrollPane(jTree);
+    }
+
+    protected static TreeModel getTreeModel() {
+        root = new DefaultMutableTreeNode("Root");
+
+        a = new DefaultMutableTreeNode("A");
+        root.add(a);
+        a1 = new DefaultMutableTreeNode("a1");
+        a.add(a1);
+
+        b = new DefaultMutableTreeNode("B");
+        root.add(b);
+        b1 = new DefaultMutableTreeNode("b1");
+        b.add(b1);
+        b2 = new DefaultMutableTreeNode("b2");
+        b.add(b2);
+
+        c = new DefaultMutableTreeNode("C");
+        root.add(c);
+        c1 = new DefaultMutableTreeNode("c1");
+        c.add(c1);
+        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);
+        }
+    }
+}
+
+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);
+        }
+    }
+}
\ No newline at end of file