001: package abbot.tester;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import javax.swing.SwingUtilities;
006: import java.lang.ref.*;
007: import java.util.*;
008:
009: import abbot.Log;
010: import abbot.util.AWT;
011: import abbot.util.Properties;
012: import abbot.util.WeakAWTEventListener;
013: import abbot.util.NamedTimer;
014:
015: /** Keep track of all known root windows, and all known showing/hidden/closed
016: * windows.
017: */
018: public class WindowTracker {
019:
020: private static class Holder {
021: public static final WindowTracker INSTANCE = new WindowTracker();
022: }
023:
024: /** Maps unique event queues to the set of root windows found on each
025: queue.
026: */
027: private Map contexts = new WeakHashMap();
028: /** Maps components to their corresponding event queues. */
029: private Map queues;
030: private ContextTracker contextTracker;
031: /** Windows which for which isShowing is true but are not yet ready for
032: input. */
033: private Map pendingWindows = new WeakHashMap();
034: /** Windows which we deem are ready to use. */
035: private Map openWindows = new WeakHashMap();
036: /** Windows which are not visible. */
037: private Map hiddenWindows = new WeakHashMap();
038: /** Windows which have sent a WINDOW_CLOSE event. */
039: private Map closedWindows = new WeakHashMap();
040: private WindowReadyTracker windowReadyTracker;
041: private java.awt.Robot robot;
042: static int WINDOW_READY_DELAY = Properties.getProperty(
043: "abbot.window_ready_delay", 10000, 0, 60000);
044: private Timer windowReadyTimer;
045:
046: /** Only ever want one of these. */
047: public static WindowTracker getTracker() {
048: return Holder.INSTANCE;
049: }
050:
051: /** Create an instance of WindowTracker which will track all windows
052: * coming and going on the current and subsequent app contexts.
053: * WARNING: if an applet loads this class, it will only ever see stuff in
054: * its own app context.
055: */
056: WindowTracker() {
057: contextTracker = new ContextTracker();
058: windowReadyTracker = new WindowReadyTracker();
059: // hold the event queue references weakly
060: // each queue maps to a set of components (actually a weak hash map to
061: // allow GC of the component keys).
062: contexts.put(Toolkit.getDefaultToolkit().getSystemEventQueue(),
063: new WeakHashMap());
064: // hold both the component references and the event queues weakly
065: queues = new WeakHashMap();
066: // Populate stuff that may already have shown/been hidden
067: Frame[] frames = Frame.getFrames();
068: synchronized (openWindows) {
069: for (int i = 0; i < frames.length; i++) {
070: scanExistingWindows(frames[i]);
071: }
072: }
073: try {
074: robot = new java.awt.Robot();
075: } catch (AWTException e) {
076: }
077: windowReadyTimer = new NamedTimer("Window Ready Timer", true);
078:
079: long mask = WindowEvent.WINDOW_EVENT_MASK
080: | ComponentEvent.COMPONENT_EVENT_MASK;
081: new WeakAWTEventListener(contextTracker, mask);
082: mask = InputEvent.MOUSE_MOTION_EVENT_MASK
083: | InputEvent.MOUSE_EVENT_MASK
084: | InputEvent.PAINT_EVENT_MASK;
085: new WeakAWTEventListener(windowReadyTracker, mask);
086: }
087:
088: private void scanExistingWindows(Window w) {
089: // Make sure we catch subsequent show/hide events for this window
090: new WindowWatcher(w);
091: Window[] windows = w.getOwnedWindows();
092: for (int i = 0; i < windows.length; i++) {
093: scanExistingWindows(windows[i]);
094: }
095: openWindows.put(w, Boolean.TRUE);
096: if (!w.isShowing()) {
097: hiddenWindows.put(w, Boolean.TRUE);
098: }
099: noteContext(w);
100: }
101:
102: /** Returns whether the window is ready to receive OS-level event input.
103: A window's "isShowing" flag may be set true before the WINDOW_OPENED
104: event is generated, and even after the WINDOW_OPENED is sent the
105: window peer is not guaranteed to be ready.
106: */
107: public boolean isWindowReady(Window w) {
108: synchronized (openWindows) {
109: if (openWindows.containsKey(w)
110: && !hiddenWindows.containsKey(w)) {
111: return true;
112: }
113: }
114: if (robot != null)
115: checkWindow(w, robot);
116: return false;
117: }
118:
119: /** Return the event queue corresponding to the given component. In most
120: cases, this is the same as
121: Component.getToolkit().getSystemEventQueue(), but in the case of
122: applets will bypass the AppContext and provide the real event queue.
123: */
124: public EventQueue getQueue(Component c) {
125: // Components above the applet in the hierarchy may or may not share
126: // the same context with the applet itself.
127: while (!(c instanceof java.applet.Applet)
128: && c.getParent() != null)
129: c = c.getParent();
130: synchronized (contexts) {
131: WeakReference ref = (WeakReference) queues.get(c);
132: EventQueue q = ref != null ? (EventQueue) ref.get() : null;
133: if (q == null)
134: q = c.getToolkit().getSystemEventQueue();
135: return q;
136: }
137: }
138:
139: /** Returns all known event queues. */
140: public Collection getEventQueues() {
141: HashSet set = new HashSet();
142: synchronized (contexts) {
143: set.addAll(contexts.keySet());
144: Iterator iter = queues.values().iterator();
145: while (iter.hasNext()) {
146: WeakReference ref = (WeakReference) iter.next();
147: EventQueue q = (EventQueue) ref.get();
148: if (q != null)
149: set.add(q);
150: }
151: }
152: return set;
153: }
154:
155: /** Return all available root Windows. A root Window is one
156: * that has a null parent. Nominally this means a list similar to that
157: * returned by Frame.getFrames(), but in the case of an Applet may return
158: * a few Dialogs as well.
159: */
160: public Collection getRootWindows() {
161: Set set = new HashSet();
162: // Use Frame.getFrames() here in addition to our watched set, just in
163: // case any of them is missing from our set.
164: synchronized (contexts) {
165: Iterator iter = contexts.keySet().iterator();
166: while (iter.hasNext()) {
167: EventQueue queue = (EventQueue) iter.next();
168: Map map = (Map) contexts.get(queue);
169: set.addAll(map.keySet());
170: }
171: }
172: Frame[] frames = Frame.getFrames();
173: for (int i = 0; i < frames.length; i++) {
174: set.add(frames[i]);
175: }
176: //Log.debug(String.valueOf(list.size()) + " total Frames");
177: return set;
178: }
179:
180: /** Provides tracking of window visibility state. We explicitly add this
181: * on WINDOW_OPEN and remove it on WINDOW_CLOSE to avoid having to process
182: * extraneous ComponentEvents.
183: */
184: private class WindowWatcher extends WindowAdapter implements
185: ComponentListener {
186: public WindowWatcher(Window w) {
187: w.addComponentListener(this );
188: w.addWindowListener(this );
189: }
190:
191: public void componentShown(ComponentEvent e) {
192: markWindowShowing((Window) e.getSource());
193: }
194:
195: public void componentHidden(ComponentEvent e) {
196: synchronized (openWindows) {
197: //Log.log("Marking " + e.getSource() + " hidden");
198: hiddenWindows.put(e.getSource(), Boolean.TRUE);
199: pendingWindows.remove(e.getSource());
200: }
201: }
202:
203: public void windowClosed(WindowEvent e) {
204: e.getWindow().removeWindowListener(this );
205: e.getWindow().removeComponentListener(this );
206: }
207:
208: public void componentResized(ComponentEvent e) {
209: }
210:
211: public void componentMoved(ComponentEvent e) {
212: }
213: }
214:
215: /** Whenever we get a window that's on a new event dispatch thread, take
216: * note of the thread, since it may correspond to a new event queue and
217: * AppContext.
218: */
219: // FIXME what if it has the same app context? can we check?
220: private class ContextTracker implements AWTEventListener {
221: public void eventDispatched(AWTEvent ev) {
222:
223: ComponentEvent event = (ComponentEvent) ev;
224: Component comp = event.getComponent();
225: // This is our sole means of accessing other app contexts
226: // (if running within an applet). We look for window events
227: // beyond OPENED in order to catch windows that have already
228: // opened by the time we start listening but which are not
229: // in the Frame.getFrames list (i.e. they are on a different
230: // context). Specifically watch for COMPONENT_SHOWN on applets,
231: // since we may not get frame events for them.
232: if (!(comp instanceof java.applet.Applet)
233: && !(comp instanceof Window)) {
234: return;
235: }
236:
237: int id = ev.getID();
238: if (id == WindowEvent.WINDOW_OPENED) {
239: noteOpened(comp);
240: } else if (id == WindowEvent.WINDOW_CLOSED) {
241: noteClosed(comp);
242: } else if (id == WindowEvent.WINDOW_CLOSING) {
243: // ignore
244: }
245: // Take note of all other window events
246: else if ((id >= WindowEvent.WINDOW_FIRST && id <= WindowEvent.WINDOW_LAST)
247: || id == ComponentEvent.COMPONENT_SHOWN) {
248: synchronized (openWindows) {
249: if (!getRootWindows().contains(comp)
250: || closedWindows.containsKey(comp)) {
251: noteOpened(comp);
252: }
253: }
254: }
255: // The context for root-level windows may change between
256: // WINDOW_OPENED and subsequent events.
257: synchronized (contexts) {
258: WeakReference ref = (WeakReference) queues.get(comp);
259: if (ref != null
260: && !comp.getToolkit().getSystemEventQueue()
261: .equals(ref.get())) {
262: noteContext(comp);
263: }
264: }
265: }
266: }
267:
268: private class WindowReadyTracker implements AWTEventListener {
269: public void eventDispatched(AWTEvent e) {
270: if (e instanceof MouseEvent) {
271: Component c = (Component) e.getSource();
272: Window w = c instanceof Window ? (Window) c
273: : SwingUtilities.getWindowAncestor(c);
274: markWindowReady(w);
275: }
276: }
277: }
278:
279: private void noteContext(Component comp) {
280: EventQueue queue = comp.getToolkit().getSystemEventQueue();
281: synchronized (contexts) {
282: Map map = (Map) contexts.get(queue);
283: if (map == null) {
284: contexts.put(queue, map = new WeakHashMap());
285: }
286: if (comp instanceof Window && comp.getParent() == null) {
287: map.put(comp, Boolean.TRUE);
288: }
289: queues.put(comp, new WeakReference(queue));
290: }
291: }
292:
293: private void noteOpened(Component comp) {
294: //Log.log("Noting " + comp + " opened");
295: noteContext(comp);
296: // Attempt to ensure the window is ready for input before recognizing
297: // it as "open". There is no Java API for this, so we institute an
298: // empirically tested delay.
299: if (comp instanceof Window) {
300: new WindowWatcher((Window) comp);
301: markWindowShowing((Window) comp);
302: // Native components don't receive events anyway...
303: if (comp instanceof FileDialog) {
304: markWindowReady((Window) comp);
305: }
306: }
307: }
308:
309: private void noteClosed(Component comp) {
310: if (comp.getParent() == null) {
311: EventQueue queue = comp.getToolkit().getSystemEventQueue();
312: synchronized (contexts) {
313: Map whm = (Map) contexts.get(queue);
314: if (whm != null)
315: whm.remove(comp);
316: else {
317: EventQueue foundQueue = null;
318: Iterator iter = contexts.keySet().iterator();
319: while (iter.hasNext()) {
320: EventQueue q = (EventQueue) iter.next();
321: Map map = (Map) contexts.get(q);
322: if (map.containsKey(comp)) {
323: foundQueue = q;
324: map.remove(comp);
325: }
326: }
327: if (foundQueue == null) {
328: Log.log("Got WINDOW_CLOSED on "
329: + Robot.toString(comp)
330: + " on a previously unseen context: "
331: + queue + "(" + Thread.currentThread()
332: + ")");
333: } else {
334: Log.log("Window " + Robot.toString(comp)
335: + " sent WINDOW_CLOSED on " + queue
336: + " but sent WINDOW_OPENED on "
337: + foundQueue);
338: }
339: }
340: }
341: }
342: synchronized (openWindows) {
343: //Log.log("Marking " + comp + " closed");
344: openWindows.remove(comp);
345: hiddenWindows.remove(comp);
346: closedWindows.put(comp, Boolean.TRUE);
347: pendingWindows.remove(comp);
348: }
349: }
350:
351: /** Mark the given Window as ready for input. Indicate whether any
352: * pending "mark ready" task should be canceled.
353: */
354: private void markWindowReady(Window w) {
355: synchronized (openWindows) {
356: // If the window was closed after the check timer started running,
357: // it will have canceled the pending ready.
358: // Make sure it's still on the pending list before we actually
359: // mark it ready.
360: if (pendingWindows.containsKey(w)) {
361: //Log.log("Noting " + w + " ready");
362: closedWindows.remove(w);
363: hiddenWindows.remove(w);
364: openWindows.put(w, Boolean.TRUE);
365: pendingWindows.remove(w);
366: }
367: }
368: }
369:
370: /** Indicate a window has set isShowing true and needs to be marked ready
371: when it is actually ready.
372: */
373: private void markWindowShowing(final Window w) {
374: synchronized (openWindows) {
375: // At worst, time out and say the window is ready
376: // after the configurable delay
377: TimerTask task = new TimerTask() {
378: public void run() {
379: markWindowReady(w);
380: }
381: };
382: windowReadyTimer.schedule(task, WINDOW_READY_DELAY);
383: pendingWindows.put(w, task);
384: }
385: }
386:
387: private static int sign = 1;
388:
389: /** Actively check whether the given window is ready for input.
390: * @param robot
391: * @see #isWindowReady
392: */
393: private void checkWindow(final Window w, java.awt.Robot robot) {
394: // Must avoid frame borders, which are insensitive to mouse
395: // motion (at least on w32).
396: final Insets insets = AWT.getInsets(w);
397: final int width = w.getWidth();
398: final int height = w.getHeight();
399: int x = w.getX() + insets.left
400: + (width - (insets.left + insets.right)) / 2;
401: int y = w.getY() + insets.top
402: + (height - (insets.top + insets.bottom)) / 2;
403: if (x != 0 && y != 0) {
404: robot.mouseMove(x, y);
405: if (width > height)
406: robot.mouseMove(x + sign, y);
407: else
408: robot.mouseMove(x, y + sign);
409: sign = -sign;
410: }
411: synchronized (openWindows) {
412: if (pendingWindows.containsKey(w) && isEmptyFrame(w)) {
413: // Force the frame to be large enough to receive events
414: SwingUtilities.invokeLater(new Runnable() {
415: public void run() {
416: int nw = Math.max(width, insets.left
417: + insets.right + 3);
418: int nh = Math.max(height, insets.top
419: + insets.bottom + 3);
420: w.setSize(nw, nh);
421: }
422: });
423: }
424: }
425: }
426:
427: /** We can't get any motion events on an empty frame. */
428: private boolean isEmptyFrame(Window w) {
429: Insets insets = AWT.getInsets(w);
430: return insets.top + insets.bottom == w.getHeight()
431: || insets.left + insets.right == w.getWidth();
432: }
433: }
|