package abbot.tester;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.TreePath;
import abbot.WaitTimedOutError;
import abbot.i18n.Strings;
import abbot.script.ArgumentParser;
import abbot.script.Condition;
import abbot.util.AWT;
/** Provide operations on a JTree component.
The JTree substructure is a "row", and JTreeLocation provides different
identifiers for a row.
<ul>
<li>Select an item by row index
<li>Select an item by tree path (the string representation of the full
path).
</ul>
@see abbot.tester.JTreeLocation
*/
// TODO: multi-select
// TODO: expand/collapse actions
public class JTreeTester extends JComponentTester {
/** Returns whether the given point is in one of the JTree's node
* expansion controls.
*/
public static boolean isLocationInExpandControl(JTree tree, int x, int y) {
int row = tree.getRowForLocation(x, y);
if (row == -1) {
row = tree.getClosestRowForLocation(x, y);
if (row != -1) {
Rectangle rect = tree.getRowBounds(row);
if (row == tree.getRowCount()-1) {
if (y >= rect.y + rect.height)
return false;
}
// An approximation: use a square area to the left of the row
// bounds.
TreePath path = tree.getPathForRow(row);
if (path == null || tree.getModel().
isLeaf(path.getLastPathComponent()))
return false;
if (tree.getUI() instanceof BasicTreeUI) {
try {
java.lang.reflect.Method method =
BasicTreeUI.class.
getDeclaredMethod("isLocationInExpandControl",
new Class[] {
TreePath.class,
int.class, int.class,
});
method.setAccessible(true);
Object b = method.invoke(tree.getUI(), new Object[] {
path, new Integer(x), new Integer(y),
});
return b.equals(Boolean.TRUE);
}
catch(Exception e) {
}
}
// fall back to a best guess
//return x >= rect.x - rect.height && x < rect.x;
String msg = "Can't determine location of tree expansion "
+ "control for " + tree.getUI();
throw new RuntimeException(msg);
}
}
return false;
}
/** Return the {@link String} representation of the final component of the
* given {@link TreePath}, or <code>null</code> if one can not be
* obtained. Assumes the path is visible.
*/
public static String valueToString(JTree tree, TreePath path) {
Object value = path.getLastPathComponent();
int row = tree.getRowForPath(path);
// The default renderer will rely on JTree.convertValueToText
Component cr = tree.getCellRenderer().
getTreeCellRendererComponent(tree, value, false,
tree.isExpanded(row),
tree.getModel().isLeaf(value),
row, false);
String string = null;
if (cr instanceof JLabel) {
String label = ((JLabel)cr).getText();
if (label != null)
label = label.trim();
if (!"".equals(label)
&& !ArgumentParser.isDefaultToString(label)) {
string = label;
}
}
if (string == null) {
string = tree.convertValueToText(value, false,
tree.isExpanded(row),
tree.getModel().isLeaf(value),
row, false);
if (ArgumentParser.isDefaultToString(string))
string = null;
}
if (string == null) {
String s = ArgumentParser.toString(value);
string = s == ArgumentParser.DEFAULT_TOSTRING ? null : s;
}
return string;
}
/** Return the String representation of the given TreePath, or null if one
* can not be obtained. Assumes the path is visible.
*/
public static TreePath pathToStringPath(JTree tree, TreePath path) {
if (path == null)
return null;
String string = valueToString(tree, path);
if (string != null) {
// Prepend the parent value, if any
if (path.getPathCount() > 1) {
TreePath parent = pathToStringPath(tree, path.getParentPath());
if (parent == null)
return null;
return parent.pathByAddingChild(string);
}
return new TreePath(string);
}
return null;
}
/** Click at the given location. If the location indicates a path, ensure
it is visible first.
*/
public void actionClick(Component c, ComponentLocation loc) {
if (loc instanceof JTreeLocation) {
TreePath path = ((JTreeLocation)loc).getPath((JTree)c);
if (path != null)
makeVisible(c, path);
}
super.actionClick(c, loc);
}
/** Select the given row. If the row is already selected, does nothing. */
public void actionSelectRow(Component c, ComponentLocation loc) {
JTree tree = (JTree)c;
if (loc instanceof JTreeLocation) {
TreePath path = ((JTreeLocation)loc).getPath((JTree)c);
if (path == null) {
String msg = Strings.get("tester.JTree.path_not_found",
new Object[] { loc });
throw new LocationUnavailableException(msg);
}
makeVisible(c, path);
}
Point where = loc.getPoint(c);
int row = tree.getRowForLocation(where.x, where.y);
if (tree.getLeadSelectionRow() != row
|| tree.getSelectionCount() != 1) {
// NOTE: the row bounds *do not* include the expansion handle
Rectangle rect = tree.getRowBounds(row);
// NOTE: if there's no icon, this may start editing
actionClick(tree, rect.x + 1, rect.y + rect.height/2);
}
}
/** Select the given row. If the row is already selected, does nothing.
Equivalent to actionSelectRow(c, new JTreeLocation(row)).
*/
public void actionSelectRow(Component tree, int row) {
actionSelectRow(tree, new JTreeLocation(row));
}
/** Simple click on the given row. */
public void actionClickRow(Component tree, int row) {
actionClick(tree, new JTreeLocation(row));
}
/** Click with modifiers on the given row.
@deprecated Use the ComponentLocation version.
*/
public void actionClickRow(Component tree, int row, String modifiers) {
actionClick(tree, new JTreeLocation(row), AWT.getModifiers(modifiers));
}
/** Multiple click on the given row.
@deprecated Use the ComponentLocation version.
*/
public void actionClickRow(Component c, int row,
String modifiers, int count) {
actionClick(c, new JTreeLocation(row), AWT.getModifiers(modifiers), count);
}
/** Make the given path visible, if possible, and returns whether any
* action was taken.
* @throws LocationUnavailableException if no corresponding path can be
* found.
*/
protected boolean makeVisible(Component c, TreePath path) {
return makeVisible(c, path, false);
}
private boolean makeVisible(Component c, final TreePath path,
boolean expandWhenFound) {
final JTree tree = (JTree)c;
// Match, make visible, and expand the path one component at a time,
// from uppermost ancestor on down, since children may be lazily
// loaded/created
boolean changed = false;
if (path.getPathCount() > 1) {
changed = makeVisible(c, path.getParentPath(), true);
if (changed)
waitForIdle();
}
final TreePath realPath = JTreeLocation.findMatchingPath(tree, path);
if (expandWhenFound) {
if (!tree.isExpanded(realPath)) {
// Use this method instead of a toggle action to avoid
// any component visibility requirements
invokeAndWait(new Runnable() {
public void run() {
tree.expandPath(realPath);
}
});
}
final Object o = realPath.getLastPathComponent();
// Wait for a child to show up
try {
wait(new Condition() {
public boolean test() {
return tree.getModel().getChildCount(o) != 0;
}
public String toString() {
return Strings.get("tester.Component.show_wait",
new Object[] { path.toString() });
}
}, componentDelay);
changed = true;
}
catch(WaitTimedOutError e) {
throw new LocationUnavailableException(e.getMessage());
}
}
return changed;
}
/** Ensure all elements of the given path are visible. */
public void actionMakeVisible(Component c, TreePath path) {
makeVisible(c, path);
}
/** Select the given path, expanding parent nodes if necessary. */
public void actionSelectPath(Component c, TreePath path) {
actionSelectRow(c, new JTreeLocation(path));
}
/** Change the open/closed state of the given row, if possible.
@deprecated Use the ComponentLocation version instead.
*/
public void actionToggleRow(Component c, int row) {
actionToggleRow(c, new JTreeLocation(row));
}
/** Change the open/closed state of the given row, if possible. */
// NOTE: a reasonable assumption is that the toggle control is just to the
// left of the row bounds and is roughly a square the dimensions of the
// row height. clicking in the center of that square should work.
public void actionToggleRow(Component c, ComponentLocation loc) {
JTree tree = (JTree)c;
// Alternatively, we can reflect into the UI and do a single click
// on the appropriate expand location, but this is safer.
if (tree.getToggleClickCount() != 0) {
actionClick(tree, loc, InputEvent.BUTTON1_MASK,
tree.getToggleClickCount());
}
else {
// BasicTreeUI provides this method; punt if we can't find it
if (!(tree.getUI() instanceof BasicTreeUI))
throw new ActionFailedException("Can't toggle row for "
+ tree.getUI());
try {
java.lang.reflect.Method method =
BasicTreeUI.class.
getDeclaredMethod("toggleExpandState",
new Class[] {
TreePath.class
});
method.setAccessible(true);
Point where = loc.getPoint(tree);
method.invoke(tree.getUI(), new Object[] {
tree.getPathForLocation(where.x, where.y)
});
}
catch(Exception e) {
throw new ActionFailedException(e.toString());
}
}
}
/** Determine whether a given path exists, expanding ancestor nodes as
* necessary to find it.
* @return Whether the given path on the given tree exists.
*/
public boolean assertPathExists(Component tree, TreePath path) {
try {
makeVisible(tree, path);
return true;
}
catch(LocationUnavailableException e) {
return false;
}
}
/** Parse the String representation of a JTreeLocation into the actual
JTreeLocation object.
*/
public ComponentLocation parseLocation(String encoded) {
return new JTreeLocation().parse(encoded);
}
/** Convert the coordinate into a more meaningful location. Namely, use a
* path, row, or coordinate.
*/
public ComponentLocation getLocation(Component c, Point p) {
JTree tree = (JTree)c;
if (tree.getRowCount() == 0)
return new JTreeLocation(p);
Rectangle rect = tree.getRowBounds(tree.getRowCount()-1);
int maxY = rect.y + rect.height;
if (p.y > maxY)
return new JTreeLocation(p);
// TODO: ignore clicks to the left of the expansion control, or maybe
// embed them in the location.
TreePath path = tree.getClosestPathForLocation(p.x, p.y);
TreePath stringPath = pathToStringPath(tree, path);
if (stringPath != null) {
// if the root is hidden, drop it from the path
if (!tree.isRootVisible()) {
Object[] objs = stringPath.getPath();
Object[] subs = new Object[objs.length-1];
System.arraycopy(objs, 1, subs, 0, subs.length);
stringPath = new TreePath(subs);
}
return new JTreeLocation(stringPath);
}
int row = tree.getClosestRowForLocation(p.x, p.y);
if (row != -1) {
return new JTreeLocation(row);
}
return new JTreeLocation(p);
}
}