1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /* @test
  25  * @bug 8129830 8132771
  26  * @summary JTree drag/drop on lower half of last child of container incorrect
  27  * @run main LastNodeLowerHalfDrop
  28  */
  29 
  30 import java.awt.Point;
  31 import java.awt.Rectangle;
  32 import java.awt.Robot;
  33 import java.awt.datatransfer.DataFlavor;
  34 import java.awt.datatransfer.Transferable;
  35 import java.awt.datatransfer.UnsupportedFlavorException;
  36 import java.awt.event.InputEvent;
  37 import java.util.ArrayList;
  38 import java.util.Enumeration;
  39 import java.util.List;
  40 import javax.swing.DropMode;
  41 import javax.swing.JComponent;
  42 import javax.swing.JFrame;
  43 import javax.swing.JScrollPane;
  44 import javax.swing.JTree;
  45 import javax.swing.SwingUtilities;
  46 import javax.swing.TransferHandler;
  47 import javax.swing.tree.DefaultMutableTreeNode;
  48 import javax.swing.tree.DefaultTreeModel;
  49 import javax.swing.tree.TreeModel;
  50 import javax.swing.tree.TreeNode;
  51 import javax.swing.tree.TreePath;
  52 import javax.swing.tree.TreeSelectionModel;
  53 
  54 public class LastNodeLowerHalfDrop {
  55 
  56     private static DefaultMutableTreeNode b1;
  57     private static DefaultMutableTreeNode b2;
  58     private static DefaultMutableTreeNode c;
  59     private static JTree jTree;
  60     private static DefaultMutableTreeNode a;
  61     private static DefaultMutableTreeNode b;
  62     private static DefaultMutableTreeNode a1;
  63     private static Point dragPoint;
  64     private static Point dropPoint;
  65     private static JFrame f;
  66     private static DefaultMutableTreeNode c1;
  67     private static DefaultMutableTreeNode root;
  68 
  69 
  70     public static void main(String[] args) throws Exception {
  71         SwingUtilities.invokeAndWait(new Runnable() {
  72             @Override
  73             public void run() {
  74                 f = new JFrame();
  75                 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  76                 f.add(new LastNodeLowerHalfDrop().getContent());
  77                 f.setSize(400, 400);
  78                 f.setLocationRelativeTo(null);
  79                 f.setVisible(true);
  80             }
  81         });
  82         testCase(b2, a1, +0.4f);
  83         if (!"b2".equals(jTree.getModel().
  84                 getChild(a, a.getChildCount() - 1).toString())) {
  85             cleanUp();
  86             throw new RuntimeException("b1 was not inserted "
  87                     +"in the last position in a");
  88         }
  89         testCase(c1, c, -0.4f);
  90         if (!"c1".equals(jTree.getModel().getChild(root, 2).toString())) {
  91             cleanUp();
  92             throw new RuntimeException("c1 was not inserted "
  93                     +"between c and b nodes");
  94         }
  95         cleanUp();
  96     }
  97 
  98     private static void cleanUp() throws Exception {
  99         SwingUtilities.invokeAndWait(new Runnable() {
 100             @Override
 101             public void run() {
 102                 f.dispose();
 103             }
 104         });
 105     }
 106 
 107     private static void testCase(final DefaultMutableTreeNode drag,
 108             final DefaultMutableTreeNode drop, final float shift)
 109             throws Exception {
 110         Robot robot = new Robot();
 111         robot.waitForIdle();
 112         SwingUtilities.invokeAndWait(new Runnable() {
 113             @Override
 114             public void run() {
 115                 Rectangle rectDrag =
 116                         jTree.getPathBounds(new TreePath(drag.getPath()));
 117                 dragPoint = new Point((int)rectDrag.getCenterX(),
 118                         (int) rectDrag.getCenterY());
 119                 SwingUtilities.convertPointToScreen(dragPoint, jTree);
 120                 Rectangle rectDrop =
 121                         jTree.getPathBounds(new TreePath(drop.getPath()));
 122                 dropPoint = new Point(rectDrop.x + 5,
 123                         (int) (rectDrop.getCenterY() + shift * rectDrop.height));
 124                 SwingUtilities.convertPointToScreen(dropPoint, jTree);
 125             }
 126         });
 127 
 128         robot.mouseMove(dragPoint.x, dragPoint.y);
 129         robot.mousePress(InputEvent.BUTTON1_MASK);
 130         robot.delay(1000);
 131         robot.mouseMove(dropPoint.x, dropPoint.y);
 132         robot.delay(1000);
 133         robot.mouseRelease(InputEvent.BUTTON1_MASK);
 134         robot.delay(1000);
 135         robot.waitForIdle();
 136     }
 137 
 138     private JScrollPane getContent() {
 139         jTree = new JTree(getTreeModel());
 140         jTree.setRootVisible(false);
 141         jTree.setDragEnabled(true);
 142         jTree.setDropMode(DropMode.INSERT);
 143         jTree.setTransferHandler(new TreeTransferHandler());
 144         jTree.getSelectionModel().setSelectionMode(
 145                 TreeSelectionModel.SINGLE_TREE_SELECTION);
 146         expandTree(jTree);
 147         return new JScrollPane(jTree);
 148     }
 149 
 150     protected static TreeModel getTreeModel() {
 151         root = new DefaultMutableTreeNode("Root");
 152 
 153         a = new DefaultMutableTreeNode("A");
 154         root.add(a);
 155         a1 = new DefaultMutableTreeNode("a1");
 156         a.add(a1);
 157 
 158         b = new DefaultMutableTreeNode("B");
 159         root.add(b);
 160         b1 = new DefaultMutableTreeNode("b1");
 161         b.add(b1);
 162         b2 = new DefaultMutableTreeNode("b2");
 163         b.add(b2);
 164 
 165         c = new DefaultMutableTreeNode("C");
 166         root.add(c);
 167         c1 = new DefaultMutableTreeNode("c1");
 168         c.add(c1);
 169         return new DefaultTreeModel(root);
 170     }
 171 
 172     private void expandTree(JTree tree) {
 173         DefaultMutableTreeNode root = (DefaultMutableTreeNode) tree.getModel()
 174                 .getRoot();
 175         Enumeration e = root.breadthFirstEnumeration();
 176         while (e.hasMoreElements()) {
 177             DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
 178             if (node.isLeaf()) {
 179                 continue;
 180             }
 181             int row = tree.getRowForPath(new TreePath(node.getPath()));
 182             tree.expandRow(row);
 183         }
 184     }
 185 }
 186 
 187 class TreeTransferHandler extends TransferHandler {
 188     DataFlavor nodesFlavor;
 189     DataFlavor[] flavors = new DataFlavor[1];
 190     DefaultMutableTreeNode[] nodesToRemove;
 191 
 192     public TreeTransferHandler() {
 193         try {
 194             String mimeType = DataFlavor.javaJVMLocalObjectMimeType
 195                     + ";class=\""
 196                     + javax.swing.tree.DefaultMutableTreeNode[].class.getName()
 197                     + "\"";
 198             nodesFlavor = new DataFlavor(mimeType);
 199             flavors[0] = nodesFlavor;
 200         } catch (ClassNotFoundException e) {
 201             System.out.println("ClassNotFound: " + e.getMessage());
 202         }
 203     }
 204 
 205     @Override
 206     public boolean canImport(TransferHandler.TransferSupport support) {
 207         if (!support.isDrop()) {
 208             return false;
 209         }
 210         support.setShowDropLocation(true);
 211         if (!support.isDataFlavorSupported(nodesFlavor)) {
 212             return false;
 213         }
 214         // Do not allow a drop on the drag source selections.
 215         JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
 216         JTree tree = (JTree) support.getComponent();
 217         int dropRow = tree.getRowForPath(dl.getPath());
 218         int[] selRows = tree.getSelectionRows();
 219         for (int i = 0; i < selRows.length; i++) {
 220             if (selRows[i] == dropRow) {
 221                 return false;
 222             }
 223         }
 224         // Do not allow MOVE-action drops if a non-leaf node is
 225         // selected unless all of its children are also selected.
 226         int action = support.getDropAction();
 227         if (action == MOVE) {
 228             return haveCompleteNode(tree);
 229         }
 230         // Do not allow a non-leaf node to be copied to a level
 231         // which is less than its source level.
 232         TreePath dest = dl.getPath();
 233         DefaultMutableTreeNode target = (DefaultMutableTreeNode)
 234                 dest.getLastPathComponent();
 235         TreePath path = tree.getPathForRow(selRows[0]);
 236         DefaultMutableTreeNode firstNode = (DefaultMutableTreeNode)
 237                 path.getLastPathComponent();
 238         if (firstNode.getChildCount() > 0
 239                 && target.getLevel() < firstNode.getLevel()) {
 240             return false;
 241         }
 242         return true;
 243     }
 244 
 245     private boolean haveCompleteNode(JTree tree) {
 246         int[] selRows = tree.getSelectionRows();
 247         TreePath path = tree.getPathForRow(selRows[0]);
 248         DefaultMutableTreeNode first = (DefaultMutableTreeNode)
 249                 path.getLastPathComponent();
 250         int childCount = first.getChildCount();
 251         // first has children and no children are selected.
 252         if (childCount > 0 && selRows.length == 1) {
 253             return false;
 254         }
 255         // first may have children.
 256         for (int i = 1; i < selRows.length; i++) {
 257             path = tree.getPathForRow(selRows[i]);
 258             DefaultMutableTreeNode next = (DefaultMutableTreeNode)
 259                     path.getLastPathComponent();
 260             if (first.isNodeChild(next)) {
 261                 // Found a child of first.
 262                 if (childCount > selRows.length - 1) {
 263                     // Not all children of first are selected.
 264                     return false;
 265                 }
 266             }
 267         }
 268         return true;
 269     }
 270 
 271     @Override
 272     protected Transferable createTransferable(JComponent c) {
 273         JTree tree = (JTree) c;
 274         TreePath[] paths = tree.getSelectionPaths();
 275         if (paths != null) {
 276             // Make up a node array of copies for transfer and
 277             // another for/of the nodes that will be removed in
 278             // exportDone after a successful drop.
 279             List<DefaultMutableTreeNode> copies = new ArrayList<>();
 280             List<DefaultMutableTreeNode> toRemove = new ArrayList<>();
 281             DefaultMutableTreeNode node = (DefaultMutableTreeNode)
 282                     paths[0].getLastPathComponent();
 283             DefaultMutableTreeNode copy = copy(node);
 284             copies.add(copy);
 285             toRemove.add(node);
 286             for (int i = 1; i < paths.length; i++) {
 287                 DefaultMutableTreeNode next = (DefaultMutableTreeNode) paths[i]
 288                         .getLastPathComponent();
 289                 // Do not allow higher level nodes to be added to list.
 290                 if (next.getLevel() < node.getLevel()) {
 291                     break;
 292                 } else if (next.getLevel() > node.getLevel()) {  // child node
 293                     copy.add(copy(next));
 294                     // node already contains child
 295                 } else {                                        // sibling
 296                     copies.add(copy(next));
 297                     toRemove.add(next);
 298                 }
 299             }
 300             DefaultMutableTreeNode[] nodes = copies
 301                     .toArray(new DefaultMutableTreeNode[copies.size()]);
 302             nodesToRemove = toRemove.toArray(
 303                     new DefaultMutableTreeNode[toRemove.size()]);
 304             return new NodesTransferable(nodes);
 305         }
 306         return null;
 307     }
 308 
 309     /**
 310      * Defensive copy used in createTransferable.
 311      */
 312     private DefaultMutableTreeNode copy(TreeNode node) {
 313         return new DefaultMutableTreeNode(node);
 314     }
 315 
 316     @Override
 317     protected void exportDone(JComponent source, Transferable data, int action) {
 318         if ((action & MOVE) == MOVE) {
 319             JTree tree = (JTree) source;
 320             DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
 321             // Remove nodes saved in nodesToRemove in createTransferable.
 322             for (DefaultMutableTreeNode nodesToRemove1 : nodesToRemove) {
 323                 model.removeNodeFromParent(nodesToRemove1);
 324             }
 325         }
 326     }
 327 
 328     @Override
 329     public int getSourceActions(JComponent c) {
 330         return COPY_OR_MOVE;
 331     }
 332 
 333     @Override
 334     public boolean importData(TransferHandler.TransferSupport support) {
 335         if (!canImport(support)) {
 336             return false;
 337         }
 338         // Extract transfer data.
 339         DefaultMutableTreeNode[] nodes = null;
 340         try {
 341             Transferable t = support.getTransferable();
 342             nodes = (DefaultMutableTreeNode[]) t.getTransferData(nodesFlavor);
 343         } catch (UnsupportedFlavorException ufe) {
 344             System.out.println("UnsupportedFlavor: " + ufe.getMessage());
 345         } catch (java.io.IOException ioe) {
 346             System.out.println("I/O error: " + ioe.getMessage());
 347         }
 348         // Get drop location info.
 349         JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
 350         int childIndex = dl.getChildIndex();
 351         TreePath dest = dl.getPath();
 352         DefaultMutableTreeNode parent = (DefaultMutableTreeNode)
 353                 dest.getLastPathComponent();
 354         JTree tree = (JTree) support.getComponent();
 355         DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
 356         // Configure for drop mode.
 357         int index = childIndex;    // DropMode.INSERT
 358         if (childIndex == -1) {     // DropMode.ON
 359             index = parent.getChildCount();
 360         }
 361         // Add data to model.
 362         for (DefaultMutableTreeNode node : nodes) {
 363             model.insertNodeInto(node, parent, index++);
 364         }
 365         return true;
 366     }
 367 
 368     @Override
 369     public String toString() {
 370         return getClass().getName();
 371     }
 372 
 373     public class NodesTransferable implements Transferable {
 374         DefaultMutableTreeNode[] nodes;
 375 
 376         public NodesTransferable(DefaultMutableTreeNode[] nodes) {
 377             this.nodes = nodes;
 378         }
 379 
 380         @Override
 381         public Object getTransferData(DataFlavor flavor)
 382                 throws UnsupportedFlavorException {
 383             if (!isDataFlavorSupported(flavor)) {
 384                 throw new UnsupportedFlavorException(flavor);
 385             }
 386             return nodes;
 387         }
 388 
 389         @Override
 390         public DataFlavor[] getTransferDataFlavors() {
 391             return flavors;
 392         }
 393 
 394         @Override
 395         public boolean isDataFlavorSupported(DataFlavor flavor) {
 396             return nodesFlavor.equals(flavor);
 397         }
 398     }
 399 }