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 }