001 /* 002 * Created on Jan 12, 2008 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 * 014 * Copyright @2008-2010 the original author or authors. 015 */ 016 package org.fest.swing.driver; 017 018 import static org.fest.assertions.Assertions.assertThat; 019 import static org.fest.swing.core.MouseButton.LEFT_BUTTON; 020 import static org.fest.swing.core.MouseButton.RIGHT_BUTTON; 021 import static org.fest.swing.driver.CommonValidations.validateCellReader; 022 import static org.fest.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing; 023 import static org.fest.swing.driver.JTreeChildrenShowUpCondition.untilChildrenShowUp; 024 import static org.fest.swing.driver.JTreeClearSelectionTask.clearSelectionOf; 025 import static org.fest.swing.driver.JTreeEditableQuery.isEditable; 026 import static org.fest.swing.driver.JTreeExpandPathTask.expandTreePath; 027 import static org.fest.swing.driver.JTreeMatchingPathQuery.*; 028 import static org.fest.swing.driver.JTreeNodeTextQuery.nodeText; 029 import static org.fest.swing.driver.JTreeToggleExpandStateTask.toggleExpandState; 030 import static org.fest.swing.driver.JTreeVerifySelectionTask.verifyNoSelection; 031 import static org.fest.swing.driver.JTreeVerifySelectionTask.verifySelection; 032 import static org.fest.swing.edt.GuiActionRunner.execute; 033 import static org.fest.swing.exception.ActionFailedException.actionFailure; 034 import static org.fest.swing.timing.Pause.pause; 035 import static org.fest.swing.util.Arrays.isEmptyIntArray; 036 import static org.fest.util.Arrays.isEmpty; 037 import static org.fest.util.Strings.concat; 038 039 import java.awt.Point; 040 import java.awt.Rectangle; 041 import javax.swing.JPopupMenu; 042 import javax.swing.JTree; 043 import javax.swing.plaf.TreeUI; 044 import javax.swing.plaf.basic.BasicTreeUI; 045 import javax.swing.tree.TreePath; 046 047 import org.fest.assertions.Description; 048 import org.fest.swing.annotation.RunsInCurrentThread; 049 import org.fest.swing.annotation.RunsInEDT; 050 import org.fest.swing.cell.JTreeCellReader; 051 import org.fest.swing.core.*; 052 import org.fest.swing.edt.*; 053 import org.fest.swing.exception.*; 054 import org.fest.swing.util.Pair; 055 import org.fest.swing.util.Triple; 056 import org.fest.util.VisibleForTesting; 057 058 /** 059 * Understands functional testing of <code>{@link JTree}</code>s: 060 * <ul> 061 * <li>user input simulation</li> 062 * <li>state verification</li> 063 * <li>property value query</li> 064 * </ul> 065 * This class is intended for internal use only. Please use the classes in the package 066 * <code>{@link org.fest.swing.fixture}</code> in your tests. 067 * 068 * @author Alex Ruiz 069 */ 070 public class JTreeDriver extends JComponentDriver { 071 072 private static final String EDITABLE_PROPERTY = "editable"; 073 private static final String SELECTION_PROPERTY = "selection"; 074 075 private final JTreeLocation location; 076 private final JTreePathFinder pathFinder; 077 078 /** 079 * Creates a new </code>{@link JTreeDriver}</code>. 080 * @param robot the robot to use to simulate user input. 081 */ 082 public JTreeDriver(Robot robot) { 083 super(robot); 084 location = new JTreeLocation(); 085 pathFinder = new JTreePathFinder(); 086 } 087 088 /** 089 * Clicks the given row. 090 * @param tree the target <code>JTree</code>. 091 * @param row the given row. 092 * @throws IllegalStateException if the <code>JTree</code> is disabled. 093 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 094 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 095 * visible rows in the <code>JTree</code>. 096 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 097 * @since 1.2 098 */ 099 @RunsInEDT 100 public void clickRow(JTree tree, int row) { 101 Point p = scrollToRow(tree, row); 102 robot.click(tree, p); 103 } 104 105 /** 106 * Clicks the given row. 107 * @param tree the target <code>JTree</code>. 108 * @param row the given row. 109 * @param button the mouse button to use. 110 * @throws NullPointerException if the given button is <code>null</code>. 111 * @throws IllegalStateException if the <code>JTree</code> is disabled. 112 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 113 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 114 * visible rows in the <code>JTree</code>. 115 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 116 * @since 1.2 117 */ 118 @RunsInEDT 119 public void clickRow(JTree tree, int row, MouseButton button) { 120 validateIsNotNull(button); 121 clickRow(tree, row, button, 1); 122 } 123 124 /** 125 * Clicks the given row. 126 * @param tree the target <code>JTree</code>. 127 * @param row the given row. 128 * @param mouseClickInfo specifies the mouse button to use and how many times to click. 129 * @throws NullPointerException if the given <code>MouseClickInfo</code> is <code>null</code>. 130 * @throws IllegalStateException if the <code>JTree</code> is disabled. 131 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 132 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 133 * visible rows in the <code>JTree</code>. 134 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 135 * @since 1.2 136 */ 137 @RunsInEDT 138 public void clickRow(JTree tree, int row, MouseClickInfo mouseClickInfo) { 139 validateIsNotNull(mouseClickInfo); 140 clickRow(tree, row, mouseClickInfo.button(), mouseClickInfo.times()); 141 } 142 143 @RunsInEDT 144 private void clickRow(JTree tree, int row, MouseButton button, int times) { 145 Point p = scrollToRow(tree, row); 146 robot.click(tree, p, button, times); 147 } 148 149 /** 150 * Double-clicks the given row. 151 * @param tree the target <code>JTree</code>. 152 * @param row the given row. 153 * @throws IllegalStateException if the <code>JTree</code> is disabled. 154 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 155 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 156 * visible rows in the <code>JTree</code>. 157 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 158 * @since 1.2 159 */ 160 @RunsInEDT 161 public void doubleClickRow(JTree tree, int row) { 162 Point p = scrollToRow(tree, row); 163 doubleClick(tree, p); 164 } 165 166 /** 167 * Right-clicks the given row. 168 * @param tree the target <code>JTree</code>. 169 * @param row the given row. 170 * @throws IllegalStateException if the <code>JTree</code> is disabled. 171 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 172 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 173 * visible rows in the <code>JTree</code>. 174 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 175 * @since 1.2 176 */ 177 @RunsInEDT 178 public void rightClickRow(JTree tree, int row) { 179 Point p = scrollToRow(tree, row); 180 rightClick(tree, p); 181 } 182 183 @RunsInEDT 184 private Point scrollToRow(JTree tree, int row) { 185 Point p = scrollToRow(tree, row, location).ii; 186 robot.waitForIdle(); 187 return p; 188 } 189 190 /** 191 * Clicks the given path, expanding parent nodes if necessary. 192 * @param tree the target <code>JTree</code>. 193 * @param path the path to path. 194 * @throws IllegalStateException if the <code>JTree</code> is disabled. 195 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 196 * @throws LocationUnavailableException if the given path cannot be found. 197 */ 198 @RunsInEDT 199 public void clickPath(JTree tree, String path) { 200 Point p = scrollToPath(tree, path); 201 robot.click(tree, p); 202 } 203 204 /** 205 * Clicks the given path, expanding parent nodes if necessary. 206 * @param tree the target <code>JTree</code>. 207 * @param path the path to path. 208 * @param button the mouse button to use. 209 * @throws NullPointerException if the given button is <code>null</code>. 210 * @throws IllegalStateException if the <code>JTree</code> is disabled. 211 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 212 * @throws LocationUnavailableException if the given path cannot be found. 213 * @since 1.2 214 */ 215 @RunsInEDT 216 public void clickPath(JTree tree, String path, MouseButton button) { 217 validateIsNotNull(button); 218 clickPath(tree, path, button, 1); 219 } 220 221 private void validateIsNotNull(MouseButton button) { 222 if (button == null) throw new NullPointerException("The given MouseButton should not be null"); 223 } 224 225 /** 226 * Clicks the given path, expanding parent nodes if necessary. 227 * @param tree the target <code>JTree</code>. 228 * @param path the path to path. 229 * @param mouseClickInfo specifies the mouse button to use and how many times to click. 230 * @throws NullPointerException if the given <code>MouseClickInfo</code> is <code>null</code>. 231 * @throws IllegalStateException if the <code>JTree</code> is disabled. 232 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 233 * @throws LocationUnavailableException if the given path cannot be found. 234 * @since 1.2 235 */ 236 @RunsInEDT 237 public void clickPath(JTree tree, String path, MouseClickInfo mouseClickInfo) { 238 validateIsNotNull(mouseClickInfo); 239 clickPath(tree, path, mouseClickInfo.button(), mouseClickInfo.times()); 240 } 241 242 private void validateIsNotNull(MouseClickInfo mouseClickInfo) { 243 if (mouseClickInfo == null) throw new NullPointerException("The given MouseClickInfo should not be null"); 244 } 245 246 private void clickPath(JTree tree, String path, MouseButton button, int times) { 247 Point p = scrollToPath(tree, path); 248 robot.click(tree, p, button, times); 249 } 250 251 /** 252 * Double-clicks the given path. 253 * @param tree the target <code>JTree</code>. 254 * @param path the path to double-click. 255 * @throws IllegalStateException if the <code>JTree</code> is disabled. 256 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 257 * @throws LocationUnavailableException if the given path cannot be found. 258 * @since 1.2 259 */ 260 @RunsInEDT 261 public void doubleClickPath(JTree tree, String path) { 262 Point p = scrollToPath(tree, path); 263 doubleClick(tree, p); 264 } 265 266 private Point scrollToPath(JTree tree, String path) { 267 Point p = scrollToMatchingPath(tree, path).iii; 268 robot.waitForIdle(); 269 return p; 270 } 271 272 private void doubleClick(JTree tree, Point p) { 273 robot.click(tree, p, LEFT_BUTTON, 2); 274 } 275 276 /** 277 * Right-clicks the given path, expanding parent nodes if necessary. 278 * @param tree the target <code>JTree</code>. 279 * @param path the path to path. 280 * @throws IllegalStateException if the <code>JTree</code> is disabled. 281 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 282 * @throws LocationUnavailableException if the given path cannot be found. 283 * @since 1.2 284 */ 285 @RunsInEDT 286 public void rightClickPath(JTree tree, String path) { 287 Point p = scrollToPath(tree, path); 288 rightClick(tree, p); 289 } 290 291 private void rightClick(JTree tree, Point p) { 292 robot.click(tree, p, RIGHT_BUTTON, 1); 293 } 294 295 /** 296 * Expands the given row, is possible. If the row is already expanded, this method will not do anything. 297 * <p> 298 * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a 299 * square the dimensions of the row height. Clicking in the center of that square should work. 300 * </p> 301 * @param tree the target <code>JTree</code>. 302 * @param row the given row. 303 * @throws IllegalStateException if the <code>JTree</code> is disabled. 304 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 305 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 306 * visible rows in the <code>JTree</code>. 307 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 308 * @throws ActionFailedException if this method fails to expand the row. 309 * @since 1.2 310 */ 311 @RunsInEDT 312 public void expandRow(JTree tree, int row) { 313 Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location); 314 robot.waitForIdle(); 315 if (info.i) return; // already expanded 316 toggleCell(tree, info.ii, info.iii); 317 } 318 319 /** 320 * Collapses the given row, is possible. If the row is already collapsed, this method will not do anything. 321 * <p> 322 * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a 323 * square the dimensions of the row height. Clicking in the center of that square should work. 324 * </p> 325 * @param tree the target <code>JTree</code>. 326 * @param row the given row. 327 * @throws IllegalStateException if the <code>JTree</code> is disabled. 328 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 329 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 330 * visible rows in the <code>JTree</code>. 331 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 332 * @throws ActionFailedException if this method fails to collapse the row. 333 * @since 1.2 334 */ 335 @RunsInEDT 336 public void collapseRow(JTree tree, int row) { 337 Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location); 338 robot.waitForIdle(); 339 if (!info.i) return; // already collapsed 340 toggleCell(tree, info.ii, info.iii); 341 } 342 343 /** 344 * Change the open/closed state of the given row, if possible. 345 * <p> 346 * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a 347 * square the dimensions of the row height. Clicking in the center of that square should work. 348 * </p> 349 * @param tree the target <code>JTree</code>. 350 * @param row the given row. 351 * @throws IllegalStateException if the <code>JTree</code> is disabled. 352 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 353 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 354 * visible rows in the <code>JTree</code>. 355 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 356 * @throws ActionFailedException if this method fails to toggle the row. 357 */ 358 @RunsInEDT 359 public void toggleRow(JTree tree, int row) { 360 Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location); 361 robot.waitForIdle(); 362 toggleCell(tree, info.ii, info.iii); 363 } 364 365 /* 366 * Returns: 367 * 1. if the row is expanded 368 * 2. the location of the row 369 * 3. the number of mouse clicks to toggle a row 370 */ 371 @RunsInEDT 372 private static Triple<Boolean, Point, Integer> scrollToRowAndGetToggleInfo(final JTree tree, final int row, 373 final JTreeLocation location) { 374 return execute(new GuiQuery<Triple<Boolean, Point, Integer>>() { 375 protected Triple<Boolean, Point, Integer> executeInEDT() { 376 validateIsEnabledAndShowing(tree); 377 Point p = scrollToVisible(tree, row, location); 378 return new Triple<Boolean, Point, Integer>(tree.isExpanded(row), p, tree.getToggleClickCount()); 379 } 380 }); 381 } 382 383 /** 384 * Expands the given path, is possible. If the path is already expanded, this method will not do anything. 385 * <p> 386 * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a 387 * square the dimensions of the row height. Clicking in the center of that square should work. 388 * </p> 389 * @param tree the target <code>JTree</code>. 390 * @param path the path to expand. 391 * @throws IllegalStateException if the <code>JTree</code> is disabled. 392 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 393 * @throws LocationUnavailableException if the given path cannot be found. 394 * @throws ActionFailedException if this method fails to expand the path. 395 * @since 1.2 396 */ 397 @RunsInEDT 398 public void expandPath(JTree tree, String path) { 399 Triple<Boolean, Point, Integer> info = scrollToMatchingPathAndGetToggleInfo(tree, path, pathFinder, location); 400 if (info.i) return; // already expanded 401 toggleCell(tree, info.ii, info.iii); 402 } 403 404 /** 405 * Collapses the given path, is possible. If the path is already expanded, this method will not do anything. 406 * <p> 407 * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a 408 * square the dimensions of the row height. Clicking in the center of that square should work. 409 * </p> 410 * @param tree the target <code>JTree</code>. 411 * @param path the path to collapse. 412 * @throws IllegalStateException if the <code>JTree</code> is disabled. 413 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 414 * @throws LocationUnavailableException if the given path cannot be found. 415 * @throws ActionFailedException if this method fails to collapse the path. 416 * @since 1.2 417 */ 418 @RunsInEDT 419 public void collapsePath(JTree tree, String path) { 420 Triple<Boolean, Point, Integer> info = scrollToMatchingPathAndGetToggleInfo(tree, path, pathFinder, location); 421 if (!info.i) return; // already collapsed 422 toggleCell(tree, info.ii, info.iii); 423 } 424 425 /* 426 * Returns: 427 * 1. if the node is expanded 428 * 2. the location of the node 429 * 3. the number of mouse clicks to toggle a node 430 */ 431 @RunsInEDT 432 private static Triple<Boolean, Point, Integer> scrollToMatchingPathAndGetToggleInfo(final JTree tree, 433 final String path, final JTreePathFinder pathFinder, final JTreeLocation location) { 434 return execute(new GuiQuery<Triple<Boolean, Point, Integer>>() { 435 protected Triple<Boolean, Point, Integer> executeInEDT() { 436 validateIsEnabledAndShowing(tree); 437 TreePath matchingPath = matchingPathFor(tree, path, pathFinder); 438 Point p = scrollToTreePath(tree, matchingPath, location); 439 return new Triple<Boolean, Point, Integer>(tree.isExpanded(matchingPath), p, tree.getToggleClickCount()); 440 } 441 }); 442 } 443 444 @RunsInEDT 445 private void toggleCell(JTree tree, Point p, int toggleClickCount) { 446 if (toggleClickCount == 0) { 447 toggleRowThroughTreeUI(tree, p); 448 robot.waitForIdle(); 449 return; 450 } 451 robot.click(tree, p, LEFT_BUTTON, toggleClickCount); 452 } 453 454 @RunsInEDT 455 private static void toggleRowThroughTreeUI(final JTree tree, final Point p) { 456 execute(new GuiTask() { 457 protected void executeInEDT() { 458 TreeUI treeUI = tree.getUI(); 459 if (!(treeUI instanceof BasicTreeUI)) throw actionFailure(concat("Can't toggle row for ", treeUI)); 460 toggleExpandState(tree, p); 461 } 462 }); 463 } 464 465 /** 466 * Selects the given rows. 467 * @param tree the target <code>JTree</code>. 468 * @param rows the rows to select. 469 * @throws NullPointerException if the array of rows is <code>null</code>. 470 * @throws IllegalArgumentException if the array of rows is empty. 471 * @throws IllegalStateException if the <code>JTree</code> is disabled. 472 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 473 * @throws IndexOutOfBoundsException if any of the given rows is less than zero or equal than or greater than the 474 * number of visible rows in the <code>JTree</code>. 475 * @throws LocationUnavailableException if a tree path for any of the given rows cannot be found. 476 */ 477 @RunsInEDT 478 public void selectRows(final JTree tree, final int[] rows) { 479 validateRows(rows); 480 clearSelection(tree); 481 new MultipleSelectionTemplate(robot) { 482 int elementCount() { 483 return rows.length; 484 } 485 void selectElement(int index) { 486 selectRow(tree, rows[index]); 487 } 488 }.multiSelect(); 489 } 490 491 private void validateRows(final int[] rows) { 492 if (rows == null) throw new NullPointerException("The array of rows should not be null"); 493 if (isEmptyIntArray(rows)) throw new IllegalArgumentException("The array of rows should not be empty"); 494 } 495 496 @RunsInEDT 497 private void clearSelection(final JTree tree) { 498 clearSelectionOf(tree); 499 robot.waitForIdle(); 500 } 501 502 /** 503 * Selects the given row. 504 * @param tree the target <code>JTree</code>. 505 * @param row the row to select. 506 * @throws IllegalStateException if the <code>JTree</code> is disabled. 507 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 508 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 509 * visible rows in the <code>JTree</code>. 510 */ 511 @RunsInEDT 512 public void selectRow(JTree tree, int row) { 513 scrollAndSelectRow(tree, row); 514 } 515 516 /** 517 * Selects the given paths, expanding parent nodes if necessary. 518 * @param tree the target <code>JTree</code>. 519 * @param paths the paths to select. 520 * @throws NullPointerException if the array of rows is <code>null</code>. 521 * @throws IllegalArgumentException if the array of rows is empty. 522 * @throws IllegalStateException if the <code>JTree</code> is disabled. 523 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 524 * @throws LocationUnavailableException if any the given path cannot be found. 525 */ 526 @RunsInEDT 527 public void selectPaths(final JTree tree, final String[] paths) { 528 validatePaths(paths); 529 clearSelection(tree); 530 new MultipleSelectionTemplate(robot) { 531 int elementCount() { 532 return paths.length; 533 } 534 void selectElement(int index) { 535 selectPath(tree, paths[index]); 536 } 537 }.multiSelect(); 538 } 539 540 private void validatePaths(final String[] paths) { 541 if (paths == null) throw new NullPointerException("The array of paths should not be null"); 542 if (isEmpty(paths)) throw new IllegalArgumentException("The array of paths should not be empty"); 543 } 544 545 /** 546 * Selects the given path, expanding parent nodes if necessary. Unlike <code>{@link #clickPath(JTree, String)}</code>, 547 * this method will not click the path if it is already selected 548 * @param tree the target <code>JTree</code>. 549 * @param path the path to select. 550 * @throws IllegalStateException if the <code>JTree</code> is disabled. 551 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 552 * @throws LocationUnavailableException if the given path cannot be found. 553 */ 554 @RunsInEDT 555 public void selectPath(JTree tree, String path) { 556 selectMatchingPath(tree, path); 557 } 558 559 /** 560 * Shows a pop-up menu at the position of the node in the given row. 561 * @param tree the target <code>JTree</code>. 562 * @param row the given row. 563 * @return a driver that manages the displayed pop-up menu. 564 * @throws IllegalStateException if the <code>JTree</code> is disabled. 565 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 566 * @throws ComponentLookupException if a pop-up menu cannot be found. 567 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 568 * visible rows in the <code>JTree</code>. 569 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 570 */ 571 @RunsInEDT 572 public JPopupMenu showPopupMenu(JTree tree, int row) { 573 Pair<Boolean, Point> info = scrollToRow(tree, row, location); 574 Point p = info.ii; 575 return robot.showPopupMenu(tree, p); 576 } 577 578 /** 579 * Shows a pop-up menu at the position of the last node in the given path. The last node in the given path will be 580 * made visible (by expanding the parent node(s)) if it is not visible. 581 * @param tree the target <code>JTree</code>. 582 * @param path the given path. 583 * @return a driver that manages the displayed pop-up menu. 584 * @throws IllegalStateException if the <code>JTree</code> is disabled. 585 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 586 * @throws ComponentLookupException if a pop-up menu cannot be found. 587 * @throws LocationUnavailableException if the given path cannot be found. 588 * @see #separator(String) 589 */ 590 @RunsInEDT 591 public JPopupMenu showPopupMenu(JTree tree, String path) { 592 Triple<TreePath, Boolean, Point> info = scrollToMatchingPath(tree, path); 593 robot.waitForIdle(); 594 Point where = info.iii; 595 return robot.showPopupMenu(tree, where); 596 } 597 598 /** 599 * Starts a drag operation at the location of the given row. 600 * @param tree the target <code>JTree</code>. 601 * @param row the given row. 602 * @throws IllegalStateException if the <code>JTree</code> is disabled. 603 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 604 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 605 * visible rows in the <code>JTree</code>. 606 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 607 */ 608 @RunsInEDT 609 public void drag(JTree tree, int row) { 610 Point p = scrollAndSelectRow(tree, row); 611 drag(tree, p); 612 } 613 614 @RunsInEDT 615 private Point scrollAndSelectRow(JTree tree, int row) { 616 Pair<Boolean, Point> info = scrollToRow(tree, row, location); 617 Point p = info.ii; 618 if (!info.i) robot.click(tree, p); // path not selected, click to select 619 return p; 620 } 621 622 /** 623 * Ends a drag operation at the location of the given row. 624 * @param tree the target <code>JTree</code>. 625 * @param row the given row. 626 * @throws IllegalStateException if the <code>JTree</code> is disabled. 627 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 628 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 629 * visible rows in the <code>JTree</code>. 630 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 631 * @throws ActionFailedException if there is no drag action in effect. 632 */ 633 @RunsInEDT 634 public void drop(JTree tree, int row) { 635 drop(tree, scrollToRow(tree, row, location).ii); 636 } 637 638 /* 639 * Returns: 640 * 1. if the node is expanded 641 * 2. the location of the node 642 */ 643 @RunsInEDT 644 private static Pair<Boolean, Point> scrollToRow(final JTree tree, final int row, final JTreeLocation location) { 645 return execute(new GuiQuery<Pair<Boolean, Point>>() { 646 protected Pair<Boolean, Point> executeInEDT() { 647 validateIsEnabledAndShowing(tree); 648 Point p = scrollToVisible(tree, row, location); 649 boolean selected = tree.getSelectionCount() == 1 && tree.isRowSelected(row); 650 return new Pair<Boolean, Point>(selected, p); 651 } 652 }); 653 } 654 655 @RunsInCurrentThread 656 private static Point scrollToVisible(JTree tree, int row, JTreeLocation location) { 657 Pair<Rectangle, Point> boundsAndCoordinates = location.rowBoundsAndCoordinates(tree, row); 658 tree.scrollRectToVisible(boundsAndCoordinates.i); 659 return boundsAndCoordinates.ii; 660 } 661 662 /** 663 * Starts a drag operation at the location of the given <code>{@link TreePath}</code>. 664 * @param tree the target <code>JTree</code>. 665 * @param path the given path. 666 * @throws IllegalStateException if the <code>JTree</code> is disabled. 667 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 668 * @throws LocationUnavailableException if the given path cannot be found. 669 * @see #separator(String) 670 */ 671 @RunsInEDT 672 public void drag(JTree tree, String path) { 673 Point p = selectMatchingPath(tree, path); 674 drag(tree, p); 675 } 676 677 @RunsInEDT 678 private Point selectMatchingPath(JTree tree, String path) { 679 Triple<TreePath, Boolean, Point> info = scrollToMatchingPath(tree, path); 680 robot.waitForIdle(); 681 Point where = info.iii; 682 if (!info.ii) robot.click(tree, where); // path not selected, click to select 683 return where; 684 } 685 686 /** 687 * Ends a drag operation at the location of the given <code>{@link TreePath}</code>. 688 * @param tree the target <code>JTree</code>. 689 * @param path the given path. 690 * @throws IllegalStateException if the <code>JTree</code> is disabled. 691 * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen. 692 * @throws LocationUnavailableException if the given path cannot be found. 693 * @throws ActionFailedException if there is no drag action in effect. 694 * @see #separator(String) 695 */ 696 @RunsInEDT 697 public void drop(JTree tree, String path) { 698 Point p = scrollToMatchingPath(tree, path).iii; 699 drop(tree, p); 700 } 701 702 /* 703 * returns: 704 * 1. the found matching path 705 * 2. whether the path is already selected 706 * 3. the location where the path is in the JTree 707 */ 708 @RunsInEDT 709 private Triple<TreePath, Boolean, Point> scrollToMatchingPath(JTree tree, String path) { 710 TreePath matchingPath = verifyJTreeIsReadyAndFindMatchingPath(tree, path, pathFinder); 711 makeVisible(tree, matchingPath, false); 712 Pair<Boolean, Point> info = scrollToPathToSelect(tree, matchingPath, location); 713 return new Triple<TreePath, Boolean, Point>(matchingPath, info.i, info.ii); 714 } 715 716 /* 717 * returns: 718 * 1. whether the path is already selected 719 * 2. the location where the path is in the JTree 720 */ 721 @RunsInEDT 722 private static Pair<Boolean, Point> scrollToPathToSelect(final JTree tree, final TreePath path, final JTreeLocation location) { 723 return execute(new GuiQuery<Pair<Boolean, Point>>() { 724 protected Pair<Boolean, Point> executeInEDT() { 725 boolean isSelected = tree.getSelectionCount() == 1 && tree.isPathSelected(path); 726 return new Pair<Boolean, Point>(isSelected, scrollToTreePath(tree, path, location)); 727 } 728 }); 729 } 730 731 @RunsInCurrentThread 732 private static Point scrollToTreePath(JTree tree, TreePath path, JTreeLocation location) { 733 Pair<Rectangle, Point> boundsAndCoordinates = location.pathBoundsAndCoordinates(tree, path); 734 tree.scrollRectToVisible(boundsAndCoordinates.i); 735 return boundsAndCoordinates.ii; 736 } 737 738 @RunsInEDT 739 private boolean makeParentVisible(JTree tree, TreePath path) { 740 boolean changed = makeVisible(tree, path.getParentPath(), true); 741 if (changed) robot.waitForIdle(); 742 return changed; 743 } 744 745 /** 746 * Matches, makes visible, and expands the path one component at a time, from uppermost ancestor on down, since 747 * children may be lazily loaded/created. 748 * @param tree the target <code>JTree</code>. 749 * @param path the tree path to make visible. 750 * @param expandWhenFound indicates if nodes should be expanded or not when found. 751 * @return if it was necessary to make visible and/or expand a node in the path. 752 */ 753 @RunsInEDT 754 private boolean makeVisible(JTree tree, TreePath path, boolean expandWhenFound) { 755 boolean changed = false; 756 if (path.getPathCount() > 1) changed = makeParentVisible(tree, path); 757 if (!expandWhenFound) return changed; 758 expandTreePath(tree, path); 759 waitForChildrenToShowUp(tree, path); 760 return true; 761 } 762 763 @RunsInEDT 764 private void waitForChildrenToShowUp(JTree tree, TreePath path) { 765 int timeout = robot.settings().timeoutToBeVisible(); 766 try { 767 pause(untilChildrenShowUp(tree, path), timeout); 768 } catch (WaitTimedOutError e) { 769 throw new LocationUnavailableException(e.getMessage()); 770 } 771 } 772 773 /** 774 * Asserts that the given <code>{@link JTree}</code>'s selected rows are equal to the given one. 775 * @param tree the target <code>JTree</code>. 776 * @param rows the indices of the rows, expected to be selected. 777 * @throws NullPointerException if the array of row indices is <code>null</code>. 778 * @throws AssertionError if the given <code>JTree</code> selection is not equal to the given rows. 779 */ 780 @RunsInEDT 781 public void requireSelection(JTree tree, int[] rows) { 782 if (rows == null) throw new NullPointerException("The array of row indices should not be null"); 783 verifySelection(tree, rows, selectionProperty(tree)); 784 } 785 786 /** 787 * Asserts that the given <code>{@link JTree}</code>'s selected paths are equal to the given one. 788 * @param tree the target <code>JTree</code>. 789 * @param paths the given paths, expected to be selected. 790 * @throws NullPointerException if the array of paths is <code>null</code>. 791 * @throws LocationUnavailableException if any of the given paths cannot be found. 792 * @throws AssertionError if the given <code>JTree</code> selection is not equal to the given paths. 793 * @see #separator(String) 794 */ 795 @RunsInEDT 796 public void requireSelection(JTree tree, String[] paths) { 797 if (paths == null) throw new NullPointerException("The array of paths should not be null"); 798 verifySelection(tree, paths, pathFinder, selectionProperty(tree)); 799 } 800 801 /** 802 * Asserts that the given <code>{@link JTree}</code> does not have any selection. 803 * @param tree the given <code>JTree</code>. 804 * @throws AssertionError if the <code>JTree</code> has a selection. 805 */ 806 @RunsInEDT 807 public void requireNoSelection(JTree tree) { 808 verifyNoSelection(tree, selectionProperty(tree)); 809 } 810 811 @RunsInEDT 812 private Description selectionProperty(JTree tree) { 813 return propertyName(tree, SELECTION_PROPERTY); 814 } 815 816 /** 817 * Asserts that the given <code>{@link JTree}</code> is editable. 818 * @param tree the given <code>JTree</code>. 819 * @throws AssertionError if the <code>JTree</code> is not editable. 820 */ 821 @RunsInEDT 822 public void requireEditable(JTree tree) { 823 assertEditable(tree, true); 824 } 825 826 /** 827 * Asserts that the given <code>{@link JTree}</code> is not editable. 828 * @param tree the given <code>JTree</code>. 829 * @throws AssertionError if the <code>JTree</code> is editable. 830 */ 831 @RunsInEDT 832 public void requireNotEditable(JTree tree) { 833 assertEditable(tree, false); 834 } 835 836 @RunsInEDT 837 private void assertEditable(JTree tree, boolean editable) { 838 assertThat(isEditable(tree)).as(editableProperty(tree)).isEqualTo(editable); 839 } 840 841 @RunsInEDT 842 private static Description editableProperty(JTree tree) { 843 return propertyName(tree, EDITABLE_PROPERTY); 844 } 845 846 /** 847 * Returns the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s. 848 * @return the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s. 849 */ 850 public String separator() { 851 return pathFinder.separator(); 852 } 853 854 /** 855 * Updates the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s. 856 * @param newSeparator the new separator. 857 * @throws NullPointerException if the given separator is <code>null</code>. 858 */ 859 public void separator(String newSeparator) { 860 if (newSeparator == null) throw new NullPointerException("The path separator should not be null"); 861 pathFinder.separator(newSeparator); 862 } 863 864 /** 865 * Updates the implementation of <code>{@link JTreeCellReader}</code> to use when comparing internal values of a 866 * <code>{@link JTree}</code> and the values expected in a test. 867 * @param newCellReader the new <code>JTreeCellValueReader</code> to use. 868 * @throws NullPointerException if <code>newCellReader</code> is <code>null</code>. 869 */ 870 public void cellReader(JTreeCellReader newCellReader) { 871 validateCellReader(newCellReader); 872 pathFinder.cellReader(newCellReader); 873 } 874 875 /** 876 * Verifies that the given row index is valid. 877 * @param tree the given <code>JTree</code>. 878 * @param row the given index. 879 * @throws IndexOutOfBoundsException if the given index is less than zero or equal than or greater than the number of 880 * visible rows in the <code>JTree</code>. 881 * @since 1.2 882 */ 883 @RunsInEDT 884 public void validateRow(JTree tree, int row) { 885 location.validIndex(tree, row); 886 } 887 888 /** 889 * Verifies that the given node path exists. 890 * @param tree the given <code>JTree</code>. 891 * @param path the given path. 892 * @throws LocationUnavailableException if the given path cannot be found. 893 * @since 1.2 894 */ 895 @RunsInEDT 896 public void validatePath(JTree tree, String path) { 897 matchingPathFor(tree, path, pathFinder); 898 } 899 900 /** 901 * Returns the <code>String</code> representation of the node at the given path. 902 * @param tree the given <code>JTree</code>. 903 * @param path the given path. 904 * @return the <code>String</code> representation of the node at the given path. 905 * @throws LocationUnavailableException if the given path cannot be found. 906 * @since 1.2 907 */ 908 @RunsInEDT 909 public String nodeValue(JTree tree, String path) { 910 return nodeText(tree, path, pathFinder); 911 } 912 913 /** 914 * Returns the <code>String</code> representation of the node at the given row index. 915 * @param tree the given <code>JTree</code>. 916 * @param row the given row. 917 * @return the <code>String</code> representation of the node at the given row index. 918 * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of 919 * visible rows in the <code>JTree</code>. 920 * @throws LocationUnavailableException if a tree path for the given row cannot be found. 921 * @since 1.2 922 */ 923 public String nodeValue(JTree tree, int row) { 924 return nodeText(tree, row, location, pathFinder); 925 } 926 927 @VisibleForTesting 928 JTreeCellReader cellReader() { return pathFinder.cellReader(); } 929 }