001: package abbot.tester;
002:
003: import abbot.Log;
004:
005: import java.awt.*;
006: import java.awt.event.InputEvent;
007:
008: import java.lang.reflect.InvocationTargetException;
009:
010: import javax.accessibility.AccessibleContext;
011:
012: import javax.swing.JLabel;
013: import javax.swing.JTree;
014: import javax.swing.tree.*;
015: import javax.swing.plaf.basic.BasicTreeUI;
016:
017: import abbot.WaitTimedOutError;
018: import abbot.script.ArgumentParser;
019: import abbot.util.AWT;
020: import abbot.util.Condition;
021: import abbot.i18n.Strings;
022:
023: import java.lang.reflect.Method;
024:
025: import javax.accessibility.AccessibleText;
026:
027: import javax.swing.JComponent;
028:
029: /** Provide operations on a JTree component.
030: The JTree substructure is a "row", and JTreeLocation provides different
031: identifiers for a row.
032: <ul>
033: <li>Select an item by row index
034: <li>Select an item by tree path (the string representation of the full
035: path).
036: </ul>
037: @see abbot.tester.JTreeLocation
038: */
039: // TODO: multi-select
040: // TODO: expand/collapse actions
041: public class JTreeTester extends JComponentTester {
042:
043: /** Returns whether the given point is in one of the JTree's node
044: * expansion controls.
045: */
046: public static boolean isLocationInExpandControl(JTree tree, int x,
047: int y) {
048: int row = tree.getRowForLocation(x, y);
049: if (row == -1) {
050: row = tree.getClosestRowForLocation(x, y);
051: if (row != -1) {
052: Rectangle rect = tree.getRowBounds(row);
053: if (row == tree.getRowCount() - 1) {
054: if (y >= rect.y + rect.height)
055: return false;
056: }
057: // An approximation: use a square area to the left of the row
058: // bounds.
059: TreePath path = tree.getPathForRow(row);
060: if (path == null
061: || tree.getModel().isLeaf(
062: path.getLastPathComponent()))
063: return false;
064:
065: if (tree.getUI() instanceof BasicTreeUI) {
066: try {
067: java.lang.reflect.Method method = BasicTreeUI.class
068: .getDeclaredMethod(
069: "isLocationInExpandControl",
070: new Class[] { TreePath.class,
071: int.class, int.class, });
072: method.setAccessible(true);
073: Object b = method.invoke(tree.getUI(),
074: new Object[] { path, new Integer(x),
075: new Integer(y), });
076: return b.equals(Boolean.TRUE);
077: } catch (Exception e) {
078: }
079: }
080: // fall back to a best guess
081: //return x >= rect.x - rect.height && x < rect.x;
082: String msg = "Can't determine location of tree expansion "
083: + "control for " + tree.getUI();
084: throw new LocationUnavailableException(msg);
085: }
086: }
087: return false;
088: }
089:
090: /** Return a unique {@link String} representation of the final component of
091: * the given {@link TreePath}, or <code>null</code> if one can not be
092: * obtained. Assumes the path is visible.
093: */
094: public static String valueToString(JTree tree, TreePath path) {
095: return valueToString(tree, path, true);
096: }
097:
098: /** Return the {@link String} representation of the final component of the
099: * given {@link TreePath}, or <code>null</code> if one can not be
100: * obtained. Assumes the path is visible.
101: * Optionally include a unique trailing index.
102: */
103: private static String valueToString(JTree tree, TreePath path,
104: boolean includeIndex) {
105: Object value = path.getLastPathComponent();
106: int row = tree.getRowForPath(path);
107: // The default renderer will rely on JTree.convertValueToText
108: Component cr = tree.getCellRenderer()
109: .getTreeCellRendererComponent(tree, value, false,
110: tree.isExpanded(row),
111: tree.getModel().isLeaf(value), row, false);
112: String string = convertRendererToString(cr);
113:
114: if (string == null) {
115: string = tree.convertValueToText(value, false, tree
116: .isExpanded(row), tree.getModel().isLeaf(value),
117: row, false);
118: if (ArgumentParser.isDefaultToString(string))
119: string = null;
120: }
121:
122: if (string == null) {
123: String s = ArgumentParser.toString(value);
124: string = s == ArgumentParser.DEFAULT_TOSTRING ? null : s;
125: }
126:
127: if (includeIndex) {
128: // If there are preceding siblings which produce the same
129: // string value, obtain one that is unique by adding an index
130: TreePath parentPath = path.getParentPath();
131: if (parentPath != null) {
132: Object parent = parentPath.getLastPathComponent();
133: int idx = tree.getModel()
134: .getIndexOfChild(parent, value);
135: int count = 0;
136: for (int i = 0; i < idx; i++) {
137: Object child = tree.getModel().getChild(parent, i);
138: TreePath childPath = parentPath
139: .pathByAddingChild(child);
140: String text = valueToString(tree, childPath, false);
141: // string == text deals with double null null case
142: if ((string == text)
143: || (string != null && string.equals(text))) {
144: ++count;
145: }
146: }
147:
148: // If the string is null then ensure that we at least
149: // traces something out
150: //
151:
152: if (count > 0 || string == null) {
153: if (string != null) {
154: string += "[" + count + "]";
155: } else {
156: string = "[" + count + "]";
157: }
158: }
159: }
160: }
161: return string;
162: }
163:
164: /** Return the String representation of the given TreePath, or null if one
165: * can not be obtained. Assumes the path is visible.
166: */
167: public static TreePath pathToStringPath(JTree tree, TreePath path) {
168: if (path == null)
169: return null;
170:
171: String string = valueToString(tree, path);
172: if (string != null) {
173: // Prepend the parent value, if any
174: if (path.getPathCount() > 1) {
175: TreePath parent = pathToStringPath(tree, path
176: .getParentPath());
177: if (parent == null)
178: return null;
179: return parent.pathByAddingChild(string);
180: }
181: return new TreePath(string);
182: }
183: return null;
184: }
185:
186: /** Click at the given location. If the location indicates a path, ensure
187: it is visible first.
188: */
189: public void actionClick(Component c, ComponentLocation loc) {
190: if (loc instanceof JTreeLocation) {
191: TreePath path = ((JTreeLocation) loc).getPath((JTree) c);
192: if (path != null)
193: makeVisible(c, path);
194: }
195: super .actionClick(c, loc);
196: }
197:
198: /** Select the given row. If the row is already selected, does nothing. */
199: public void actionSelectRow(Component c, ComponentLocation loc) {
200: JTree tree = (JTree) c;
201: if (loc instanceof JTreeLocation) {
202: JTreeLocation jTreeLocation = (JTreeLocation) loc;
203: TreePath path = jTreeLocation.getPath((JTree) c);
204: if (path == null) {
205: String msg = Strings.get("tester.JTree.path_not_found",
206: new Object[] { loc });
207: throw new LocationUnavailableException(msg);
208: }
209: makeVisible(c, path);
210:
211: // Scroll to that location, makes the action click easier as the
212: // code in there for scrolling is more fragiles as it doesn't
213: // have access to the tree path.
214: //
215: // Need to fetch path again as data can be different now that
216: // the tree is properly expanded
217: //
218:
219: tree.scrollPathToVisible(jTreeLocation.getPath(tree));
220: }
221: Point where = loc.getPoint(c);
222: int row = tree.getRowForLocation(where.x, where.y);
223: if (tree.getLeadSelectionRow() != row
224: || tree.getSelectionCount() != 1) {
225: // NOTE: the row bounds *do not* include the expansion handle
226: Rectangle rect = tree.getRowBounds(row);
227: // NOTE: if there's no icon, this may start editing
228: actionClick(tree, rect.x + 1, rect.y + rect.height / 2);
229: }
230: }
231:
232: /** Select the given row. If the row is already selected, does nothing.
233: Equivalent to actionSelectRow(c, new JTreeLocation(row)).
234: */
235: public void actionSelectRow(Component tree, int row) {
236: actionSelectRow(tree, new JTreeLocation(row));
237: }
238:
239: /** Simple click on the given row. */
240: public void actionClickRow(Component tree, int row) {
241: actionClick(tree, new JTreeLocation(row));
242: }
243:
244: /** Click with modifiers on the given row.
245: @deprecated Use the ComponentLocation version.
246: */
247: public void actionClickRow(Component tree, int row, String modifiers) {
248: actionClick(tree, new JTreeLocation(row), AWT
249: .getModifiers(modifiers));
250: }
251:
252: /** Multiple click on the given row.
253: @deprecated Use the ComponentLocation version.
254: */
255: public void actionClickRow(Component c, int row, String modifiers,
256: int count) {
257: actionClick(c, new JTreeLocation(row), AWT
258: .getModifiers(modifiers), count);
259: }
260:
261: /** Make the given path visible, if possible, and returns whether any
262: * action was taken.
263: * @throws LocationUnavailableException if no corresponding path can be
264: * found.
265: */
266: protected boolean makeVisible(Component c, TreePath path) {
267: return makeVisible(c, path, false);
268: }
269:
270: private boolean makeVisible(Component c, final TreePath path,
271: boolean expandWhenFound) {
272: return makeVisible(c, path, componentDelay, expandWhenFound);
273: }
274:
275: private boolean makeVisible(Component c, final TreePath path,
276: final int timeout, boolean expandWhenFound) {
277: final JTree tree = (JTree) c;
278: // Match, make visible, and expand the path one component at a time,
279: // from uppermost ancestor on down, since children may be lazily
280: // loaded/created
281: boolean changed = false;
282: if (path.getPathCount() > 1) {
283: changed = makeVisible(c, path.getParentPath(), true);
284: if (changed)
285: waitForIdle();
286: }
287:
288: // Wait for the node to exist
289: //
290:
291: final TreePath found[] = new TreePath[1];
292:
293: try {
294: wait(new Condition() {
295: public boolean test() {
296: try {
297: found[0] = JTreeLocation.findMatchingPath(tree,
298: path);
299: return found[0] != null;
300: } catch (LocationUnavailableException lue) {
301: return false;
302: }
303: }
304:
305: public String toString() {
306: return Strings.get("tester.Component.show_wait",
307: new Object[] { path.toString() });
308: }
309: }, timeout);
310: } catch (WaitTimedOutError e) {
311: throw new LocationUnavailableException(e.getMessage());
312: }
313:
314: //
315:
316: final TreePath realPath = found[0];
317: if (expandWhenFound) {
318: if (!tree.isExpanded(realPath)) {
319: // Use this method instead of a toggle action to avoid
320: // any component visibility requirements
321: invokeAndWait(new Runnable() {
322: public void run() {
323: tree.expandPath(realPath);
324: }
325: });
326: }
327: final Object o = realPath.getLastPathComponent();
328: // Wait for a child to show up
329: try {
330: wait(new Condition() {
331: public boolean test() {
332: return tree.getModel().getChildCount(o) != 0;
333: }
334:
335: public String toString() {
336: return Strings.get(
337: "tester.Component.show_wait",
338: new Object[] { path.toString() });
339: }
340: }, timeout);
341: changed = true;
342: } catch (WaitTimedOutError e) {
343: throw new LocationUnavailableException(e.getMessage());
344: }
345: }
346: return changed;
347: }
348:
349: /** Ensure all elements of the given path are visible. */
350: public void actionMakeVisible(Component c, TreePath path) {
351: makeVisible(c, path);
352: }
353:
354: /** Select the given path, expanding parent nodes if necessary. */
355: public void actionSelectPath(Component c, TreePath path) {
356: actionSelectRow(c, new JTreeLocation(path));
357: }
358:
359: /** Change the open/closed state of the given row, if possible.
360: @deprecated Use the ComponentLocation version instead.
361: */
362: public void actionToggleRow(Component c, int row) {
363: actionToggleRow(c, new JTreeLocation(row));
364: }
365:
366: /** Change the open/closed state of the given row, if possible. */
367: // NOTE: a reasonable assumption is that the toggle control is just to the
368: // left of the row bounds and is roughly a square the dimensions of the
369: // row height. clicking in the center of that square should work.
370: public void actionToggleRow(Component c, ComponentLocation loc) {
371: JTree tree = (JTree) c;
372: // Alternatively, we can reflect into the UI and do a single click
373: // on the appropriate expand location, but this is safer.
374: if (tree.getToggleClickCount() != 0) {
375: actionClick(tree, loc, InputEvent.BUTTON1_MASK, tree
376: .getToggleClickCount());
377: } else {
378: // BasicTreeUI provides this method; punt if we can't find it
379: if (!(tree.getUI() instanceof BasicTreeUI))
380: throw new ActionFailedException("Can't toggle row for "
381: + tree.getUI());
382: try {
383: java.lang.reflect.Method method = BasicTreeUI.class
384: .getDeclaredMethod("toggleExpandState",
385: new Class[] { TreePath.class });
386: method.setAccessible(true);
387: Point where = loc.getPoint(tree);
388: method.invoke(tree.getUI(), new Object[] { tree
389: .getPathForLocation(where.x, where.y) });
390: } catch (Exception e) {
391: throw new ActionFailedException(e.toString());
392: }
393: }
394: }
395:
396: /** Determine whether a given path exists, expanding ancestor nodes as
397: * necessary to find it.
398: * @return Whether the given path on the given tree exists.
399: */
400: public boolean assertPathExists(Component tree, TreePath path) {
401: return assertPathExists(tree, path, false);
402: }
403:
404: /** Determine whether a given path exists, expanding ancestor nodes as
405: * necessary to find it.
406: * @param invert Whether it invert the sense of this operation, when inverted
407: * it assumed a timeout of zero.
408: * @return Whether the given path on the given tree exists.
409: */
410: public boolean assertPathExists(Component tree, TreePath path,
411: boolean invert) {
412: return assertPathExists(tree, path,
413: invert ? 0 : componentDelay, invert);
414: }
415:
416: /** Determine whether a given path exists, expanding ancestor nodes as
417: * necessary to find it.
418: * @param timeout How long to wait for the tree path to become avaliable
419: * @param invert Whether it invert the sense of this operation
420: * @return Whether the given path on the given tree exists.
421: */
422: public boolean assertPathExists(Component tree, TreePath path,
423: int timeout, boolean invert) {
424: try {
425: makeVisible(tree, path, timeout, false);
426: return !invert;
427: } catch (LocationUnavailableException e) {
428: return invert;
429: }
430: }
431:
432: /** Parse the String representation of a JTreeLocation into the actual
433: JTreeLocation object.
434: */
435: public ComponentLocation parseLocation(String encoded) {
436: return new JTreeLocation().parse(encoded);
437: }
438:
439: /** Convert the coordinate into a more meaningful location. Namely, use a
440: * path, row, or coordinate.
441: */
442: public ComponentLocation getLocation(Component c, Point p) {
443: JTree tree = (JTree) c;
444: if (tree.getRowCount() == 0)
445: return new JTreeLocation(p);
446: Rectangle rect = tree.getRowBounds(tree.getRowCount() - 1);
447: int maxY = rect.y + rect.height;
448: if (p.y > maxY)
449: return new JTreeLocation(p);
450:
451: // TODO: ignore clicks to the left of the expansion control, or maybe
452: // embed them in the location.
453: TreePath path = tree.getClosestPathForLocation(p.x, p.y);
454: TreePath stringPath = pathToStringPath(tree, path);
455: if (stringPath != null) {
456: // if the root is hidden, drop it from the path
457: if (!tree.isRootVisible()) {
458: Object[] objs = stringPath.getPath();
459: Object[] subs = new Object[objs.length - 1];
460: System.arraycopy(objs, 1, subs, 0, subs.length);
461: stringPath = new TreePath(subs);
462: }
463: return new JTreeLocation(stringPath);
464: }
465: int row = tree.getClosestRowForLocation(p.x, p.y);
466: if (row != -1) {
467: return new JTreeLocation(row);
468: }
469: return new JTreeLocation(p);
470: }
471:
472: }
|