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