/*******************************************************************************
* Copyright (c) 2012 Google, Inc.
* 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:
* Google, Inc. - initial API and implementation
*******************************************************************************/
package com.windowtester.runtime.swt.internal.finder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.WeakHashMap;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Caret;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.CoolBar;
import org.eclipse.swt.widgets.CoolItem;
import org.eclipse.swt.widgets.Decorations;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Tracker;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import abbot.Log;
import abbot.finder.swt.SWTHierarchy;
import abbot.tester.swt.ControlTester;
import abbot.tester.swt.Robot;
import abbot.tester.swt.RunnableWithResult;
import com.windowtester.internal.runtime.IWidgetIdentifier;
import com.windowtester.internal.runtime.util.StringUtils;
import com.windowtester.runtime.WidgetLocator;
import com.windowtester.runtime.internal.factory.WTRuntimeManager;
import com.windowtester.runtime.locator.IWidgetReference;
import com.windowtester.runtime.swt.internal.selector.UIProxy;
import com.windowtester.runtime.swt.internal.widgets.ISWTWidgetReference;
import com.windowtester.runtime.swt.internal.widgets.SWTUIException;
public class SWTHierarchyHelper {
private final WeakHashMap _ownerToChildMap = new WeakHashMap();
/**
* To properly connect menus with their owners we need to do an exhaustive search or widgets looking at their
* children. To disable this, set this flag to false.
* TODO: a future approach might exploit caching...
*/
private static final boolean FIND_OWNER_ENABLED = true;
static ControlTester _controlTester = new ControlTester();
/** Cached display used for sync execs and for the creation of an SWTHierachy in the null
* parent elaboration case.
*/
private Display _display;
public SWTHierarchyHelper(Display display) {
_display = display;
}
public SWTHierarchyHelper() {
this(Display.getDefault());
}
/**
* Get this widget's index relative to its parent widget.
* <p>Note that indexes only matter in the case where there is at least one sibling
* that matches the target widget exactly (by class and name/label). Other cases
* return -1.
* @param w - the widget
* @param parent - the parent widget
* @return an index, or -1 if is the only child
* FIXME: return 0 in only-child case
*/
public int getIndex(Widget w, Widget parent) {
List children = getChildren(parent, w.getClass());
int count = 0; //the match counter
int index = -1; //the index of our target widget
//only child case...
if (children.size() == 1)
return index;
for (Iterator iter = children.iterator(); iter.hasNext();) {
Widget child = (Widget)iter.next();
//using exact matches...
if (child.getClass().isAssignableFrom(w.getClass()) && w.getClass().isAssignableFrom(child.getClass())) {
//also check for nameOrLabelMatch
if (nameAndOrLabelDataMatch(w, child))
++count;
}
if (child == w)
index = count-1; //indexes are zero-indexed
}
return (count > 1) ? index : -1;
//throw new IllegalStateException("unfound child");
}
private Collection getAllMenuItems(Menu m) {
/* get all menus and menu items rooted at this menu */
ArrayList list = new ArrayList();
if (m != null) {
MenuItem [] items = m.getItems();
list.add(m);
for (int i=0;i<items.length;i++) {
list.addAll(getAllMenuItems(items[i].getMenu()));
list.add(items[i]);
}
}
return list;
}
/**
* Checks to see that widget names/labels match.
* @param w1 - the first widget
* @param w2 - the second widget
* @return true if they match
*/
private boolean nameAndOrLabelDataMatch(Widget w1, Widget w2) {
String text1 = getWidgetText(w1);
String text2 = getWidgetText(w2);
if (text1 == null)
return text2 == null;
return text1.equals(text2);
}
/**
* Extract the text from the given widget.
* @param w - the widget in question
* @return the widget's text
*/
public String getWidgetText(final Widget w) {
return (String) Robot.syncExec(w.getDisplay(),
new RunnableWithResult() {
public Object runWithResult() {
if (w instanceof Control) {
//controlShowing = ((Control)w).getVisible() && ((Control)w).getShell().getVisible();
//System.out.println("Widget " + w + " showing: " + controlShowing);
}
if (w instanceof Button) {
return (((Button) w).getText());
}
//!pq: Combo text data is too volatile for matching...
if (w instanceof Combo) {
//return (((Combo)w).getText());
return null;
}
if (w instanceof Decorations) {
return (((Decorations) w).getText());
}
if (w instanceof Group) {
return (((Group) w).getText());
}
if (w instanceof Item) {
if (w instanceof TableItem
&& ((TableItem) w).getParent()
.getColumnCount() > 0) {
//int columns = ((TableItem)w).getParent().getColumnCount();
return (((TableItem) w).getText(0));
//TODO: this isn't quite right...
// String[] lWtexts = new String[columns];
// for (int i=0;i<lWtexts.length;i++) {
// lWtexts[i] = ((TableItem)w).getText(i);
// }
// setWtexts(lWtexts);
} else {
return (((Item) w).getText());
}
}
if (w instanceof Label) {
return (((Label) w).getText());
}
// !pq: Text data is too volatile for matching...
// if (w instanceof Text) {
// return (((Text)w).getText());
// }
if (w instanceof Menu) {
//TODO: still unclear what to do here...
// Menu m = (Menu)w;
// String text = m.getParent().getText();
// Menu parentMenu = m.getParentMenu();
// String s = m.toString();
}
//fall through ....
return null;
}
});
}
/**
* Get the children (of a particular class) of a given parent widget.
* @param parent - the parent widget
* @param cls - the class of child widgets of interest
* @return a list of children
*/
public List getChildren(Widget parent, Class cls) {
List children = getChildren(parent);
//prune non-exact class matches
List pruned = new ArrayList();
for (Iterator iter = children.iterator(); iter.hasNext();) {
Object child = iter.next();
Class childClass = child.getClass();
if (cls.isAssignableFrom(childClass) && childClass.isAssignableFrom(cls))
pruned.add(child);
}
return pruned;
}
public List getChildren(final Widget w) {
//in case this is called for a null parent, we need to resort to a cached display
Display display = w == null ? _display : w.getDisplay();
return (List)UIProxy.syncExec(display, new RunnableWithResult(){
public Object runWithResult() {
return getChildren0(w);
}
});
}
/**
* Get the children of the given widget.
* <p>
* (Taken from SWTHieracrhy and modified.)
* @param w - the widget in question.
* @return the list of children
* @see abbot.finder.swt.SWTHierarchy
*/
private List getChildren0(Widget w) {
List localList = new ArrayList();
//null parent case
if (w == null) {
addCheck(localList, new SWTHierarchy(_display).getRoots());
}
if (w instanceof Decorations) {
Decorations d = (Decorations)w;
localList.addAll(getAllMenuItems(d.getMenuBar()));
addCheck(localList,d.getMenuBar());
}
if (w instanceof Control) {
Control c = (Control)w;
addCheck(localList,getAllMenuItems(c.getMenu()));
//menu added in getAllMenuItems
//addCheck(localList,c.getMenu());
}
if(w instanceof Scrollable){
addCheck(localList,((Scrollable)w).getVerticalBar());
addCheck(localList,((Scrollable)w).getHorizontalBar());
}
if(w instanceof TreeItem){
Widget[] widgets = ((TreeItem)w).getItems();
addCheck(localList,(Arrays.asList(widgets)));
}
if(w instanceof Menu){
Widget[] widgets = ((Menu)w).getItems();
addCheck(localList,Arrays.asList(widgets));
}
if(w instanceof MenuItem){
Widget childMenu = ((MenuItem)w).getMenu();
addCheck(localList,childMenu);
}
if (w instanceof Composite) {
if (w instanceof ToolBar) {
addCheck(localList,Arrays.asList(((ToolBar)w).getItems()));
}
if (w instanceof Table) {
addCheck(localList,Arrays.asList(((Table)w).getItems()));
addCheck(localList,Arrays.asList(((Table)w).getColumns()));
}
if (w instanceof Tree) {
addCheck(localList,Arrays.asList(((Tree)w).getItems()));
}
if (w instanceof CoolBar) {
addCheck(localList,Arrays.asList(((CoolBar)w).getItems()));
}
if(w instanceof TabFolder){
Widget[] widgets = ((TabFolder)w).getItems();
addCheck(localList,Arrays.asList(widgets));
}
if(w instanceof CTabFolder){
Widget[] widgets = ((CTabFolder)w).getItems();
addCheck(localList,Arrays.asList(widgets));
}
Composite cont = (Composite)w;
Control [] children = cont.getChildren();
// for (int i=0;i<children.length;i++) {
// if (children[i] instanceof Composite
// && ((Composite)children[i]).getChildren().length > 0)
// {
// addCheck(localList,getWidgets(children[i]));
// }
// }
addCheck(localList,Arrays.asList(children));
}
return localList;
//outerList = localList;
}
/**
* Retrieve the parent of the given widget.
* @param widget - the widget in question
* @return the widget's parent, or null if it has none
*/
public Widget getParent(final Widget widget) {
// if (widget == null)
// throw new AssertionError("null widget");
//TODO: handle null widget argument case here!
/**
* The issue is that while some shell's answer other shells as their parents, they do not show
* up that way in the hierarchy. To work around this, we short circuit the call to getParent() in the
* case where the widget in question is a Shell.
*/
if (widget instanceof Shell)
return null;
return (Widget)Robot.syncExec(widget.getDisplay(), new RunnableWithResult(){
public Object runWithResult(){
if(widget instanceof Control)
return ((Control)widget).getParent();
if(widget instanceof Caret)
return ((Caret)widget).getParent();
if(widget instanceof Menu) {
/**
* Some menus (context) have shells as there parents but a more specific parent exists.
* To find these, we need to look into the hierarchy to find whether this menu is a child of
* another widget. Since this is a potentially expensive operation, it can be disabled
* by setting a flag (see: findOwner).
*/
Menu menu = (Menu)widget;
Widget parentOfMenu = findOwner(widget);
if (parentOfMenu != null) {
//System.out.println("parent != null:: " + parentOfMenu);
return parentOfMenu;
}
return menu.getParent();
//return (parentOfMenu != null) ? parentOfMenu : menu.getParent();
}
if(widget instanceof ScrollBar)
return ((ScrollBar)widget).getParent();
if(widget instanceof CoolItem)
return ((CoolItem)widget).getParent();
if(widget instanceof MenuItem)
return ((MenuItem)widget).getParent();
if(widget instanceof TabItem)
return ((TabItem)widget).getParent();
if(widget instanceof TableColumn)
return ((TableColumn)widget).getParent();
if(widget instanceof TableItem)
return ((TableItem)widget).getParent();
if(widget instanceof ToolItem)
return ((ToolItem)widget).getParent();
if(widget instanceof TreeItem)
return ((TreeItem)widget).getParent();
if(widget instanceof DragSource)
return ((DragSource)widget).getControl().getParent();
if(widget instanceof DropTarget)
return ((DropTarget)widget).getControl().getParent();
if(widget instanceof Tracker)
Log.debug("requested the parent of a Tracker- UNFINDABLE");
//fall through
return null;
}
});
}
/**
* Look at all of the widgets in the hierarchy seeking one who claims this menu as its own.
* @param menu - the menu in question
* @return the menu's owner or null if there is none
*/
private Widget findOwner(Widget menu) {
//short circuit if disabled
if (!FIND_OWNER_ENABLED)
return null;
//check cache first (notice null values might exist)
if (_ownerToChildMap.containsKey(menu))
return (Widget)_ownerToChildMap.get(menu);
SWTHierarchy hierarchy = new SWTHierarchy(menu.getDisplay());
Widget[] widgets = hierarchy.getWidgets();
Widget owner = null;
for (int i = 0; i < widgets.length && owner == null; i++) {
Widget w = widgets[i];
//TODO: see if there are other types that support menus
if (w instanceof Control) {
Control c = (Control)w;
Menu child = c.getMenu();
//System.out.println(c + " -> child: " + UIProxy.getToString(child));
if (child == menu) {
owner = c;
}
}
}
_ownerToChildMap.put(menu, owner);
return owner;
}
/////////////////////////////////////////////////////////////////////////////////
//
// Debugging Helpers
//
/////////////////////////////////////////////////////////////////////////////////
/**
* Get a String that represents the current widget hierarchy starting at the
* active shell as root. ThTE_Eui.wis output is meant to be suitable for debugging
* purposes.
* @param root the root from which to derive the hierarchy description
* @return a String representation of the hierarchy
* @return
*/
public String dumpWidgets() {
return dumpWidgets(ShellFinder.getActiveShell(_display));
}
/**
* Get a String that represents the current widget hierarchy starting at the
* given root. This output is meant to be suitable for debugging purposes.
* @param root the root from which to derive the hierarchy description
* @return a String representation of the hierarchy
*/
public String dumpWidgets(Widget root) {
StringBuffer sb = new StringBuffer();
accumulateWidgetInfo(new SWTHierarchy(_display),sb, root, 0);
return sb.toString();
}
private void accumulateWidgetInfo(SWTHierarchy hierarchy, StringBuffer buffer, Widget w, int level) {
buffer.append((indent(level)));
boolean visible = SWTHierarchyHelper.isVisible(w);
if (!visible)
buffer.append("[");
buffer.append(UIProxy.getToString(w) + "<HC|"+w.hashCode()+">");
if (!visible)
buffer.append("]->(invisible)");
buffer.append(StringUtils.NEW_LINE);
Collection childWidgets = hierarchy.getWidgets(w);
Iterator iter = childWidgets.iterator();
while (iter.hasNext()) {
accumulateWidgetInfo(hierarchy, buffer, (Widget)iter.next(), level+1);
}
}
private StringBuffer indent(int level) {
StringBuffer indentation = new StringBuffer("");
for (int i=0;i<level;i++)
indentation.append(" ");
return indentation;
}
/////////////////////////////////////////////////////////////////////////////////
//
// Internal
//
/////////////////////////////////////////////////////////////////////////////////
/**
* Add the contents of this collection to this other collection only if
* it is non-empty.
* @param dest - the destination collection
* @param src - the source collection
*/
private void addCheck(Collection dest, Collection src) {
/* add object to collection if non-null */
if (src.size() > 0) {
// Iterator iter = src.iterator();
// while (iter.hasNext()) {
// dest.addAll(getWidgets((Widget)iter.next()));
// }
dest.addAll(src);
}
}
/**
* Add this object to this collection only if the object is non-null.
* @param c - the collection
* @param o - the object to add
*/
private void addCheck(Collection c, Object o) {
/* add object to collection if non-null */
if (o != null) {
c.add(o);
// c.addAll(getWidgets((Widget)o));
}
}
//TODO: this should really take an interface (like IScopeLocator) and should be overriden for the specific cases
public int getIndex(Widget w, IWidgetIdentifier scopeLocator) {
//TODO: decide whether we want view/shell relative indexes
return WidgetLocator.UNASSIGNED;
}
/**
* Check whether this widget is visible.
*/
public static boolean isVisible(Widget w) {
if (w == null)
return false;
if (w.isDisposed())
return false;
return guardedVisibilityCheck(w);
}
private static boolean guardedVisibilityCheck(Widget w) {
// try {
// // ask the menu watcher if the given items menu is open
// if (w instanceof MenuItem)
// // TODO: use widget references rather than these static methods
//// return MenuWatcher.getInstance(w.getDisplay()).isVisible((MenuItem) w);
//// return new MenuItemReference((MenuItem) w).isVisible();
// return ((MenuItemReference) WTRuntimeManager.asReference(w)).isVisible();
// Control control = FinderUtil.getControl(w);
// if (control.isDisposed())
// return false;
// //for some reason Links return false when asked if "isVisible"...
// if (control instanceof Link) {
// return _controlTester.getVisible(control);
// }
// return _controlTester.isVisible(control);
// } catch (SWTException e) {
// // ignore
// }
IWidgetReference ref = WTRuntimeManager.asReference(w);
if (ref instanceof ISWTWidgetReference<?>) {
//there's a chance that a widget may get disposed during our check so we have
//to guard against exceptions
try {
return ((ISWTWidgetReference<?>) ((ISWTWidgetReference<?>) ref)).isVisible();
}
catch (SWTUIException e) {
return false;
}
}
return false;
}
}