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