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 }