Saturday, March 31, 2007

Display Busy Cursor in Java

Most guides and books will tell you that it is very easy to change the cursor in a Java application. For example, the following fragment of code sets the cursor to a busy cursor while it performs some processing, and then returns the cursor to the default cursor again afterwards.


import java.awt.Cursor;

// Setting cursor for any Component:
component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
doProcessing();
component.setCursor(Cursor.getDefaultCursor());

This is a good start but, somewhat surprisingly, this short code fragment contains a bug. The problem is that the main processing method might throw an exception. If the exception is not caught within the processing method, then the Java Virtual Machine unwinds the call stack repeatedly until it finds a try context that will catch the given exception. In other words, the flow of control may jump out of this (apparently) linear flow of control and we therefore cannot guarantee that the default cursor is restored. In short, if an exception is thrown, the busy cursor will remain there indefinitely!

Restoring the Cursor
Thankfully, there is an easy way out of this. Java's finally clause is guaranteed to be executed even when an exception is thrown and not caught in the current scope.

Therefore the following code fragment is guaranteed to restore the cursor to its default, regardless of whether the processing method terminates normally or throws an exception:

try {
component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
doProcessing();
} finally {
component.setCursor(Cursor.getDefaultCursor());
}

This is much better, but there remains a problem in that we have to remember to use the same idiom whenever we want to control the cursor. We are also mixing the cursor control code with the main 'business logic' of the processing. Wouldn't it be great if the cursor control could be factored out as a separate concern?

A Cursor Controller Class
I am now going to assume that the occasions when we would wish to display the busy cursor are all instigated by a user action. Therefore, our Java application will already have a java.awt.event.ActionListener defined to deal with the business logic for each of those actions. All that we have to do is somehow 'sandwich' the ActionListener between some additional code that sets and restores the cursor. The way we can do this is to write a method that accepts an ActionListener as input and returns a modified ActionListener as its output. The modified ActionListener takes care of cursor control, but also performs the same action processing as the original ActionListener.

This approach is implemented by the following class, which has a single public (and static) method.

import java.awt.Component;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public final class CursorController {

public final static Cursor busyCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
public final static Cursor defaultCursor = Cursor.getDefaultCursor();

private CursorController() {}

public static ActionListener createListener(final Component component, final ActionListener mainActionListener) {
ActionListener actionListener = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
try {
component.setCursor(busyCursor);
mainActionListener.actionPerformed(ae);
} finally {
component.setCursor(defaultCursor);
}
}
};
return actionListener;
}
}

The idea is that in your main GUI code, you would write something like the following:

class MyApplication extends JFrame {
...
JButton button = new JButton("Do It!");
ActionListener doIt = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
doProcessing();
}
};
ActionListener cursorDoIt = CursorController.createListener(this, doIt);
button.addActionListener(cursorDoIt);
...
}

This may seem like a lot of effort, but it will save effort in the long run as all the control for the busy cursor is contained in one place. That means, for example, it is easy to add some code to time the execution of all actions involving the busy cursor and to send those timings to a log file. With this approach I need only write this additional handling code once in the source code, instead of having to add it to all the places in the code where intensive action processing is performed.

A Delayed Busy Cursor
Another obvious improvement is to wait a little while before changing the busy cursor. There are two reasons for doing this. Firstly, we can never be sure how long it will be before an action is processed. An action that is normally processed within milliseconds might be held up by a garbage collection in the Java Virtual Machine, or some other process on the same machine that is deemed to have a higher priority. Therefore it would be a good idea to divert all actions through a busy cursor controller, even if some of them usually execute very quickly. Secondly, if we divert all actions through our controller, we do not want to constantly change the cursor to busy and then back to its default again; at least, not to the point that the cursor appears to be "twitchy" and is irritating for the user. It is therefore a good idea to divert all actions through a cursor controller, but to set a delay period. If the processing has not finished at the end of the delay period, then we display the busy cursor until processing has finished. This approach is sufficient to eliminate "twitchy" behaviour of the cursor and provides for a consistent user-interface with a good level of user feedback.

The following is an enhanced version of CursorController that implements that strategy. As before, we supply an ActionListener to the createListener() method, and it returns an ActionListener. The difference is that this time, the ActionListener uses a Timer to wait for a fixed time interval before displaying a busy cursor. If it takes less than half a second (500 milliseconds) to service the user action, then the busy cursor is not displayed.

import java.awt.Component;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.util.Timer;
import java.util.TimerTask;

public class CursorController {
public static final Cursor busyCursor = new Cursor(Cursor.WAIT_CURSOR);
public static final Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR);
public static final int delay = 500; // in milliseconds

private CursorController() {}

public static ActionListener createListener(final Component component, final ActionListener mainActionListener) {
ActionListener actionListener = new ActionListener() {
public void actionPerformed(final ActionEvent ae) {

TimerTask timerTask = new TimerTask() {
public void run() {
component.setCursor(busyCursor);
}
};
Timer timer = new Timer();

try {
timer.schedule(timerTask, delay);
mainActionListener.actionPerformed(ae);
} finally {
timer.cancel();
component.setCursor(defaultCursor);
}
}
};
return actionListener;
}
}

No comments: