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