Abstract: Instead of programmatically setting the wait cursor, we can also let the java.awt.EventQueue do the heavy lifting for us. Nathan Arthur explains how.
Welcome to the 75th edition of The Java(tm) Specialists' Newsletter. This week we are re-looking at the problem of wait cursors. I am very grateful to Nathan Arthur and his company for allowing us to publish this article. They have been through a lot of effort in producing this code, and I am sure it will help you as well. There are two things that I would like you to note in this article. First, it is practical, real-world code. There might be bugs, if you find any, please let us know. It is extremely useful, well-written code. Second, Nathan is passionate about unit testing. The more I work as a programmer, the more I feel the same passion as Nathan.
There are many ways to skin a cat. [I will not describe them in this newsletter, since that would be slightly off-topic.] In the same way, there are many waits t'askin a User to wait. [In the unlikely event that one of my old English teachers is reading this newsletter - the previous sentence construction was intentional.]
Enough of me, let us listen to what Nathan has to say (you at the back of the class, please be quiet and listen as well. Yes, you.) If you would like to send thanks and comments to Nathan, please email to truist-waitcursor@truist.com.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
A few months ago, I received the "Wait, Cursor, Wait!" edition of this newsletter. I had just spent man-weeks trying to discover better approaches to using wait cursors in my application, and I had two immediate reactions: First, it amused me that it came out only a few days after I had just done all that work. Second, it worried me that it might present a simpler solution than the one I found, thus making all the time I had spent a waste. Thankfully, it did not. It presented a solution very similar to the solution my application had started with. You will therefore benefit from my weeks of hard labour :-)
For some background, you should probably read issue #16 ("Blocking Queue") and issue #7 ("java.awt.EventQueue") of The Java(tm) Specialists' Newsletter. At the end of this article, we will have implemented our own EventQueue.
Before I get into the background, I would like to discuss a few things I noted in "Wait, Cursor, Wait!"
First, the use of the GlassPane is not quite enough in that example - you will notice that you can still use the keyboard to tab around to components in the parent frame.
Second, Java modal dialogs on Windows are "natively" modal, which you can see by clicking on the frame behind a modal dialog - the title bar of the modal dialog will flash. This does not happen if the dialog is not modal, and there is no way to mimic the behavior in java.
Third, at the end of "Solution 1," there is a paragraph explaining that the strange wait cursor behavior is caused by the first event pump being blocked while the modal dialog is open. This is not actually the cause of the problem - the cause is explained in the Sun bug #4282540: the wait cursor does not paint on the frame because that is the correct Windows behavior for frames behind modal dialogs. Presumably, that is implemented in the frame's native peer. I would be interested to see what happens on other operating systems with different modal dialog / frame cursor behaviors.
Because of all these, I prefer to use actual modal dialogs, not use the glass pane for the wait cursor, and to live with the wait cursor not showing on the parent frame. I also would really prefer to have an automatic wait cursor solution, so I do not have to explicitly deal with it everywhere, and so the application always shows it even if I forgot to turn it on.
The original wait cursor implementation I set out to improve was this:
import java.awt.*; import java.awt.event.InputEvent; import java.util.*; import javax.swing.SwingUtilities; public class WaitControl { private static int waiting = 0; private static ArrayList events = new ArrayList(); private static final Cursor DEFAULT_CURSOR = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); private static final Cursor WAIT_CURSOR = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); public static Window startWait(Component componentInWindow) { Window window = getEnclosingWindow(componentInWindow); if (window == null) { return null; } waiting++; // Only wait if we are not already if (waiting == 1) { window.setCursor(WAIT_CURSOR); EventQueue q = window.getToolkit().getSystemEventQueue(); try { while (q.peekEvent() != null) { events.add(q.getNextEvent()); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } return window; } public static void endWait(Component componentInWindow) { Window window = getEnclosingWindow(componentInWindow); if (window == null) { return; } if (waiting > 0) { waiting--; // Only stop when all waiting is done if (waiting == 0) { EventQueue q = window.getToolkit().getSystemEventQueue(); try { while (q.peekEvent() != null) { AWTEvent event = q.getNextEvent(); if (!(event instanceof InputEvent)) { events.add(event); } } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } for (Iterator it = events.iterator(); it.hasNext();) { q.postEvent((AWTEvent) it.next()); } window.setCursor(DEFAULT_CURSOR); events.clear(); } } } public static void fullEndWait(Window window) { if (waiting > 0) { waiting = 1; endWait(window); } } public static Window getEnclosingWindow(Component componentInWindow) { if (componentInWindow instanceof Window) { return (Window) componentInWindow; } else if (componentInWindow != null) { return SwingUtilities.windowForComponent(componentInWindow); } else { return null; } } }
At a high level, this implementation is simple. It filters input events by taking them directly off the system event queue, which means we do not have to use the GlassPane, and can use modal dialogs.
One of the many problems with this solution was that we always
had to remember to call endWait()
with the correct
component and the right number of times. Ideally we would have
preferred for the wait cursor to appear (and disappear) automagically.
Also, as it happened, we though we had found a way to make it work automatically. We found it online, in an article on JavaWorld.com, titled "Automate the hourglass cursor." There were some problems with that approach:
The first flaw is subtle does not cause much noticeable bad
behavior. The problem lies in the
assumption made about how wait()
and interrupt()
work together.
Specifically, line 51 of the code assumes that if interrupt()
is called during the wait()
, an InterruptedException
will
be thrown. However, the InterruptedException
will only be thrown if the delay timeout
has not ended yet. If the timeout has ended, but the thread
is still in contention for the object's monitor, interrupt()
will simply set the interrupt status of the thread to true
,
without throwing the exception.
If you install the code as written, and use the application, you will occasionally see little flashes of the wait cursor that do not make sense. In addition, if you run the unit test at the end of the article, nearly all the tests will fail.
The flaw is fixable, however. Simply add these two lines of code after line 51:
if (Thread.interrupted()) continue;
This performs essentially the same function as the exception would have, and fixes the problem. Note that we use Thread.interrupted() and not isInterrupted() because Thread.interrupted() resets the flag, while isInterrupted() does not.
Another problem is that modal dialogs cause the dispatchEvent() method to be recursive. I will explain why this is true below, but for the moment, just take my word for it - opening a modal dialog in the call to super.dispatchEvent() will cause dispatchEvent() to get called again before super.dispatchEvent() returns. This causes a problem.
The problem is fairly obvious - two calls to waitTimer.startTimer() in a row, without an intervening call to waitTimer.stopTimer(), will cause both wait() calls in the run() loop to be passed, resulting in a wait cursor being set on the dialog, and the state tracking to be broken. As a user, you will not notice this much, because usually the cursor will get immediately reset, but this does cause problems if the code that was showing the dialog takes a long time after the dialog is closed. No cursor will get set, because the stopTimer() call from the first call to dispatchEvent will then execute, canceling any cursor that would have otherwise been set. In addition, the tracking is at this point out of sync, and can cause further problems (although it usually resets itself correctly).
This is a common case - imagine starting some long operation, at the beginning of which you open a dialog to ask a user to make a choice. In this case, the long operation will not (reliably) have a wait cursor, because of this bug in the code. The flaw is not simple to fix, and requires a detailed understanding of how modal dialogs and the event queue mechanism work.
Since this is The Java(tm) Specialists' Newsletter, I can probably assume that you have a basic understanding of the system event queue. However, I have found that very few Java developers have a clear idea of what happens when a modal dialog is opened.
First, all events generated by the user (mouse, keyboard, system, etc.) are passed through the VM, and all program events (paints, actions, etc.) are placed on the system event queue. (Note that there is usually exactly one event queue per program, but sometimes an event queue will be shared across applets.) These events are generated asynchronously, and are simply placed on the queue from whichever thread they were generated in. This is happening for the entire life of the AWT program.
Second, and at the same time, there is a special thread (commonly called the "AWT event thread" and named "AWT-EventQueue-0") that is constantly removing events (in FIFO order) from the event queue, and dispatching them. Note that the currently dispatching event is not on the queue - it is removed before it is dispatched. All GUI operations should be performed on this thread, because the AWT and Swing code is not thread safe. There is always exactly one active event thread per event queue.
Third, when a modal dialog is shown, a new event pump is (normally) started, which takes over for the previous event pump and starts pulling events off the event queue and dispatching them. This new pump shuts down when the modal dialog is closed.
The confusion usually happens because people believe that starting a new pump means starting a new thread, but that is not true. (In fact, it would not work.) What really happens is that Dialog.show() simply takes over the job of pumping the event queue. This is why calling setVisible() on a modal dialog does not return until the dialog is closed - setVisible() continues pumping events.
There is only ever one active event thread. When a modal dialog is opened, this happens as part of an event on the event thread, and that event simply does not quit, and starts pumping events so that events continue to happen.
This explains why EventQueue.dispatchEvent() is recursive with modal dialogs - the dialog itself will start pumping events (and call dispatchEvent()) before the event that showed it returns.
If you are interested in the details of all this, I would
suggest reading the code. Start with java.awt.EventQueue
,
and pay attention to postEvent()
, postEventPrivate()
, and the
other version of postEvent()
. Pay particular attention to
getNextEvent()
and dispatchEvent()
. Also, look at the push()
and pop()
methods.
Then look at java.awt.EventDispatchThread
, specifically
at run()
, pumpEvents()
, the other pumpEvents()
,
pumpEventsForHierarchy()
, and pumpOneEventForHierarchy()
.
I do not really understand why this code filters events for
the modal dialog, because there should not ever be events
outside of the modal dialog, but perhaps it has some use on
another platform. If somebody knows, I would love to hear it!
Finally, look at java.awt.Dialog
, specifically at show()
.
That will bring it all together.
We have now seen three implementations of a wait cursor manager - the one presented in issue #65, the one presented at the top of this document, and the one on JavaWorld.com. None of them is perfect, but they all have some good features. We will take the best from each of them, and add a few improvements of our own. Specifically, we want to build a cursor manager that:
The final implementation of these "requirements" relies on four classes and an interface, and two unit tests. The primary class is WaitCursorEventQueue, and it works with a CursorManager class, which uses a DispatchedEvent class. The remaining class is DelayTimer, which, with the DelayTimerCallback interface, implements a generic delay timer that can be used for other purposes, if needed.
I first present the DelayTimer. It is thread safe, and supports any series of calls to startTimer() and stopTimer().
[HK: I personally prefer to use the interrupted flag to indicate that I want to quit a thread. However, I did not want to break anything, so here is the code as it was :-]
/** * This class implements a delay timer that will call trigger() * on the DelayTimerCallback delay milliseconds after * startTimer() was called, if stopTimer() was not called first. * The timer will only throw events after startTimer() is called. * Until then, it does nothing. It is safe to call stopTimer() * and startTimer() repeatedly. * * Note that calls to trigger() will happen on the timer thread. * * This class is multiple-thread safe. */ public class DelayTimer extends Thread { private final DelayTimerCallback callback; private final Object mutex = new Object(); private final Object triggeredMutex = new Object(); private final long delay; private boolean quit; private boolean triggered; private long waitTime; public DelayTimer(DelayTimerCallback callback, long delay) { this.callback = callback; this.delay = delay; setDaemon(true); start(); } /** * Calling this method twice will reset the timer. */ public void startTimer() { synchronized (mutex) { waitTime = delay; mutex.notify(); } } public void stopTimer() { try { synchronized (mutex) { synchronized (triggeredMutex) { if (triggered) { triggeredMutex.wait(); } } waitTime = 0; mutex.notify(); } } catch (InterruptedException ie) { System.err.println("trigger failure"); ie.printStackTrace(System.err); } } public void run() { try { while (!quit) { synchronized (mutex) { //we rely on wait(0) being implemented to wait forever here if (waitTime < 0) { triggered = true; waitTime = 0; } else { long saveWaitTime = waitTime; waitTime = -1; mutex.wait(saveWaitTime); } } try { if (triggered) { callback.trigger(); } } catch (Exception e) { System.err.println( "trigger() threw exception, continuing"); e.printStackTrace(System.err); } finally { synchronized (triggeredMutex) { triggered = false; triggeredMutex.notify(); } } } } catch (InterruptedException ie) { System.err.println("interrupted in run"); ie.printStackTrace(System.err); } } public void quit() { synchronized (mutex) { this.quit = true; mutex.notify(); } } }
The DelayTimer relies on DelayTimerCallback:
public interface DelayTimerCallback { public void trigger(); }
We rely on the correct functioning of this class for the WaitCursorEventQueue, so it is very important that it have a unit test:
import junit.framework.TestCase; public class DelayTimerTest extends TestCase { private static final int TIMEOUT = 100; private static final int BUFFER = 20; private static final int MORE_THAN_HALF = 60; private DelayTimer timer; private TestDelayTimerCallback callback; public DelayTimerTest(String name) { super(name); } public void setUp() { callback = new TestDelayTimerCallback(); timer = new DelayTimer(callback, TIMEOUT); } public void tearDown() { timer.quit(); } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } public void testNotStarted() { sleep(TIMEOUT + BUFFER); assertEquals("no trigger without start()", 0, callback.getTriggerCount()); } public void testNoTriggerIfTooShort() { timer.startTimer(); assertEquals("no trigger if too fast", 0, callback.getTriggerCount()); timer.stopTimer(); sleep(TIMEOUT + BUFFER); assertEquals("no trigger after stop", 0, callback.getTriggerCount()); } public void testTimerRestarts() { timer.startTimer(); sleep(MORE_THAN_HALF); timer.startTimer(); sleep(MORE_THAN_HALF); timer.stopTimer(); assertEquals("timer is restarted on calls to start()", 0, callback.getTriggerCount()); } public void testTimerTriggersThenStops() { timer.startTimer(); sleep(TIMEOUT + BUFFER); timer.stopTimer(); sleep(TIMEOUT + BUFFER); assertEquals("timer triggered event", 1, callback.getTriggerCount()); } public void testTimerOnlyTriggersOneEvent() { timer.startTimer(); sleep(TIMEOUT + BUFFER); assertEquals("timer triggered event", 1, callback.getTriggerCount()); sleep(TIMEOUT + BUFFER); assertEquals("timer did not trigger another event", 1, callback.getTriggerCount()); timer.stopTimer(); } public void testTimerStopsTwice() { timer.startTimer(); timer.stopTimer(); timer.stopTimer(); sleep(TIMEOUT + BUFFER); assertEquals("timer did not trigger event", 0, callback.getTriggerCount()); } public void testTriggers() { timer.startTimer(); sleep(TIMEOUT + BUFFER); timer.stopTimer(); timer.stopTimer(); assertEquals("timer triggered only 1 event", 1, callback.getTriggerCount()); } public void testTriggerTrigger() { timer.startTimer(); sleep(TIMEOUT + BUFFER); assertEquals("timer triggered first event", 1, callback.getTriggerCount()); timer.startTimer(); sleep(TIMEOUT + BUFFER); assertEquals("timer triggered second event", 2, callback.getTriggerCount()); sleep(TIMEOUT + BUFFER); timer.stopTimer(); assertEquals("timer did not trigger another event", 2, callback.getTriggerCount()); } public void testStopTimerHappensAfterTrigger() { FancyTestDelayTimerCallback callback = new FancyTestDelayTimerCallback(); timer = new DelayTimer(callback, TIMEOUT); timer.startTimer(); sleep(TIMEOUT + BUFFER); assertTrue("timer is in trigger", callback.inTrigger); assertTrue("timer has not thrown exception", !callback.exception); assertTrue("timer is not out of trigger", !callback.outOfTrigger); Runnable runnable = new Runnable() { public void run() { timer.stopTimer(); } }; Thread testThread = new Thread(runnable, "test thread"); testThread.start(); sleep(TIMEOUT + BUFFER); assertTrue("stopTimer() has not returned", testThread.isAlive()); synchronized (callback) { callback.notify(); } sleep(TIMEOUT + BUFFER); assertTrue("timer has not thrown exception", !callback.exception); assertTrue("timer is out of trigger", callback.outOfTrigger); assertTrue("stopTimer() has returned", !testThread.isAlive()); } public void testMishMash() { timer.startTimer(); sleep(MORE_THAN_HALF); timer.startTimer(); sleep(MORE_THAN_HALF); timer.stopTimer(); sleep(MORE_THAN_HALF); timer.startTimer(); timer.stopTimer(); timer.stopTimer(); sleep(MORE_THAN_HALF); timer.startTimer(); timer.startTimer(); assertEquals("no event yet", 0, callback.getTriggerCount()); sleep(TIMEOUT + BUFFER); timer.stopTimer(); assertEquals("got event yet", 1, callback.getTriggerCount()); } private class FancyTestDelayTimerCallback implements DelayTimerCallback { public boolean exception; public boolean inTrigger; public boolean outOfTrigger; public synchronized void trigger() { inTrigger = true; try { wait(); } catch (InterruptedException e) { exception = true; e.printStackTrace(); } finally { outOfTrigger = true; } } } private class TestDelayTimerCallback implements DelayTimerCallback { private int triggerCount; public int getTriggerCount() { return triggerCount; } public void trigger() { triggerCount++; } } }
Now that we have seen the DelayTimer, I am going to skip ahead now to WaitCursorEventQueue itself, which is a fairly simple class:
import java.awt.*; /** * Suggested serving size: * Toolkit.getDefaultToolkit().getSystemEventQueue().push(new WaitCursorEventQueue(70)); */ public class WaitCursorEventQueue extends EventQueue implements DelayTimerCallback { private final CursorManager cursorManager; private final DelayTimer waitTimer; public WaitCursorEventQueue(int delay) { this.waitTimer = new DelayTimer(this, delay); this.cursorManager = new CursorManager(waitTimer); } public void close() { waitTimer.quit(); pop(); } protected void dispatchEvent(AWTEvent event) { cursorManager.push(event.getSource()); waitTimer.startTimer(); try { super.dispatchEvent(event); } finally { waitTimer.stopTimer(); cursorManager.pop(); } } public AWTEvent getNextEvent() throws InterruptedException { waitTimer.stopTimer(); //started by pop(), this catches modal dialogs //closing that do work afterwards return super.getNextEvent(); } public void trigger() { cursorManager.setCursor(); } }
The most significant method in this class is dispatchEvent(). CursorManager manages a stack of events (and their sources) so it can handle modal dialogs and set the cursor on the right window.
If a modal dialog opened during the call to super.dispatchEvent(), we will get another call to dispatchEvent(). This will tell the CursorManager that a new event is started and call startTimer() again. We rely on the fact that a second call to startTimer() will restart the timer, resetting the delay. (This way, a modal dialog does not immediately get a wait cursor set.) Once this new event is over, we will stop the timer, which stops it completely, even though startTimer() was called twice. The CursorManager will still have the event that opened the modal dialog on its stack, which is correct, because that event has not finished yet. Once the modal dialog is closed, that event will finish, and take the event off the CursorManager's stack.
The CursorManager class might be more appropriately named "WindowManager" or "EventManager", but none of those names are quite right either, and CursorManager is what it started as, so I will leave it that way. This class' primary job is to assist the WaitCursorEventQueue in managing the events, by handling modal dialogs (recursiveness), filtering events, and helping to manage the WaitTimer. Here is the code:
import java.awt.*; import java.awt.event.InputEvent; import java.util.*; class CursorManager { private final DelayTimer waitTimer; private final Stack dispatchedEvents; private boolean needsCleanup; public CursorManager(DelayTimer waitTimer) { this.dispatchedEvents = new Stack(); this.waitTimer = waitTimer; } private void cleanUp() { if (((DispatchedEvent) dispatchedEvents.peek()).resetCursor()) { clearQueueOfInputEvents(); } } private void clearQueueOfInputEvents() { EventQueue q = Toolkit.getDefaultToolkit().getSystemEventQueue(); synchronized (q) { ArrayList nonInputEvents = gatherNonInputEvents(q); for (Iterator it = nonInputEvents.iterator(); it.hasNext();) q.postEvent((AWTEvent)it.next()); } } private ArrayList gatherNonInputEvents(EventQueue systemQueue) { ArrayList events = new ArrayList(); while (systemQueue.peekEvent() != null) { try { AWTEvent nextEvent = systemQueue.getNextEvent(); if (!(nextEvent instanceof InputEvent)) { events.add(nextEvent); } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } return events; } public void push(Object source) { if (needsCleanup) { waitTimer.stopTimer(); cleanUp(); //this corrects the state when a modal dialog //opened last time round } dispatchedEvents.push(new DispatchedEvent(source)); needsCleanup = true; } public void pop() { cleanUp(); dispatchedEvents.pop(); if (!dispatchedEvents.isEmpty()) { //this will be stopped if getNextEvent() is called - //used to watch for modal dialogs closing waitTimer.startTimer(); } else { needsCleanup = false; } } public void setCursor() { ((DispatchedEvent) dispatchedEvents.peek()).setCursor(); } }
Most of the code is self-explanatory. Only pop() is a bit complicated. Once it has unset the cursor and popped the event, it checks to see if there are any outstanding events on the stack. If so, it knows that there must be a modal dialog open and needs to take special precautions.
Yes, it is a bit heavy-handed to start the timer on every single event when a modal dialog is up - but it works, and it does not actually impose much overhead.
The last interesting class is DispatchedEvent. If you recall, we already know that it represents an event on the EventQueue, and that it can set and unset the cursor on the appropriate window.
import java.awt.*; import javax.swing.SwingUtilities; class DispatchedEvent { private final Object mutex = new Object(); private final Object source; private Component parent; private Cursor lastCursor; public DispatchedEvent(Object source) { this.source = source; } public void setCursor() { synchronized (mutex) { parent = findVisibleParent(); if (parent != null) { lastCursor = (parent.isCursorSet() ? parent.getCursor() : null); parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } } } public boolean resetCursor() { synchronized (mutex) { if (parent != null) { parent.setCursor(lastCursor); parent = null; return true; } return false; } } private Component findVisibleParent() { Component result = null; if (source instanceof Component) { result = SwingUtilities.getRoot((Component) source); } else if (source instanceof MenuComponent) { MenuContainer mParent = ((MenuComponent) source).getParent(); if (mParent instanceof Component) { result = SwingUtilities.getRoot((Component) mParent); } } if ((result != null) && result.isVisible()) { return result; } else { return null; } } }
We have finally gotten through all the code, but how do we know it works? That is where unit testing comes in. Some people argue that you cannot unit test multi-threaded code, but I disagree - you can unit test it, so long as you are willing to live with an occasional false negative. What you do is write the test so that if there is a bug in the algorithm, the unit test will show it. This can be done by writing tests that will pass only if all the threads line up the way you want them to, and all the events happen in the right order and right way.
If you have written a unit test that way then a run of the unit test might go three possible ways. First, it might hang, in which case you know you know there is a bug. Sometimes tracking it down can be extremely difficult, but it can be done. Second, the test might fail. In that case, it is usually a good idea to rerun the test and see if it fails repeatedly. If there really is something broken in the code, then the test should fail consistently. If it does not always fail, then you have to analyze why it is sometimes failing. If there is no bug, but just a race condition in the test, ignore it. If you find a bug, you can write a test that will reliably demonstrate it, and then fix the problem. Finally, all the tests might pass. If that is the case, then you either have working code or not enough tests. If you are not sure if you have enough tests, try going through all the code commenting out one line at a time, and running the tests. Every single commented out line should cause a repeatable failure. [HK: I particularly like the last two sentences, excluding my own. Ok, let us include mine. Make it the last five sentences. :-]
This unit test was designed and built with all of this in mind. Commenting out lines of code causes a test to fail. The tests do occasionally fail unreliably, but every time that has happened, I have been able to understand why. They have never locked up. Finally, two of the tests exist because of bugs found in the initial implementation of this code that the unit tests were occasionally catching.
Note also that this test relies on two timing parameters - how long the delay should be for a trigger, and how long an event should take to ensure that the trigger would have happened. Both of these are configurable constants at the top of the test, and You will probably need to adjust the numbers to your machine/OS/VM.
import java.awt.*; import java.awt.event.*; import java.lang.reflect.InvocationTargetException; import javax.swing.*; import junit.framework.TestCase; public class WaitCursorEventQueueTest extends TestCase { private static final int TIMEOUT = 200; private static final int BUFFER = 30; private static final Cursor BASE_CURSOR = Cursor.getPredefinedCursor( Cursor.CROSSHAIR_CURSOR); private CursorReportingDialog dialog; private CursorReportingDialog dialog2; private CursorReportingFrame frame; private TestWaitCursorEventQueue eventQueue; public WaitCursorEventQueueTest(String name) { super(name); } public void setUp() { eventQueue = new TestWaitCursorEventQueue(TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(eventQueue); frame = new CursorReportingFrame(); frame.pack(); frame.setBounds(-1000, -1000, 100, 100); frame.setVisible(true); dialog = new CursorReportingDialog(frame); dialog.pack(); dialog.setBounds(-1000, -1000, 100, 100); dialog2 = new CursorReportingDialog(dialog); dialog2.pack(); dialog2.setBounds(-1000, -1000, 100, 100); } public void tearDown() throws InvocationTargetException, InterruptedException { flushQueue(); eventQueue.close(); eventQueue = null; flushQueue(); frame.dispose(); frame = null; dialog.dispose(); dialog = null; } private void flushQueue() throws InvocationTargetException, InterruptedException { SwingUtilities.invokeAndWait(new Runnable() { public void run() { } }); } private void postEvent(Object source, Runnable event) { eventQueue.postEvent(new InvocationEvent(source, event)); } private void hangOut(long timeout) { try { Thread.sleep(timeout); } catch (InterruptedException e) { e.printStackTrace(); } } public void testNoCursor() throws InvocationTargetException, InterruptedException { DelayEvent event = new DelayEvent(TIMEOUT - BUFFER); postEvent(frame, event); postEvent(frame, event); postEvent(frame, event); postEvent(frame, event); flushQueue(); assertEquals("no cursor set", 0, frame.getCursorSetCount()); assertEquals("no cursor reset", 0, frame.getCursorResetCount()); } public void testCursor() throws InvocationTargetException, InterruptedException { DelayEvent event = new DelayEvent(TIMEOUT + BUFFER); postEvent(frame, event); flushQueue(); assertEquals("1 cursor set", 1, frame.getCursorSetCount()); assertEquals("1 cursor reset", 1, frame.getCursorResetCount()); } public void testDialog() throws InvocationTargetException, InterruptedException { postEvent(frame, new DialogShowEvent(dialog, true, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); } public void testCursorAndDialog() throws InvocationTargetException, InterruptedException { TestRunnable testAndShow = new TestRunnable() { public void run() { testsPassed &= (1 == frame.getCursorSetCount()); testsPassed &= (0 == frame.getCursorResetCount()); dialog.setVisible(true); } }; postEvent(frame, new DelayEvent(TIMEOUT + BUFFER, testAndShow)); flushQueue(); assertTrue("Delay worked", testAndShow.getTestsPassed()); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset cursor", 1, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame never reset another cursor", 1, frame.getCursorResetCount()); } public void testCursorAndDialogAndCursor() throws InvocationTargetException, InterruptedException { TestRunnable testAndShow = new TestRunnable() { public void run() { testsPassed &= (1 == frame.getCursorSetCount()); testsPassed &= (0 == frame.getCursorResetCount()); dialog.setVisible(true); hangOut(TIMEOUT + BUFFER); } }; postEvent(frame, new DelayEvent(TIMEOUT + BUFFER, testAndShow)); flushQueue(); assertTrue("Delay worked", testAndShow.getTestsPassed()); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset cursor", 1, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame got another cursor", 2, frame.getCursorSetCount()); assertEquals("frame reset another cursor", 2, frame.getCursorResetCount()); } /** * This test checks the condition where the EventDispatchThread * does not call EventQueue.getNextEvent() within TIMEOUT, * (presumably) because of thread contention, even when there * is not a dialog going down. Note that this case only actually * matters if there is a dialog currently up. */ public void testDelayedGetNextEvent() throws InvocationTargetException, InterruptedException { postEvent(frame, new DialogShowEvent(dialog, true, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); eventQueue.setGetDelay(TIMEOUT + BUFFER); postEvent(frame, new DelayEvent(TIMEOUT - BUFFER)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("frame got a cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset a cursor", 1, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("frame did not get another cursor", 1, frame.getCursorSetCount()); assertEquals("frame did not reset another cursor", 1, frame.getCursorResetCount()); } public void testTwoDialogs() throws InvocationTargetException, InterruptedException { TestRunnable testAndShow = new TestRunnable() { public void run() { testsPassed &= (1 == frame.getCursorSetCount()); testsPassed &= (0 == frame.getCursorResetCount()); dialog.setVisible(true); } }; postEvent(frame, new DelayEvent(TIMEOUT + BUFFER, testAndShow)); flushQueue(); assertTrue("Delay worked", testAndShow.getTestsPassed()); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset cursor", 1, frame.getCursorResetCount()); TestRunnable testAndShow2 = new TestRunnable() { public void run() { testsPassed &= (1 == dialog.getCursorSetCount()); testsPassed &= (0 == dialog.getCursorResetCount()); dialog2.setVisible(true); } }; postEvent(dialog, new DelayEvent(TIMEOUT + BUFFER, testAndShow2)); flushQueue(); assertTrue("Delay worked", testAndShow.getTestsPassed()); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got cursor", 0, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset cursor", 0, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 1, dialog.getCursorSetCount()); assertEquals("dialog reset cursor", 1, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset cursor", 1, frame.getCursorResetCount()); postEvent(dialog2, new DelayEvent(TIMEOUT + BUFFER)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 got cursor", 1, dialog2.getCursorSetCount()); assertEquals("dialog2 reset cursor", 1, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 1, dialog.getCursorSetCount()); assertEquals("dialog reset cursor", 1, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame reset cursor", 1, frame.getCursorResetCount()); postEvent(dialog2, new DialogShowEvent(dialog2, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got another cursor", 1, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset another cursor", 1, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 1, dialog.getCursorSetCount()); assertEquals("dialog never reset another cursor", 1, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame never reset another cursor", 1, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got another cursor", 1, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset another cursor", 1, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 1, dialog.getCursorSetCount()); assertEquals("dialog never reset another cursor", 1, dialog.getCursorResetCount()); assertEquals("frame never got another cursor", 1, frame.getCursorSetCount()); assertEquals("frame never reset another cursor", 1, frame.getCursorResetCount()); } public void testCursorAndTwoDialogsAndCursor() throws InvocationTargetException, InterruptedException { postEvent(frame, new DialogShowEvent(dialog, true, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog never got cursor", 0, dialog.getCursorSetCount()); assertEquals("dialog never reset cursor", 0, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); TestRunnable testAndShow = new TestRunnable() { public void run() { testsPassed &= (1 == dialog.getCursorSetCount()); testsPassed &= (0 == dialog.getCursorResetCount()); dialog2.setVisible(true); hangOut(TIMEOUT + BUFFER); } }; postEvent(dialog, new DelayEvent(TIMEOUT + BUFFER, testAndShow)); flushQueue(); assertTrue("Delay worked", testAndShow.getTestsPassed()); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got cursor", 0, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset cursor", 0, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 1, dialog.getCursorSetCount()); assertEquals("dialog reset cursor", 1, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); postEvent(dialog2, new DialogShowEvent(dialog2, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got cursor", 0, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset cursor", 0, dialog2.getCursorResetCount()); assertEquals("dialog got another cursor", 2, dialog.getCursorSetCount()); assertEquals("dialog reset another cursor", 2, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); postEvent(dialog, new DialogShowEvent(dialog, false, 0)); flushQueue(); hangOut(TIMEOUT + BUFFER); assertEquals("dialog2 never got cursor", 0, dialog2.getCursorSetCount()); assertEquals("dialog2 never reset cursor", 0, dialog2.getCursorResetCount()); assertEquals("dialog never got another cursor", 2, dialog.getCursorSetCount()); assertEquals("dialog never reset another cursor", 2, dialog.getCursorResetCount()); assertEquals("frame never got cursor", 0, frame.getCursorSetCount()); assertEquals("frame never reset cursor", 0, frame.getCursorResetCount()); } private class CursorReportingDialog extends Dialog { private int cursorResetCount; private int cursorSetCount; public CursorReportingDialog(Frame owner) { super(owner, true); init(); } public CursorReportingDialog(Dialog owner) { super(owner, "", true); init(); } private void init() { setCursor(BASE_CURSOR); this.cursorSetCount = 0; this.cursorResetCount = 0; } public int getCursorSetCount() { return cursorSetCount; } public int getCursorResetCount() { return cursorResetCount; } public void setCursor(Cursor cursor) { super.setCursor(cursor); if (BASE_CURSOR.equals(cursor)) { cursorResetCount++; } else { cursorSetCount++; } } } private class CursorReportingFrame extends Frame { private int cursorResetCount; private int cursorSetCount; public CursorReportingFrame() { super(); setCursor(BASE_CURSOR); cursorSetCount = 0; cursorResetCount = 0; } public int getCursorSetCount() { return cursorSetCount; } public int getCursorResetCount() { return cursorResetCount; } public void setCursor(Cursor cursor) { super.setCursor(cursor); if (BASE_CURSOR.equals(cursor)) { cursorResetCount++; } else { cursorSetCount++; } } } private abstract class TestRunnable implements Runnable { protected boolean testsPassed = true; public boolean getTestsPassed() { return testsPassed; } } private class DelayEvent implements Runnable { private Runnable callback; private int delay; public DelayEvent(int delay) { this(delay, null); } public DelayEvent(int delay, Runnable callback) { this.delay = delay; this.callback = callback; } public void run() { try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } if (callback != null) { callback.run(); } } } private class DialogShowEvent implements Runnable { private Dialog dialog; private boolean visible; private int delay; public DialogShowEvent(Dialog dialog, boolean visible, int delay) { this.dialog = dialog; this.visible = visible; this.delay = delay; } public void run() { dialog.setVisible(visible); if (delay > 0) { hangOut(delay); } } } private class TestWaitCursorEventQueue extends WaitCursorEventQueue { private int getDelay; public TestWaitCursorEventQueue(int delay) { super(delay); } public AWTEvent getNextEvent() throws InterruptedException { if (getDelay > 0) { hangOut(getDelay); getDelay = 0; } return super.getNextEvent(); } public void setGetDelay(int getDelay) { this.getDelay = getDelay; } } }
Two types of events are posted to the queue - DelayEvents and DialogShowEvents. DelayEvents are simply events that take some period of time to complete. They can also be configured to call a Runnable callback, to trigger other evaluations or other events. DialogShowEvents either show or hide a dialog, and can also take some period of time after the dialog is shown/hidden to complete. Instances of these two events are passed to postEvent(), along with an event source, and are wrapped in an InvocationEvent and posted to the event queue. Different combinations of these events make up most of the differences between the individual tests.
There are also two GUI classes - CursorReportingDialog and CursorReportingFrame. They will both track cursor sets and resets, and report them to tests that ask. They are usually found in assert() statements.
Finally, you should check out setUp(), tearDown(), flushQueue(), and hangOut(). Past those, everything else is just a test method.
When this code was first written, I did nothing to examine its performance, so I did not really know how efficient it was. However, that is telling - I have never noticed (as a user) any slowdown because of it. Nevertheless, I have received a question about it (thanks Herman!), which made me realize that I had not examined it. Therefore, I took some time and wrote a unit test that shows the performance numbers for this code. I wrote it as a unit test, just to make it easy to run and in case I ever wanted to really codify what "good performance" meant for this code.
import java.awt.*; import java.awt.event.*; import java.util.Random; import javax.swing.*; import junit.framework.TestCase; public class WaitCursorEventQueuePerformanceTest extends TestCase { private static final long FAST = 5; private static final long SLOW = 50; private static final long MIXED = -1; private static final long TIMEOUT = 15; private Dialog dialog; private Frame frame; public WaitCursorEventQueuePerformanceTest(String name) { super(name); } protected void setUp() throws Exception { frame = new Frame(); frame.pack(); frame.setBounds(-1000, -1000, 100, 100); frame.setVisible(true); dialog = new Dialog(frame, true); dialog.pack(); dialog.setBounds(-1000, -1000, 100, 100); } protected void tearDown() throws Exception { frame.dispose(); dialog.dispose(); } private long postEvents(long time) throws InterruptedException { InvocationEvent repeatEvent = new InvocationEvent( frame, new TimedEvent(time)); InvocationEvent finalEvent = new InvocationEvent( frame, new TimedEvent(time), this, false); EventQueue q = Toolkit.getDefaultToolkit().getSystemEventQueue(); long startTime = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) q.postEvent(repeatEvent); synchronized (this) { q.postEvent(finalEvent); wait(); //we will be notified by finalEvent when it gets posted } long endTime = System.currentTimeMillis(); return (endTime - startTime); } public void testNormalPerformanceWithFastEvents() throws InterruptedException { System.out.println("\nnormal with fast: " + postEvents(FAST)); } public void testNormalPerformanceWithSlowEvents() throws InterruptedException { System.out.println("\nnormal with slow: " + postEvents(SLOW)); } public void testNormalPerformanceWithMixedEvents() throws InterruptedException { System.out.println("\nnormal with random: " + postEvents(MIXED)); } public void testWaitQueuePerformanceWithFastEvents() throws InterruptedException { WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with fast: " + postEvents(FAST)); waitQueue.close(); } public void testWaitQueuePerformanceWithSlowEvents() throws InterruptedException { WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with slow: " + postEvents(SLOW)); waitQueue.close(); } public void testWaitQueuePerformanceWithMixedEvents() throws InterruptedException { WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with random: " + postEvents(MIXED)); waitQueue.close(); } public void testWaitQueuePerformanceWithDialogWithFastEvents() throws InterruptedException { SwingUtilities.invokeLater(new Runnable() { public void run() { dialog.setVisible(true); } }); WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with dialog with fast: " + postEvents(FAST)); waitQueue.close(); } public void testWaitQueuePerformanceWithDialogWithSlowEvents() throws InterruptedException { SwingUtilities.invokeLater(new Runnable() { public void run() { dialog.setVisible(true); } }); WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with dialog with slow: " + postEvents(SLOW)); waitQueue.close(); } public void testWaitQueuePerformanceWithDialogWithMixedEvents() throws InterruptedException { SwingUtilities.invokeLater(new Runnable() { public void run() { dialog.setVisible(true); } }); WaitCursorEventQueue waitQueue = new WaitCursorEventQueue( (int) TIMEOUT); Toolkit.getDefaultToolkit().getSystemEventQueue() .push(waitQueue); System.out.println("\nwait with dialog with random: " + postEvents(MIXED)); waitQueue.close(); } private class TimedEvent implements Runnable { private Random random; private long time; public TimedEvent(long time) { this.time = time; if (time == MIXED) { random = new Random(1000); } } public void run() { try { if (time == MIXED) { Thread.sleep( (long) (random.nextDouble() * (SLOW - FAST) + FAST)); } else { Thread.sleep(time); } } catch (InterruptedException e) { e.printStackTrace(); } } } }
I will leave the analysis of this code up to you, and I will admit that it is not a perfect performance test, but it is good enough for these purposes. You will notice that there are four basic tunable parameters - how long a fast event takes, how long a slow event takes, how long the timer on the WaitCursorEventQueue is, and how many events are used for each test. With the numbers shown here, on my machine, after a reboot, with no other activity, I get:
. normal with fast: 5118 . normal with slow: 50772 . normal with random: 27629 . wait with fast: 5168 . wait with slow: 50132 . wait with random: 27660 . wait with dialog with fast: 5127 . wait with dialog with slow: 50173 . wait with dialog with random: 27720 Time: 251.963 OK (9 tests)
There are three sets of tests - the normal EventQueue, the WaitCursorEventQueue, and the WaitCursorEventQueue with a modal dialog up (because of the extra processing done while modal dialogs are up). Each set checks the time for 1000 fast events, 1000 slow events, and a series of 1000 random-length events. (Note that because the Random object is seeded with the same number every time, all three random sequences will be the same.)
These results are not significant. Running the test repeatedly gives small variations in the numbers, but there is not any repeatable slowdown demonstratable with the WaitCursorEventQueue. Happily, this confirms my subjective experiences.
This code was hard work, and I did not do it all myself. I did it for my company (who has given me permission to publish it), and I had help. I would like to thank Jason Trump for brainstorming, debugging, and sorting out threading logic. I would also like to thank Ben Schroeder for being so instrumental in teaching me threading so that I could understand it well, years ago.
Well, that's it! You now have an automatic wait cursor manager, which you can install and use in any java program you like. You should not have to think about the wait cursor ever again! I hope this article was clear enough to make it understandable, and not too long. If you have questions, please feel free to email me at truist-waitcursor@truist.com.
Enjoy!
Nathan
Well, I certainly enjoyed editing this newsletter :-) Thanks Nathan, that was a really useful newsletter...
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.