/******************************************************************************* * 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.Event; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; 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.Robot; import abbot.tester.swt.RunnableWithResult; import com.windowtester.internal.runtime.IWidgetIdentifier; import com.windowtester.runtime.swt.internal.locator.WidgetIdentifier; import com.windowtester.runtime.swt.internal.selector.UIProxy; /** * A service/factory class that performs various widget querying services and * performs identifying widget info inference. * <br> * Note: instances cache results of calculations. If the hierarchy changes between uses, results may be * invalid. In cases where the hierarchy is changing, a new instance must be created. * */ public class WidgetLocatorService { /** * Set this to <code>true</code> to enable new API generation. (this is a temporary hack.) */ // public static boolean NEW_API = false; /** * 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; //a list of keys which we want to propagate to locators // private static final String[] INTERESTING_KEYS = {"name"}; private final WeakHashMap<Widget, Widget> _ownerToChildMap = new WeakHashMap<Widget, Widget>(); /** Cached display used for sync execs and for the creation of an SWTHierachy in the null * parent elaboration case. */ private Display _display; /** A flag to indicate we reached the root of the hierarchy in elaboration*/ private boolean _nullParentCase; /** * Identify widget locator strategy. (Note: cast is due to faulty type inference in the IDE.) */ //private IWidgetIdentifierStrategy _widgetIdentifier = NEW_API ? (IWidgetIdentifierStrategy)WidgetIdentifier.getInstance() : new ScopedWidgetIdentifierBuilder(); private IWidgetIdentifierStrategy _widgetIdentifier = WidgetIdentifier.getInstance(); /** * Given a widget and an associated event, infers the (minimal) WidgetLocator that uniquely * identifies the widget. * @param w - the target widget * @return the identifying WidgetLocator or null if there was an error in identification */ public IWidgetIdentifier inferIdentifyingInfo(Widget w, Event event) { return _widgetIdentifier.identify(w, event); } /** * Given a widget, infers the (minimal) WidgetLocator that uniquely * identifies the widget. * @param w - the target widget * @return the identifying WidgetLocator or null if there was an error in identification */ public IWidgetIdentifier inferIdentifyingInfo(Widget w) { /* * pulling inference into separate strategy */ return _widgetIdentifier.identify(w); //return inferIdentifyingInfo_v10(w); } // /* // * This is the inference algorithm as implemented in public release v1.0. // * // * This will ultimately be made to go away. In the meantime, we keep it // * here for easy rollback. // */ // private WidgetLocator inferIdentifyingInfo_v10(Widget w) { // //cache display instance // _display = w.getDisplay(); // // boolean found = false; // WidgetLocator info = getInfo(w); // do { // try { // // // if (rootReached()) { // WidgetIdentificationException e = new WidgetIdentificationException("root reached and explored; aborting search"); // LogHandler.log(e); // return null; //indicate a failure // } // // Matcher m = getMatcher(info); // TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, "using matcher: " + m); // TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, WidgetLocatorService.getJavaString(info)); // //BasicFinder2.getDefault().dbPrintWidgets(); // Widget widget = BasicFinder2.getDefault().find(m); // //getting here means we have identifying criteria // //or it should! there's a bug in the finder which is not catching all multiple widget instances: // //so: sanity check to be sure // sanityCheckFoundWidget(w, widget); // found = true; // } catch (WidgetNotFoundException e) { // // this is an ERROR! // //BasicFinder2.getDefault().dbPrintWidgets(); // LogHandler.log(e); // return null; //indicate a failure // } catch (MultipleWidgetsFoundException e) { // //ignore, and keep looking... // TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, "...multiple widgets found(" + UIProxy.getToString(w) + "): elaborating"); // info = elaborate(info, w); // } // } while (!found); // // return info; // } // /** // * Generate a Matcher that can be used to identify the widget described // * by this WidgetLocator object. // * @return a Matcher that matches this object. // * @see Matcher // */ // public static Matcher getMatcher(WidgetLocator wl) { // // return MatcherFactory.getMatcher(wl); // // //// int index = wl.getIndex(); //// String nameOrLabel = wl.getNameOrLabel(); //// WidgetLocator parentInfo = wl.getParentInfo(); //// Class cls = wl.getTargetClass(); //// //// if (index != WidgetLocator.UNASSIGNED) { //// if (nameOrLabel != null) { //// return (parentInfo == null) ? getTargetMatcher(wl) //// : new HierarchyMatcher(cls, nameOrLabel, index, getMatcher(parentInfo)); //// } else { //// return (parentInfo == null) ? getTargetMatcher(wl) //// : new HierarchyMatcher(cls, index, getMatcher(parentInfo)); //// } //// } else { //// if (nameOrLabel != null) { //// return (parentInfo == null) ? getTargetMatcher(wl) //// : new HierarchyMatcher(cls, nameOrLabel, getMatcher(parentInfo)); //// } else { //// return (parentInfo == null) ? getTargetMatcher(wl) //// : new HierarchyMatcher(cls, getMatcher(parentInfo)); //// } //// } // } // /** // * Get the matcher for the target widget. // * @return the target matcher // */ // private static Matcher getTargetMatcher(WidgetLocator wl) { // int index = wl.getIndex(); // String nameOrLabel = wl.getNameOrLabel(); // Class cls = wl.getTargetClass(); // // // //FIXME: refactor and centralize (duplicated in HierarchyMatcher constructor); also notice uses of IndexMatcher -- should be removed... // if (index == WidgetLocator.UNASSIGNED) { // if (nameOrLabel == null) { // return new ExactClassMatcher(cls); // } else // return new CompositeMatcher(new Matcher[] { // new ExactClassMatcher(cls), // new NameOrLabelMatcher(nameOrLabel) }); // } // if (nameOrLabel == null) { // return new IndexMatcher( // new ExactClassMatcher(cls), index); // } else // return new CompositeMatcher(new Matcher[] { // new ExactClassMatcher(cls), // new IndexMatcher( // new NameOrLabelMatcher(nameOrLabel), index) }); // } // // private void sanityCheckFoundWidget(Widget original, Widget found) throws MultipleWidgetsFoundException { // /** // * CCombos are a special case: their button's get the selection event but we want to identify // * them by the button's parent CCombo. // */ // if (found instanceof CCombo) { // if (original instanceof Button) // original = new CComboTester().getParent((Button)original); // } // // if (found != original) // throw new MultipleWidgetsFoundException("internally generated, sanity check", new Widget[]{original, found}); // } // /** // * Takes an Info object and elaborates on its parentInfo. (Used to // * disambiguate inof with multiple matches. // * @param info - the info to elaborate. // * @param w - the widget being identified. // * @return elaborated WidgetLocator // * FIXME: handle null parent case... // */ // private WidgetLocator elaborate(WidgetLocator info, Widget w) { // // TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, "elaborating on: " + info + " widget=" + w); // // WidgetLocator root = info; // // boolean elaborated = false; // // WidgetLocator parentInfo = null; // while(!elaborated) { // parentInfo = info.getParentInfo(); // // Widget parent = getParent(w); // if (parent == null) { // TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, UIProxy.getToString(w) + " has null parent"); // //throw new AssertionError("null parent in elaboration"); // //!pq: enable this to help debug the null parent case // //new SWTHierarchy(w.getDisplay()).dbPrintWidgets(); // _nullParentCase = true; // } // // // if (parentInfo == null) { // info.setParentInfo(getInfo(parent)); // int index = getIndex(w,parent); // if (index != WidgetLocator.UNASSIGNED) // info.setIndex(index); // elaborated = true; // } // w = parent; //setup for next iteration // info = parentInfo; // } // return root; // } /** * Check whether the root of the hierachy was reached in the last info elaboration. */ public boolean rootReached() { return _nullParentCase; } // /** // * Generates a Java string that when interepreted creates an object identical // * to this one. By default class names are unqualified. // * @return a Java string describing this object // */ // public static String getJavaString(WidgetLocator locator) { // return getJavaString(locator, false); // } // // /** // * Generates a Java string that when interepreted creates an object identical // * to this one. // * @param qualify - falg indicating whether to qualify class names. // * @return a Java string describing this object // */ // public static String getJavaString(WidgetLocator wl, boolean qualify) { // StringBuffer sb = new StringBuffer(); // // // if (wl instanceof ViewLocator) { // // sb.append("new ViewLocator("); // sb.append("\"").append(wl.getNameOrLabel()).append("\""); // // } else if (wl instanceof LabeledLocator) { // // sb.append("new LabeledLocator("); // sb.append(getClassName(wl.getTargetClass(), qualify)).append(", \""); // sb.append(wl.getNameOrLabel()).append("\""); // // } else if (wl instanceof ShellLocator) { // // sb.append("new ShellLocator("); // sb.append("\"").append(wl.getNameOrLabel()).append("\", "); // sb.append(((ShellLocator)wl).isModal()); // // } else { // // /* // * View Locators don't use the class field // */ // // sb.append("new WidgetLocator("); // sb.append(getClassName(wl.getTargetClass(), qualify)); // // //name // String nameOrLabel = wl.getNameOrLabel(); // if (nameOrLabel != null) // sb.append(", \"").append(nameOrLabel).append("\""); // // // // } // // // // //index // int index = wl.getIndex(); // if (index != WidgetLocator.UNASSIGNED) // sb.append(", ").append(index); // // //parent // WidgetLocator parentInfo = wl.getParentInfo(); // if (parentInfo != null) // sb.append(", ").append(getJavaString(parentInfo, qualify)); // // //close // sb.append(")"); // return sb.toString(); // } // private static String getClassName(Class cls, boolean qualify) { // // get the simple name of the class // int lastPeriod = cls.getName().lastIndexOf('.'); // String simpleName = (lastPeriod >= 0) ? cls.getName().substring( // lastPeriod + 1) : cls.getName(); // // // cls // String clsName = (qualify) ? cls.getName() : simpleName; // clsName +=".class"; // return clsName; // } /** * 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 && visibility if (nameAndOrLabelDataMatch(w, child) && SWTHierarchyHelper.isVisible(child)) ++count; } if (child == w) index = count-1; //indexes are zero-indexed } return (count > 1) ? index : -1; //throw new IllegalStateException("unfound child"); } /** * 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); } /** * 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; } 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; } /** * 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)); } } /** * 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); } } // /** // * Create an (unelaborated) info object for this widget. // * @param w - the widget to describe. // * @return an info object that describes the widget. // */ // private WidgetLocator getInfo(Widget w) { // if (w == null) { // //return new WidgetLocator(NullParent.class);//TODO: handle NullParent case... // return null; // } // /** // * CCombos require special treatment as the chevron is a button and receives the click event. // * Instead of that button, we want to be identifying the combo itself (the button's parent). // */ // if (w instanceof Button) { // Widget parent = new ButtonTester().getParent((Button)w); // if (parent instanceof CCombo) // w = parent; // } // // Class cls = w.getClass(); // /** // * We don't want the combo text to be part of the identifying information since it // * is only set to the value AFTER it is selected... // * Text values are also too volatile to use as identifiers. // * // */ // String text = (w instanceof Combo || w instanceof CCombo || w instanceof Text || w instanceof StyledText)? null : getWidgetText(w); // WidgetLocator locator = (text != null) ? new WidgetLocator(cls, text) // : new WidgetLocator(cls); // // setDataValues(locator, w); // // return locator; // } // //propagate values of interest from the widget to the locator // private void setDataValues(WidgetLocator locator, Widget w) { // String key; // Object value; // WidgetTester tester = new WidgetTester(); // for (int i= 0; i < INTERESTING_KEYS.length; ++i) { // key = INTERESTING_KEYS[i]; // value = tester.getData(w, key); // if (value != null) // locator.setData(key, value.toString()); // } // } /** * Extract the text from the given widget. * @param w - the widget in question * @return the widget's text */ public static 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(); } if (w instanceof Link) { return ((Link)w).getText(); } //fall through .... return null; }}); } /** * 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 synchronized 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; if (widget == null || widget.isDisposed()) 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; } public static final class WidgetIdentificationException extends RuntimeException { private static final long serialVersionUID = -2487576955669523193L; public WidgetIdentificationException(String msg) { super(msg); } } }