macro playback fixed
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / IdeEventQueue.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ide;
17
18
19 import com.intellij.Patches;
20 import com.intellij.concurrency.JobSchedulerImpl;
21 import com.intellij.ide.dnd.DnDManager;
22 import com.intellij.ide.dnd.DnDManagerImpl;
23 import com.intellij.openapi.Disposable;
24 import com.intellij.openapi.application.Application;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.application.ModalityState;
27 import com.intellij.openapi.application.impl.ApplicationImpl;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher;
30 import com.intellij.openapi.keymap.impl.IdeMouseEventDispatcher;
31 import com.intellij.openapi.progress.ProcessCanceledException;
32 import com.intellij.openapi.util.ActionCallback;
33 import com.intellij.openapi.util.Condition;
34 import com.intellij.openapi.util.Disposer;
35 import com.intellij.openapi.util.SystemInfo;
36 import com.intellij.openapi.util.registry.Registry;
37 import com.intellij.openapi.wm.IdeFocusManager;
38 import com.intellij.openapi.wm.ex.WindowManagerEx;
39 import com.intellij.util.Alarm;
40 import com.intellij.util.ReflectionUtil;
41 import com.intellij.util.containers.ContainerUtil;
42 import com.intellij.util.containers.HashMap;
43 import org.jetbrains.annotations.NotNull;
44
45 import javax.swing.*;
46 import java.awt.*;
47 import java.awt.event.*;
48 import java.beans.PropertyChangeEvent;
49 import java.beans.PropertyChangeListener;
50 import java.lang.reflect.Field;
51 import java.lang.reflect.Method;
52 import java.util.*;
53
54
55 /**
56  * @author Vladimir Kondratyev
57  * @author Anton Katilin
58  */
59
60 public class IdeEventQueue extends EventQueue {
61
62   private static final Logger LOG = Logger.getInstance("#com.intellij.ide.IdeEventQueue");
63
64   private static final boolean DEBUG = LOG.isDebugEnabled();
65
66   /**
67    * Adding/Removing of "idle" listeners should be thread safe.
68    */
69   private final Object myLock = new Object();
70
71   private final ArrayList<Runnable> myIdleListeners = new ArrayList<Runnable>(2);
72
73   private final ArrayList<Runnable> myActivityListeners = new ArrayList<Runnable>(2);
74
75   private final Alarm myIdleRequestsAlarm = new Alarm();
76
77   private final Alarm myIdleTimeCounterAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
78
79   private long myIdleTime;
80
81   private final Map<Runnable, MyFireIdleRequest> myListener2Request = new HashMap<Runnable, MyFireIdleRequest>();
82   // IdleListener -> MyFireIdleRequest
83
84   private final IdeKeyEventDispatcher myKeyEventDispatcher = new IdeKeyEventDispatcher(this);
85
86   private final IdeMouseEventDispatcher myMouseEventDispatcher = new IdeMouseEventDispatcher();
87
88   private final IdePopupManager myPopupManager = new IdePopupManager();
89
90
91   private boolean mySuspendMode;
92
93   /**
94    * We exit from suspend mode when focus owner changes and no more WindowEvent.WINDOW_OPENED events
95    * <p/>
96    * in the queue. If WINDOW_OPENED event does exists in the queus then we restart the alarm.
97    */
98
99   private Component myFocusOwner;
100
101   private final Runnable myExitSuspendModeRunnable = new ExitSuspendModeRunnable();
102
103   /**
104    * We exit from suspend mode when this alarm is triggered and no mode WindowEvent.WINDOW_OPENED
105    * <p/>
106    * events in the queue. If WINDOW_OPENED event does exist then we restart the alarm.
107    */
108   private final Alarm mySuspendModeAlarm = new Alarm();
109
110   /**
111    * Counter of processed events. It is used to assert that data context lives only inside single
112    * <p/>
113    * Swing event.
114    */
115
116   private int myEventCount;
117
118
119   private boolean myIsInInputEvent = false;
120
121   private AWTEvent myCurrentEvent = null;
122
123   private long myLastActiveTime;
124
125   private WindowManagerEx myWindowManager;
126
127
128   private final Set<EventDispatcher> myDispatchers = new LinkedHashSet<EventDispatcher>();
129   private final Set<EventDispatcher> myPostprocessors = new LinkedHashSet<EventDispatcher>();
130
131   private Set<Runnable> myReady = new HashSet<Runnable>();
132   private boolean myKeyboardBusy;
133
134   private static class IdeEventQueueHolder {
135     private static final IdeEventQueue INSTANCE = new IdeEventQueue();
136   }
137
138   public static IdeEventQueue getInstance() {
139     return IdeEventQueueHolder.INSTANCE;
140   }
141
142   private IdeEventQueue() {
143     addIdleTimeCounterRequest();
144     final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
145
146     //noinspection HardCodedStringLiteral
147     keyboardFocusManager.addPropertyChangeListener("permanentFocusOwner", new PropertyChangeListener() {
148
149       public void propertyChange(final PropertyChangeEvent e) {
150         final Application application = ApplicationManager.getApplication();
151         if (application == null) {
152
153           // We can get focus event before application is initialized
154           return;
155         }
156         application.assertIsDispatchThread();
157         final Window focusedWindow = keyboardFocusManager.getFocusedWindow();
158         final Component focusOwner = keyboardFocusManager.getFocusOwner();
159         if (mySuspendMode && focusedWindow != null && focusOwner != null && focusOwner != myFocusOwner && !(focusOwner instanceof Window)) {
160           exitSuspendMode();
161         }
162       }
163     });
164   }
165
166
167   public void setWindowManager(final WindowManagerEx windowManager) {
168     myWindowManager = windowManager;
169   }
170
171
172   private void addIdleTimeCounterRequest() {
173     Application application = ApplicationManager.getApplication();
174     if (application != null && application.isUnitTestMode()) return;
175
176     myIdleTimeCounterAlarm.cancelAllRequests();
177     myLastActiveTime = System.currentTimeMillis();
178     myIdleTimeCounterAlarm.addRequest(new Runnable() {
179       public void run() {
180         myIdleTime += System.currentTimeMillis() - myLastActiveTime;
181         addIdleTimeCounterRequest();
182       }
183     }, 20000, ModalityState.NON_MODAL);
184   }
185
186
187   public boolean shouldNotTypeInEditor() {
188     return myKeyEventDispatcher.isWaitingForSecondKeyStroke() || mySuspendMode;
189   }
190
191
192   private void enterSuspendMode() {
193     mySuspendMode = true;
194     myFocusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
195     mySuspendModeAlarm.cancelAllRequests();
196     mySuspendModeAlarm.addRequest(myExitSuspendModeRunnable, 750);
197   }
198
199
200   /**
201    * Exits supend mode and pumps all suspended events.
202    */
203
204   private void exitSuspendMode() {
205     if (shallEnterSuspendMode()) {
206
207       // We have to exit from suspend mode (focus owner changes or alarm is triggered) but
208
209       // WINDOW_OPENED isn't dispatched yet. In this case we have to restart the alarm until
210
211       // all WINDOW_OPENED event will be processed.
212       mySuspendModeAlarm.cancelAllRequests();
213       mySuspendModeAlarm.addRequest(myExitSuspendModeRunnable, 250);
214     }
215     else {
216
217       // Now we can pump all suspended events.
218       mySuspendMode = false;
219       myFocusOwner = null; // to prevent memory leaks
220     }
221   }
222
223
224   public void addIdleListener(@NotNull final Runnable runnable, final int timeout) {
225     LOG.assertTrue(timeout > 0);
226     synchronized (myLock) {
227       myIdleListeners.add(runnable);
228       final MyFireIdleRequest request = new MyFireIdleRequest(runnable, timeout);
229       myListener2Request.put(runnable, request);
230       myIdleRequestsAlarm.addRequest(request, timeout);
231     }
232   }
233
234
235   public void removeIdleListener(@NotNull final Runnable runnable) {
236     synchronized (myLock) {
237       final boolean wasRemoved = myIdleListeners.remove(runnable);
238       if (!wasRemoved) {
239         LOG.assertTrue(false, "unknown runnable: " + runnable);
240       }
241       final MyFireIdleRequest request = myListener2Request.remove(runnable);
242       LOG.assertTrue(request != null);
243       myIdleRequestsAlarm.cancelRequest(request);
244     }
245   }
246
247
248   public void addActivityListener(@NotNull final Runnable runnable) {
249     synchronized (myLock) {
250       myActivityListeners.add(runnable);
251     }
252   }
253
254   public void addActivityListener(@NotNull final Runnable runnable, Disposable parentDisposable) {
255     synchronized (myLock) {
256       ContainerUtil.add(runnable, myActivityListeners, parentDisposable);
257     }
258   }
259
260
261   public void removeActivityListener(@NotNull final Runnable runnable) {
262     synchronized (myLock) {
263       final boolean wasRemoved = myActivityListeners.remove(runnable);
264       if (!wasRemoved) {
265         LOG.assertTrue(false, "unknown runnable: " + runnable);
266       }
267     }
268   }
269
270
271   public void addDispatcher(final EventDispatcher dispatcher, Disposable parent) {
272     _addProcessor(dispatcher, parent, myDispatchers);
273   }
274
275   public void removeDispatcher(EventDispatcher dispatcher) {
276     myDispatchers.remove(dispatcher);
277   }
278
279   public boolean containsDispatcher(EventDispatcher dispatcher) {
280     return myDispatchers.contains(dispatcher);
281   }
282
283   public void addPostprocessor(EventDispatcher dispatcher, Disposable parent) {
284     _addProcessor(dispatcher, parent, myPostprocessors);
285   }
286
287   public void removePostprocessor(EventDispatcher dispatcher) {
288     myPostprocessors.remove(dispatcher);
289   }
290
291   private void _addProcessor(final EventDispatcher dispatcher, Disposable parent, Set<EventDispatcher> set) {
292     set.add(dispatcher);
293     if (parent != null) {
294       Disposer.register(parent, new Disposable() {
295         public void dispose() {
296           removeDispatcher(dispatcher);
297         }
298       });
299     }
300   }
301
302   public int getEventCount() {
303     return myEventCount;
304   }
305
306
307   public void setEventCount(int evCount) {
308     myEventCount = evCount;
309   }
310
311
312   public AWTEvent getTrueCurrentEvent() {
313     return myCurrentEvent;
314   }
315
316   //[jeka] commented for performance reasons
317
318   /*
319
320   public void postEvent(final AWTEvent e) {
321
322     // [vova] sometime people call SwingUtilities.invokeLater(null). To
323
324     // find such situations we will specially check InvokationEvents
325
326     try {
327
328       if (e instanceof InvocationEvent) {
329
330         //noinspection HardCodedStringLiteral
331
332         final Field field = InvocationEvent.class.getDeclaredField("runnable");
333
334         field.setAccessible(true);
335
336         final Object runnable = field.get(e);
337
338         if (runnable == null) {
339
340           //noinspection HardCodedStringLiteral
341
342           throw new IllegalStateException("InvocationEvent contains null runnable: " + e);
343
344         }
345
346       }
347
348     }
349
350     catch (final Exception exc) {
351
352       throw new Error(exc);
353
354     }
355
356     super.postEvent(e);
357
358   }
359
360   */
361
362   public void dispatchEvent(final AWTEvent e) {
363     long t = 0;
364
365     boolean wasInputEvent = myIsInInputEvent;
366     myIsInInputEvent = e instanceof InputEvent || e instanceof InputMethodEvent || e instanceof WindowEvent || e instanceof ActionEvent;
367     AWTEvent oldEvent = myCurrentEvent;
368     myCurrentEvent = e;
369
370     JobSchedulerImpl.suspend();
371     try {
372       _dispatchEvent(e);
373     }
374     finally {
375       myIsInInputEvent = wasInputEvent;
376       myCurrentEvent = oldEvent;
377       JobSchedulerImpl.resume();
378
379       for (EventDispatcher each : myPostprocessors) {
380         each.dispatch(e);
381       }
382
383       if (e instanceof KeyEvent) {
384         maybeReady();
385       }
386
387       if (DEBUG) {
388         final long processTime = System.currentTimeMillis() - t;
389         if (processTime > 100) {
390           LOG.debug("Long event: " + processTime + "ms - " + toDebugString(e));
391         }
392       }
393     }
394   }
395
396   @SuppressWarnings({"ALL"})
397   private static String toDebugString(final AWTEvent e) {
398     if (e instanceof InvocationEvent) {
399       try {
400         final Field f = InvocationEvent.class.getDeclaredField("runnable");
401         f.setAccessible(true);
402         Object runnable = f.get(e);
403
404         return "Invoke Later[" + runnable.toString() + "]";
405       }
406       catch (NoSuchFieldException e1) {
407       }
408       catch (IllegalAccessException e1) {
409       }
410     }
411     return e.toString();
412   }
413
414
415   private void _dispatchEvent(final AWTEvent e) {
416     if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
417       DnDManagerImpl dndManager = (DnDManagerImpl)DnDManager.getInstance();
418       if (dndManager != null) {
419         dndManager.setLastDropHandler(null);
420       }
421     }
422
423
424     myEventCount++;
425
426
427     if (processAppActivationEvents(e)) return;
428
429     fixStickyFocusedComponents(e);
430
431     if (!myPopupManager.isPopupActive()) {
432       enterSuspendModeIfNeeded(e);
433     }
434
435     if (e instanceof KeyEvent) {
436       myKeyboardBusy = e.getID() != KeyEvent.KEY_RELEASED || ((KeyEvent)e).getModifiers() != 0;
437     }
438
439     if (typeAheadDispatchToFocusManager(e)) return;
440
441     if (e instanceof WindowEvent) {
442       ActivityTracker.getInstance().inc();
443     }
444
445
446     // Process "idle" and "activity" listeners
447     if (e instanceof KeyEvent || e instanceof MouseEvent) {
448       ActivityTracker.getInstance().inc();
449
450       synchronized (myLock) {
451         myIdleRequestsAlarm.cancelAllRequests();
452         for (Runnable idleListener : myIdleListeners) {
453           final MyFireIdleRequest request = myListener2Request.get(idleListener);
454           if (request == null) {
455             LOG.error("There is no request for " + idleListener);
456           }
457           int timeout = request.getTimeout();
458           myIdleRequestsAlarm.addRequest(request, timeout, ModalityState.NON_MODAL);
459         }
460         if (KeyEvent.KEY_PRESSED == e.getID() ||
461             KeyEvent.KEY_TYPED == e.getID() ||
462             MouseEvent.MOUSE_PRESSED == e.getID() ||
463             MouseEvent.MOUSE_RELEASED == e.getID() ||
464             MouseEvent.MOUSE_CLICKED == e.getID()) {
465           addIdleTimeCounterRequest();
466           for (Runnable activityListener : myActivityListeners) {
467             activityListener.run();
468           }
469         }
470       }
471     }
472     if (myPopupManager.isPopupActive() && myPopupManager.dispatch(e)) {
473       return;
474     }
475
476     for (EventDispatcher eachDispatcher : myDispatchers) {
477       if (eachDispatcher.dispatch(e)) {
478         return;
479       }
480     }
481
482     if (e instanceof InputMethodEvent) {
483       if (SystemInfo.isMac && myKeyEventDispatcher.isWaitingForSecondKeyStroke()) {
484         return;
485       }
486     }
487     if (e instanceof InputEvent && Patches.SPECIAL_WINPUT_METHOD_PROCESSING) {
488       final InputEvent inputEvent = (InputEvent)e;
489       if (!inputEvent.getComponent().isShowing()) {
490         return;
491       }
492     }
493     if (e instanceof ComponentEvent && myWindowManager != null) {
494       myWindowManager.dispatchComponentEvent((ComponentEvent)e);
495     }
496     if (e instanceof KeyEvent) {
497       if (mySuspendMode || !myKeyEventDispatcher.dispatchKeyEvent((KeyEvent)e)) {
498         defaultDispatchEvent(e);
499       }
500       else {
501         ((KeyEvent)e).consume();
502         defaultDispatchEvent(e);
503       }
504     }
505     else if (e instanceof MouseEvent) {
506       if (!myMouseEventDispatcher.dispatchMouseEvent((MouseEvent)e)) {
507         defaultDispatchEvent(e);
508       }
509     }
510     else {
511       defaultDispatchEvent(e);
512     }
513   }
514
515   private void fixStickyFocusedComponents(AWTEvent e) {
516     if (!(e instanceof InputEvent)) return;
517
518     final KeyboardFocusManager mgr = KeyboardFocusManager.getCurrentKeyboardFocusManager();
519     final Window wnd = mgr.getActiveWindow();
520     Window showingWindow = wnd;
521
522     if (Registry.is("actionSystem.fixStickyFocusedWindows")) {
523       if (wnd != null && !wnd.isShowing()) {
524         while (showingWindow != null) {
525           if (showingWindow.isShowing()) break;
526           showingWindow = (Window)showingWindow.getParent();
527         }
528
529         if (showingWindow == null) {
530           final Frame[] allFrames = Frame.getFrames();
531           for (Frame each : allFrames) {
532             if (each.isShowing()) {
533               showingWindow = each;
534               break;
535             }
536           }
537         }
538
539
540         if (showingWindow != null && showingWindow != wnd) {
541           final Method setActive =
542             ReflectionUtil.findMethod(KeyboardFocusManager.class.getDeclaredMethods(), "setGlobalActiveWindow", Window.class);
543           if (setActive != null) {
544             try {
545               setActive.setAccessible(true);
546               setActive.invoke(mgr, (Window)showingWindow);
547             }
548             catch (Exception exc) {
549               LOG.info(exc);
550             }
551           }
552         }
553       }
554     }
555
556     if (Registry.is("actionSystem.fixNullFocusedComponent")) {
557       final Component focusOwner = mgr.getFocusOwner();
558       if (focusOwner == null) {
559         if (showingWindow != null) {
560           final IdeFocusManager fm = IdeFocusManager.findInstanceByComponent(showingWindow);
561           fm.doWhenFocusSettlesDown(new Runnable() {
562             public void run() {
563               if (mgr.getFocusOwner() == null) {
564                 final Application app = ApplicationManager.getApplication();
565                 if (app != null && app.isActive()) {
566                   fm.requestDefaultFocus(false);
567                 }
568               }
569             }
570           });
571         }
572       }
573     }
574   }
575
576   private void enterSuspendModeIfNeeded(AWTEvent e) {
577     if (e instanceof KeyEvent) {
578       if (!mySuspendMode && shallEnterSuspendMode()) {
579         enterSuspendMode();
580       }
581     }
582   }
583
584   private boolean shallEnterSuspendMode() {
585     return peekEvent(WindowEvent.WINDOW_OPENED) != null;
586   }
587
588   private static boolean processAppActivationEvents(AWTEvent e) {
589     final Application app = ApplicationManager.getApplication();
590     if (!(app instanceof ApplicationImpl)) return false;
591
592     ApplicationImpl appImpl = (ApplicationImpl)app;
593
594     boolean consumed = false;
595     if (e instanceof WindowEvent) {
596       WindowEvent we = (WindowEvent)e;
597       if (we.getID() == WindowEvent.WINDOW_GAINED_FOCUS && we.getWindow() != null) {
598         if (we.getOppositeWindow() == null && !appImpl.isActive()) {
599           consumed = appImpl.tryToApplyActivationState(true, we.getWindow());
600         }
601       }
602       else if (we.getID() == WindowEvent.WINDOW_LOST_FOCUS && we.getWindow() != null) {
603         if (we.getOppositeWindow() == null && appImpl.isActive()) {
604           consumed = appImpl.tryToApplyActivationState(false, we.getWindow());
605         }
606       }
607     }
608
609     return consumed && Patches.REQUEST_FOCUS_MAY_ACTIVATE_APP;
610   }
611
612
613   private void defaultDispatchEvent(final AWTEvent e) {
614     try {
615       super.dispatchEvent(e);
616     }
617     catch (ProcessCanceledException pce) {
618       throw pce;
619     }
620     catch (Throwable exc) {
621       LOG.error("Error during dispatching of " + e, exc);
622     }
623   }
624
625   private static boolean typeAheadDispatchToFocusManager(AWTEvent e) {
626     if (e instanceof KeyEvent) {
627       final KeyEvent event = (KeyEvent)e;
628       if (!event.isConsumed()) {
629         final IdeFocusManager focusManager = IdeFocusManager.findInstanceByComponent(event.getComponent());
630         return focusManager.dispatch(event);
631       }
632     }
633
634     return false;
635   }
636
637
638   public void flushQueue() {
639     while (true) {
640       AWTEvent event = peekEvent();
641       if (event == null) return;
642       try {
643         AWTEvent event1 = getNextEvent();
644         dispatchEvent(event1);
645       }
646       catch (Exception e) {
647         LOG.error(e); //?
648       }
649     }
650   }
651
652   public void pumpEventsForHierarchy(Component modalComponent, Condition<AWTEvent> exitCondition) {
653     AWTEvent event;
654     do {
655       try {
656         event = getNextEvent();
657         boolean eventOk = true;
658         if (event instanceof InputEvent) {
659           final Object s = event.getSource();
660           if (s instanceof Component) {
661             Component c = (Component)s;
662             Window modalWindow = SwingUtilities.windowForComponent(modalComponent);
663             while (c != null && c != modalWindow) c = c.getParent();
664             if (c == null) {
665               eventOk = false;
666               ((InputEvent)event).consume();
667             }
668           }
669         }
670
671         if (eventOk) {
672           dispatchEvent(event);
673         }
674       }
675       catch (Throwable e) {
676         LOG.error(e);
677         event = null;
678       }
679     }
680     while (!exitCondition.value(event));
681   }
682
683
684   public interface EventDispatcher {
685     boolean dispatch(AWTEvent e);
686   }
687
688
689   private final class MyFireIdleRequest implements Runnable {
690     private final Runnable myRunnable;
691     private final int myTimeout;
692
693
694     public MyFireIdleRequest(final Runnable runnable, final int timeout) {
695       myTimeout = timeout;
696       myRunnable = runnable;
697     }
698
699
700     public void run() {
701       myRunnable.run();
702       synchronized (myLock) {
703         myIdleRequestsAlarm.addRequest(this, myTimeout, ModalityState.NON_MODAL);
704       }
705     }
706
707     public int getTimeout() {
708       return myTimeout;
709     }
710   }
711
712
713   private final class ExitSuspendModeRunnable implements Runnable {
714
715     public void run() {
716       if (mySuspendMode) {
717         exitSuspendMode();
718       }
719     }
720   }
721
722
723   public long getIdleTime() {
724     return myIdleTime;
725   }
726
727
728   public IdePopupManager getPopupManager() {
729     return myPopupManager;
730   }
731
732   public IdeKeyEventDispatcher getKeyEventDispatcher() {
733     return myKeyEventDispatcher;
734   }
735
736   public void blockNextEvents(final MouseEvent e) {
737     myMouseEventDispatcher.blockNextEvents(e);
738   }
739
740   public boolean isSuspendMode() {
741     return mySuspendMode;
742   }
743
744   public boolean hasFocusEventsPending() {
745     return peekEvent(FocusEvent.FOCUS_GAINED) != null || peekEvent(FocusEvent.FOCUS_LOST) != null;
746   }
747
748   private boolean isReady() {
749     return !myKeyboardBusy && myKeyEventDispatcher.isReady();
750   }
751
752   public void maybeReady() {
753     flushReady();
754   }
755
756   private void flushReady() {
757     if (myReady.size() == 0 || !isReady()) return;
758
759     Runnable[] ready = myReady.toArray(new Runnable[myReady.size()]);
760     myReady.clear();
761
762     for (Runnable each : ready) {
763       each.run();
764     }
765   }
766
767   public void doWhenReady(final Runnable runnable) {
768     if (EventQueue.isDispatchThread()) {
769       myReady.add(runnable);
770       maybeReady();
771     } else {
772       SwingUtilities.invokeLater(new Runnable() {
773         public void run() {
774           myReady.add(runnable);
775           maybeReady();
776         }
777       });
778     }
779   }
780 }