/******************************************************************************* * 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.locator; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.List; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.forms.widgets.FormText; import org.eclipse.ui.forms.widgets.Hyperlink; import abbot.finder.swt.Matcher; import abbot.finder.swt.SWTHierarchy; import abbot.tester.swt.ButtonTester; import abbot.tester.swt.CComboTester; import abbot.tester.swt.ComboTester; import abbot.tester.swt.MenuItemTester; import abbot.tester.swt.WidgetTester; import com.windowtester.internal.debug.IRuntimePluginTraceOptions; import com.windowtester.internal.debug.TraceHandler; import com.windowtester.internal.runtime.IWidgetIdentifier; import com.windowtester.runtime.WidgetLocator; import com.windowtester.runtime.swt.internal.abbot.matcher.TreeItemByPathMatcher; import com.windowtester.runtime.swt.internal.debug.LogHandler; import com.windowtester.runtime.swt.internal.finder.FilteredTreeHelper; import com.windowtester.runtime.swt.internal.finder.IWidgetIdentifierStrategy; import com.windowtester.runtime.swt.internal.finder.SWTHierarchyHelper; import com.windowtester.runtime.swt.internal.finder.WidgetLocatorService; import com.windowtester.runtime.swt.internal.finder.eclipse.views.IViewHandle; import com.windowtester.runtime.swt.internal.finder.eclipse.views.ViewFinder; import com.windowtester.runtime.swt.internal.finder.legacy.SearchScopeHelper; import com.windowtester.runtime.swt.internal.finder.legacy.WidgetFinder; import com.windowtester.runtime.swt.internal.finder.matchers.AdapterFactory; import com.windowtester.runtime.swt.internal.locator.forms.FormTextReference; import com.windowtester.runtime.swt.internal.locator.forms.HyperlinkControlReference; import com.windowtester.runtime.swt.internal.locator.forms.HyperlinkLocatorScopeFactory; import com.windowtester.runtime.swt.internal.locator.forms.HyperlinkReference; import com.windowtester.runtime.swt.internal.locator.jface.DialogFinder; import com.windowtester.runtime.swt.internal.matchers.MenuItemByPathMatcher; import com.windowtester.runtime.swt.internal.selector.UIProxy; import com.windowtester.runtime.swt.internal.widgets.SWTWidgetReference; import com.windowtester.runtime.swt.locator.ButtonLocator; import com.windowtester.runtime.swt.locator.CComboItemLocator; import com.windowtester.runtime.swt.locator.CTabItemLocator; import com.windowtester.runtime.swt.locator.ComboItemLocator; import com.windowtester.runtime.swt.locator.FilteredTreeItemLocator; import com.windowtester.runtime.swt.locator.LabeledLocator; import com.windowtester.runtime.swt.locator.LabeledTextLocator; import com.windowtester.runtime.swt.locator.ListItemLocator; import com.windowtester.runtime.swt.locator.MenuItemLocator; import com.windowtester.runtime.swt.locator.NamedWidgetLocator; import com.windowtester.runtime.swt.locator.SWTWidgetLocator; import com.windowtester.runtime.swt.locator.ShellLocator; import com.windowtester.runtime.swt.locator.StyledTextLocator; import com.windowtester.runtime.swt.locator.TabItemLocator; import com.windowtester.runtime.swt.locator.TableItemLocator; import com.windowtester.runtime.swt.locator.TreeItemLocator; import com.windowtester.runtime.swt.locator.eclipse.ContributedToolItemLocator; import com.windowtester.runtime.swt.locator.eclipse.ViewLocator; import com.windowtester.runtime.swt.locator.forms.HyperlinkLocator; import com.windowtester.runtime.swt.locator.forms.IHyperlinkReference; import com.windowtester.runtime.swt.locator.jface.DialogMessageLocator; /** * A V2 API WidgetLocator Factory. */ public class WidgetIdentifier implements IWidgetIdentifierStrategy { /** * A service that maps widgets to corresponding API V2 locators. * For example, Button -> ButtonLocator. */ class LocatorMapper { //helpers for extracting path info private final MenuItemTester _menuItemTester = new MenuItemTester(); /** * Map this widget to a corresponding locator (note: this is a factory method * --- one will be created). */ WidgetLocator map(Widget w) { String text = getText(w); if (isA(w, Button.class)) return new ButtonLocator(text); if (isA(w, MenuItem.class)) return new MenuItemLocator(_menuItemTester.getPathString((MenuItem)w)); if (isA(w, TreeItem.class)) { TreeItem item = (TreeItem)w; String path = TreeItemByPathMatcher.extractPathString(item); if (isInFilteredTree(item)) return new FilteredTreeItemLocator(path); return new TreeItemLocator(path); } if (isA(w, CTabItem.class)) return new CTabItemLocator(text); if (isA(w, TabItem.class)) return new TabItemLocator(text); if (isA(w, Combo.class)) return new ComboItemLocator(text); if (isA(w, CCombo.class)) return new CComboItemLocator(text); if (isA(w, TableItem.class)) return new TableItemLocator(text); if (isA(w, List.class)) return new ListItemLocator(text); if (isA(w, Hyperlink.class)) return new HyperlinkLocator(text); if (isA(w, Shell.class)) return new ShellLocator(text); if (isA(w, StyledText.class)) return new StyledTextLocator(); //fall through return new SWTWidgetLocator(w.getClass(), text); } private boolean isInFilteredTree(TreeItem item) { return FilteredTreeHelper.containedInFilteredTree(item); } private boolean isA(Widget w, Class targetClass) { return w.getClass().equals(targetClass); } private String getText(Widget w) { if (w instanceof CCombo) return new CComboTester().getText((CCombo)w); if (w instanceof Combo) return new ComboTester().getText((Combo)w); if (w instanceof Hyperlink) return HyperlinkControlReference.forControl((Hyperlink)w).getText(); //default case: return WidgetLocatorService.getWidgetText(w); } } //vet and break out into new class static class ContributedWidgetLocatorRegistry { static ContributedWidgetLocatorRegistry _instance; public static ContributedWidgetLocatorRegistry getInstance() { if (_instance == null) _instance = new ContributedWidgetLocatorRegistry(); return _instance; } public WidgetLocator findProposal(Widget w, Event event) { /* * This will be broken into separate classes (or pushed into * the locators themselves) * for now the logic is local */ String name = UIProxy.getData(w, "name"); // added class to NamedWidgetLocator : 5/2/07 :kp if (name != null) { NamedWidgetLocator namedLocator = new NamedWidgetLocator(w.getClass(),name); WidgetLocator locator = WidgetIdentifier.getInstance().getMapper().map(w); if (locator instanceof VirtualItemLocator) { locator.setParentInfo(namedLocator); return locator; } return namedLocator; } if (w instanceof ToolItem) { ToolItem item = (ToolItem)w; String id = ContributedToolItemLocator.getAssociatedContributionID(item); if (id != null) return new ContributedToolItemLocator(id); } if (w instanceof Control) { Control messageControl = DialogFinder.findActiveDialogMessageControl(); if (messageControl == w) return new DialogMessageLocator(); } if (w instanceof FormText && event != null) { FormText text = (FormText)w; IHyperlinkReference link = FormTextReference.forText(text).findHyperlinkAt(event.x, event.y); if (link != null) { HyperlinkLocator locator = HyperlinkReference.toLocator(link); return HyperlinkLocatorScopeFactory.addScope(locator, text); } } return null; } } LocatorMapper _mapper = new LocatorMapper(); private static WidgetIdentifier _instance = new WidgetIdentifier(); public static WidgetIdentifier getInstance() { return _instance; } /** A list of keys which we want to propagate to locators */ private static final String[] INTERESTING_KEYS = { "name" }; /** Limits elaborations to protect against pathologically nested widgets hanging the recorder */ private static final int MAX_ELABORATIONS = 10; /** For use in checking for unique matches */ private final WidgetFinder _finder = new WidgetFinder(); /** For use in elaboration (created once per call it identify) */ private SWTHierarchyHelper _hierarchyHelper; private SearchScopeHelper _searchScopeHelper; /** for use in adapting locators to matchers */ private AdapterFactory _adapterFactory; /* (non-Javadoc) * @see com.windowtester.swt.locator.IWidgetIdentifierStrategy#identify(org.eclipse.swt.widgets.Widget, org.eclipse.swt.widgets.Event) */ public IWidgetIdentifier identify(Widget w, Event event) { if (w == null) return null; Display display = w.getDisplay(); //cache the helpers for use in elaboration _hierarchyHelper = new SWTHierarchyHelper(display); _searchScopeHelper = new SearchScopeHelper(new SWTHierarchy(display)); //get top-level scope WidgetLocator scope = findTopLevelScope(w); //get locator describing the target widget itself WidgetLocator locator = getLocator(w, event); //post-process special case //locator = optionallyHandleVirtualNamedCase(w, locator); //attach scope attachScope(locator, scope);//note: it can be null /* * And here we make a BIG assumption. * * If the locator is a ViewLocator we assume that the match is definite. (In any case, * we don't know how to elaborate so this is actually a BEST GUESS. */ if (scope instanceof ViewLocator) return locator; Matcher matcher = adaptToMatcher(locator); Shell shellSearchScope = _searchScopeHelper.getShellSearchScope(matcher); if (locator instanceof VirtualItemLocator) { //look ahead and see if we have a match //the idea here is to create the parents (combos, lists) that //qualify the virtual items (but only do this if we need to elaborate) if (!isUniquelyIdentifying(matcher, shellSearchScope)) { //rename me! locator.setParentInfo(optionallySynthesizeVirtualParent(w)); } } int count = 0; //elaborate until done (notice: null locator indicates a failure) while(!isUniquelyIdentifying(matcher, shellSearchScope) && locator != null) { // System.out.println("locator: " + locator + " not uniquely matching, elaborating..."); locator = elaborate(locator, w); if (locator != null) matcher = adaptToMatcher(locator); if (++count >= MAX_ELABORATIONS) { TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, "maximum identifier elaborations (" + MAX_ELABORATIONS + ") exceeded - identification cancelled"); return null; } // new DebugHelper.printWidgets(); } return locator; } /* (non-Javadoc) * @see com.windowtester.swt.locator.IWidgetIdentifierStrategy#identify(org.eclipse.swt.widgets.Widget) */ public IWidgetIdentifier identify(Widget w) { return identify(w, null); } /** * Advance until we find an empty parent slot. */ private void attachScope(WidgetLocator locator, WidgetLocator scope) { //specially marked locators do not get scoped if (locator instanceof IUnscopedLocator) return; //moreover: namedLocators do not get scoped either (for now...) if (isNamed(locator)) return; //yet ANOTHER TreeItem hack -- tree item implict scope //gets clobbered by a view scope //NOTE: this should be for Tables and Lists too? if (locator instanceof TreeItemLocator) { WidgetLocator parent = locator.getParentInfo(); if (parent.getClass() == SWTWidgetLocator.class && scope instanceof ViewLocator) { locator.setParentInfo(scope); return; } } /* * the regular case climbs the stack and attachs scope to the top */ while (locator.getParentInfo() != null) { locator = locator.getParentInfo(); } locator.setParentInfo(scope); } /** * Test whether this or any ancestor is named. */ private boolean isNamed(WidgetLocator locator) { do { if (locator instanceof NamedWidgetLocator) return true; locator = locator.getParentInfo(); } while (locator != null); return false; } private Matcher adaptToMatcher(WidgetLocator locator) { //menu item special case... if (locator instanceof MenuItemLocator) { final MenuItemByPathMatcher pathMatcher = new MenuItemByPathMatcher(((MenuItemLocator)locator).getPath()); return new Matcher(){ public boolean matches(Widget w) { return pathMatcher.matches(SWTWidgetReference.forWidget(w)); } }; } /** * A little fudging here to handle tree items. * * The rub: tree items have a matcher that matches based on path BUT * we don't want to use this in identification... In identification we * are looking to match the parent Tree. * * So... to provision, we pop up to fetch the parent locator (UNLESS its a ViewLocator!) */ if (locator instanceof TreeItemLocator) { if (!(locator.getParentInfo() instanceof ViewLocator)) //tree items scoped with view locators are sufficient locator = locator.getParentInfo(); } return getAdapterFactory().adapt(locator); } private AdapterFactory getAdapterFactory() { if (_adapterFactory == null) _adapterFactory = new AdapterFactory(); return _adapterFactory; } /** * Find top-level scope (Shell | View) -- might be <code>null</code>. */ private WidgetLocator findTopLevelScope(Widget w) { if (!PlatformUI.isWorkbenchRunning()) return null; //bail out if the platform is not running //as a final sanity check, wrap this, in case we get a failure: try { // 1 check for view scope IViewHandle handle = ViewFinder.find(w); if (handle != null) return new ViewLocator(handle.getId()); } catch (IllegalStateException e) { LogHandler.log(e); } // TODO: remove this wrapper post 2.0 //2 check for shell scope //TODO: when to use Shell Scope? // IShellHandle shellHandle = ShellFinder.find(w); // if (shellHandle != null && shellHandle.isModal()) // return new ShellLocator(shellHandle.getTitle(), shellHandle.isModal()); //handle other cases here... return null; } /** * Takes a WidgetLocator object and elaborates on it until is uniquely identifying. * If no uniquely identifying locator can be inferred, a <code>null</code> value * is returned. * */ private WidgetLocator elaborate(WidgetLocator info, Widget w) { //a pointer to the original locator for returning (in the success case) WidgetLocator root = info; //a bit of a hack: virtual item locators get located by their parent info if (info instanceof VirtualItemLocator) info = info.getParentInfo(); TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, "elaborating on: " + info + " widget=" + w); boolean elaborated = false; WidgetLocator parentInfo = null; while(!elaborated) { //get parent info of the current (top-most) locator parentInfo = info.getParentInfo(); //get the parent of the current (top-most) widget in the target's hierarchy Widget parent = _hierarchyHelper.getParent(w); /* * if the parent is null at this point, we've failed to elaborate and we * need to just return */ if (parent == null) { TraceHandler.trace(IRuntimePluginTraceOptions.HIERARCHY_INFO, UIProxy.getToString(w) + " has null parent, aborting elaboration"); return null; } //if the parent is a scope locator, connect to it if (isScopeLocator(parentInfo)) { handleScopeLocatorCase(info, parentInfo, w, parent); elaborated = true; //if the parent is null, create a new parent and attach it } else if (parentInfo == null) { info.setParentInfo(getLocator(parent)); setIndex(info, w, parent); elaborated = true; } /* * setup for next iteration */ w = parent; info = parentInfo; } return root; } private WidgetLocator optionallySynthesizeVirtualParent(Widget w) { if (w instanceof Combo || w instanceof CCombo || w instanceof List) return new SWTWidgetLocator(w.getClass()); return null; } LocatorMapper getMapper() { return _mapper; } /** * Check to see if the given locator is a scope locator. */ private boolean isScopeLocator(WidgetLocator locator) { //TODO: ideally this will be an interface: IScopeLocator? return locator instanceof ViewLocator || locator instanceof ShellLocator; } /** * Handle case where parent locator is a scoping locator. */ private void handleScopeLocatorCase(WidgetLocator currentTopLocator, WidgetLocator scopeLocator, Widget currentWidget, Widget widgetParent) { //1. create a new parent WidgetLocator newParent = getLocator(widgetParent); //attatch it to our old top locator currentTopLocator.setParentInfo(newParent); setIndex(currentTopLocator, currentWidget, widgetParent); int scopeRelativeIndex = _hierarchyHelper.getIndex(currentWidget, scopeLocator); if (scopeRelativeIndex != WidgetLocator.UNASSIGNED) newParent.setIndex(scopeRelativeIndex); newParent.setParentInfo(scopeLocator); } /** * Set the index for this locator that describes the given widget relative to the given parent. */ private void setIndex(WidgetLocator locator, Widget currentWidget, Widget widgetParent) { int index = _hierarchyHelper.getIndex(currentWidget,widgetParent); if (index != WidgetLocator.UNASSIGNED) locator.setIndex(index); } /** * Does this matcher uniquely identify a widget in this Shell? */ private boolean isUniquelyIdentifying(Matcher matcher, Shell shellSearchScope) { return _finder.find(shellSearchScope, matcher, 0 /* no retries */).getType() == WidgetFinder.MATCH; } /** * Create an (unelaborated) info object for this widget. * @param w - the widget to describe. * @return an info object that describes the widget. */ private WidgetLocator getLocator(Widget w, Event event) { if (w == null) { 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). * * Actually: we want to ignore the button selections altogether since they are followed by a selection * which is the "real" event. To do this, we simply return null. */ if (w instanceof Button) { Widget parent = new ButtonTester().getParent((Button) w); if (parent instanceof CCombo) return new NoOpLocator(); } //first check for a contributed locator WidgetLocator locator = checkForContributedLocator(w, event); //failing that, check for a labeled case if (locator == null) locator = checkForLabeledLocatorCase(w); /** * Another tree item special case. If the tree item is labeled, the labeled locator needs to be properly * connected */ if (locator != null && w instanceof TreeItem) { //create item locator WidgetLocator itemLocator = _mapper.map(w); //connect parent label itemLocator.setParentInfo(locator); locator = itemLocator; } //check for item in named Tree parent case if (locator == null) locator = checkForNamedTreeCase(w); //if not labled case or item in named tree case, use the mapper if (locator == null) locator = _mapper.map(w); setDataValues(locator, w); return locator; } /** * Create an (unelaborated) info object for this widget. * @param w - the widget to describe. * @return an info object that describes the widget. */ private WidgetLocator getLocator(Widget w) { return getLocator(w, null); } private WidgetLocator checkForNamedTreeCase(Widget w) { if (!(w instanceof TreeItem)) return null; Widget parent = _hierarchyHelper.getParent(w); if (!(parent instanceof Tree)) return null; // sanity Tree tree = (Tree) parent; WidgetLocator parentLocator = getLocator(tree); if (parentLocator instanceof NamedWidgetLocator) { //get the tree item locator WidgetLocator childLocator = _mapper.map(w); if (!(childLocator instanceof TreeItemLocator)) return null; TreeItemLocator treeItemLocator = (TreeItemLocator)childLocator; treeItemLocator.setParentInfo(parentLocator); return treeItemLocator; } return null; } //see if a contributed locator matches this widget private WidgetLocator checkForContributedLocator(Widget w, Event event) { return ContributedWidgetLocatorRegistry.getInstance().findProposal(w, event); } private WidgetLocator checkForLabeledLocatorCase(Widget w) { //treeitems are actually matched on their trees, so update the widget accordingly if (w instanceof TreeItem) w = _hierarchyHelper.getParent(w); Widget parent = _hierarchyHelper.getParent(w); if (!(parent instanceof Composite)) return null; //labels are not themselves considered labelable; if (w instanceof Label) return null; //for NOW, lists are not considered labeled -- but they should be in the future! if (w instanceof List) return null; //Labeled buttons don't generally make sense; if (w instanceof Button) return null; //labeled hyperlinks do not make sense if (w instanceof Hyperlink) return null; final Composite comp = (Composite)parent; final Class cls = w.getClass();//our target class final String labelText[] = new String[1]; final boolean found[] = new boolean[1]; final Widget[] widget = new Widget[]{w}; /* * Iterate over children looking for a Label widget. * If we find one, if the next widget of the class of our target widget * is the target widget, we have a labeled locator case. */ w.getDisplay().syncExec(new Runnable() { public void run() { Control[] children = comp.getChildren(); Control child; for (int i = 0; i < children.length; i++) { child = children[i]; //look for next widget of target class if (labelText[0] != null) { if (child.getClass().equals(cls)) { found[0] = child == widget[0]; /* * only kick out if we've found it * (there may be mutiple label text pairs in a composite) */ if (found[0]) return; } } //set up for next iteration if (child instanceof Label) labelText[0] = ((Label)child).getText(); } } }); if (found[0]) { //here we guard against the case where the label is empty -- this is not a legitimate case for a labeled locator if (labelText[0] == null || labelText[0].trim().length() == 0) return null; LabeledLocator locator = new LabeledLocator(cls, labelText[0]); //combos and ccombos are more elaborate cases (embed label in item locator) if (cls == Combo.class) return new ComboItemLocator(new ComboTester().getText((Combo)w), locator); if (cls == CCombo.class) return new CComboItemLocator(new CComboTester().getText((CCombo)w), locator); //Labeled Text gets special treatment since it's so common if (cls == Text.class) return new LabeledTextLocator(labelText[0]); return locator; } return null; } /** * 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()); } } }