/******************************************************************************* * 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.swt.locator; import org.eclipse.swt.custom.CCombo; 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.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Widget; import org.eclipse.ui.PlatformUI; import abbot.finder.swt.Matcher; import abbot.finder.swt.SWTHierarchy; import abbot.tester.swt.ButtonTester; import abbot.tester.swt.WidgetTester; import com.windowtester.finder.swt.SearchScopeHelper; import com.windowtester.finder.swt.WidgetFinder; import com.windowtester.internal.debug.IRuntimePluginTraceOptions; import com.windowtester.internal.debug.TraceHandler; import com.windowtester.internal.runtime.IWidgetIdentifier; import com.windowtester.runtime.swt.internal.debug.LogHandler; 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.selector.UIProxy; import com.windowtester.swt.WidgetLocator; import com.windowtester.swt.locator.eclipse.ViewLocator; public class ScopedWidgetIdentifierBuilder implements IWidgetIdentifierStrategy { /** A list of keys which we want to propagate to locators */ private static final String[] INTERESTING_KEYS = { "name" }; /** 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; /* (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) { return identify(w); } /** * Generates a <code>WidgetLocator</code> that uniquely identifies this widget * relative to the current widget hierarchy. If no uniquely identifying locator is found * <code>null</code> is returned. * * @see com.windowtester.swt.locator.IWidgetIdentifierStrategy#identify(org.eclipse.swt.widgets.Widget) */ public IWidgetIdentifier identify(Widget w) { 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); //attach scope locator.setParentInfo(scope); //note: it can be null Matcher matcher = MatcherFactory.getMatcher(locator); Shell shellSearchScope = _searchScopeHelper.getShellSearchScope(matcher); // int count = 0; //elaborate until done (notice: null locator indicates a failure) while(!isUniquelyIdentifying(matcher, shellSearchScope) && locator != null) { locator = elaborate(locator, w); if (locator != null) matcher = MatcherFactory.getMatcher(locator); // if (++count == 3) // new SWTHierarchy(shellSearchScope.getDisplay()).dbPrintWidgets(); } return locator; } /** * 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; 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; } /** * 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 macther 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) { 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). */ if (w instanceof Button) { Widget parent = new ButtonTester().getParent((Button) w); if (parent instanceof CCombo) w = parent; } WidgetLocator locator = checkForLabeledLocatorCase(w); //if we didn't find a label locator, create a standard locator if (locator == null) { 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 : _hierarchyHelper.getWidgetText(w); locator = (text != null) ? new WidgetLocator(cls, text) : new WidgetLocator(cls); } setDataValues(locator, w); return locator; } private WidgetLocator checkForLabeledLocatorCase(final Widget w) { Widget parent = _hierarchyHelper.getParent(w); if (!(parent instanceof Composite)) return null; //labels are not themselves considered labelable; if (w instanceof Label) 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]; /* * 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 == w; /* * 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]) return new LabeledLocator(cls, labelText[0]); 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()); } } }