Package org.fest.swing.core

Source Code of org.fest.swing.core.BasicRobot

/*
* Created on Sep 29, 2006
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright @2006-2013 the original author or authors.
*/
package org.fest.swing.core;

import static java.awt.event.InputEvent.BUTTON1_MASK;
import static java.awt.event.InputEvent.BUTTON2_MASK;
import static java.awt.event.InputEvent.BUTTON3_MASK;
import static java.awt.event.KeyEvent.CHAR_UNDEFINED;
import static java.awt.event.KeyEvent.KEY_TYPED;
import static java.awt.event.KeyEvent.VK_UNDEFINED;
import static java.awt.event.WindowEvent.WINDOW_CLOSING;
import static java.lang.System.currentTimeMillis;
import static javax.swing.SwingUtilities.getWindowAncestor;
import static javax.swing.SwingUtilities.isEventDispatchThread;
import static org.fest.swing.awt.AWT.centerOf;
import static org.fest.swing.awt.AWT.visibleCenterOf;
import static org.fest.swing.core.ActivateWindowTask.activateWindow;
import static org.fest.swing.core.ComponentIsFocusableQuery.isFocusable;
import static org.fest.swing.core.ComponentRequestFocusTask.giveFocusTo;
import static org.fest.swing.core.FocusOwnerFinder.focusOwner;
import static org.fest.swing.core.FocusOwnerFinder.inEdtFocusOwner;
import static org.fest.swing.core.InputModifiers.unify;
import static org.fest.swing.core.MouseButton.LEFT_BUTTON;
import static org.fest.swing.core.MouseButton.RIGHT_BUTTON;
import static org.fest.swing.core.Scrolling.scrollToVisible;
import static org.fest.swing.core.WindowAncestorFinder.windowAncestorOf;
import static org.fest.swing.edt.GuiActionRunner.execute;
import static org.fest.swing.exception.ActionFailedException.actionFailure;
import static org.fest.swing.format.Formatting.format;
import static org.fest.swing.format.Formatting.inEdtFormat;
import static org.fest.swing.hierarchy.NewHierarchy.ignoreExistingComponents;
import static org.fest.swing.keystroke.KeyStrokeMap.keyStrokeFor;
import static org.fest.swing.query.ComponentShowingQuery.isShowing;
import static org.fest.swing.timing.Pause.pause;
import static org.fest.swing.util.Modifiers.keysFor;
import static org.fest.swing.util.Modifiers.updateModifierWithKeyCode;
import static org.fest.swing.util.TimeoutWatch.startWatchWithTimeoutOf;
import static org.fest.util.Lists.newArrayList;
import static org.fest.util.Preconditions.checkNotNull;
import static org.fest.util.Strings.concat;

import java.applet.Applet;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.InvocationEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;

import org.fest.swing.annotation.RunsInCurrentThread;
import org.fest.swing.annotation.RunsInEDT;
import org.fest.swing.edt.GuiQuery;
import org.fest.swing.edt.GuiTask;
import org.fest.swing.exception.ActionFailedException;
import org.fest.swing.exception.ComponentLookupException;
import org.fest.swing.exception.WaitTimedOutError;
import org.fest.swing.hierarchy.ComponentHierarchy;
import org.fest.swing.hierarchy.ExistingHierarchy;
import org.fest.swing.input.InputState;
import org.fest.swing.lock.ScreenLock;
import org.fest.swing.monitor.WindowMonitor;
import org.fest.swing.util.Pair;
import org.fest.swing.util.TimeoutWatch;
import org.fest.swing.util.ToolkitProvider;
import org.fest.util.VisibleForTesting;

/**
* Default implementation of {@link Robot}.
*
* @author Alex Ruiz
* @author Yvonne Wang
*
* @see Robot
*/
public class BasicRobot implements Robot {
  private static final int POPUP_DELAY = 10000;
  private static final int POPUP_TIMEOUT = 5000;
  private static final int WINDOW_DELAY = 20000;

  private static final ComponentMatcher POPUP_MATCHER = new TypeMatcher(JPopupMenu.class, true);

  @GuardedBy("this")
  private volatile boolean active;

  private static final Runnable EMPTY_RUNNABLE = new Runnable() {
    @Override
    public void run() {}
  };

  private static final int BUTTON_MASK = BUTTON1_MASK | BUTTON2_MASK | BUTTON3_MASK;

  private static Toolkit toolkit = ToolkitProvider.instance().defaultToolkit();
  private static WindowMonitor windowMonitor = WindowMonitor.instance();
  private static InputState inputState = new InputState(toolkit);

  private final ComponentHierarchy hierarchy;
  private final Object screenLockOwner;
  private final ComponentFinder finder;
  private final Settings settings;
  private final AWTEventPoster eventPoster;
  private final InputEventGenerator eventGenerator;
  private final UnexpectedJOptionPaneFinder unexpectedJOptionPaneFinder;

  /**
   * Creates a new {@link Robot} with a new AWT hierarchy. The created {@code Robot} will not be able to access any
   * AWT and Swing {@code Component}s that were created before it.
   *
   * @return the created {@code Robot}.
   */
  public static @Nonnull Robot robotWithNewAwtHierarchy() {
    Object screenLockOwner = acquireScreenLock();
    return new BasicRobot(screenLockOwner, ignoreExistingComponents());
  }

  public static @Nonnull Robot robotWithNewAwtHierarchyWithoutScreenLock() {
    return new BasicRobot(null, ignoreExistingComponents());
  }

  /**
   * Creates a new {@link Robot} that has access to all the AWT and Swing {@code Component}s in the AWT hierarchy.
   *
   * @return the created {@code Robot}.
   */
  public static @Nonnull Robot robotWithCurrentAwtHierarchy() {
    Object screenLockOwner = acquireScreenLock();
    return new BasicRobot(screenLockOwner, new ExistingHierarchy());
  }

  // TODO document
  public static @Nonnull Robot robotWithCurrentAwtHierarchyWithoutScreenLock() {
    return new BasicRobot(null, new ExistingHierarchy());
  }

  private static @Nonnull Object acquireScreenLock() {
    Object screenLockOwner = new Object();
    ScreenLock.instance().acquire(screenLockOwner);
    return screenLockOwner;
  }

  @VisibleForTesting
  BasicRobot(@Nullable Object screenLockOwner, @Nonnull ComponentHierarchy hierarchy) {
    this.screenLockOwner = screenLockOwner;
    this.hierarchy = hierarchy;
    settings = new Settings();
    eventGenerator = new RobotEventGenerator(settings);
    eventPoster = new AWTEventPoster(toolkit, inputState, windowMonitor, settings);
    finder = new BasicComponentFinder(hierarchy, settings);
    unexpectedJOptionPaneFinder = new UnexpectedJOptionPaneFinder(finder);
    active = true;
  }

  /** {@inheritDoc} */
  @Override
  public @Nonnull ComponentPrinter printer() {
    return finder().printer();
  }

  /** {@inheritDoc} */
  @Override
  public @Nonnull ComponentFinder finder() {
    return finder;
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void showWindow(@Nonnull Window w) {
    showWindow(w, null, true);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void showWindow(@Nonnull Window w, @Nonnull Dimension size) {
    showWindow(w, size, true);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void showWindow(@Nonnull final Window w, @Nullable final Dimension size, final boolean pack) {
    EventQueue.invokeLater(new Runnable() {
      @Override
      public void run() {
        if (pack) {
          packAndEnsureSafePosition(w);
        }
        if (size != null) {
          w.setSize(size);
        }
        w.setVisible(true);
      }
    });
    waitForWindow(w);
  }

  @RunsInCurrentThread
  private void packAndEnsureSafePosition(@Nonnull Window w) {
    w.pack();
    w.setLocation(100, 100);
  }

  @RunsInEDT
  private void waitForWindow(@Nonnull Window w) {
    long start = currentTimeMillis();
    while (!windowMonitor.isWindowReady(w) || !isShowing(w)) {
      long elapsed = currentTimeMillis() - start;
      if (elapsed > WINDOW_DELAY) {
        throw new WaitTimedOutError(concat("Timed out waiting for Window to open (", String.valueOf(elapsed), "ms)"));
      }
      pause();
    }
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void close(@Nonnull Window w) {
    WindowEvent event = new WindowEvent(w, WINDOW_CLOSING);
    // If the window contains an applet, send the event on the applet's queue instead to ensure a shutdown from the
    // applet's context (assists AppletViewer cleanup).
    Component applet = findAppletDescendent(w);
    EventQueue eventQueue = windowMonitor.eventQueueFor(applet != null ? applet : w);
    checkNotNull(eventQueue).postEvent(event);
    waitForIdle();
  }

  /**
   * Returns the {@code Applet} descendant of the given AWT {@code Container}, if any.
   *
   * @param c the given {@code Container}.
   * @return the {@code Applet} descendant of the given AWT {@code Container}, or {@code null} if none is found.
   */
  @RunsInEDT
  private @Nullable Applet findAppletDescendent(@Nonnull Container c) {
    List<Component> found = newArrayList(finder.findAll(c, new TypeMatcher(Applet.class)));
    if (found.size() == 1) {
      return (Applet) found.get(0);
    }
    return null;
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void focusAndWaitForFocusGain(@Nonnull Component c) {
    focus(c, true);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void focus(@Nonnull Component c) {
    focus(c, false);
  }

  @RunsInEDT
  private void focus(@Nonnull Component target, boolean wait) {
    Component currentOwner = inEdtFocusOwner();
    if (currentOwner == target) {
      return;
    }
    FocusMonitor focusMonitor = FocusMonitor.attachTo(target);
    // for pointer focus
    moveMouse(target);
    // Make sure the correct window is in front
    activateWindowOfFocusTarget(target, currentOwner);
    giveFocusTo(target);
    try {
      if (wait) {
        TimeoutWatch watch = startWatchWithTimeoutOf(settings().timeoutToBeVisible());
        while (!focusMonitor.hasFocus()) {
          if (watch.isTimeOut()) {
            throw actionFailure(concat("Focus change to ", format(target), " failed"));
          }
          pause();
        }
      }
    } finally {
      target.removeFocusListener(focusMonitor);
    }
  }

  @RunsInEDT
  private void activateWindowOfFocusTarget(@Nullable Component target, @Nullable Component currentOwner) {
    Pair<Window, Window> windowAncestors = windowAncestorsOf(currentOwner, target);
    Window currentOwnerAncestor = windowAncestors.first;
    Window targetAncestor = windowAncestors.second;
    if (currentOwnerAncestor == targetAncestor) {
      return;
    }
    activate(checkNotNull(targetAncestor));
    waitForIdle();
  }

  @RunsInEDT
  private static Pair<Window, Window> windowAncestorsOf(final @Nullable Component one, final @Nullable Component two) {
    return execute(new GuiQuery<Pair<Window, Window>>() {
      @Override
      protected Pair<Window, Window> executeInEDT() throws Throwable {
        return Pair.of(windowAncestor(one), windowAncestor(two));
      }

      private @Nullable Window windowAncestor(Component c) {
        return (c != null) ? windowAncestorOf(c) : null;
      }
    });
  }

  /**
   * Activates the given AWT {@code Window}. "Activate" means that the given window gets the keyboard focus.
   *
   * @param w the window to activate.
   */
  @RunsInEDT
  private void activate(@Nonnull Window w) {
    activateWindow(w);
    moveMouse(w); // For pointer-focus systems
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public synchronized void cleanUp() {
    cleanUp(true);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public synchronized void cleanUpWithoutDisposingWindows() {
    cleanUp(false);
  }

  @RunsInEDT
  private void cleanUp(boolean disposeWindows) {
    try {
      if (disposeWindows) {
        disposeWindows(hierarchy);
      }
      releaseMouseButtons();
    } finally {
      active = false;
      releaseScreenLock();
    }
  }

  private void releaseScreenLock() {
    ScreenLock screenLock = ScreenLock.instance();
    if (screenLock.acquiredBy(screenLockOwner)) {
      screenLock.release(screenLockOwner);
    }
  }

  @RunsInEDT
  private static void disposeWindows(final @Nonnull ComponentHierarchy hierarchy) {
    execute(new GuiTask() {
      @Override
      protected void executeInEDT() {
        for (Container c : hierarchy.roots()) {
          if (c instanceof Window) {
            dispose(hierarchy, (Window) c);
          }
        }
      }
    });
  }

  @RunsInCurrentThread
  private static void dispose(final @Nonnull ComponentHierarchy hierarchy, @Nonnull Window w) {
    hierarchy.dispose(w);
    w.setVisible(false);
    w.dispose();
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Component c) {
    click(c, LEFT_BUTTON);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void rightClick(@Nonnull Component c) {
    click(c, RIGHT_BUTTON);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Component c, @Nonnull MouseButton button) {
    click(c, button, 1);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void doubleClick(@Nonnull Component c) {
    click(c, LEFT_BUTTON, 2);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Component c, @Nonnull MouseButton button, int times) {
    Point where = visibleCenterOf(c);
    if (c instanceof JComponent) {
      where = scrollIfNecessary((JComponent) c);
    }
    click(c, where, button, times);
  }

  private @Nonnull Point scrollIfNecessary(@Nonnull JComponent c) {
    scrollToVisible(this, c);
    return visibleCenterOf(c);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Component c, @Nonnull Point where) {
    click(c, where, LEFT_BUTTON, 1);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Point where, @Nonnull MouseButton button, int times) {
    doClick(null, where, button, times);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void click(@Nonnull Component c, @Nonnull Point where, @Nonnull MouseButton button, int times) {
    doClick(c, where, button, times);
  }

  private void doClick(@Nullable Component c, @Nonnull Point where, @Nonnull MouseButton button, int times) {
    int mask = button.mask;
    int modifierMask = mask & ~BUTTON_MASK;
    mask &= BUTTON_MASK;
    pressModifiers(modifierMask);
    // From Abbot: Adjust the auto-delay to ensure we actually get a multiple click
    // In general clicks have to be less than 200ms apart, although the actual setting is not readable by Java.
    int delayBetweenEvents = settings.delayBetweenEvents();
    if (shouldSetDelayBetweenEventsToZeroWhenClicking(times)) {
      settings.delayBetweenEvents(0);
    }
    if (c == null) {
      eventGenerator.pressMouse(where, mask);
      for (int i = times; i > 1; i--) {
        eventGenerator.releaseMouse(mask);
        eventGenerator.pressMouse(where, mask);
      }
    } else {
      eventGenerator.pressMouse(c, where, mask);
      for (int i = times; i > 1; i--) {
        eventGenerator.releaseMouse(mask);
        eventGenerator.pressMouse(c, where, mask);
      }
    }
    settings.delayBetweenEvents(delayBetweenEvents);
    eventGenerator.releaseMouse(mask);
    releaseModifiers(modifierMask);
    waitForIdle();
  }

  private boolean shouldSetDelayBetweenEventsToZeroWhenClicking(int times) {
    return times > 1 /* FEST-137: && settings.delayBetweenEvents() * 2 > 200 */;
  }

  /** {@inheritDoc} */
  @Override
  public void pressModifiers(int modifierMask) {
    for (int modifierKey : keysFor(modifierMask)) {
      pressKey(modifierKey);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void releaseModifiers(int modifierMask) {
    // For consistency, release in the reverse order of press.
    int[] modifierKeys = keysFor(modifierMask);
    for (int i = modifierKeys.length - 1; i >= 0; i--) {
      releaseKey(modifierKeys[i]);
    }
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void moveMouse(@Nonnull Component c) {
    moveMouse(c, visibleCenterOf(c));
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void moveMouse(@Nonnull Component c, @Nonnull Point p) {
    moveMouse(c, p.x, p.y);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void moveMouse(@Nonnull Component c, int x, int y) {
    if (!waitForComponentToBeReady(c, settings.timeoutToBeVisible())) {
      throw actionFailure(concat("Could not obtain position of component ", format(c)));
    }
    eventGenerator.moveMouse(c, x, y);
    waitForIdle();
  }

  /** {@inheritDoc} */
  @Override
  public void moveMouse(@Nonnull Point p) {
    moveMouse(p.x, p.y);
  }

  /** {@inheritDoc} */
  @Override
  public void moveMouse(int x, int y) {
    eventGenerator.moveMouse(x, y);
  }

  /** {@inheritDoc} */
  @Override
  public void pressMouse(@Nonnull MouseButton button) {
    eventGenerator.pressMouse(button.mask);
  }

  /** {@inheritDoc} */
  @Override
  public void pressMouse(@Nonnull Component c, @Nonnull Point where) {
    pressMouse(c, where, LEFT_BUTTON);
  }

  /** {@inheritDoc} */
  @Override
  public void pressMouse(@Nonnull Component c, @Nonnull Point where, @Nonnull MouseButton button) {
    jitter(c, where);
    moveMouse(c, where.x, where.y);
    eventGenerator.pressMouse(c, where, button.mask);
  }

  /** {@inheritDoc} */
  @Override
  public void pressMouse(@Nonnull Point where, @Nonnull MouseButton button) {
    eventGenerator.pressMouse(where, button.mask);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void releaseMouse(@Nonnull MouseButton button) {
    mouseRelease(button.mask);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void releaseMouseButtons() {
    int buttons = inputState.buttons();
    if (buttons == 0) {
      return;
    }
    mouseRelease(buttons);
  }

  /** {@inheritDoc} */
  @Override
  public void rotateMouseWheel(@Nonnull Component c, int amount) {
    moveMouse(c);
    rotateMouseWheel(amount);
  }

  /** {@inheritDoc} */
  @Override
  public void rotateMouseWheel(int amount) {
    eventGenerator.rotateMouseWheel(amount);
    waitForIdle();
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void jitter(@Nonnull Component c) {
    jitter(c, visibleCenterOf(c));
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void jitter(@Nonnull Component c, @Nonnull Point where) {
    int x = where.x;
    int y = where.y;
    moveMouse(c, (x > 0 ? x - 1 : x + 1), y);
  }

  // Wait the given number of milliseconds for the component to be showing and ready.
  @RunsInEDT
  private boolean waitForComponentToBeReady(@Nonnull Component c, long timeout) {
    if (isReadyForInput(c)) {
      return true;
    }
    TimeoutWatch watch = startWatchWithTimeoutOf(timeout);
    while (!isReadyForInput(c)) {
      if (c instanceof JPopupMenu) {
        // wiggle the mouse over the parent menu item to ensure the sub-menu shows
        Pair<Component, Point> invokerAndCenterOfInvoker = invokerAndCenterOfInvoker((JPopupMenu) c);
        Component invoker = invokerAndCenterOfInvoker.first;
        if (invoker instanceof JMenu) {
          jitter(invoker, invokerAndCenterOfInvoker.second);
        }
      }
      if (watch.isTimeOut()) {
        return false;
      }
      pause();
    }
    return true;
  }

  @RunsInEDT
  private static @Nonnull Pair<Component, Point> invokerAndCenterOfInvoker(final @Nonnull JPopupMenu popupMenu) {
    Pair<Component, Point> result = execute(new GuiQuery<Pair<Component, Point>>() {
      @Override
      protected Pair<Component, Point> executeInEDT() {
        Component invoker = checkNotNull(popupMenu.getInvoker());
        return Pair.of(invoker, centerOf(invoker));
      }
    });
    return checkNotNull(result);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void enterText(@Nonnull String text) {
    checkNotNull(text);
    if (text.isEmpty()) {
      return;
    }
    for (char character : text.toCharArray()) {
      type(character);
    }
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void type(char character) {
    KeyStroke keyStroke = keyStrokeFor(character);
    if (keyStroke == null) {
      Component focus = focusOwner();
      if (focus == null) {
        return;
      }
      KeyEvent keyEvent = keyEventFor(focus, character);
      // Allow any pending robot events to complete; otherwise we might stuff the typed event before previous
      // robot-generated events are posted.
      waitForIdle();
      eventPoster.postEvent(focus, keyEvent);
      return;
    }
    keyPressAndRelease(keyStroke.getKeyCode(), keyStroke.getModifiers());
  }

  private KeyEvent keyEventFor(Component c, char character) {
    return new KeyEvent(c, KEY_TYPED, currentTimeMillis(), 0, VK_UNDEFINED, character);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void pressAndReleaseKey(int keyCode, @Nonnull int... modifiers) {
    keyPressAndRelease(keyCode, unify(modifiers));
    waitForIdle();
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void pressAndReleaseKeys(@Nonnull int... keyCodes) {
    for (int keyCode : keyCodes) {
      keyPressAndRelease(keyCode, 0);
      waitForIdle();
      pause(50); // it seems that even when waiting for idle the events are not completely propagated
    }
  }

  @RunsInEDT
  private void keyPressAndRelease(int keyCode, int modifiers) {
    int updatedModifiers = updateModifierWithKeyCode(keyCode, modifiers);
    pressModifiers(updatedModifiers);
    if (updatedModifiers == modifiers) {
      doPressKey(keyCode);
      eventGenerator.releaseKey(keyCode);
    }
    releaseModifiers(updatedModifiers);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void pressKey(int keyCode) {
    doPressKey(keyCode);
    waitForIdle();
  }

  @RunsInEDT
  private void doPressKey(int keyCode) {
    eventGenerator.pressKey(keyCode, CHAR_UNDEFINED);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void releaseKey(int keyCode) {
    eventGenerator.releaseKey(keyCode);
    waitForIdle();
  }

  @RunsInEDT
  private void mouseRelease(int buttons) {
    eventGenerator.releaseMouse(buttons);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void waitForIdle() {
    waitIfNecessary();
    Collection<EventQueue> queues = windowMonitor.allEventQueues();
    if (queues.size() == 1) {
      waitForIdle(checkNotNull(toolkit.getSystemEventQueue()));
      return;
    }
    // FIXME this resurrects dead event queues
    for (EventQueue queue : queues) {
      waitForIdle(checkNotNull(queue));
    }
  }

  private void waitIfNecessary() {
    int delayBetweenEvents = settings.delayBetweenEvents();
    int eventPostingDelay = settings.eventPostingDelay();
    if (eventPostingDelay > delayBetweenEvents) {
      pause(eventPostingDelay - delayBetweenEvents);
    }
  }

  private void waitForIdle(@Nonnull EventQueue eventQueue) {
    if (EventQueue.isDispatchThread()) {
      throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread");
    }
    // Abbot: as of Java 1.3.1, robot.waitForIdle only waits for the last event on the queue at the time of this
    // invocation to be processed. We need better than that. Make sure the given event queue is empty when this method
    // returns.
    // We always post at least one idle event to allow any current event dispatch processing to finish.
    long start = currentTimeMillis();
    int count = 0;
    do {
      // Timed out waiting for idle
      int idleTimeout = settings.idleTimeout();
      if (postInvocationEvent(eventQueue, idleTimeout)) {
        break;
      }
      // Timed out waiting for idle event queue
      if (currentTimeMillis() - start > idleTimeout) {
        break;
      }
      ++count;
      // Force a yield
      pause();
      // Abbot: this does not detect invocation events (i.e. what gets posted with EventQueue.invokeLater), so if
      // someone is repeatedly posting one, we might get stuck. Not too worried, since if a Runnable keeps calling
      // invokeLater on itself, *nothing* else gets much chance to run, so it seems to be a bad programming practice.
    } while (eventQueue.peekEvent() != null);
  }

  // Indicates whether we timed out waiting for the invocation to run
  @RunsInEDT
  private boolean postInvocationEvent(@Nonnull EventQueue eventQueue, long timeout) {
    Object lock = new RobotIdleLock();
    synchronized (lock) {
      eventQueue.postEvent(new InvocationEvent(toolkit, EMPTY_RUNNABLE, lock, true));
      long start = currentTimeMillis();
      try {
        // NOTE: on fast linux systems when showing a dialog, if we don't provide a timeout, we're never notified, and
        // the test will wait forever (up through 1.5.0_05).
        lock.wait(timeout);
        return (currentTimeMillis() - start) >= settings.idleTimeout();
      } catch (InterruptedException e) {
      }
      return false;
    }
  }

  private static class RobotIdleLock {
    RobotIdleLock() {
    }
  }

  /** {@inheritDoc} */
  @Override
  public boolean isDragging() {
    return inputState.dragInProgress();
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public @Nonnull JPopupMenu showPopupMenu(@Nonnull Component invoker) {
    return showPopupMenu(invoker, visibleCenterOf(invoker));
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public @Nonnull JPopupMenu showPopupMenu(@Nonnull Component invoker, @Nonnull Point location) {
    if (isFocusable(invoker)) {
      focusAndWaitForFocusGain(invoker);
    }
    click(invoker, location, RIGHT_BUTTON, 1);
    JPopupMenu popup = findActivePopupMenu();
    if (popup == null) {
      throw new ComponentLookupException(concat("Unable to show popup at ", location, " on ", inEdtFormat(invoker)));
    }
    long start = currentTimeMillis();
    while (!isWindowAncestorReadyForInput(popup) && currentTimeMillis() - start > POPUP_DELAY) {
      pause();
    }
    return popup;
  }

  @RunsInEDT
  private boolean isWindowAncestorReadyForInput(final JPopupMenu popup) {
    Boolean result = execute(new GuiQuery<Boolean>() {
      @Override
      protected Boolean executeInEDT() {
        Window ancestor = checkNotNull(getWindowAncestor(popup));
        return isReadyForInput(ancestor);
      }
    });
    return checkNotNull(result);
  }

  /**
   * <p>
   * Indicates whether the given AWT or Swing {@code Component} is ready for input.
   * </p>
   *
   * <p>
   * <b>Note:</b> This method is accessed in the current executing thread. Such thread may or may not be the event
   * dispatch thread (EDT.) Client code must call this method from the EDT.
   * </p>
   *
   * @param c the given {@code Component}.
   * @return {@code true} if the given {@code Component} is ready for input, {@code false} otherwise.
   * @throws ActionFailedException if the given {@code Component} does not have a {@code Window} ancestor.
   */
  @Override
  @RunsInCurrentThread
  public boolean isReadyForInput(@Nonnull Component c) {
    Window w = windowAncestorOf(c);
    if (w == null) {
      throw actionFailure(concat("Component ", format(c), " does not have a Window ancestor"));
    }
    return c.isShowing() && windowMonitor.isWindowReady(w);
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public @Nullable JPopupMenu findActivePopupMenu() {
    JPopupMenu popup = activePopupMenu();
    if (popup != null || isEventDispatchThread()) {
      return popup;
    }
    TimeoutWatch watch = startWatchWithTimeoutOf(POPUP_TIMEOUT);
    while ((popup = activePopupMenu()) == null) {
      if (watch.isTimeOut()) {
        break;
      }
      pause(100);
    }
    return popup;
  }

  @RunsInEDT
  private @Nullable JPopupMenu activePopupMenu() {
    List<Component> found = newArrayList(finder().findAll(POPUP_MATCHER));
    if (found.size() == 1) {
      return (JPopupMenu) found.get(0);
    }
    return null;
  }

  /** {@inheritDoc} */
  @RunsInEDT
  @Override
  public void requireNoJOptionPaneIsShowing() {
    unexpectedJOptionPaneFinder.requireNoJOptionPaneIsShowing();
  }

  /** {@inheritDoc} */
  @Override
  public @Nonnull Settings settings() {
    return settings;
  }

  /** {@inheritDoc} */
  @Override
  public @Nonnull ComponentHierarchy hierarchy() {
    return hierarchy;
  }

  /** {@inheritDoc} */
  @Override
  public synchronized boolean isActive() {
    return active;
  }

  @VisibleForTesting
  final @Nullable Object screenLockOwner() {
    return screenLockOwner;
  }
}
TOP

Related Classes of org.fest.swing.core.BasicRobot

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.