Tips 'N Tricks
Java Tip 87: Automate the hourglass cursor
Force Java's UI event queue to decide when to show the hourglass cursor
Summary
Applications have long used the hourglass cursor to visually inform users that a task may take longer than usual. Predicting a task's processing time, and hence knowing when to display an hourglass cursor, is becoming increasingly difficult because of the ever widening performance range of traditional desktop PCs. For network-enabled applications, the guesswork is further complicated by the unknown variable of network latency. This tip eliminates the guesswork by automating the hourglass cursor display whenever processing an event exceeds a predetermined length of time. (1,800 words)
By Kyle Davis
Java Tips
|
|
For a comprehensive list of Java Tips published in
JavaWorld, see the
Java Tips Index
Do you have a tip that would benefit JavaWorld's
readers? We would like to pass it on! Submit your tip to
javatips@javaworld.com.
|
udging the amount of time required to execute a given task in a development process is nearly impossible. Executing the task on the actual target machine, however, lets you actively monitor how long each task takes. Then you simply measure the execution time and change the cursor to an hourglass when necessary. Fortunately, most tasks for which you typically display the hourglass cursor are executed during the processing of user interface (UI) events. Placing code within the stream of UI events enables you to measure processing times and automate the hourglass as desired.
The solution
By extending the capabilities of Java's UI event dispatch queue, you can easily insert your own timing mechanism into the UI events stream. The event queue distributes every UI event that occurs in an application. By replacing the standard event queue with your own, you can intercept every event passing through the queue. Your queue can then measure the time it takes to process each event and change the cursor if needed.
In this article, I will provide the full source code to accomplish this tip. But first, let's examine the solution in more detail. You first create an event queue class that extends the standard java.awt.EventQueue class. The new event queue contains a special timer thread for measuring the processing time for each event. It starts the timer before dispatching an event. Then, if the event is not processed within your predetermined time limit, the timer thread changes the cursor to an hourglass. After processing is complete, the queue changes the cursor back to the default cursor if necessary.
Below you will find the implementation of the dispatchEvent() method for the new event queue.
14: protected void dispatchEvent(AWTEvent event) {
15: waitTimer.startTimer(event.getSource());
16: try {
17: super.dispatchEvent(event);
18: }
19: finally {
20: waitTimer.stopTimer();
21: }
22: }
All UI events go through the EventQueue.dispatchEvent() method. This method delivers the events to the correct UI components, which then process the events. As you can see, the queue overrides the default behavior of dispatchEvent() and wraps the standard functionality of the EventQueue class in your timing routines.
Line 15 notifies your timing thread that an event is about to be dispatched so that it will begin measuring the event's execution time. Note that you are passing the event's source -- the object to which this event will eventually be delivered. If the timing thread determines that the event is taking too long to process, it will need to know the event's source to determine which java.awt.Window 's cursor should be changed to the hourglass.
Line 20 stops the timer and, if necessary, changes the cursor back to the default cursor. A try -finally block wraps the event dispatch because the stopTimer() method must be executed regardless of whether an exception is thrown during event processing.
The startTimer() method is very straightforward. It simply stores a reference to the event's source and notifies the timer thread.
29: synchronized void startTimer(Object source) {
30: this.source = source;
31: notify();
32: }
Analyzing the timer thread
Now look at the implementation of the run() method for the timer thread, the real workhorse of the thread. First note that the entire run() method is synchronized, as are both the startTimer() and stopTimer() methods. This synchronization prevents multithreading race conditions by letting the startTimer() or stopTimer() methods execute only when the run() method enters either of the two wait() statements to release the monitor. After the timing thread starts, it waits indefinitely at line 47 to be notified and awakened by the startTimer() method.
46: //wait for notification from startTimer()
47: wait();
After receiving notification from the startTimer() method, the timing thread pauses again at the next statement.
49: //wait for event processing to reach the threshold, or
50: //interruption from stopTimer()
51: wait(delay);
Here, the thread waits for the specified length of time to pass. Once it reaches this statement, there are only two possible paths of execution. The event currently being dispatched by your event queue may return from processing and interrupt the thread, at which point an InterruptedException will be thrown and will force the thread to wrap around to the top of the loop and wait for the next event. However, if processing takes longer than your specified delay period, your timer thread will awaken on its own and continue executing the following code.
53: if (source instanceof Component)
54: parent =
SwingUtilities.getRoot((Component)source);
55: else if (source instanceof MenuComponent) {
56: MenuContainer mParent =
57: ((MenuComponent)source).getParent();
58: if (mParent instanceof Component)
59: parent = SwingUtilities.getRoot(
60: (Component)mParent);
61: }
62:
63: if (parent != null && parent.isShowing())
64: parent.setCursor(
65: Cursor.getPredefinedCursor(
66: Cursor.WAIT_CURSOR));
Here, you first determine whether the source is a java.awt.Component or a java.awt.MenuComponent , both of which indicate that you can eventually locate a java.awt.Window whose cursor you need to change. Note that you must handle a java.awt.MenuComponent as a special case because it does not extend java.awt.Component like the other UI classes in the AWT (Abstract Windowing Toolkit) package. Then a Swing utility method locates the window that contains the source component. Once you have that parent window, you can change that window's cursor to an hourglass. You can call the Component.setCursor() method outside the normal event-dispatch thread because, as a quick perusal of the java.awt.Component source code will reveal, setCursor() is a synchronized method.
You may notice that this timing thread waits until the last moment before trying to find the source object's parent window. Although it may seem reasonable to find the parent within the startTimer() method, this particular implementation enjoys a performance advantage. Hundreds of events can flow through your queue if you simply move the mouse cursor around a window, so you want to introduce as little processing overhead as possible from your event queue. You can accomplish this by performing extra processing only when absolutely necessary, including finding the source object's parent window. In your timer thread, you need to find the parent window only if the specified delay passes and you are forced to display the hourglass cursor.
Meanwhile, back in your event queue, processing of the dispatched event will eventually be completed (hopefully!), at which point the stopTimer() method will be invoked.
34: synchronized void stopTimer() {
35: if (parent == null)
36: interrupt();
37: else {
38: parent.setCursor(null);
39: parent = null;
40: }
41: }
If the parent instance variable for your timer thread is still null, then you can be certain that the timer has not yet changed the cursor to an hourglass. In this situation, you are required to interrupt only the timer thread, which will throw an InterruptedException within the thread. Execution of the run() method will then immediately drop to the catch block near the bottom of the loop, at which point it will loop back to the wait() statement and wait for notification of the next event.
Now, to use your queue, you just need to replace the standard event queue with an instance of the special WaitCursorEventQueue , which can be accomplished with the following few lines of code.
EventQueue waitQueue = new WaitCursorEventQueue(500);
Toolkit.getDefaultToolkit().getSystemEventQueue().push(waitQueue);
Part of the trick to this tip is choosing the duration after which the hourglass should appear. The delay should be long enough that the hourglass will not appear for most events processed within the UI thread, but it should be short enough so that the user perceives near-immediate feedback when a more intensive task begins processing. In moderate usability testing, a delay in the range of about 500 milliseconds, as shown above, appears to work quite well.
Full code listing
Below is the complete source code for WaitCursorEventQueue.java .
1: import java.awt.*;
2: import java.awt.event.*;
3: import javax.swing.SwingUtilities;
4:
5: public class WaitCursorEventQueue extends EventQueue {
6:
7: public WaitCursorEventQueue(int delay) {
8: this.delay = delay;
9: waitTimer = new WaitCursorTimer();
10: waitTimer.setDaemon(true);
11: waitTimer.start();
12: }
13:
14: protected void dispatchEvent(AWTEvent event) {
15: waitTimer.startTimer(event.getSource());
16: try {
17: super.dispatchEvent(event);
18: }
19: finally {
20: waitTimer.stopTimer();
21: }
22: }
23:
24: private int delay;
25: private WaitCursorTimer waitTimer;
26:
27: private class WaitCursorTimer extends Thread {
28:
29: synchronized void startTimer(Object source) {
30: this.source = source;
31: notify();
32: }
33:
34: synchronized void stopTimer() {
35: if (parent == null)
36: interrupt();
37: else {
38: parent.setCursor(null);
39: parent = null;
40: }
41: }
42:
43: public synchronized void run() {
44: while (true) {
45: try {
46: //wait for notification from startTimer()
47: wait();
48:
49: //wait for event processing to reach the threshold, or
50: //interruption from stopTimer()
51: wait(delay);
52:
53: if (source instanceof Component)
54: parent =
SwingUtilities.getRoot((Component)source);
55: else if (source instanceof MenuComponent) {
56: MenuContainer mParent =
57: ((MenuComponent)source).getParent();
58: if (mParent instanceof Component)
59: parent = SwingUtilities.getRoot(
60: (Component)mParent);
61: }
62:
63: if (parent != null && parent.isShowing())
64: parent.setCursor(
65: Cursor.getPredefinedCursor(
66: Cursor.WAIT_CURSOR));
67: }
68: catch (InterruptedException ie) { }
69: }
70: }
71:
72: private Object source;
73: private Component parent;
74: }
75: }
Potential setbacks
You should be aware of a few caveats. If your application manually changes the cursor for an entire window, especially while processing UI events, the new event queue will likely cause unexpected results, such as changing the cursor you specified back to the default. This tip will also not work in an applet in the Java Virtual Machines included with currently available Web browsers because of the applet's reliance on JDK 1.2 features. Using Sun's Java plug-in provides the necessary JDK 1.2 compliance, but Toolkit.getSystemEventQueue() is a security-checked method so this tip precludes the use of unsigned applets. It should also be noted that a few third-party tools modify the event queue and could possibly interfere with the operation of the WaitCursorEventQueue . With a few minor enhancements, the WaitCursorEventQueue could even let you disable the hourglass automation at will or for selected event types.
The implementation I've provided offers a simple solution adequate for many applications and, I hope, gives an understandable explanation of Java's event queue processing. Maybe you will never have to think about that hourglass again.
|
|
About the author
Kyle Davis has a BS in computer science and works as a software engineer with ChannelPoint, a Colorado-based business-to-business provider of ecommerce technology for the insurance industry. Kyle has seven years' experience in the software industry, including more than two years of Java development. He has been exposed to an array of technologies used everywhere from the government sector to large corporate enterprises and small start-ups.
|
|