/******************************************************************************* * 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.internal.swing; import java.awt.Component; import java.awt.Container; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import javax.swing.AbstractButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import abbot.finder.Matcher; import com.windowtester.internal.swing.locator.IWidgetIdentifierStrategy; import com.windowtester.internal.swing.locator.MatcherFactory; import com.windowtester.internal.swing.locator.ScopedComponentIdentifierBuilder; import com.windowtester.runtime.swing.SwingWidgetLocator; /** * 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 { /** * 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 = {}; private IWidgetIdentifierStrategy _widgetIdentifier = new ScopedComponentIdentifierBuilder(); /** * 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(SwingWidgetLocator wl) { return MatcherFactory.getMatcher(wl); } /** * 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(Component w, Component 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();) { Component child = (Component)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"); } /** * 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(Component w1, Component 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(Component parent, Class cls) { List children = new ArrayList(); if (parent instanceof Container){ Component[] components = ((Container)parent).getComponents(); addCheck(children,Arrays.asList(components)); } //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; } /** * 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); } } /** * Extract the text from the given widget. * @param w - the widget in question * @return the widget's text */ public String getWidgetText(Component w) { if ((w instanceof AbstractButton) && !(w instanceof JMenuItem)) { return (((AbstractButton)w).getText()); } if (w instanceof JLabel) { return (((JLabel)w).getText()); } //fall through .... return null; } /** * Create an (unelaborated) info object for this widget. * @param w - the widget to describe. * @return an info object that describes the widget. */ private SwingWidgetLocator getInfo(Component 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); String text = getWidgetText(w); SwingWidgetLocator locator = (text != null) ? new SwingWidgetLocator(cls, text) : new SwingWidgetLocator(cls); setDataValues(locator, w); return locator; } // propagate values of interest from the widget to the locator private void setDataValues(SwingWidgetLocator locator, Component w) { String key; Object value = null; for (int i= 0; i < INTERESTING_KEYS.length; ++i) { key = INTERESTING_KEYS[i]; if (w instanceof JComponent) value = ((JComponent)w).getClientProperty(key); if (value != null) locator.setData(key, value.toString()); } } /** * 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 SwingWidgetLocator inferIdentifyingInfo(Component w) { /* * pulling inference into separate strategy */ return _widgetIdentifier.identify(w); } }