001 /*
002 * Created on Sep 29, 2006
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
005 * the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
010 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011 * specific language governing permissions and limitations under the License.
012 *
013 * Copyright @2006-2010 the original author or authors.
014 */
015 package org.fest.swing.core;
016
017 import static java.awt.event.InputEvent.BUTTON1_MASK;
018 import static java.awt.event.InputEvent.BUTTON2_MASK;
019 import static java.awt.event.InputEvent.BUTTON3_MASK;
020 import static java.awt.event.KeyEvent.CHAR_UNDEFINED;
021 import static java.awt.event.KeyEvent.KEY_TYPED;
022 import static java.awt.event.KeyEvent.VK_UNDEFINED;
023 import static java.awt.event.WindowEvent.WINDOW_CLOSING;
024 import static java.lang.System.currentTimeMillis;
025 import static javax.swing.SwingUtilities.getWindowAncestor;
026 import static javax.swing.SwingUtilities.isEventDispatchThread;
027 import static org.fest.swing.awt.AWT.centerOf;
028 import static org.fest.swing.awt.AWT.visibleCenterOf;
029 import static org.fest.swing.core.ActivateWindowTask.activateWindow;
030 import static org.fest.swing.core.ComponentIsFocusableQuery.isFocusable;
031 import static org.fest.swing.core.ComponentRequestFocusTask.giveFocusTo;
032 import static org.fest.swing.core.FocusOwnerFinder.focusOwner;
033 import static org.fest.swing.core.FocusOwnerFinder.inEdtFocusOwner;
034 import static org.fest.swing.core.InputModifiers.unify;
035 import static org.fest.swing.core.MouseButton.LEFT_BUTTON;
036 import static org.fest.swing.core.MouseButton.RIGHT_BUTTON;
037 import static org.fest.swing.core.Scrolling.scrollToVisible;
038 import static org.fest.swing.core.WindowAncestorFinder.windowAncestorOf;
039 import static org.fest.swing.edt.GuiActionRunner.execute;
040 import static org.fest.swing.exception.ActionFailedException.actionFailure;
041 import static org.fest.swing.format.Formatting.format;
042 import static org.fest.swing.format.Formatting.inEdtFormat;
043 import static org.fest.swing.hierarchy.NewHierarchy.ignoreExistingComponents;
044 import static org.fest.swing.keystroke.KeyStrokeMap.keyStrokeFor;
045 import static org.fest.swing.query.ComponentShowingQuery.isShowing;
046 import static org.fest.swing.timing.Pause.pause;
047 import static org.fest.swing.util.Modifiers.keysFor;
048 import static org.fest.swing.util.Modifiers.updateModifierWithKeyCode;
049 import static org.fest.swing.util.TimeoutWatch.startWatchWithTimeoutOf;
050 import static org.fest.util.Strings.concat;
051 import static org.fest.util.Strings.isEmpty;
052
053 import java.applet.Applet;
054 import java.awt.Component;
055 import java.awt.Container;
056 import java.awt.Dimension;
057 import java.awt.EventQueue;
058 import java.awt.Point;
059 import java.awt.Toolkit;
060 import java.awt.Window;
061 import java.awt.event.InvocationEvent;
062 import java.awt.event.KeyEvent;
063 import java.awt.event.WindowEvent;
064 import java.util.ArrayList;
065 import java.util.Collection;
066 import java.util.List;
067
068 import javax.swing.JComponent;
069 import javax.swing.JMenu;
070 import javax.swing.JPopupMenu;
071 import javax.swing.KeyStroke;
072
073 import net.jcip.annotations.GuardedBy;
074
075 import org.fest.swing.annotation.RunsInCurrentThread;
076 import org.fest.swing.annotation.RunsInEDT;
077 import org.fest.swing.edt.GuiQuery;
078 import org.fest.swing.edt.GuiTask;
079 import org.fest.swing.exception.ActionFailedException;
080 import org.fest.swing.exception.ComponentLookupException;
081 import org.fest.swing.exception.WaitTimedOutError;
082 import org.fest.swing.hierarchy.ComponentHierarchy;
083 import org.fest.swing.hierarchy.ExistingHierarchy;
084 import org.fest.swing.input.InputState;
085 import org.fest.swing.lock.ScreenLock;
086 import org.fest.swing.monitor.WindowMonitor;
087 import org.fest.swing.util.Pair;
088 import org.fest.swing.util.TimeoutWatch;
089 import org.fest.util.VisibleForTesting;
090
091 /**
092 * Understands simulation of user events on a GUI <code>{@link Component}</code>.
093 *
094 * @author Alex Ruiz
095 * @author Yvonne Wang
096 */
097 public class BasicRobot implements Robot {
098
099 private static final int POPUP_DELAY = 10000;
100 private static final int POPUP_TIMEOUT = 5000;
101 private static final int WINDOW_DELAY = 20000;
102
103 private static final ComponentMatcher POPUP_MATCHER = new TypeMatcher(JPopupMenu.class, true);
104
105 @GuardedBy("this") private volatile boolean active;
106
107 private static final Runnable EMPTY_RUNNABLE = new Runnable() {
108 public void run() {}
109 };
110
111 private static final int BUTTON_MASK = BUTTON1_MASK | BUTTON2_MASK | BUTTON3_MASK;
112
113 private static Toolkit toolkit = Toolkit.getDefaultToolkit();
114 private static WindowMonitor windowMonitor = WindowMonitor.instance();
115 private static InputState inputState = new InputState(toolkit);
116
117 /** Provides access to all the components in the hierarchy. */
118 private final ComponentHierarchy hierarchy;
119
120 private final Object screenLockOwner;
121
122 /** Looks up <code>{@link java.awt.Component}</code>s. */
123 private final ComponentFinder finder;
124
125 private final Settings settings;
126
127 private final AWTEventPoster eventPoster;
128 private final InputEventGenerator eventGenerator;
129 private final UnexpectedJOptionPaneFinder unexpectedJOptionPaneFinder;
130
131 /**
132 * Creates a new <code>{@link Robot}</code> with a new AWT hierarchy. The created <code>Robot</code> will not be able
133 * to access any components that were created before it.
134 * @return the created <code>Robot</code>.
135 */
136 public static Robot robotWithNewAwtHierarchy() {
137 Object screenLockOwner = acquireScreenLock();
138 return new BasicRobot(screenLockOwner, ignoreExistingComponents());
139 }
140
141 /**
142 * Creates a new <code>{@link Robot}</code> that has access to all the GUI components in the AWT hierarchy.
143 * @return the created <code>Robot</code>.
144 */
145 public static Robot robotWithCurrentAwtHierarchy() {
146 Object screenLockOwner = acquireScreenLock();
147 return new BasicRobot(screenLockOwner, new ExistingHierarchy());
148 }
149
150 private static Object acquireScreenLock() {
151 Object screenLockOwner = new Object();
152 ScreenLock.instance().acquire(screenLockOwner);
153 return screenLockOwner;
154 }
155
156 @VisibleForTesting
157 BasicRobot(Object screenLockOwner, ComponentHierarchy hierarchy) {
158 this.screenLockOwner = screenLockOwner;
159 this.hierarchy = hierarchy;
160 settings = new Settings();
161 eventGenerator = new RobotEventGenerator(settings);
162 eventPoster = new AWTEventPoster(toolkit, inputState, windowMonitor, settings);
163 finder = new BasicComponentFinder(this.hierarchy, settings);
164 unexpectedJOptionPaneFinder = new UnexpectedJOptionPaneFinder(finder);
165 active = true;
166 }
167
168 /** {@inheritDoc} */
169 public ComponentPrinter printer() {
170 return finder().printer();
171 }
172
173 /** {@inheritDoc} */
174 public ComponentFinder finder() {
175 return finder;
176 }
177
178 /** {@inheritDoc} */
179 @RunsInEDT
180 public void showWindow(Window w) {
181 showWindow(w, null, true);
182 }
183
184 /** {@inheritDoc} */
185 @RunsInEDT
186 public void showWindow(Window w, Dimension size) {
187 showWindow(w, size, true);
188 }
189
190 /** {@inheritDoc} */
191 @RunsInEDT
192 public void showWindow(final Window w, final Dimension size, final boolean pack) {
193 EventQueue.invokeLater(new Runnable() {
194 public void run() {
195 if (pack) packAndEnsureSafePosition(w);
196 if (size != null) w.setSize(size);
197 w.setVisible(true);
198 }
199 });
200 waitForWindow(w);
201 }
202
203 @RunsInCurrentThread
204 private void packAndEnsureSafePosition(Window w) {
205 w.pack();
206 w.setLocation(100, 100);
207 }
208
209 @RunsInEDT
210 private void waitForWindow(Window w) {
211 long start = currentTimeMillis();
212 while (!windowMonitor.isWindowReady(w) || !isShowing(w)) {
213 long elapsed = currentTimeMillis() - start;
214 if (elapsed > WINDOW_DELAY)
215 throw new WaitTimedOutError(concat("Timed out waiting for Window to open (", String.valueOf(elapsed), "ms)"));
216 pause();
217 }
218 }
219
220 /** {@inheritDoc} */
221 @RunsInEDT
222 public void close(Window w) {
223 WindowEvent event = new WindowEvent(w, WINDOW_CLOSING);
224 // If the window contains an applet, send the event on the applet's queue instead to ensure a shutdown from the
225 // applet's context (assists AppletViewer cleanup).
226 Component applet = findAppletDescendent(w);
227 EventQueue eventQueue = windowMonitor.eventQueueFor(applet != null ? applet : w);
228 eventQueue.postEvent(event);
229 waitForIdle();
230 }
231
232 /**
233 * Returns the <code>{@link Applet}</code> descendant of the given <code>{@link Container}</code>, if any.
234 * @param c the given <code>Container</code>.
235 * @return the <code>Applet</code> descendant of the given <code>Container</code>, or <code>null</code> if none
236 * is found.
237 */
238 @RunsInEDT
239 private Applet findAppletDescendent(Container c) {
240 List<Component> found = new ArrayList<Component>(finder.findAll(c, new TypeMatcher(Applet.class)));
241 if (found.size() == 1) return (Applet)found.get(0);
242 return null;
243 }
244
245 /** {@inheritDoc} */
246 @RunsInEDT
247 public void focusAndWaitForFocusGain(Component c) {
248 focus(c, true);
249 }
250
251 /** {@inheritDoc} */
252 @RunsInEDT
253 public void focus(Component c) {
254 focus(c, false);
255 }
256
257 @RunsInEDT
258 private void focus(Component target, boolean wait) {
259 Component currentOwner = inEdtFocusOwner();
260 if (currentOwner == target) return;
261 FocusMonitor focusMonitor = FocusMonitor.attachTo(target);
262 // for pointer focus
263 moveMouse(target);
264 // Make sure the correct window is in front
265 activateWindowOfFocusTarget(target, currentOwner);
266 giveFocusTo(target);
267 try {
268 if (wait) {
269 TimeoutWatch watch = startWatchWithTimeoutOf(settings().timeoutToBeVisible());
270 while (!focusMonitor.hasFocus()) {
271 if (watch.isTimeOut()) throw actionFailure(concat("Focus change to ", format(target), " failed"));
272 pause();
273 }
274 }
275 } finally {
276 target.removeFocusListener(focusMonitor);
277 }
278 }
279
280 @RunsInEDT
281 private void activateWindowOfFocusTarget(Component target, Component currentOwner) {
282 Pair<Window, Window> windowAncestors = windowAncestorsOf(currentOwner, target);
283 Window currentOwnerAncestor = windowAncestors.i;
284 Window targetAncestor = windowAncestors.ii;
285 if (currentOwnerAncestor == targetAncestor) return;
286 activate(targetAncestor);
287 waitForIdle();
288 }
289
290 @RunsInEDT
291 private static Pair<Window, Window> windowAncestorsOf(final Component one, final Component two) {
292 return execute(new GuiQuery<Pair<Window, Window>>() {
293 protected Pair<Window, Window> executeInEDT() throws Throwable {
294 return new Pair<Window, Window>(windowAncestor(one), windowAncestor(two));
295 }
296
297 private Window windowAncestor(Component c) {
298 return (c != null) ? windowAncestorOf(c) : null;
299 }
300 });
301 }
302
303 /**
304 * Activates the given <code>{@link Window}</code>. "Activate" means that the given window gets the keyboard focus.
305 * @param w the window to activate.
306 */
307 @RunsInEDT
308 private void activate(Window w) {
309 activateWindow(w);
310 moveMouse(w); // For pointer-focus systems
311 }
312
313 /** {@inheritDoc} */
314 @RunsInEDT
315 public synchronized void cleanUp() {
316 cleanUp(true);
317 }
318
319 /** {@inheritDoc} */
320 @RunsInEDT
321 public synchronized void cleanUpWithoutDisposingWindows() {
322 cleanUp(false);
323 }
324
325 @RunsInEDT
326 private void cleanUp(boolean disposeWindows) {
327 try {
328 if (disposeWindows) disposeWindows(hierarchy);
329 releaseMouseButtons();
330 } finally {
331 active = false;
332 releaseScreenLock();
333 }
334 }
335
336 private void releaseScreenLock() {
337 ScreenLock screenLock = ScreenLock.instance();
338 if (screenLock.acquiredBy(screenLockOwner)) screenLock.release(screenLockOwner);
339 }
340
341 @RunsInEDT
342 private static void disposeWindows(final ComponentHierarchy hierarchy) {
343 execute(new GuiTask() {
344 protected void executeInEDT() {
345 for (Container c : hierarchy.roots()) if (c instanceof Window) dispose(hierarchy, (Window)c);
346 }
347 });
348 }
349
350 @RunsInCurrentThread
351 private static void dispose(final ComponentHierarchy hierarchy, Window w) {
352 hierarchy.dispose(w);
353 w.setVisible(false);
354 w.dispose();
355 }
356
357 /** {@inheritDoc} */
358 @RunsInEDT
359 public void click(Component c) {
360 click(c, LEFT_BUTTON);
361 }
362
363 /** {@inheritDoc} */
364 @RunsInEDT
365 public void rightClick(Component c) {
366 click(c, RIGHT_BUTTON);
367 }
368
369 /** {@inheritDoc} */
370 @RunsInEDT
371 public void click(Component c, MouseButton button) {
372 click(c, button, 1);
373 }
374
375 /** {@inheritDoc} */
376 @RunsInEDT
377 public void doubleClick(Component c) {
378 click(c, LEFT_BUTTON, 2);
379 }
380
381 /** {@inheritDoc} */
382 @RunsInEDT
383 public void click(Component c, MouseButton button, int times) {
384 Point where = visibleCenterOf(c);
385 if (c instanceof JComponent)
386 where = scrollIfNecessary((JComponent) c, where);
387 click(c, where, button, times);
388 }
389
390 private Point scrollIfNecessary(JComponent c, Point p) {
391 scrollToVisible(this, c);
392 return visibleCenterOf(c);
393 }
394
395 /** {@inheritDoc} */
396 @RunsInEDT
397 public void click(Component c, Point where) {
398 click(c, where, LEFT_BUTTON, 1);
399 }
400
401 /** {@inheritDoc} */
402 @RunsInEDT
403 public void click(Point where, MouseButton button, int times) {
404 click(null, where, button, times);
405 }
406
407 /** {@inheritDoc} */
408 @RunsInEDT
409 public void click(Component c, Point where, MouseButton button, int times) {
410 int mask = button.mask;
411 int modifierMask = mask & ~BUTTON_MASK;
412 mask &= BUTTON_MASK;
413 pressModifiers(modifierMask);
414 // From Abbot: Adjust the auto-delay to ensure we actually get a multiple click
415 // In general clicks have to be less than 200ms apart, although the actual setting is not readable by Java.
416 int delayBetweenEvents = settings.delayBetweenEvents();
417 if (shouldSetDelayBetweenEventsToZeroWhenClicking(times)) settings.delayBetweenEvents(0);
418 eventGenerator.pressMouse(c, where, mask);
419 for (int i = times; i > 1; i--) {
420 eventGenerator.releaseMouse(mask);
421 eventGenerator.pressMouse(c, where, mask);
422 }
423 settings.delayBetweenEvents(delayBetweenEvents);
424 eventGenerator.releaseMouse(mask);
425 releaseModifiers(modifierMask);
426 waitForIdle();
427 }
428
429 private boolean shouldSetDelayBetweenEventsToZeroWhenClicking(int times) {
430 return times > 1 /*FEST-137: && settings.delayBetweenEvents() * 2 > 200*/;
431 }
432
433 /** {@inheritDoc} */
434 public void pressModifiers(int modifierMask) {
435 for (int modifierKey : keysFor(modifierMask))
436 pressKey(modifierKey);
437 }
438
439 /** {@inheritDoc} */
440 public void releaseModifiers(int modifierMask) {
441 // For consistency, release in the reverse order of press.
442 int[] modifierKeys = keysFor(modifierMask);
443 for (int i = modifierKeys.length - 1; i >= 0; i--)
444 releaseKey(modifierKeys[i]);
445 }
446
447 /** {@inheritDoc} */
448 @RunsInEDT
449 public void moveMouse(Component c) {
450 moveMouse(c, visibleCenterOf(c));
451 }
452
453 /** {@inheritDoc} */
454 @RunsInEDT
455 public void moveMouse(Component c, Point p) {
456 moveMouse(c, p.x, p.y);
457 }
458
459 /** {@inheritDoc} */
460 @RunsInEDT
461 public void moveMouse(Component c, int x, int y) {
462 if (!waitForComponentToBeReady(c, settings.timeoutToBeVisible()))
463 throw actionFailure(concat("Could not obtain position of component ", format(c)));
464 eventGenerator.moveMouse(c, x, y);
465 waitForIdle();
466 }
467
468 /** {@inheritDoc} */
469 public void moveMouse(Point p) {
470 moveMouse(p.x, p.y);
471 }
472
473 /** {@inheritDoc} */
474 public void moveMouse(int x, int y) {
475 eventGenerator.moveMouse(x, y);
476 }
477
478 /** {@inheritDoc} */
479 public void pressMouse(MouseButton button) {
480 eventGenerator.pressMouse(button.mask);
481 }
482
483 /** {@inheritDoc} */
484 public void pressMouse(Component c, Point where) {
485 pressMouse(c, where, LEFT_BUTTON);
486 }
487
488 /** {@inheritDoc} */
489 public void pressMouse(Component c, Point where, MouseButton button) {
490 jitter(c, where);
491 moveMouse(c, where.x, where.y);
492 eventGenerator.pressMouse(c, where, button.mask);
493 }
494
495 /** {@inheritDoc} */
496 public void pressMouse(Point where, MouseButton button) {
497 eventGenerator.pressMouse(where, button.mask);
498 }
499
500 /** {@inheritDoc} */
501 @RunsInEDT
502 public void releaseMouse(MouseButton button) {
503 mouseRelease(button.mask);
504 }
505
506 /** {@inheritDoc} */
507 @RunsInEDT
508 public void releaseMouseButtons() {
509 int buttons = inputState.buttons();
510 if (buttons == 0) return;
511 mouseRelease(buttons);
512 }
513
514 /** {@inheritDoc} */
515 public void rotateMouseWheel(Component c, int amount) {
516 moveMouse(c);
517 rotateMouseWheel(amount);
518 }
519
520 /** {@inheritDoc} */
521 public void rotateMouseWheel(int amount) {
522 eventGenerator.rotateMouseWheel(amount);
523 waitForIdle();
524 }
525
526 /** {@inheritDoc} */
527 @RunsInEDT
528 public void jitter(Component c) {
529 jitter(c, visibleCenterOf(c));
530 }
531
532 /** {@inheritDoc} */
533 @RunsInEDT
534 public void jitter(Component c, Point where) {
535 int x = where.x;
536 int y = where.y;
537 moveMouse(c, (x > 0 ? x - 1 : x + 1), y);
538 }
539
540 // Wait the given number of milliseconds for the component to be showing and ready.
541 @RunsInEDT
542 private boolean waitForComponentToBeReady(Component c, long timeout) {
543 if (isReadyForInput(c)) return true;
544 TimeoutWatch watch = startWatchWithTimeoutOf(timeout);
545 while (!isReadyForInput(c)) {
546 if (c instanceof JPopupMenu) {
547 // wiggle the mouse over the parent menu item to ensure the sub-menu shows
548 Pair<Component, Point> invokerAndCenterOfInvoker = invokerAndCenterOfInvoker((JPopupMenu)c);
549 Component invoker = invokerAndCenterOfInvoker.i;
550 if (invoker instanceof JMenu) jitter(invoker, invokerAndCenterOfInvoker.ii);
551 }
552 if (watch.isTimeOut()) return false;
553 pause();
554 }
555 return true;
556 }
557
558 @RunsInEDT
559 private static Pair<Component, Point> invokerAndCenterOfInvoker(final JPopupMenu popupMenu) {
560 return execute(new GuiQuery<Pair<Component, Point>>() {
561 protected Pair<Component, Point> executeInEDT() {
562 Component invoker = popupMenu.getInvoker();
563 return new Pair<Component, Point>(invoker, centerOf(invoker));
564 }
565 });
566 }
567
568 /** {@inheritDoc} */
569 @RunsInEDT
570 public void enterText(String text) {
571 if (isEmpty(text)) return;
572 for (char character : text.toCharArray()) type(character);
573 }
574
575 /** {@inheritDoc} */
576 @RunsInEDT
577 public void type(char character) {
578 KeyStroke keyStroke = keyStrokeFor(character);
579 if (keyStroke == null) {
580 Component focus = focusOwner();
581 if (focus == null) return;
582 KeyEvent keyEvent = keyEventFor(focus, character);
583 // Allow any pending robot events to complete; otherwise we might stuff the typed event before previous
584 // robot-generated events are posted.
585 waitForIdle();
586 eventPoster.postEvent(focus, keyEvent);
587 return;
588 }
589 keyPressAndRelease(keyStroke.getKeyCode(), keyStroke.getModifiers());
590 }
591
592 private KeyEvent keyEventFor(Component c, char character) {
593 return new KeyEvent(c, KEY_TYPED, System.currentTimeMillis(), 0, VK_UNDEFINED, character);
594 }
595
596 /** {@inheritDoc} */
597 @RunsInEDT
598 public void pressAndReleaseKey(int keyCode, int... modifiers) {
599 keyPressAndRelease(keyCode, unify(modifiers));
600 waitForIdle();
601 }
602
603 /** {@inheritDoc} */
604 @RunsInEDT
605 public void pressAndReleaseKeys(int... keyCodes) {
606 for (int keyCode : keyCodes) {
607 keyPressAndRelease(keyCode, 0);
608 waitForIdle();
609 pause(50); // it seems that even when waiting for idle the events are not completely propagated
610 }
611 }
612
613 @RunsInEDT
614 private void keyPressAndRelease(int keyCode, int modifiers) {
615 int updatedModifiers = updateModifierWithKeyCode(keyCode, modifiers);
616 pressModifiers(updatedModifiers);
617 if (updatedModifiers == modifiers) {
618 doPressKey(keyCode);
619 eventGenerator.releaseKey(keyCode);
620 }
621 releaseModifiers(updatedModifiers);
622 }
623
624 /** {@inheritDoc} */
625 @RunsInEDT
626 public void pressKey(int keyCode) {
627 doPressKey(keyCode);
628 waitForIdle();
629 }
630
631 @RunsInEDT
632 private void doPressKey(int keyCode) {
633 eventGenerator.pressKey(keyCode, CHAR_UNDEFINED);
634 }
635
636 /** {@inheritDoc} */
637 @RunsInEDT
638 public void releaseKey(int keyCode) {
639 eventGenerator.releaseKey(keyCode);
640 waitForIdle();
641 }
642
643 @RunsInEDT
644 private void mouseRelease(int buttons) {
645 eventGenerator.releaseMouse(buttons);
646 }
647
648 /** {@inheritDoc} */
649 @RunsInEDT
650 public void waitForIdle() {
651 waitIfNecessary();
652 Collection<EventQueue> queues = windowMonitor.allEventQueues();
653 if (queues.size() == 1) {
654 waitForIdle(toolkit.getSystemEventQueue());
655 return;
656 }
657 // FIXME this resurrects dead event queues
658 for (EventQueue queue : queues) waitForIdle(queue);
659 }
660
661 private void waitIfNecessary() {
662 int delayBetweenEvents = settings.delayBetweenEvents();
663 int eventPostingDelay = settings.eventPostingDelay();
664 if (eventPostingDelay > delayBetweenEvents) pause(eventPostingDelay - delayBetweenEvents);
665 }
666
667 private void waitForIdle(EventQueue eventQueue) {
668 if (EventQueue.isDispatchThread())
669 throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread");
670 // Abbot: as of Java 1.3.1, robot.waitForIdle only waits for the last event on the queue at the time of this
671 // invocation to be processed. We need better than that. Make sure the given event queue is empty when this method
672 // returns.
673 // We always post at least one idle event to allow any current event dispatch processing to finish.
674 long start = currentTimeMillis();
675 int count = 0;
676 do {
677 // Timed out waiting for idle
678 int idleTimeout = settings.idleTimeout();
679 if (postInvocationEvent(eventQueue, idleTimeout)) break;
680 // Timed out waiting for idle event queue
681 if (currentTimeMillis() - start > idleTimeout) break;
682 ++count;
683 // Force a yield
684 pause();
685 // Abbot: this does not detect invocation events (i.e. what gets posted with EventQueue.invokeLater), so if
686 // someone is repeatedly posting one, we might get stuck. Not too worried, since if a Runnable keeps calling
687 // invokeLater on itself, *nothing* else gets much chance to run, so it seems to be a bad programming practice.
688 } while (eventQueue.peekEvent() != null);
689 }
690
691 // Indicates whether we timed out waiting for the invocation to run
692 @RunsInEDT
693 private boolean postInvocationEvent(EventQueue eventQueue, long timeout) {
694 Object lock = new RobotIdleLock();
695 synchronized (lock) {
696 eventQueue.postEvent(new InvocationEvent(toolkit, EMPTY_RUNNABLE, lock, true));
697 long start = currentTimeMillis();
698 try {
699 // NOTE: on fast linux systems when showing a dialog, if we don't provide a timeout, we're never notified, and
700 // the test will wait forever (up through 1.5.0_05).
701 lock.wait(timeout);
702 return (currentTimeMillis() - start) >= settings.idleTimeout();
703 } catch (InterruptedException e) {}
704 return false;
705 }
706 }
707
708 private static class RobotIdleLock {
709 RobotIdleLock() {}
710 }
711
712 /** {@inheritDoc} */
713 public boolean isDragging() {
714 return inputState.dragInProgress();
715 }
716
717 /** {@inheritDoc} */
718 @RunsInEDT
719 public JPopupMenu showPopupMenu(Component invoker) {
720 return showPopupMenu(invoker, visibleCenterOf(invoker));
721 }
722
723 /** {@inheritDoc} */
724 @RunsInEDT
725 public JPopupMenu showPopupMenu(Component invoker, Point location) {
726 if (isFocusable(invoker)) focusAndWaitForFocusGain(invoker);
727 click(invoker, location, RIGHT_BUTTON, 1);
728 JPopupMenu popup = findActivePopupMenu();
729 if (popup == null)
730 throw new ComponentLookupException(concat("Unable to show popup at ", location, " on ", inEdtFormat(invoker)));
731 long start = currentTimeMillis();
732 while (!isWindowAncestorReadyForInput(popup) && currentTimeMillis() - start > POPUP_DELAY)
733 pause();
734 return popup;
735 }
736
737 @RunsInEDT
738 private boolean isWindowAncestorReadyForInput(final JPopupMenu popup) {
739 return execute(new GuiQuery<Boolean>() {
740 protected Boolean executeInEDT() {
741 return isReadyForInput(getWindowAncestor(popup));
742 }
743 });
744 }
745
746 /**
747 * Indicates whether the given <code>{@link Component}</code> is ready for input.
748 * <p>
749 * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
750 * responsible for calling this method from the EDT.
751 * </p>
752 * @param c the given <code>Component</code>.
753 * @return <code>true</code> if the given <code>Component</code> is ready for input, <code>false</code> otherwise.
754 * @throws ActionFailedException if the given <code>Component</code> does not have a <code>Window</code> ancestor.
755 */
756 @RunsInCurrentThread
757 public boolean isReadyForInput(Component c) {
758 Window w = windowAncestorOf(c);
759 if (w == null) throw actionFailure(concat("Component ", format(c), " does not have a Window ancestor"));
760 return c.isShowing() && windowMonitor.isWindowReady(w);
761 }
762
763 /** {@inheritDoc} */
764 @RunsInEDT
765 public JPopupMenu findActivePopupMenu() {
766 JPopupMenu popup = activePopupMenu();
767 if (popup != null || isEventDispatchThread()) return popup;
768 TimeoutWatch watch = startWatchWithTimeoutOf(POPUP_TIMEOUT);
769 while ((popup = activePopupMenu()) == null) {
770 if (watch.isTimeOut()) break;
771 pause(100);
772 }
773 return popup;
774 }
775
776 @RunsInEDT
777 private JPopupMenu activePopupMenu() {
778 List<Component> found = new ArrayList<Component>(finder().findAll(POPUP_MATCHER));
779 if (found.size() == 1) return (JPopupMenu)found.get(0);
780 return null;
781 }
782
783 /** {@inheritDoc} */
784 @RunsInEDT
785 public void requireNoJOptionPaneIsShowing() {
786 unexpectedJOptionPaneFinder.requireNoJOptionPaneIsShowing();
787 }
788
789 /** {@inheritDoc} */
790 public Settings settings() {
791 return settings;
792 }
793
794 /** {@inheritDoc} */
795 public ComponentHierarchy hierarchy() {
796 return hierarchy;
797 }
798
799 /** {@inheritDoc} */
800 public synchronized boolean isActive() { return active; }
801
802 @VisibleForTesting
803 final Object screenLockOwner() { return screenLockOwner; }
804 }