1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.test.jemmy.misc.base.wrappers;
  34 
  35 import java.awt.Toolkit;
  36 import java.awt.datatransfer.DataFlavor;
  37 import java.awt.datatransfer.Transferable;
  38 import java.awt.datatransfer.UnsupportedFlavorException;
  39 import java.awt.im.InputContext;
  40 import java.io.File;
  41 import java.io.IOException;
  42 import java.lang.reflect.Method;
  43 import java.util.ArrayList;
  44 import java.util.HashMap;
  45 import java.util.List;
  46 import java.util.Map;
  47 
  48 import org.eclipse.core.runtime.jobs.Job;
  49 import org.eclipse.swt.SWTException;
  50 import org.eclipse.swt.custom.CTabFolder;
  51 import org.eclipse.swt.widgets.Composite;
  52 import org.eclipse.swt.widgets.Control;
  53 import org.eclipse.swt.widgets.Display;
  54 import org.eclipse.swt.widgets.Menu;
  55 import org.eclipse.swt.widgets.MenuItem;
  56 import org.eclipse.swt.widgets.Shell;
  57 import org.eclipse.swt.widgets.Table;
  58 import org.eclipse.swt.widgets.Widget;
  59 import org.jemmy.Point;
  60 import org.jemmy.action.AbstractExecutor;
  61 import org.jemmy.control.Wrap;
  62 import org.jemmy.env.Environment;
  63 import org.jemmy.env.TestOut;
  64 import org.jemmy.image.awt.AWTImage;
  65 import org.jemmy.image.awt.AWTRobotCapturer;
  66 import org.jemmy.image.awt.AverageDistanceImageComparator;
  67 import org.jemmy.image.awt.FilesystemImageLoader;
  68 import org.jemmy.image.awt.StrictImageComparator;
  69 import org.jemmy.image.Image;
  70 import org.jemmy.image.ImageComparator;
  71 import org.jemmy.input.awt.AWTRobotInputFactory;
  72 import org.jemmy.input.DefaultCharBindingMap;
  73 import org.jemmy.interfaces.Focusable;
  74 import org.jemmy.interfaces.Keyboard.KeyboardButton;
  75 import org.jemmy.interfaces.Keyboard.KeyboardButtons;
  76 import org.jemmy.interfaces.Keyboard.KeyboardModifiers;
  77 import org.jemmy.interfaces.Mouse.MouseButtons;
  78 import org.jemmy.interfaces.Parent;
  79 import org.jemmy.lookup.AbstractLookup;
  80 import org.jemmy.lookup.Lookup;
  81 import org.jemmy.operators.Screen;
  82 import org.jemmy.resources.StringComparePolicy;
  83 import org.jemmy.swt.SWTMenu;
  84 import org.jemmy.swt.Shells;
  85 import org.jemmy.swt.lookup.ByItemLookup;
  86 import org.jemmy.swt.lookup.ByName;
  87 import org.jemmy.swt.lookup.ByTextControlLookup;
  88 import org.jemmy.swt.lookup.ByTextShell;
  89 import org.junit.Assert;
  90 
  91 import org.openjdk.jmc.test.jemmy.misc.fetchers.Fetcher;
  92 import org.openjdk.jmc.test.jemmy.misc.fetchers.FetcherWithInput;
  93 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCProgressIndicator;
  94 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCTabFolder;
  95 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCTable;
  96 
  97 /**
  98  * The base class for the Mission Control Jemmy wrappers
  99  */
 100 public class MCJemmyBase {
 101         private static final boolean VERBOSE_JEMMY_LOGGING = "true"
 102                         .equalsIgnoreCase(System.getProperty("mc.test.jemmy.verbose.logging"));
 103         private static final long EDITOR_LOAD_WAIT_DEFAULT_TIMEOUT_MS = 30000;
 104         private static final long EDITOR_LOAD_WAIT_TIMEOUT_MS = Long.getLong("jmc.test.editor.load.wait.timeout",
 105                         EDITOR_LOAD_WAIT_DEFAULT_TIMEOUT_MS);
 106         public static final long VISIBLE_LOOKUP_DEFAULT_TIMEOUT_MS = 10000;
 107         private static final long VISIBLE_LOOKUP_TIMEOUT_MS = Long.getLong("jmc.test.visible.lookup.timeout",
 108                         VISIBLE_LOOKUP_DEFAULT_TIMEOUT_MS);
 109         private static final int BETWEEN_KEYSTROKES_SLEEP = 100;
 110         public static final int LOOKUP_SLEEP_TIME_MS = 100;
 111         protected static Wrap<? extends Shell> shell;
 112         protected Wrap<? extends Control> control;
 113         public static final KeyboardButtons SELECTION_BUTTON;
 114         public static final KeyboardButtons EXPAND_BUTTON;
 115         public static final KeyboardButtons COLLAPSE_BUTTON;
 116         public static final KeyboardButtons CLOSE_BUTTON;
 117         public static final KeyboardModifiers SHORTCUT_MODIFIER;
 118         public static final String OS_NAME;
 119         private static final int IDLE_LOOP_COUNT = 3;
 120         private static final int IDLE_LOOP_TIME_STEP = 100;
 121         private static final int IDLE_LOOP_TIMEOUT_MS = 10000;
 122         private static Integer unnamedImageCounter = 0;
 123         private static final Map<Class<?>, Class<?>> primitiveMap = new HashMap<>();
 124         protected static Wrap<? extends Shell> focusedSection;
 125 
 126         static {
 127                 Environment.getEnvironment().setOutput(AbstractLookup.OUTPUT,
 128                                 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput());
 129                 Environment.getEnvironment().setOutput(AbstractExecutor.NON_QUEUE_ACTION_OUTPUT,
 130                                 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput());
 131                 Environment.getEnvironment().setOutput(AbstractExecutor.QUEUE_ACTION_OUTPUT,
 132                                 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput());
 133                 Environment.getEnvironment().setOutput(Wrap.OUTPUT,
 134                                 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput());
 135                 Environment.getEnvironment().setInputFactory(new AWTRobotInputFactory());
 136                 Environment.getEnvironment().setImageCapturer(new AWTRobotCapturer());
 137                 Environment.getEnvironment().setImageLoader(new FilesystemImageLoader());
 138 
 139                 /**
 140                  * Overriding because on Linux it is wrongly assumed that SPACE does a proper selection
 141                  * (which is not the case with check or radio style where the menu isn't closed)
 142                  */
 143                 Environment.getEnvironment().setProperty(KeyboardButton.class, SWTMenu.SELECTION_BUTTON_PROP,
 144                                 KeyboardButtons.ENTER);
 145 
 146                 if ("sv".equals(InputContext.getInstance().getLocale().getLanguage())) {
 147                         Environment.getEnvironment().setProperty("LANG", "sv");
 148                 }
 149                 AWTImage.setImageRoot(getResultDir());
 150                 AWTImage.setComparator(new AverageDistanceImageComparator());
 151                 Screen.SCREEN.getEnvironment().setInputFactory(new AWTRobotInputFactory());
 152 
 153                 OS_NAME = System.getProperty("os.name").toLowerCase();
 154                 SELECTION_BUTTON = OS_NAME.contains("linux") ? KeyboardButtons.SPACE : KeyboardButtons.ENTER;
 155                 EXPAND_BUTTON = OS_NAME.contains("os x") ? KeyboardButtons.RIGHT : KeyboardButtons.ADD;
 156                 COLLAPSE_BUTTON = OS_NAME.contains("os x") ? KeyboardButtons.LEFT : KeyboardButtons.SUBTRACT;
 157                 CLOSE_BUTTON = KeyboardButtons.W;
 158                 SHORTCUT_MODIFIER = OS_NAME.contains("os x") ? KeyboardModifiers.META_DOWN_MASK
 159                                 : KeyboardModifiers.CTRL_DOWN_MASK;
 160 
 161                 Environment.getEnvironment().setProperty(Boolean.class, SWTMenu.SKIPS_DISABLED_PROP,
 162                                 (OS_NAME.contains("windows")) ? false : true);
 163 
 164                 // keyboard re-mapping for Mac OS X with Swedish keyboard
 165                 if ("sv".equalsIgnoreCase(InputContext.getInstance().getLocale().getLanguage()) && OS_NAME.contains("os x")) {
 166                         // first making sure that the DefaultCharBindingMap has been loaded and initialized
 167                         getShell().keyboard();
 168                         DefaultCharBindingMap map = (DefaultCharBindingMap) Environment.getEnvironment().getBindingMap();
 169                         map.removeChar('+');
 170                         map.addChar('+', KeyboardButtons.MINUS);
 171                         map.removeChar('-');
 172                         map.addChar('-', KeyboardButtons.SLASH);
 173                         map.removeChar('_');
 174                         map.addChar('_', KeyboardButtons.SLASH, KeyboardModifiers.SHIFT_DOWN_MASK);
 175                         map.removeChar('/');
 176                         map.addChar('/', KeyboardButtons.D7, KeyboardModifiers.SHIFT_DOWN_MASK);
 177                         map.removeChar('\\');
 178                         map.addChar('\\', KeyboardButtons.D7, KeyboardModifiers.SHIFT_DOWN_MASK, KeyboardModifiers.ALT_DOWN_MASK);
 179                         map.removeChar(':');
 180                         map.addChar(':', KeyboardButtons.PERIOD, KeyboardModifiers.SHIFT_DOWN_MASK);
 181                         map.removeChar(';');
 182                         map.addChar(';', KeyboardButtons.COMMA, KeyboardModifiers.SHIFT_DOWN_MASK);
 183                         map.removeChar('~');
 184                         map.addChar('~', KeyboardButtons.CLOSE_BRACKET, KeyboardModifiers.ALT_DOWN_MASK);
 185                         map.removeChar('=');
 186                         map.addChar('=', KeyboardButtons.D0, KeyboardModifiers.SHIFT_DOWN_MASK);
 187                 }
 188 
 189                 primitiveMap.put(boolean.class, Boolean.class);
 190                 primitiveMap.put(byte.class, Byte.class);
 191                 primitiveMap.put(char.class, Character.class);
 192                 primitiveMap.put(short.class, Short.class);
 193                 primitiveMap.put(int.class, Integer.class);
 194                 primitiveMap.put(long.class, Long.class);
 195                 primitiveMap.put(float.class, Float.class);
 196                 primitiveMap.put(double.class, Double.class);
 197         }
 198 
 199         protected static File getResultDir() {
 200                 if (System.getProperty("results.dir") != null) {
 201                         return new File(System.getProperty("results.dir"));
 202                 } else {
 203                         return new File(System.getProperty("user.dir"));
 204                 }
 205         }
 206 
 207         /**
 208          * Gets the main shell of Mission Control
 209          *
 210          * @return the main shell of Mission Control
 211          */
 212         protected static Wrap<? extends Shell> getShell() {
 213                 return Shells.SHELLS.lookup(Shell.class, new ByTextShell<>("JDK Mission Control")).wrap();
 214         }
 215 
 216         /**
 217          * Gets a shell by text
 218          *
 219          * @param text
 220          *            the text string to lookup the shell with
 221          * @return the associated shell
 222          */
 223         protected static Wrap<? extends Shell> getShellByText(String text) {
 224                 return Shells.SHELLS.lookup(Shell.class, new ByTextShell<>(text)).wrap();
 225         }
 226 
 227         /**
 228          * Tries to set focus on Mission Control
 229          */
 230         public static void focusMc() {
 231                 getShell().as(Focusable.class).focuser().focus();
 232         }
 233 
 234         /**
 235          * Checks if supplied control is identical to our control
 236          *
 237          * @param otherControl
 238          *            the control shall be compared with this control
 239          * @return {@code true} if the controls are equal
 240          */
 241         public Boolean controlsAreEqual(Control otherControl) {
 242                 return (otherControl.equals(this.control.getControl()));
 243         }
 244 
 245         /**
 246          * Determines if this control has a specific control as an ancestor or not
 247          *
 248          * @param possibleAncestor
 249          *            the control that should be checked for among the ancestors
 250          * @return {@code true} if the control provided as parameter is an ancestor of this control
 251          */
 252         public Boolean hasAsAncestor(MCJemmyBase possibleAncestor) {
 253                 final Control control = this.control.getControl();
 254                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 255                         @Override
 256                         public void run() {
 257                                 Boolean parentFound = false;
 258                                 try {
 259                                         Composite ancestor = control.getParent();
 260                                         while (!(ancestor instanceof Shell)) {
 261                                                 if (possibleAncestor.controlsAreEqual(ancestor)) {
 262                                                         parentFound = true;
 263                                                         break;
 264                                                 }
 265                                                 ancestor = ancestor.getParent();
 266                                         }
 267                                 } catch (SWTException e) {
 268                                         suppressWidgetDisposedException(e);
 269                                 }
 270                                 setOutput(parentFound);
 271                         }
 272                 };
 273                 Display.getDefault().syncExec(fetcher);
 274                 return fetcher.getOutput();
 275         }
 276 
 277         /**
 278          * Does a lookup waiting for the editor with the supplied title to show up before returning
 279          *
 280          * @param title
 281          *            the title of the editor
 282          * @return {@code true} if the editor was found, otherwise {@code false}
 283          */
 284         public static boolean waitForEditor(String title) {
 285                 return waitForEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title);
 286         }
 287 
 288         /**
 289          * Search for an editor with a time limit on retries
 290          *
 291          * @param maxWait
 292          *            the max number of milliseconds to repeat the lookup of the editor (unless found).
 293          * @param title
 294          *            the title of the editor to find (exact match)
 295          * @return {@code true} if the editor was found, otherwise {@code false}
 296          */
 297         public static boolean waitForEditor(long maxWait, String title) {
 298                 return waitForEditor(title, StringComparePolicy.EXACT, maxWait);
 299         }
 300 
 301         /**
 302          * Search for an editor with a retry count
 303          *
 304          * @param title
 305          *            the title of the editor to find (exact match)
 306          * @param maxRetries
 307          *            The max number of times to retry the lookup of the editor. Each retry might take a
 308          *            considerable amount of time so use this wisely.
 309          * @return {@code true} if the editor was found, otherwise {@code false}
 310          */
 311         public static boolean waitForEditor(String title, int maxRetries) {
 312                 return waitForEditor(title, StringComparePolicy.EXACT, maxRetries);
 313         }
 314 
 315         /**
 316          * Search for an editor with a retry count
 317          *
 318          * @param title
 319          *            the title of the editor to find
 320          * @param policy
 321          *            a {@link StringComparePolicy} used for matching the title text
 322          * @param maxRetries
 323          *            The max number of times to retry the lookup of the editor. Each retry might take a
 324          *            considerable amount of time so use this wisely.
 325          * @return {@code true} if the editor was found, otherwise {@code false}
 326          */
 327         public static boolean waitForEditor(String title, StringComparePolicy policy, int maxRetries) {
 328                 boolean found = false;
 329                 while (!found && maxRetries > 0) {
 330                         found = isEditorLoadComplete(getEditorLookup(title, policy));
 331                         if (!found) {
 332                                 sleep(LOOKUP_SLEEP_TIME_MS);
 333                         }
 334                         maxRetries--;
 335                 }
 336                 return found;
 337         }
 338 
 339         /**
 340          * Search for an editor with a time limit on retries
 341          *
 342          * @param title
 343          *            the title of the editor to find
 344          * @param policy
 345          *            a {@link StringComparePolicy} used for matching the title with the title text
 346          * @param maxWait
 347          *            the max number of milliseconds to repeat the lookup of the editor (unless found)
 348          * @return {@code true} if the editor was found, otherwise {@code false}
 349          */
 350         public static boolean waitForEditor(String title, StringComparePolicy policy, long maxWait) {
 351                 boolean found = false;
 352                 long maxTimeStamp = System.currentTimeMillis() + maxWait;
 353                 while (!found && System.currentTimeMillis() < maxTimeStamp) {
 354                         found = isEditorLoadComplete(getEditorLookup(title, policy));
 355                         if (!found) {
 356                                 sleep(LOOKUP_SLEEP_TIME_MS);
 357                         }
 358                 }
 359                 return found;
 360         }
 361 
 362         @SuppressWarnings("unchecked")
 363         private static Lookup<CTabFolder> getEditorLookup(String title, StringComparePolicy policy) {
 364                 return getShell().as(Parent.class, CTabFolder.class).lookup(CTabFolder.class,
 365                                 new ByItemLookup<CTabFolder>(title, policy));
 366         }
 367 
 368         private static boolean isEditorLoadComplete(Lookup<CTabFolder> editorLookup) {
 369                 boolean hasProgressIndicator = false;
 370                 int numOfEditors = editorLookup.size();
 371                 if (numOfEditors == 1) {
 372                         MCTabFolder tf = new MCTabFolder(editorLookup.wrap(), getShell());
 373                         List<MCProgressIndicator> jpis = MCProgressIndicator.getVisible(getShell());
 374                         for (MCProgressIndicator jpi : jpis) {
 375                                 try {
 376                                         if (jpi.hasAsAncestor(tf)) {
 377                                                 hasProgressIndicator = true;
 378                                                 break;
 379                                         }
 380                                 } catch (SWTException e) {
 381                                         suppressWidgetDisposedException(e);
 382                                 }
 383                         }
 384                 }
 385                 return numOfEditors == 1 && !hasProgressIndicator;
 386         }
 387 
 388         /**
 389          * Search for an editor with a retry count
 390          *
 391          * @param title
 392          *            the title of the editor to find (substring match)
 393          * @param maxRetries
 394          *            The max number of times to retry the lookup of the editor. Each retry might take a
 395          *            considerable amount of time so use this wisely.
 396          * @return {@code true} if found, otherwise {@code false}
 397          */
 398         public static boolean waitForSubstringMatchedEditor(String title, int maxRetries) {
 399                 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxRetries);
 400         }
 401 
 402         /**
 403          * Search for an editor with a time limit on retries
 404          *
 405          * @param maxWait
 406          *            the max number of milliseconds to repeat the lookup of the editor.
 407          * @param title
 408          *            the title of the editor to find (substring match)
 409          * @return {@code true} if found, otherwise {@code false}
 410          */
 411         public static boolean waitForSubstringMatchedEditor(long maxWait, String title) {
 412                 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxWait);
 413         }
 414 
 415         /**
 416          * Search for an editor with a time limit (defined by {@code EDITOR_LOAD_WAIT_TIMEOUT_MS}) on
 417          * retries
 418          *
 419          * @param title
 420          *            the title of the editor to find (substring match)
 421          * @return {@code true} if found, otherwise {@code false}
 422          */
 423         public static boolean waitForSubstringMatchedEditor(String title) {
 424                 return waitForSubstringMatchedEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title);
 425         }
 426 
 427         /**
 428          * Convenience method to put test execution on hold by putting the thread to sleep
 429          *
 430          * @param millis
 431          *            the time to sleep in milliseconds.
 432          * @return The actual time slept in milliseconds. Should be equal to millis unless an
 433          *         {@link InterruptedException} was thrown.
 434          */
 435         public static long sleep(long millis) {
 436                 long time = System.currentTimeMillis();
 437                 long slept = millis;
 438                 try {
 439                         Thread.sleep(millis);
 440                 } catch (InterruptedException e) {
 441                         slept = System.currentTimeMillis() - time;
 442                 }
 443                 return slept;
 444         }
 445 
 446         /**
 447          * This method finds a widget that may or may not exist by means of doing a lookup and then
 448          * walking through the results to see if there is a match. The difference to lookups with
 449          * specific criterion is that this doesn't timeout and throw an exception if not found. Instead
 450          * null is returned. Note: This method expects the property to hold a String or a list of
 451          * Strings
 452          *
 453          * @param clazz
 454          *            the class of the widget to look for
 455          * @param text
 456          *            the string value used for matching
 457          * @param property
 458          *            the property to match with the supplied text parameter (has to return a String or
 459          *            List of String to be properly matched)
 460          * @param policy
 461          *            a {@link StringComparePolicy} to use when matching the widget
 462          * @return the control searched for or {@code null} if not found
 463          */
 464         @SuppressWarnings("unchecked")
 465         protected static <T> Wrap<? extends T> findWrap(
 466                 Class<T> clazz, String text, String property, StringComparePolicy policy) {
 467                 Lookup<? extends T> lookup = getShell().as(Parent.class, Control.class).lookup(clazz);
 468                 Wrap<? extends T> result = null;
 469                 for (int i = 0; i < lookup.size() && result == null; i++) {
 470                         Wrap<? extends T> wrap = lookup.wrap(i);
 471                         Object object = wrap.getProperty(property);
 472                         if (object instanceof List) {
 473                                 List<String> values = new ArrayList<String>().getClass().cast(object);
 474                                 for (String value : values) {
 475                                         if (policy.compare(value, text)) {
 476                                                 result = wrap;
 477                                                 break;
 478                                         }
 479                                 }
 480                         } else {
 481                                 if (policy.compare(String.class.cast(object), text)) {
 482                                         result = wrap;
 483                                 }
 484                         }
 485                 }
 486                 return result;
 487         }
 488 
 489         /**
 490          * Convenience method to find out if a widget is disposed
 491          *
 492          * @param widgetWrap
 493          *            the wrapped widget to check
 494          * @return {@code true} if disposed (or disposing), otherwise {@code false}
 495          */
 496         protected static boolean isDisposed(final Wrap<? extends Widget> widgetWrap) {
 497                 return isDisposed(widgetWrap.getControl());
 498         }
 499 
 500         /**
 501          * Convenience method to find out if a widget is disposed
 502          *
 503          * @param widget
 504          *            the widget to check
 505          * @return {@code true} if disposed (or disposing), otherwise {@code false}
 506          */
 507         protected static boolean isDisposed(final Widget widget) {
 508                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 509                         @Override
 510                         public void run() {
 511                                 if (widget != null) {
 512                                         setOutput(widget.isDisposed());
 513                                 } else {
 514                                         setOutput(true);
 515                                 }
 516                         }
 517                 };
 518                 Display.getDefault().syncExec(fetcher);
 519                 return fetcher.getOutput();
 520         }
 521 
 522         /**
 523          * Convenience method to find out if the widget of this wrapper is disposed
 524          *
 525          * @return {@code true} if disposed (or disposing), otherwise {@code false}
 526          */
 527         public boolean isDisposed() {
 528                 return isDisposed(control);
 529         }
 530 
 531         /**
 532          * Convenience method to find out if this control is enabled
 533          *
 534          * @return {@code true} if enabled, otherwise {@code false}
 535          */
 536         public boolean isEnabled() {
 537                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 538                         @Override
 539                         public void run() {
 540                                 setOutput(control.getControl().isEnabled());
 541                         }
 542                 };
 543                 Display.getDefault().syncExec(fetcher);
 544                 return fetcher.getOutput();
 545         }
 546 
 547         /**
 548          * Opens the context menu by right-clicking at a provided point
 549          *
 550          * @param p
 551          *            origin point of context (right-click)
 552          */
 553         private void openContextMenuAtPoint(Point p) {
 554                 Display.getDefault().syncExec(() -> {
 555                         control.mouse().click(1, p, MouseButtons.BUTTON3);
 556                 });
 557         }
 558 
 559         /**
 560          * Checks the isEnabled value for a menu item
 561          *
 562          * @param p
 563          *            the point on the screen at which to open the context menu
 564          * @param menuItemText
 565          *            the menu item of interest
 566          * @return the isEnabled value for the menu item of interest
 567          */
 568         public boolean isContextMenuItemEnabled(Point p, String menuItemText) {
 569                 openContextMenuAtPoint(p);
 570                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 571                         @Override
 572                         public void run() {
 573                                 Menu menu = control.getControl().getMenu();
 574                                 for (MenuItem item : menu.getItems()) {
 575                                         if(menuItemText.equals(item.getText())) {
 576                                                 setOutput(item.isEnabled());
 577                                                 break;
 578                                         }
 579                                 }
 580                         }
 581                 };
 582                 Display.getDefault().syncExec(fetcher);
 583                 return (fetcher.getOutput() == null) ? false : fetcher.getOutput();
 584         }
 585 
 586         /**
 587          * Convenience method to find out if a control is visible
 588          *
 589          * @param controlWrap
 590          *            the control to check
 591          * @return {@code true} if visible, otherwise {@code false}
 592          */
 593         protected static boolean isVisible(final Wrap<? extends Control> controlWrap) {
 594                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 595                         @Override
 596                         public void run() {
 597                                 try {
 598                                         setOutput(controlWrap.getControl().isVisible());
 599                                 } catch (SWTException e) {
 600                                         suppressWidgetDisposedException(e);
 601                                 }
 602                         }
 603                 };
 604                 Display.getDefault().syncExec(fetcher);
 605                 return (fetcher.getOutput() == null) ? false : fetcher.getOutput();
 606         }
 607 
 608         /**
 609          * Convenience method to find out if this control is visible
 610          *
 611          * @return {@code true} if visible, otherwise {@code false}
 612          */
 613         public boolean isVisible() {
 614                 return isVisible(control);
 615         }
 616 
 617         /**
 618          * Iterates through the wraps found by a lookup and returns a list of all visible wraps.
 619          *
 620          * @param lookup
 621          *            the {@link Lookup} to search through
 622          * @return a list of all visible wraps found by the lookup
 623          */
 624         protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup) {
 625                 return getVisible(lookup, true);
 626         }
 627 
 628         /**
 629          * Iterates through the wraps found by a lookup and returns a list of all visible wraps.
 630          *
 631          * @param lookup
 632          *            the {@link Lookup} to search through
 633          * @param waitForIdle
 634          *            {@code true} if "UI-update queue" should be empty before looking for controls
 635          * @return a list of all visible wraps found by the {@link Lookup}
 636          */
 637         protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup, boolean waitForIdle) {
 638                 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS);
 639         }
 640 
 641         /**
 642          * Iterates through the wraps found by a lookup and returns a list of all visible wraps.
 643          *
 644          * @param lookup
 645          *            the {@link Lookup} to search through
 646          * @param waitForIdle
 647          *            {@code true} if "UI-update queue" should be empty before looking for controls
 648          * @param maxWaitMs
 649          *            the timeout in milliseconds before ending the lookup
 650          * @return a list of all visible wraps found by the {@link Lookup}
 651          */
 652         protected static <T extends Control> List<Wrap<? extends T>> getVisible(
 653                 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs) {
 654                 return getVisible(lookup, waitForIdle, maxWaitMs, true);
 655         }
 656 
 657         /**
 658          * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will
 659          * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a
 660          * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length
 661          *
 662          * @param lookup
 663          *            the {@link Lookup} to search through
 664          * @param waitForIdle
 665          *            {@code true} if "UI-update queue" should be empty before looking for controls
 666          * @param maxWaitMs
 667          *            the timeout in milliseconds before ending the lookup
 668          * @param assertEmpty
 669          *            if {@code true} will assert the the resulting list isn't empty assertion will be
 670          *            done
 671          * @return a list of all visible wraps found by the {@link Lookup}
 672          */
 673         @SuppressWarnings("unchecked")
 674         protected static <T extends Control> List<Wrap<? extends T>> getVisible(
 675                 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs, boolean assertEmpty) {
 676                 if (waitForIdle) {
 677                         waitForIdle();
 678                 }
 679                 List<Wrap<? extends T>> list = new ArrayList<>();
 680                 long lookupEndTime = System.currentTimeMillis() + maxWaitMs;
 681                 do {
 682                         for (int i = 0; i < lookup.size(); i++) {
 683                                 Wrap<T> wrap = (Wrap<T>) lookup.wrap(i);
 684                                 if (isVisible(wrap)) {
 685                                         list.add(wrap);
 686                                 }
 687                         }
 688                         if (list.size() == 0) {
 689                                 sleep(LOOKUP_SLEEP_TIME_MS);
 690                         }
 691                 } while (list.size() == 0 && lookupEndTime > System.currentTimeMillis());
 692                 if (assertEmpty) {
 693                         Assert.assertTrue("No visible controls found", list.size() > 0);
 694                 }
 695                 return list;
 696         }
 697 
 698         /**
 699          * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will
 700          * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a
 701          * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length
 702          *
 703          * @param lookup
 704          *            the {@link Lookup} to search through
 705          * @param waitForIdle
 706          *            {@code true} if "UI-update queue" should empty before looking for controls
 707          * @param assertEmpty
 708          *            if {@code true} will assert the the resulting list isn't empty. Otherwise no
 709          *            assertion will be done
 710          * @return a list of all visible wraps found by the {@link Lookup}
 711          */
 712         protected static <T extends Control> List<Wrap<? extends T>> getVisible(
 713                 Lookup<T> lookup, boolean waitForIdle, boolean assertEmpty) {
 714                 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS, assertEmpty);
 715         }
 716 
 717         /**
 718          * Convenience method to find out if Mission Control UI is busy
 719          *
 720          * @return {@code true} if busy
 721          */
 722         public static boolean isBusy() {
 723                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
 724                         @Override
 725                         public void run() {
 726                                 setOutput(!Job.getJobManager().isIdle());
 727                         }
 728                 };
 729                 Display.getDefault().syncExec(fetcher);
 730                 return fetcher.getOutput();
 731         }
 732 
 733         /**
 734          * Inspects a Widget (graphically) for updates by comparing two snapshots of it
 735          *
 736          * @param widget
 737          *            the widget to inspect for change (graphically)
 738          * @param waitTimeMillis
 739          *            the time to wait between the snapshots of the Widget
 740          * @return {@code true} if the Widget has changed
 741          */
 742         public boolean isWidgetUpdating(Wrap<? extends Widget> widget, int waitTimeMillis) {
 743                 sleep(waitTimeMillis);
 744                 org.jemmy.image.Image firstImage = widget.getScreenImage();
 745                 sleep(waitTimeMillis);
 746                 org.jemmy.image.Image secondImage = widget.getScreenImage();
 747 
 748                 // Set the Comparator to be strict, first get and save the current
 749                 // Comparator for later restoral
 750                 ImageComparator current = AWTImage.getComparator();
 751                 AWTImage.setComparator(new StrictImageComparator());
 752                 // diff will be null if the Images are identical
 753                 org.jemmy.image.Image diff = secondImage.compareTo(firstImage);
 754                 AWTImage.setComparator(current);
 755 
 756                 return (diff == null) ? false : true;
 757         }
 758 
 759         /**
 760          * Inspects this control for updates by comparing two snapshots of it
 761          *
 762          * @param waitTimeMillis
 763          *            the time to wait between the snapshots of the Widget
 764          * @return {@code true} if the Widget has changed
 765          */
 766         public boolean isWidgetUpdating(int waitTimeMillis) {
 767                 return isWidgetUpdating(control, waitTimeMillis);
 768         }
 769 
 770         /**
 771          * Waits for background jobs to finish before executing a script line. Since background jobs
 772          * post to the UI-thread asynchronously we must ensure they get a chance to run, so we we spin
 773          * asynchronously in a count down process. IDLE_LOOP_COUNT specifies the number iterations
 774          * without the job manager being activated as being enough to make sure we are in a calm/idle
 775          * state.
 776          */
 777         public static void waitForIdle() {
 778                 int counter = IDLE_LOOP_COUNT;
 779                 long startTimestamp = System.currentTimeMillis();
 780                 while (counter > 0) {
 781                         if ((System.currentTimeMillis() - startTimestamp) > IDLE_LOOP_TIMEOUT_MS) {
 782                                 break;
 783                         }
 784                         if (isBusy()) {
 785                                 counter = IDLE_LOOP_COUNT;
 786                         } else {
 787                                 counter--;
 788                         }
 789                         sleep(IDLE_LOOP_TIME_STEP);
 790                 }
 791         }
 792 
 793         /**
 794          * Saves a picture of Mission Control's shell. If Mission Control is behind other applications,
 795          * the rectangle will show what's really on top.
 796          *
 797          * @param imageName
 798          *            the name of the saved file, ".png" will be added at the end
 799          */
 800         public static void saveMcImage(String imageName) {
 801                 Image pic = getMcImage();
 802                 pic.save(imageName + ".png");
 803         }
 804 
 805         /**
 806          * Saves a picture of Mission Control's shell. If Mission Control is behind other applications,
 807          * the rectangle will show what's really on top. The image will be given a name which is unique
 808          * for the execution.
 809          */
 810         public static void saveMcImage() {
 811                 unnamedImageCounter++;
 812                 saveMcImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png");
 813         }
 814 
 815         /**
 816          * Returns an image of the Mission Control shell
 817          *
 818          * @return an {@link Image}
 819          */
 820         public static Image getMcImage() {
 821                 return getShell().getScreenImage();
 822         }
 823 
 824         /**
 825          * Returns an image of this control
 826          *
 827          * @return an {@link Image} of this control
 828          */
 829         public Image getThisImage() {
 830                 return control.getScreenImage();
 831         }
 832 
 833         /**
 834          * Saves a picture of this control. If the control is behind other applications or Mission
 835          * Control controls, the rectangle will show what's really on top.
 836          *
 837          * @param imageName
 838          *            the name of the saved file. ".png" will be added at the end
 839          */
 840         public void saveThisImage(String imageName) {
 841                 Image pic = getThisImage();
 842                 saveImage(imageName + ".png", pic);
 843         }
 844 
 845         /**
 846          * Saves a picture of this control. If the control is behind other applications or Mission
 847          * Control controls, the rectangle will show what's really on top. The image will be given a
 848          * name which is unique for the execution.
 849          */
 850         public void saveThisImage() {
 851                 unnamedImageCounter++;
 852                 saveThisImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png");
 853         }
 854 
 855         /**
 856          * Saves the image with the specified name
 857          *
 858          * @param fileName
 859          *            the name of the image file
 860          * @param image
 861          *            the image
 862          */
 863         public static void saveImage(String fileName, Image image) {
 864                 image.save(fileName);
 865         }
 866 
 867         /**
 868          * Focuses on a specific section to use instead of the main shell.
 869          *
 870          * @param name
 871          *            the name of the section to focus on
 872          */
 873         @SuppressWarnings("unchecked")
 874         public static void focusSectionByName(String name) {
 875                 focusedSection = (Wrap<? extends Shell>) getVisible(
 876                                 getShell().as(Parent.class, Control.class).lookup(new ByName<Shell>(name))).get(0);
 877         }
 878 
 879         /**
 880          * Focuses on a specific section to use instead of the main shell.
 881          *
 882          * @param title
 883          *            the title of the section to focus on
 884          */
 885         @SuppressWarnings("unchecked")
 886         public static void focusSectionByTitle(String title) {
 887                 focusedSection = (Wrap<? extends Shell>) getVisible(
 888                                 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title))).get(0);
 889         }
 890 
 891         /**
 892          * Focuses on a specific section to use instead of the main shell.
 893          *
 894          * @param title
 895          *            the title of the section to focus on
 896          * @param waitForIdle
 897          *            if {@code true} will first wait for the UI to be idle before setting focus
 898          */
 899         @SuppressWarnings("unchecked")
 900         public static void focusSectionByTitle(String title, boolean waitForIdle) {
 901                 focusedSection = (Wrap<? extends Shell>) getVisible(
 902                                 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title)), waitForIdle)
 903                                                 .get(0);
 904         }
 905 
 906         /**
 907          * House keeping: clearing the focusedSection reference so that it won't be used in further
 908          * lookups
 909          */
 910         public static void clearFocus() {
 911                 focusedSection = null;
 912         }
 913 
 914         /**
 915          * @return a {@link List} of {@link MCTable} either in the currently focused section or
 916          *         globally in the shell
 917          */
 918         public static List<MCTable> getTables() {
 919                 if (focusedSection != null) {
 920                         return MCTable.getAll(focusedSection);
 921                 } else {
 922                         return MCTable.getAll(getShell());
 923                 }
 924         }
 925 
 926         /**
 927          * Get all tables in the focused section (if set), otherwise from the Mission Control main shell
 928          * 
 929          * @param waitForIdle
 930          *            {@code true} if "UI-update queue" should be empty before looking for controls
 931          * @return a {@link List} of {@link MCTable} either in the currently focused section or
 932          *         globally in the shell
 933          */
 934         public static List<MCTable> getTables(boolean waitForIdle) {
 935                 if (focusedSection != null) {
 936                         return MCTable.getAll(focusedSection, waitForIdle);
 937                 } else {
 938                         return MCTable.getAll(getShell(), waitForIdle);
 939                 }
 940         }
 941 
 942         /**
 943          * Runs the method and returns the result if a matching method is found. If not, null will
 944          * always be returned. Note that the method could return null as well if the operation succeeds
 945          * so this needs to be handled in a proper way by the caller.
 946          *
 947          * @param returnType
 948          *            the type of the returned object
 949          * @param object
 950          *            the object on which to run the method
 951          * @param methodName
 952          *            the name of the method to run
 953          * @param params
 954          *            an object array of parameters for the method. null if no parameters
 955          * @return The result of running the method. null if no matching method is found (name,
 956          *         parameters and return type). Note that the method could return null as well if the
 957          *         operation succeeds so this needs to be handled in a proper way by the caller.
 958          */
 959         public static <T> T runMethod(Class<T> returnType, Object object, String methodName, Object ... params) {
 960                 T result = null;
 961                 try {
 962                         Class<?>[] paramTypes = null;
 963 
 964                         if (params != null) {
 965                                 paramTypes = new Class<?>[params.length];
 966                                 for (int i = 0; i < params.length; i++) {
 967                                         paramTypes[i] = params[i].getClass();
 968                                 }
 969                         }
 970 
 971                         Method method = getCompatibleMethod(object, methodName, paramTypes);
 972                         Class<?> methodReturnType = method.getReturnType();
 973 
 974                         if (methodReturnType.isPrimitive()) {
 975                                 methodReturnType = primitiveMap.get(methodReturnType);
 976                         }
 977 
 978                         if (returnType.equals(Void.class) || returnType.isAssignableFrom(methodReturnType)) {
 979                                 result = returnType.cast(method.invoke(object, params));
 980                         }
 981                 } catch (Exception e) {
 982                         // do nothing, just return null
 983                 }
 984                 return result;
 985         }
 986 
 987         private static Method getCompatibleMethod(Object object, String methodName, Class<?> ... paramTypes)
 988                         throws SecurityException, NoSuchMethodException {
 989                 if (paramTypes != null) {
 990                         Method[] methods = object.getClass().getMethods();
 991                         for (Method method : methods) {
 992                                 Method m = method;
 993 
 994                                 if (!m.getName().equals(methodName)) {
 995                                         continue;
 996                                 }
 997 
 998                                 Class<?>[] actualTypes = m.getParameterTypes();
 999                                 if (actualTypes.length != paramTypes.length) {
1000                                         continue;
1001                                 }
1002 
1003                                 boolean found = true;
1004                                 for (int j = 0; j < actualTypes.length; j++) {
1005                                         if (!actualTypes[j].isAssignableFrom(paramTypes[j])) {
1006                                                 if (actualTypes[j].isPrimitive()) {
1007                                                         found = primitiveMap.get(actualTypes[j]).equals(paramTypes[j]);
1008                                                 } else if (paramTypes[j].isPrimitive()) {
1009                                                         found = primitiveMap.get(paramTypes[j]).equals(actualTypes[j]);
1010                                                 } else {
1011                                                         found = false;
1012                                                 }
1013                                         }
1014 
1015                                         if (!found) {
1016                                                 break;
1017                                         }
1018                                 }
1019 
1020                                 if (found) {
1021                                         return m;
1022                                 }
1023                         }
1024 
1025                         throw new NoSuchMethodException("Could not find method " + methodName + " with parameters " + paramTypes);
1026                 } else {
1027                         return object.getClass().getMethod(methodName);
1028                 }
1029         }
1030 
1031         /**
1032          * Returns the name of this control
1033          *
1034          * @return the name of this control. Null if no name has been set
1035          */
1036         public String getName() {
1037                 return control.getProperty(String.class, Wrap.NAME_PROP_NAME);
1038         }
1039 
1040         /**
1041          * @return the text of this control
1042          */
1043         public String getText() {
1044                 return control.getProperty(String.class, Wrap.TEXT_PROP_NAME);
1045         }
1046 
1047         /**
1048          * Clicks this control (once)
1049          */
1050         public void click() {
1051                 click(1);
1052         }
1053 
1054         /**
1055          * Clicks this control {@code times} times
1056          *
1057          * @param times
1058          *            the number of times to mouse click this control
1059          */
1060         public void click(int times) {
1061                 control.mouse().click(times);
1062         }
1063 
1064         /**
1065          * Default implementation of a copy to clipboard for any wrapper
1066          */
1067         public void copyToClipboard() {
1068                 control.as(Focusable.class).focuser().focus();
1069                 sleep(BETWEEN_KEYSTROKES_SLEEP);
1070                 control.keyboard().pushKey(KeyboardButtons.A, SHORTCUT_MODIFIER);
1071                 sleep(BETWEEN_KEYSTROKES_SLEEP);
1072                 control.keyboard().pushKey(KeyboardButtons.C, SHORTCUT_MODIFIER);
1073                 sleep(BETWEEN_KEYSTROKES_SLEEP);
1074         }
1075 
1076         /**
1077          * Gets the system clipboard contents as a string
1078          *
1079          * @return the clipboard contents
1080          */
1081         public static String getStringFromClipboard() {
1082                 return getFromClipBoard(String.class);
1083         }
1084 
1085         /**
1086          * Finds out if content assist is present. The assumption is that content assist is present if
1087          * there is a Shell with a single Composite that in turn has a single child of type Table
1088          *
1089          * @return {@code true} if content assist is present, otherwise {@code false}
1090          */
1091         public static boolean isContentAssistPresent() {
1092                 boolean found = true;
1093                 long endTime = System.currentTimeMillis() + 1000;
1094                 while (found && System.currentTimeMillis() < endTime) {
1095                         FetcherWithInput<List<Wrap<? extends Shell>>, Boolean> fetcher = new FetcherWithInput<List<Wrap<? extends Shell>>, Boolean>(
1096                                         getVisible(Shells.SHELLS.lookup(Shell.class), false, false)) {
1097 
1098                                 @Override
1099                                 public void run() {
1100                                         boolean isPresent = false;
1101                                         for (Wrap<? extends Shell> shellWrap : getInput()) {
1102                                                 Control[] shellChildren = shellWrap.getControl().getChildren();
1103                                                 if (shellChildren.length == 1 && shellChildren[0] instanceof Composite) {
1104                                                         Control[] compositeChildren = Composite.class.cast(shellChildren[0]).getChildren();
1105                                                         if (compositeChildren.length == 1 && compositeChildren[0] instanceof Table) {
1106                                                                 isPresent = true;
1107                                                                 break;
1108                                                         }
1109                                                 }
1110                                         }
1111                                         setOutput(isPresent);
1112                                 }
1113                         };
1114                         Display.getDefault().syncExec(fetcher);
1115                         found = fetcher.getOutput();
1116                         if (found) {
1117                                 sleep(100);
1118                         }
1119                 }
1120                 return found;
1121         }
1122 
1123         private static <T> T getFromClipBoard(Class<T> returnType) {
1124                 Transferable transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
1125                 DataFlavor thisFlavor = new DataFlavor(returnType, returnType.getName());
1126                 try {
1127                         if (transferable != null && transferable.isDataFlavorSupported(thisFlavor)) {
1128                                 return returnType.cast(transferable.getTransferData(thisFlavor));
1129                         }
1130                 } catch (UnsupportedFlavorException e) {
1131                         System.out.println("Clipboard content flavor is not supported " + e.getMessage());
1132                 } catch (IOException e) {
1133                         System.out.println("Clipboard content could not be retrieved " + e.getMessage());
1134                 }
1135                 return null;
1136         }
1137 
1138         private static void suppressWidgetDisposedException(SWTException e) {
1139                 if (!e.getMessage().contains("Widget is disposed")) {
1140                         // Unexpected exception. Re-throw it
1141                         throw e;
1142                 }
1143         }
1144 
1145         /**
1146          * Setting focus on this control programmatically (if not already focused).
1147          */
1148         protected void ensureFocus() {
1149                 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() {
1150                         @Override
1151                         public void run() {
1152                                 if (!control.getControl().isFocusControl()) {
1153                                         control.getControl().setFocus();
1154                                 }
1155                                 setOutput(control.getControl().isFocusControl());
1156                         }
1157                 };
1158                 Display.getDefault().syncExec(fetcher);
1159                 if (!fetcher.getOutput()) {
1160                         // fallback if the programmatic focusing didn't work
1161                         control.as(Focusable.class).focuser().focus();
1162                 }
1163         }
1164 }