/******************************************************************************* * 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.operation; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Widget; import com.windowtester.runtime.WT; import com.windowtester.runtime.WaitTimedOutException; import com.windowtester.runtime.swt.internal.state.MouseConfig; import com.windowtester.runtime.swt.internal.widgets.DisplayReference; import com.windowtester.runtime.swt.internal.widgets.SWTUIException; import com.windowtester.runtime.swt.internal.widgets.SWTWidgetReference; /** * The root of a class hierarchy used to wait for UI thread to be in the appropriate * state, gather widget information on the UI thread, and perform the operation. * Operations are comprised of individual "steps" such as pushing an event on the OS event * queue, sending a notification directly to a widget in SWTBot style, or performing an * arbitrary widget manipulation such as setting the expansion state of a tree. */ public abstract class SWTOperation { /** * The maximum number of milliseconds to wait for the UI to be in the appropriate * state or to execute a particular step */ private static final int MAX_RETRY_PERIOD = 30000; /** * The display for this operation. */ protected static final DisplayReference displayRef = DisplayReference.getDefault(); // TODO [Dan] Initialize this via a constructor /** * The keyboard modifier keys */ private static final int[] MODIFIERS = new int[]{ WT.ALT, WT.SHIFT, WT.CTRL, WT.COMMAND }; /** * The queue used to hold step waiting to be executed.<br/> * Synchronize against this field before accessing it. */ private final List<Step> queue = new ArrayList<Step>(10); /** * The starting time used by {@link #execute()} and others determine when the retry * period has ended, or zero if {@link #execute()} has not yet been called. */ private long retryStartTime = 0; /** * The callable used to execute steps on the UI thread. Returns <code>true</code> if * execution is complete, or <code>false</code> if the callable should be called again * to finish executing after a brief delay up to the maximum number of retries. */ private final Callable<Boolean> callable = new Callable<Boolean>() { public Boolean call() throws Exception { return executeInUI(); } }; private int waitForEnabled = 0; //======================================================================= // Processing /** * Execute the operation by calling {@link #executeInUI()} on the UI thread to perform * the operation. This method does not return until all queued steps have been * executed, but that does not mean that all events pushed on the OS event queue will * have been processed. * * @throws RuntimeException if there is an exception when executing the various steps * comprising the operation * @throws IllegalStateException if this method has already been called * @throws WaitTimedOutException if the UI thread does not execute the callable with * specified number of milliseconds */ public void execute() { if (retryStartTime != 0) throw new IllegalStateException("execute() has already been called: " + this); resetRetryStartTime(); SWTOperationStepException cause = null; while (System.currentTimeMillis() - retryStartTime < MAX_RETRY_PERIOD) { try { if (executeCallable(MAX_RETRY_PERIOD)) { retryStartTime = 0; return; } } catch (SWTUIException e) { if (e.getCause() instanceof SWTOperationStepException) cause = (SWTOperationStepException) e.getCause(); else throw e; } catch (SWTOperationStepException e) { cause = e; } try { // Sleep just long enough for the UI thread to gain the upper hand // and process some OS events Thread.sleep(10); } catch (InterruptedException e) { // Ignored... fall through } } throw new WaitTimedOutException("Max retry period (" + MAX_RETRY_PERIOD + " milliseconds) exceeded. ", cause); } /** * Reset the starting time used by {@link #execute()} and others determine when the * retry period has ended. */ private void resetRetryStartTime() { retryStartTime = System.currentTimeMillis(); } /** * Execute the callable on the UI thread. This method is repeatedly called by * {@link #execute()} until this method returns <code>true</code> or the * {@link #MAX_RETRY_PERIOD} has expired. * * @param maxWaitTime the maximum wait time before abort * @return <code>true</code> if execution is complete, or <code>false</code> if * {@link #executeInUI()} should be called again to finish executing after a * brief delay up to the maximum number of retries. * @throws SWTOperationStepException if step execution failed but the step should be * re-executed after a brief delay up to the maximum number of retries. * @throws IllegalStateException if the receiver is already executing * @throws WaitTimedOutException if the UI thread does not execute the callable with * specified number of milliseconds */ protected boolean executeCallable(int maxWaitTime) { return displayRef.execute(callable, maxWaitTime); } /** * Perform the operation by pulling steps off the {@link #queue} and executing them. * This method is repeatedly called on the UI thread from {@link #callable} until this * method returns <code>true</code> or the {@link #MAX_RETRY_PERIOD} has expired. * * @return <code>true</code> if execution is complete, or <code>false</code> if * {@link #executeInUI()} should be called again to finish executing after a * brief delay up to the maximum number of retries. * @throws SWTOperationStepException if step execution failed but the step should be * re-executed after a brief delay up to the maximum number of retries. */ protected boolean executeInUI() throws Exception { while (true) { // Pop the next step off the queue Step currentStep; synchronized (queue) { if (queue.size() == 0) return true; currentStep = queue.remove(0); } // If the step is null, then return false // so that the UI thread is given a chance to process the events currently in the OS event queue // and so that this method should be called again to process subsequent steps if (currentStep == null) //throw new SWTOperationStepException("Brief wait for UI thread to process OS events and other asyncExec"); return false; // Execute the current step // If the execution fails, then save the step for later // and let the UI thread process events already in the OS event queue try { currentStep.executeInUI(); } catch (SWTOperationStepException e) { synchronized (queue) { queue.add(0, currentStep); } throw e; } // Step executed successfully, so reset retry start time and loop for more steps resetRetryStartTime(); } } //======================================================================= // Steps /** * Operations are comprised of individual "steps" such as pushing an event on the OS * event queue, sending a notification directly to a widget in SWTBot style, or * performing an arbitrary widget manipulation such as setting the expansion state of * a tree. */ protected interface Step { /** * Called by {@link SWTOperation#executeInUI()} on the UI thread to perform a step * in the operation. * * @throws SWTOperationStepException if step execution failed but the step should * be re-executed after a brief delay up to the maximum number of * retries. */ void executeInUI() throws Exception; } /** * Subclasses call this method to queue the specified step in the operation. Calling * this method with a <code>null</code> argument causes subsequent steps to be * executed in a separate call to {@link Display#asyncExec(Runnable)} from the prior * steps, allowing the UI thread to process OS events and other * {@link Display#asyncExec(Runnable)} calls. * * @param step the step to be executed when {@link #executeInUI()} is called or * <code>null</code> if there should be a "break" between the prior steps * and the following steps */ protected void queueStep(Step step) { synchronized (queue) { queue.add(step); } } //======================================================================= // SWTBot style steps /** * Queue an event for the specified widget. This event will be sent directly to the * specified widget SWTBot style rather than through the OS event queue. To insert a * "break" between steps where the UI thread can process other events already on the * OS event queue and other calls to {@link Display#asyncExec(Runnable)}, see * {@link #queueStep(Step)}. */ protected void queueWidgetEvent(Widget widget, int eventType) { queueWidgetEvent(widget, eventType, 0); } /** * Queue an event for the specified widget. This event will be sent directly to the * specified widget SWTBot style rather than through the OS event queue. To insert a * "break" between steps where the UI thread can process other events already on the * OS event queue and other calls to {@link Display#asyncExec(Runnable)}, see * {@link #queueStep(Step)}. */ protected void queueWidgetEvent(Widget widget, int eventType, int eventDetail) { queueWidgetEvent(widget, null, eventType, eventDetail); } /** * Queue an event for the specified widget. This event will be sent directly to the * specified widget SWTBot style rather than through the OS event queue. To insert a * "break" between steps where the UI thread can process other events already on the * OS event queue and other calls to {@link Display#asyncExec(Runnable)}, see * {@link #queueStep(Step)}. */ protected void queueWidgetEvent(Widget widget, Widget item, int eventType, int eventDetail) { final Event event = new Event(); event.widget = widget; event.item = item; event.display = widget.getDisplay(); event.type = eventType; event.detail = eventDetail; // First send the event directly to the widget using asyncExec // so that any widget listeners will not block us queueStep(new Step() { public void executeInUI() { displayRef.getDisplay().asyncExec(new Runnable() { public void run() { event.time = (int) System.currentTimeMillis(); event.widget.notifyListeners(event.type, event); } }); } }); // Then queue a "break" so that we hop off the UI thread // allowing the asyncExec to send the event to the widget // before we process any subsequent steps queueStep(null); } //======================================================================= // OS Event steps /** * Subclasses call this method to queue the specified event to be posted to the OS * event queue. To insert a "break" between steps where the UI thread can process * other events already on the OS event queue and other calls to * {@link Display#asyncExec(Runnable)}, see {@link #queueStep(Step)}. * * @param event the event to be posted (not <code>null</code>) */ protected void queueOSEvent(final Event event) { if (event == null) throw new IllegalArgumentException(); queueStep(new Step() { public void executeInUI() throws Exception { if (!displayRef.getDisplay().post(event)) throw new SWTOperationStepException("Failed to post OS event: " + event); } }); } /** * Examine the accelerator bits to determine if any modifier keys (Shift, Alt, * Control, Command) are specified and queue zero or more key down events for those * modifier keys. * * @param accelerator the accelerator that may specify zero or more modifier keys<br/> * ({@link WT#SHIFT} , {@link WT#CTRL}, ...) */ protected void queueModifierKeysDown(int accelerator) { for (int i = 0; i < MODIFIERS.length; i++) { int mod = MODIFIERS[i]; if ((accelerator & mod) == mod) queueKeyCodeDown(mod); } } /** * Examine the accelerator bits to determine if any modifier keys (Shift, Alt, * Control, Command) are specified and queue zero or more key up events for those * modifier keys. * * @param accelerator the accelerator that may specify zero or more modifier keys<br/> * ({@link WT#SHIFT} , {@link WT#CTRL}, ...) */ protected void queueModifierKeysUp(int accelerator) { for (int i = MODIFIERS.length - 1; i >= 0; i--) { int mod = MODIFIERS[i]; if ((accelerator & mod) == mod) queueKeyCodeUp(mod); } } /** * Queue key down event for the specified keyCode * * @param keyCode the code for the key down to be queued such as {@link WT#HOME}, * {@link WT#CTRL}, {@link WT#SHIFT}, {@link WT#END} */ protected void queueKeyCodeDown(int keyCode) { Event event = new Event(); event.type = SWT.KeyDown; event.keyCode = keyCode; queueOSEvent(event); } /** * Queue key up event for the specified keyCode * * @param keyCode the code for the key up to be queued such as {@link WT#HOME}, * {@link WT#CTRL}, {@link WT#SHIFT}, {@link WT#END} */ protected void queueKeyCodeUp(int keyCode) { Event event = new Event(); event.type = SWT.KeyUp; event.keyCode = keyCode; queueOSEvent(event); } /** * Queue key down event for the specified character * * @param ch the character */ protected void queueCharDown(char ch) { Event event = new Event(); event.type = SWT.KeyDown; event.character = ch; queueOSEvent(event); } /** * Queue key up event for the specified character * * @param ch the character */ protected void queueCharUp(char ch) { Event event = new Event(); event.type = SWT.KeyUp; event.character = ch; queueOSEvent(event); } /** * Queue a mouse down event. It is recommended to call {@link #queueMouseMove(Point)} * before calling this method to move the mouse to the location where the click will * occur in addition to passing the mouse down location to this method. * * @param button the {@link Event#button} button (1, 2, or 3). Use * {@link #getButton(int)} to convert {@link WT#BUTTON1}, * {@link WT#BUTTON2}, etc to button (1, 2, or 3) * @param pt the mouse down location or <code>null</code> if the mouse down location * is unspecified. Use {@link SWTLocation} to convert widget relative * coordinates to global coordinates */ protected void queueMouseDown(int button, Point pt) { Event event; event = new Event(); event.type = SWT.MouseDown; event.button = button; if (pt != null) { event.x = pt.x; event.y = pt.y; } queueOSEvent(event); } /** * Queue a mouse up event * * @param button the {@link Event#button} button (1, 2, or 3). Use * {@link #getButton(int)} to convert {@link WT#BUTTON1}, * {@link WT#BUTTON2}, etc to button (1, 2, or 3) * @param pt the mouse down location or <code>null</code> if the mouse up location is * unspecified. Use {@link SWTLocation} to convert widget relative * coordinates to global coordinates */ protected void queueMouseUp(int button, Point pt) { Event event; event = new Event(); event.type = SWT.MouseUp; event.button = button; if (pt != null) { event.x = pt.x; event.y = pt.y; } queueOSEvent(event); } /** * Queue a mouse move event * * @param pt the location to which the mouse should be moved. Use {@link SWTLocation} * to convert widget relative coordinates to global coordinates */ protected void queueMouseMove(Point pt) { Event event; event = new Event(); event.type = SWT.MouseMove; event.x = pt.x; event.y = pt.y; queueOSEvent(event); } /** * Queue a mouse move event to the specific location offset by 1 pixel horizontally and vertically, * then queue a second mouse move event to the specific location * * @param pt the location to which the mouse should be moved. Use {@link SWTLocation} * to convert widget relative coordinates to global coordinates */ protected void queueMouseWiggle(Point pt) { queueMouseMove(new Point(pt.x + 1, pt.y + 1)); queueMouseMove(pt); } /** * Queue mouse move, mouse down, and mouse up events. * * @param button the {@link Event#button} button (1, 2, or 3). Use * {@link #getButton(int)} to convert {@link WT#BUTTON1}, * {@link WT#BUTTON2}, etc to button (1, 2, or 3) * @param pt the mouse down location or <code>null</code> if the mouse up location is * unspecified. Use {@link SWTLocation} to convert widget relative * coordinates to global coordinates */ protected void queueMouseMoveAndClick(int button, Point pt) { // TODO: do we need a wiggle? do we need this method? or should it be inlined? queueMouseMove(pt); queueMouseDown(button, pt); queueMouseMove(pt); queueMouseUp(button, pt); } /** * Queue a step that waits for a particular menu item to become enabled. Typically, * this is called *before* calling {@link #click(int, SWTLocation, boolean)} * * @return this operation so that calls can be cascaded on a single line such as * <code>new SWTShowMenuOperation().waitForEnabled(...).openMenu(...).execute();</code> */ public SWTOperation waitForEnabled(final SWTWidgetReference<?> widgetRef) { final String className = getClass().getSimpleName(); queueStep(new Step() { public void executeInUI() throws Exception { if (!widgetRef.isEnabled()) { // TODO: change 0 to higher number to reduce noise from log if (++waitForEnabled > 0) System.out.println(className + " waiting for enabled: " + waitForEnabled + " - " + widgetRef); throw new SWTOperationStepException("Waiting for item to become enabled: " + widgetRef); } } }); return this; } /** * Extract the {@link Event#button} button from the accelerator bits. * * @param accelerator the bitwise accelerator ({@link WT#BUTTON1}, {@link WT#BUTTON2}, * {@link WT#BUTTON3}) * @return the {@link Event#button} button (1, 2, or 3) */ protected static int getButton(int accelerator) { if ((accelerator & WT.BUTTON1) == WT.BUTTON1) return MouseConfig.BUTTONS_REMAPPED ? 3 : 1; if ((accelerator & WT.BUTTON2) == WT.BUTTON2) return 2; if ((accelerator & WT.BUTTON3) == WT.BUTTON3) return MouseConfig.BUTTONS_REMAPPED ? 1 : 3; return MouseConfig.BUTTONS_REMAPPED ? 3 : 1; } }