/******************************************************************************* * 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.widgets; import static com.windowtester.internal.runtime.util.ReflectionUtils.invoke; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.Callable; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Widget; import com.windowtester.internal.runtime.reflect.Reflector; import com.windowtester.internal.runtime.util.ReflectionUtils; import com.windowtester.internal.runtime.util.StringUtils; import com.windowtester.runtime.IClickDescription; import com.windowtester.runtime.IUIContext; import com.windowtester.runtime.condition.ICondition; import com.windowtester.runtime.internal.concurrent.SafeCallable; import com.windowtester.runtime.internal.concurrent.VoidCallable; import com.windowtester.runtime.internal.factory.WTRuntimeManager; import com.windowtester.runtime.locator.IWidgetLocator; import com.windowtester.runtime.locator.IWidgetReference; import com.windowtester.runtime.swt.internal.SWTUtils; import com.windowtester.runtime.swt.internal.matchers.ByTextMatcher; import com.windowtester.runtime.swt.internal.widgets.finder.MatchCollector; /** * Widget references are used to <em>test</em> or access widgets safely on the UI thread. * <p/> * Values returned by reference accessors should themselves be safely accessible. This * means that in cases where the accessors return other widgets, these returned widgets * are themselves wrapped in proxy widget references. The exception to this rule is * {@link #getWidget()} which returns the bare {@link Widget} instance. Access to this * instance directly should be avoided if at all possible since care needs to be take to * ensure that it is safely accessed from the UI thread. * @param <T> the type of the bare widget */ public class SWTWidgetReference<T extends Widget> extends AbstractSWTDisplayable implements ISWTWidgetReference<T>, IVisitable, ISearchable { public static interface Visitor { <W extends Widget> void visit(SWTWidgetReference<W> widget); //note: not composites because non-composites such as Menus can have children <T extends SWTWidgetReference<?>> void visitEnter(T composite); <T extends SWTWidgetReference<?>> void visitLeave(T composite); } /** * A set of references used to build the results of the call to {@link SWTWidgetReference#getChildren()}. */ public static class ChildSet { //preserves insertion order final LinkedHashSet<SWTWidgetReference<?>> children = new LinkedHashSet<SWTWidgetReference<?>>(); ChildSet add(SWTWidgetReference<?> ... refs){ if (refs != null) { for (SWTWidgetReference<?> ref : refs) { if (ref != null) children.add(ref); } } return this; } SWTWidgetReference<?>[] toArray(){ return children.toArray(emptyArray()); } } protected final T widget; /** * Constructs a new instance with the given widget. * * @param w the widget. */ public SWTWidgetReference(T widget) { super(validate(widget).getDisplay()); this.widget = widget; } protected static <W extends Widget> W validate(W widget){ //TODO[pq]: consider a better exception assertWidgetNotNull(widget); //TODO [pq]: should we assert not disposed? return widget; } /* (non-Javadoc) * @see com.windowtester.runtime.swt.widgets.ISWTWidgetReference#getParent() */ public ISWTWidgetReference<?> getParent(){ //overridden in subclasses // TODO make this method abstract return null; } /* (non-Javadoc) * @see com.windowtester.runtime.swt.widgets.ISWTWidgetReference#getChildren() */ public final SWTWidgetReference<?>[] getChildren() { final ChildSet children = new ChildSet(); SWTUtils.safeExec(new VoidCallable() { @Override public void call() throws Exception { setChildren(children); } }); return children.toArray(); } /** * Add elements for inclusion in the list of children returned by {@link SWTWidgetReference#getChildren()}. * Note that {@link ChildSet} subscribes to the {@link Set} contract in that it ensures that there are no duplicates. * @param children the collecting child set */ protected void setChildren(ChildSet children){ //implemented in subclasses that have children } /* (non-Javadoc) * @see com.windowtester.runtime.swt.internal.widgets.IWidgetReference#getWidget() */ public T getWidget(){ return widget; } /** * Proxy for {@link Widget#getDisplay()}. */ public DisplayReference getDisplayRef(){ return displayRef; } /* (non-Javadoc) * @see com.windowtester.runtime.swt.widgets.ISWTWidgetReference#getDisplayBounds() */ public Rectangle getDisplayBounds() { return displayRef.execute(new Callable<Rectangle>() { public Rectangle call() throws Exception { Control parent = (Control) invoke(widget, "getParent"); Rectangle bounds = (Rectangle) invoke(widget, "getBounds"); return widget.getDisplay().map(parent, null, bounds); } }); } // /** // * Gets if the object's widget is enabled. // * // * @return <code>true</code> if the widget is enabled. // * @see Control#isEnabled() // */ // public boolean isEnabled() { // //TODO[pq]: compare with our impls. // if (widget instanceof Control) // return syncExec(new BooleanRunnable() { // public Boolean run() { // return isEnabledInternal(); // } // }); // return false; // } // /** // * Gets if the widget is enabled. // * <p> // * This method is not thread safe, and must be called from the UI thread. // * </p> // * // * @return <code>true</code> if the widget is enabled. // * @since 1.0 // */ // protected boolean isEnabledInternal() { // try { // return ((Boolean) SWTUtils.invokeMethod(widget, "isEnabled")).booleanValue(); //$NON-NLS-1$ // } catch (Exception e) { // return true; // } // } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (!(obj instanceof IWidgetReference)) return false; return widget.equals(((IWidgetReference)obj).getWidget()); } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return widget.hashCode(); } @Override public String toString() { return displayRef.execute(new SafeCallable<String>() { public String call() throws Exception { return SWTWidgetReference.this.getClass().getSimpleName() + " - " + widget.toString(); } public String handleException(Throwable e) throws Throwable { return SWTWidgetReference.this.getClass().getSimpleName() + " - widget (?) " + e; } }); } @SuppressWarnings("unchecked") protected <R extends SWTWidgetReference<?>> R[] asReferencesOfType(Widget[] widgets, Class<? extends SWTWidgetReference<?>> k){ //TODO[pq]: test me! R[] array = (R[]) ReflectionUtils.newArray(k, widgets.length); if (widgets.length == 0) return array; for (int i = 0; i < array.length; i++) { array[i] = (R) WTRuntimeManager.asReference(widgets[i]); } return array; } protected ControlReference<?>[] asControlReferences(Control[] controls){ ControlReference<?>[] references = ReflectionUtils.newArray(ControlReference.class, controls.length); for (int i = 0; i < references.length; i++) { //references[i] = new ControlReference<Control>(controls[i]); references[i] = (ControlReference<?>) forWidget(controls[i]); } return references; } protected SWTWidgetReference<?>[] asReferences(Widget[] controls){ SWTWidgetReference<?>[] references = ReflectionUtils.newArray(SWTWidgetReference.class, controls.length); for (int i = 0; i < references.length; i++) { //references[i] = new ControlReference<Control>(controls[i]); references[i] = (SWTWidgetReference<?>) forWidget(controls[i]); } return references; } public static SWTWidgetReference<?>[] newReferenceArray(int length){ return ReflectionUtils.newArray(SWTWidgetReference.class, length); } public static SWTWidgetReference<?>[] emptyArray(){ return ReflectionUtils.newArray(SWTWidgetReference.class, 0); } //NOTE: does null check @SuppressWarnings("unchecked") protected <R extends SWTWidgetReference<?>> R asReferenceOfType(Widget widget, Class<R> refType) { if (widget == null) return null; return (R) WTRuntimeManager.asReference(widget); } /* (non-Javadoc) * @see com.windowtester.runtime.locator.IWidgetLocator#findAll(com.windowtester.runtime.IUIContext) */ public IWidgetLocator[] findAll(IUIContext ui) { return new IWidgetLocator[]{this}; } /* (non-Javadoc) * @see com.windowtester.runtime.locator.IWidgetMatcher#matches(java.lang.Object) */ public boolean matches(Object widget) { // TODO[pq]: implement this legacy matches APIs (or find workaround) return false; } // TODO[pq]: potential API? // TODO[pq]: finds implicitly should ignore not visible widgets? how to search for invisibles? public ISWTWidgetReference<?>[] findWidgets(ISWTWidgetMatcher matcher){ //implemented in terms of a visit: MatchCollector collector = new MatchCollector(matcher); return (ISWTWidgetReference<?>[]) collector.findMatchesIn(this).toArray(emptyArray()); } /* (non-Javadoc) * @see com.windowtester.runtime.swt.internal.widgets.IVisitable#accept(com.windowtester.runtime.swt.internal.widgets.SWTWidgetReference.Visitor) */ public final void accept(Visitor visitor) { visitor.visit(this); visitor.visitEnter(this); visitAll(visitor, getChildren()); visitor.visitLeave(this); } protected void visitIfNotNull(SWTWidgetReference.Visitor visitor, SWTWidgetReference<?> widget) { if (widget != null) widget.accept(visitor); } protected void visitAll(SWTWidgetReference.Visitor visitor, SWTWidgetReference<?>[] widgets) { for (SWTWidgetReference<?> widget : widgets) { visitIfNotNull(visitor, widget); } } public static ISWTWidgetReference<?> forWidget(Widget widget) { return (ISWTWidgetReference<?>) WTRuntimeManager.asReference(widget); } public static ControlReference<?> forControl(Control control) { return (ControlReference<?>) forWidget(control); } private static void assertWidgetNotNull(Object widget) { if (widget == null) throw new IllegalArgumentException("widget must not be null"); } /** * Gets the text associated with the underlying widget. The text is retrieved using * the widget's <code>getText()</code> method if one is defined. If there is no such method * a <code>null</code> value is returned instead. If a client wants to know if the underlying widget * supports the <code>getText()</code> protocol, they are encouraged to call {@link #hasText()} first. * * @return the widget's text */ public String getText() { return displayRef.execute(new Callable<String>() { public String call() throws Exception { return (String) invoke(widget, "getText"); } }); } /** * Gets the text associated with the underlying widget and prepares it for matching. * <p/> * As a general rule this post-processing strips things like accelerators. So, for example, * the menu item "Ne&w\tCtrl+N" would get simplified to the value "New". * * @return the widgets's text suitable for matching */ public String getTextForMatching(){ String text = getText(); if (text == null) return text; return StringUtils.trimMenuText(text); } /** * Test if this widget has associated text (as returned by a <code>getText()</code> method). * * @return true if the widget has text, false otherwise */ public boolean hasText(){ return Reflector.forObject(widget).supports("getText"); } /** * Get the underlying widget's name as set by calling <code>widget.setData("name", "widget.name");</code> * @see Widget#setData(String, Object) */ public String getName() { Object name = getData("name"); if (name == null) return null; return name.toString(); } /** * Proxy for {@link Widget#getData()}. */ public Object getData() { return displayRef.execute(new Callable<Object>() { public Object call() throws Exception { return widget.getData(); } }); } /** * Proxy for {@link Widget#getData(String))}. */ public Object getData(final String key) { return displayRef.execute(new Callable<Object>() { public Object call() throws Exception { return widget.getData(key); } }); } /** * Proxy for {@link Widget#getStyle()}. * <p/> * @return the style. */ public int getStyle(){ return displayRef.execute(new Callable<Integer>() { public Integer call() throws Exception { return widget.getStyle(); } }); } /** * Checks if the widget has the given style. * * @param style the style. * @return <code>true</code> if the widget has the specified style bit set, else * <code>false</code>. */ public boolean hasStyle(final int style) { if (style == SWT.NONE) return false; return displayRef.execute(new Callable<Boolean>() { public Boolean call() throws Exception { return !widget.isDisposed() && (widget.getStyle() & style) != 0; } }); } /** * Tests if the associated widget is visible. */ public boolean isVisible(){ //Basic legacy impl. to be pushed down try { // // ask the menu watcher if the given items menu is open // if (w instanceof MenuItem) // return MenuWatcher.getInstance(w.getDisplay()).isVisible( // (MenuItem) w); Control control = SWTUtils.getControl(widget); 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); return SWTWidgetReference.forWidget(control).isVisible(); } catch (SWTException e) { // ignore } return false; } public boolean isMatchedBy(ISWTWidgetMatcher matcher){ return matcher.matches(this); } /** * Proxy for {@link Widget#isDisposed()}. * @deprecated If you call this method from a non-UI thread to guard * against accessing a non-disposed widget, then there is an inherent * race condition. Better to call isDisposed on the UI thread. */ public boolean isDisposed() { return widget.isDisposed(); } public boolean isEnabled() { return displayRef.execute(new Callable<Boolean>() { public Boolean call() throws Exception { return (Boolean) invoke(widget, "isEnabled"); } }); } public ICondition isEnabled(final boolean enabled){ return new ICondition(){ public boolean test() { return isEnabled() == enabled; } @Override public String toString() { return SWTWidgetReference.this.toString() + " to be enabled (" + enabled + ")"; } }; } private class WidgetHasTextCondition implements ICondition { private final String txt; private ByTextMatcher matcher; public WidgetHasTextCondition(String txt) { this.txt = txt; matcher = new ByTextMatcher(txt); } public boolean test() { return isMatchedBy(matcher); } @Override public String toString() { return SWTWidgetReference.this.toString() + " to have text :'" + txt +"'"; } } public ICondition hasText(String txt) { return new WidgetHasTextCondition(txt); } /* (non-javadoc) * @see ISWTWidgetReference#showPulldownMenu(IClickDescription) */ public MenuReference showPulldownMenu(IClickDescription click) { throw new RuntimeException(toString() + " does not have a pulldown menu"); } }