/******************************************************************************* * 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 org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import com.windowtester.runtime.WT; import com.windowtester.runtime.internal.factory.WTRuntimeManager; import com.windowtester.runtime.swt.internal.widgets.MenuItemReference; import com.windowtester.runtime.swt.internal.widgets.MenuReference; import com.windowtester.runtime.swt.internal.widgets.SWTWidgetReference; /** * Shared behavior for showing a menu and selecting a menu item */ public abstract class SWTMenuOperation extends SWTOperation { protected static String priorDebugMsg = ""; /** * The menu item being */ protected final MenuItemReference menuItemReference; /** * The location at which the mouse should be clicked to show the menu */ protected SWTLocation location; /** * The actual location at which the mouse was clicked to show the menu */ protected Point clickLoc; /** * The menu that appears as a result of the operation. This field is set by * {@link MenuFilter} and should ONLY be accessed from the UI thread. */ protected Menu menu = null; /** * A flag used to indicate the the menu item was selected. This field is set by * {@link MenuFilter} and should ONLY be accessed from the UI thread. */ protected boolean selected = false; /** * If a click occurs in an incorrect location, then this flag is set to indicate that * the selection has been canceled. This field is set by {@link MenuFilter} and should * ONLY be accessed from the UI thread. */ protected boolean clickCanceled = false; /** * If a click occurs in an incorrect location, then this field contains a message * indicating problem. This field is set by {@link MenuFilter} and should ONLY be * accessed from the UI thread. */ public String clickCanceledMessage = null; /** * Set by calling {@link #queueRetryStep(Step)} to cache a step that can be retried if * the menu item location in a dynamic menu shifts after the click occurs. This can * only be called once. */ protected Step retryStep = null; /** * A collection of debugging information appended to any exception thrown */ protected StringBuilder debugMsg = new StringBuilder(1000); /** * Construct a new operation for showing a menu or clicking a menu item * * @param menuItemReference the menu item to be clicked or <code>null</code> if a top * level menu is to be clicked. */ public SWTMenuOperation(MenuItemReference menuItemReference) { this.menuItemReference = menuItemReference; } public SWTMenuOperation waitForEnabled(SWTWidgetReference<?> widgetRef) { return (SWTMenuOperation) super.waitForEnabled(widgetRef); } /** * Queue the mouse events necessary to show the menu or select the menu item. * * @param accelerator the mouse button such as {@link WT#BUTTON1} or * {@link WT#BUTTON3} and optionally modifier keys such as {@link WT#SHIFT} * @param location the location for the mouse event * @param pauseOnMouseDown <code>true</code> if there should be a pause between * posting the mouse down and mouse up event so that the mouse down event * can be processed before the mouse up event is posted. For TableItem * context menus on both Windows and Linux, the mouse down events need to * be processed before the mouse up events are posted. This can be seen in * the TableDoubleClickTest. For TreeItem context menu on Linux, if right * mouse button is held down too long, it closes the tree item's context * menu item. * @return this operation so that calls can be cascaded on a single line such as * <code>new SWTMenuOperation().click(...).execute();</code> */ public SWTMenuOperation click(final int accelerator, final SWTLocation location, final boolean pauseOnMouseDown) { this.location = location; queueStartMenuFilter(); queueRetryStep(new Step() { public void executeInUI() throws Exception { int button = getButton(accelerator); clickLoc = location.location(); // Debug code to gather more information on menu not visible exceptions debugMsg.append("\n click at " + clickLoc + " accel=" + accelerator + " pause=" + pauseOnMouseDown); debugMsg.append("\n location " + location); debugMsg.append("\n initial active shell " + displayRef.getDisplay().getActiveShell()); String newDebugMsg = debugMsg.toString(); debugMsg.setLength(0); debugMsg.append("\nPrior show menu operation"); debugMsg.append(priorDebugMsg); debugMsg.append("\nCurrent show menu operation"); debugMsg.append(newDebugMsg); priorDebugMsg = newDebugMsg; // Linux needs a wiggle and not just a move queueMouseWiggle(clickLoc); queueMouseDown(button, clickLoc); // Linux needs a mouse move even for a mouse click // and it does not hurt on the Windows side queueMouseMove(clickLoc); if (pauseOnMouseDown) queueStep(null); queueMouseUp(button, clickLoc); // Linux (and Mac?) needs a wiggle to push through events queueMouseWiggle(clickLoc); queueWaitForMenu(); } }); return this; } /** * Answer the menu that appears as a result of executing this operation * * @return the menu or <code>null</code> if the operation is not complete */ public MenuReference getMenu() { if (menu == null) return null; return (MenuReference) WTRuntimeManager.asReference(menu); } //=================================================================================== // SWT Event Listener /** * Queue a step that adds a listener to detect a menu becoming visible */ protected void queueStartMenuFilter() { queueStep(new Step() { public void executeInUI() throws Exception { menuFilter.start(SWTMenuOperation.this); } }); } /** * Queue a step that can be retried if the menu item location in a dynamic shifts * after the click occurs. This can only be called once. * * @param step the step that can be retried */ protected void queueRetryStep(Step step) { if (retryStep != null) throw new IllegalStateException("Retry step already set"); retryStep = step; queueStep(step); } /** * Queue a step that waits until a menu becomes visible */ protected void queueWaitForMenu() { queueStep(new Step() { private boolean first = true; public void executeInUI() throws Exception { // If the click was canceled, then retry if possible if (clickCanceled) { clickCanceled = false; menu = null; if (selected) { selected = false; retryAfterBadSelection(clickCanceledMessage); } else { System.out.println(clickCanceledMessage); if (retryStep == null) throw new RuntimeException(clickCanceledMessage); System.out.println("Retrying last menu item click"); queueStep(null); queueStep(retryStep); } } // Check to see if the menu is visible or menu item selected else if ((menu == null || !menu.isVisible()) && !selected) { // Check for a dynamic menu item that has shifted location String message; if (clickLoc != null && !clickLoc.equals(location.location())) { message = "Menu item moved since click: " + menuItemReference + "\n clicked at " + clickLoc + "\n but now location is " + location.location() + debugMsg + "\n final active shell " + displayRef.getDisplay().getActiveShell(); if (first) { first = false; System.out.println(message); } } else message = "Menu not visible or selected: " + menuItemReference + debugMsg + "\n final active shell " + displayRef.getDisplay().getActiveShell(); throw new SWTOperationStepException(message); } } }); } /** * Called after an incorrect selection where the menus are no longer visible. * Subclasses may override. * * @param message the exception message */ protected abstract void retryAfterBadSelection(String message); protected static final MenuFilter menuFilter = new MenuFilter(); /** * Watch SWT Events waiting for a menu to become visible. */ protected static class MenuFilter implements Listener { private SWTMenuOperation operation = null; private boolean isListening = false; private int openMenuCount = 0; void start(SWTMenuOperation operation) { this.operation = operation; if (!isListening) { isListening = true; displayRef.getDisplay().addFilter(SWT.Arm, this); displayRef.getDisplay().addFilter(SWT.Selection, this); displayRef.getDisplay().addFilter(SWT.Show, this); displayRef.getDisplay().addFilter(SWT.Hide, this); // displayRef.getDisplay().addFilter(SWT.Dispose, this); } } int cancel() { if (operation == null) return openMenuCount; operation = null; // If an operation is waiting then a menu might be open that we missed return openMenuCount + 1; } /** * Called by the SWT infrastructure on the UI thread when an SWT event has * occurred. */ public void handleEvent(Event event) { if (event.widget instanceof Menu) { switch (event.type) { // Detect a menu becoming visible case SWT.Show : // System.out.println("Menu shown"); openMenuCount++; if (operation != null) { operation.menu = (Menu) event.widget; operation = null; } break; // If a menu is going away, assume all menus closed case SWT.Hide : // System.out.println("Menu hidden"); openMenuCount = 0; break; // The Dispose event happens out of sequence // and sometimes in the middle of a subsequent show/hide // case SWT.Dispose : // System.out.println("Dispose menu"); // // If a menu is going away, assume all menus closed // openMenuCount = 0; // break; } } else if (event.widget instanceof MenuItem) { switch (event.type) { // Detect incorrectly armed menu item and cancel the operation case SWT.Arm : // System.out.println("Menu item armed"); if (operation != null) { MenuItem actual = (MenuItem) event.widget; MenuItem expected = operation.menuItemReference.getWidget(); if (expected != null && !matches(actual, expected)) { String message = "Wrong menu item armed\n expected: " + expected + "\n but selected: " + actual; System.out.println(message); // Cancel a cascaded menu, but let non-cascaded menu continue // so that the selection event can be blocked if ((actual.getStyle() & SWT.CASCADE) != 0) { operation.clickCanceled = true; operation.clickCanceledMessage = message; operation = null; } } } break; // Detect incorrectly selected menu item and cancel the operation case SWT.Selection : // System.out.println("Menu item selected"); if (operation != null) { MenuItem actual = (MenuItem) event.widget; MenuItem expected = operation.menuItemReference.getWidget(); if (expected != null && !matches(actual, expected)) { String message = "Wrong menu item selected - canceling selection\n expected: " + expected + "\n but selected: " + actual; System.out.println(message); event.type = SWT.None; event.doit = false; operation.clickCanceled = true; operation.clickCanceledMessage = message; } operation.selected = true; operation = null; } break; } } } /** * Compare two menu items to determine if they are the same */ private boolean matches(MenuItem actual, MenuItem expected) { if (actual == expected) return true; if (actual == null || expected == null) return false; String actualText = actual.getText(); String expectedText = expected.getText(); if (actualText == expectedText) return true; if (actualText == null || expectedText == null) return false; return actualText.equals(expectedText); } } }