/*******************************************************************************
* Copyright (c) 2004, 2010 BREDEX GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BREDEX GmbH - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.jubula.rc.swing.tester.util;
import java.awt.Component;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.swing.JTree;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.Validate;
import org.eclipse.jubula.rc.common.driver.ClickOptions;
import org.eclipse.jubula.rc.common.driver.IEventThreadQueuer;
import org.eclipse.jubula.rc.common.driver.IRobot;
import org.eclipse.jubula.rc.common.driver.IRunnable;
import org.eclipse.jubula.rc.common.exception.StepExecutionException;
import org.eclipse.jubula.rc.common.implclasses.tree.AbstractTreeOperationContext;
import org.eclipse.jubula.rc.common.logger.AutServerLogger;
import org.eclipse.jubula.rc.common.util.SelectionUtil;
import org.eclipse.jubula.rc.swing.driver.EventThreadQueuerAwtImpl;
import org.eclipse.jubula.tools.internal.objects.event.EventFactory;
import org.eclipse.jubula.tools.internal.objects.event.TestErrorEvent;
/**
* This context holds the tree, the tree model and supports access to the Robot.
* It also implements some general operations on trees.
*
* @author BREDEX GmbH
* @created 09.08.2005
*/
public class TreeOperationContext
extends AbstractTreeOperationContext<JTree, Object> {
/** The AUT Server logger. */
private static AutServerLogger log =
new AutServerLogger(TreeOperationContext.class);
/** The tree model. */
private TreeModel m_model;
/**
* Creates a new instance. The JTree must have a tree model.
*
* @param queuer The queuer
* @param robot The Robot
* @param tree The tree
*/
public TreeOperationContext(IEventThreadQueuer queuer, IRobot robot,
JTree tree) {
super(queuer, robot, tree);
TreeModel model = tree.getModel();
Validate.notNull(model);
m_model = model;
}
/**
* {@inheritDoc}
*/
public Object[] getChildren(Object parent) {
if (parent == null) {
return getRootNodes();
}
int childCount = m_model.getChildCount(parent);
List<Object> childList = new ArrayList<Object>();
for (int i = 0; i < childCount; i++) {
childList.add(m_model.getChild(parent, i));
}
return childList.toArray();
}
/**
* {@inheritDoc}
*/
public Collection<String> getNodeTextList(Object node) {
Collection<String> res = new ArrayList<String>();
int row = getRowForTreeNode(node);
String valText = convertValueToText(node, row);
if (valText != null) {
res.add(valText);
}
String rendText = getRenderedText(node);
if (rendText != null) {
res.add(rendText);
}
return res;
}
/**
* {@inheritDoc}
*/
public int getNumberOfChildren(Object parent) {
if (parent == null) {
return getRootNodes().length;
}
return m_model.getChildCount(parent);
}
/**
* Calls
* {@link JTree#convertValueToText(java.lang.Object, boolean, boolean, boolean, int, boolean)}
* on the passed JTree.
* @param node
* The node.
* @param row
* The node row.
* @return The converted text
* @throws StepExecutionException
* If the method call fails.
*/
protected String convertValueToText(final Object node, final int row)
throws StepExecutionException {
return getQueuer().invokeAndWait(
"convertValueToText", new IRunnable<String>() { //$NON-NLS-1$
public String run() {
return getTree().convertValueToText(node, false,
getTree().isExpanded(row),
m_model.isLeaf(node), row, false);
}
});
}
/**
* {@inheritDoc}
*/
public String getRenderedText(final Object node) {
return getQueuer().invokeAndWait(
"getRenderedText", new IRunnable<String>() { //$NON-NLS-1$
public String run() {
int row = getRowForTreeNode(node);
JTree tree = getTree();
Component cellRendererComponent = tree
.getCellRenderer()
.getTreeCellRendererComponent(tree, node,
false, tree.isExpanded(row),
m_model.isLeaf(node), row, false);
try {
return TesterUtil
.getRenderedText(cellRendererComponent);
} catch (StepExecutionException e) {
// This is a valid case in JTrees since if there is no text
// there is also no renderer
log.warn("Renderer not supported: " + //$NON-NLS-1$
cellRendererComponent.getClass(), e);
return null;
}
}
});
}
/**
* Calls
* {@link JTree#convertValueToText(java.lang.Object, boolean, boolean, boolean, int, boolean)}
* on any tree node of the <code>treePath</code> and returns the texts as an array.
* @param treePath The tree path
* @return The array of converted texts
*/
protected String[] convertTreePathToText(Object treePath) {
final TreePath tp = (TreePath)treePath;
Object[] path = tp.getPath();
String[] values = new String[path.length];
for (int i = 0; i < path.length; i++) {
values[i] = convertValueToText(path[i], getRowForTreeNode(path[i]));
}
return values;
}
/**
* Returns the row for the given node. The row is calculated based on how
* many nodes are visible above this node.
* @param node The node for which to find the row.
* @return
* A zero-based index representing the row that the given node
* occupies in the tree.
* @throws StepExecutionException
* if the node cannot be found.
*/
protected int getRowForTreeNode(final Object node)
throws StepExecutionException {
Integer row = getQueuer().invokeAndWait(
"getRowForTreeNode", new IRunnable<Integer>() { //$NON-NLS-1$
public Integer run() {
TreePath pathToRoot = new TreePath(getPathToRoot(node));
return new Integer(getTree().getRowForPath(
pathToRoot));
}
});
return row.intValue();
}
/**
* Builds the parents of node up to and including the root node,
* where the original node is the last element in the returned array.
* The length of the returned array gives the node's depth in the
* tree.
*
* Contract adapted from javax.swing.tree.DefaultTreeModel.getPathToRoot().
*
* @param node the TreeNode to get the path for
* @return an array of TreeNodes giving the path from the root to the
* specified node.
*/
private Object[] getPathToRoot(Object node) {
Object rootNode = m_model.getRoot();
List<Object> path = getPathToRootImpl(node, rootNode);
return path.toArray();
}
/** {@inheritDoc} */
public Rectangle getNodeBounds(final Object node)
throws StepExecutionException {
final int row = getRowForTreeNode(node);
Rectangle nodeBounds = getQueuer().invokeAndWait(
"getRowBounds", new IRunnable<Rectangle>() { //$NON-NLS-1$
public Rectangle run() {
return getTree().getRowBounds(row);
}
});
if (nodeBounds == null) {
throw new StepExecutionException(
"Could not retrieve visible node bounds.", //$NON-NLS-1$
EventFactory.createActionError(TestErrorEvent.NOT_VISIBLE));
}
return nodeBounds;
}
/**
* Returns the path of all selected values.
* @return
* an array of Objects indicating the selected nodes, or null
* if nothing is currently selected.
*/
protected Object[] getSelectionPaths() {
return getQueuer().invokeAndWait("getSelectionPath", //$NON-NLS-1$
new IRunnable<TreePath[]>() {
public TreePath[] run() {
return getTree().getSelectionPaths();
}
});
}
/**
* {@inheritDoc}
*/
public boolean isVisible(final Object node) {
Boolean visible = getQueuer().invokeAndWait("isVisible", //$NON-NLS-1$
new IRunnable<Boolean>() {
public Boolean run() {
Object[] path = getPathToRoot(node);
return getTree().isVisible(new TreePath(path));
}
});
return visible.booleanValue();
}
/**
* Getter for the model
* @return Returns the model.
*/
protected TreeModel getModel() {
return m_model;
}
/**
* {@inheritDoc}
*/
public Rectangle getVisibleRowBounds(Rectangle rowBounds) {
Rectangle visibleTreeBounds = getTree().getVisibleRect();
Rectangle visibleRowBounds = visibleTreeBounds.intersection(rowBounds);
return visibleRowBounds;
}
/** {@inheritDoc} */
public void collapseNode(Object node) {
alterExpansionState(node, false);
}
/** {@inheritDoc} */
public void expandNode(Object node) {
alterExpansionState(node, true);
}
/**
* @param node
* the node
* @param shouldBeExpanded
* whether the nodes state is supposed to be expanded or
* collapsed
*/
private void alterExpansionState(Object node,
final boolean shouldBeExpanded) {
final JTree tree = getTree();
final ClassLoader oldCl = Thread.currentThread()
.getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(tree.getClass()
.getClassLoader());
final int row = getRowForTreeNode(node);
final Rectangle nodeBounds = getNodeBounds(node);
final boolean expanded = tree.isExpanded(row);
boolean doAction = expanded != shouldBeExpanded;
final IEventThreadQueuer queuer = new EventThreadQueuerAwtImpl();
queuer.invokeAndWait("scrollRowToVisible", new IRunnable<Void>() { //$NON-NLS-1$
public Void run() {
tree.scrollRowToVisible(row);
return null;
}
});
Rectangle visibleNodeBounds = getVisibleRowBounds(nodeBounds);
getRobot().move(tree, visibleNodeBounds);
if (doAction) {
if (log.isDebugEnabled()) {
log.debug(doAction ? "Expanding" : "Collapsing" //$NON-NLS-1$ //$NON-NLS-2$
+ " node: " + node); //$NON-NLS-1$
log.debug("Row : " + row); //$NON-NLS-1$
log.debug("Node bounds : " + visibleNodeBounds); //$NON-NLS-1$
}
queuer.invokeAndWait("alteringExpansionState", new IRunnable<Void>() { //$NON-NLS-1$
public Void run() {
if (shouldBeExpanded) {
tree.expandRow(row);
} else {
tree.collapseRow(row);
}
return null;
}
});
}
} finally {
Thread.currentThread().setContextClassLoader(oldCl);
}
}
/**
* {@inheritDoc}
*/
public Object[] getRootNodes() {
JTree tree = getTree();
// If the root is visible, just return that.
if (tree.isRootVisible()) {
return new Object [] {tree.getModel().getRoot()};
}
// If the root is not visible, return all direct children of the
// non-visible root.
return getChildren(tree.getModel().getRoot());
}
/**
* {@inheritDoc}
*/
public void scrollNodeToVisible(Object node) {
getTree().scrollRowToVisible(getRowForTreeNode(node));
}
/**
* {@inheritDoc}
*/
public Object getChild(Object parent, int index) {
try {
if (parent == null) {
Object [] rootNodes = getRootNodes();
return rootNodes[index];
}
return m_model.getChild(parent, index);
} catch (ArrayIndexOutOfBoundsException e) {
// FIXME zeb: Deal with child not found
return null;
}
}
/**
* {@inheritDoc}
*/
public Object getParent(Object child) {
Object parent = null;
if (child instanceof TreeNode) {
// This is the easy way.
parent = ((TreeNode)child).getParent();
} else {
// Great. The node doesn't implement TreeNode. Looks like we'll
// have to_do things the hard way.
Object[] pathToRoot = getPathToRoot(child);
if (pathToRoot.length > 1) {
// parent is the next-to-last element in the path
parent = pathToRoot[pathToRoot.length - 2];
}
}
// If the parent is the actual root node, and the root is not visible,
// then treat the child as one of the "root" nodes.
if (parent != null
&& parent.equals(m_model.getRoot())
&& !getTree().isRootVisible()) {
parent = null;
}
return parent;
}
/**
* Recursively builds and returns the path to root for <code>node</code>.
* The contract is similar to that of
* javax.swing.tree.DefaultTreeModel.getPathToRoot().
*
* @param node The node for which to find the path to root.
* @param currentNode The node currently being checked.
* @return a List containing the elements of the path in the proper order.
*/
private List<Object> getPathToRootImpl(Object node, Object currentNode) {
if (ObjectUtils.equals(currentNode, node)) {
List<Object> retList = new ArrayList<Object>();
retList.add(currentNode);
return retList;
}
int childCount = m_model.getChildCount(currentNode);
for (int i = 0; i < childCount; i++) {
List<Object> path = getPathToRootImpl(
node, m_model.getChild(currentNode, i));
if (path != null) {
// prepend the current node to the path and return
path.add(0, currentNode);
return path;
}
}
return null;
}
/**
* {@inheritDoc}
*/
public void clickNode(Object node, ClickOptions clickOps) {
scrollNodeToVisible(node);
Rectangle rowBounds = getNodeBounds(node);
Rectangle visibleRowBounds = getVisibleRowBounds(rowBounds);
getRobot().click(getTree(), visibleRowBounds, clickOps);
}
/**
* {@inheritDoc}
*/
public Object getSelectedNode() {
TreePath[] paths = getCheckedSelectedPaths();
SelectionUtil.validateSelection(paths);
return paths[0].getLastPathComponent();
}
/**
* Returns the selected tree paths.
*
* @return The path
*/
private TreePath[] getCheckedSelectedPaths() {
TreePath[] paths = (TreePath[])getSelectionPaths();
SelectionUtil.validateSelection(paths);
return paths;
}
/**
* {@inheritDoc}
*/
public int getIndexOfChild(Object parent, Object child) {
if (parent != null) {
return getTree().getModel().getIndexOfChild(parent, child);
}
Object [] rootNodes = getRootNodes();
for (int i = 0; i < rootNodes.length; i++) {
if (ObjectUtils.equals(rootNodes[i], child)) {
return i;
}
}
return -1;
}
/**
* {@inheritDoc}
*/
public boolean isLeaf(Object node) {
return m_model.isLeaf(node);
}
/**
* {@inheritDoc}
*/
public Object[] getSelectedNodes() {
TreePath[] paths = getCheckedSelectedPaths();
Object[] nodes = new Object[paths.length];
for (int i = 0; i < paths.length; i++) {
nodes[i] = paths[i].getLastPathComponent();
}
SelectionUtil.validateSelection(paths);
return nodes;
}
}