Wait, Cursor, Wait! (Java)

What is the intent of wait cursors? To tell the user: "Hey, everywhere you see a wait cursor you can't do nothing." Of course having some progress indication while the application is busy with a long operation is also a good idea. (Law 1 concerning GUIs: The GUI should ALWAYS be responsive, even if it is only indicating how busy the application is.) I recently discovered a nice class to use for feedback on long operations: javax.swing.ProgressMonitor. However, most of the time we need a wait cursor (also known as an hourglass cursor).
Most of you probably already know how to use wait cursors in Swing, but let me go ahead and give an example of a useful CursorToolkitOne class, implementing an interface for the constants:

import java.awt.*;

public interface Cursors {
  Cursor WAIT_CURSOR = 
    Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
  Cursor DEFAULT_CURSOR = 
    Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);  
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** Basic CursorToolkit that still allows mouseclicks */
public class CursorToolkitOne implements Cursors {
  private CursorToolkitOne() { }

  /** Sets cursor for specified component to Wait cursor */
  public static void startWaitCursor(JComponent component) {
    RootPaneContainer root = 
      (RootPaneContainer)component.getTopLevelAncestor();
    root.getGlassPane().setCursor(WAIT_CURSOR);
    root.getGlassPane().setVisible(true);
  }

  /** Sets cursor for specified component to normal cursor */
  public static void stopWaitCursor(JComponent component) {
    RootPaneContainer root = 
      (RootPaneContainer)component.getTopLevelAncestor();
    root.getGlassPane().setCursor(DEFAULT_CURSOR);
    root.getGlassPane().setVisible(false);
  }

  public static void main(String[] args) {
    final JFrame frame = new JFrame("Test App");
    frame.getContentPane().add(
      new JLabel("I'm a Frame"), BorderLayout.NORTH);
    frame.getContentPane().add(
      new JButton(new AbstractAction("Wait Cursor") {
        public void actionPerformed(ActionEvent event) {
          System.out.println("Setting Wait cursor on frame");
          startWaitCursor(frame.getRootPane());
        }
      }));
    frame.setSize(800, 600);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.show();
  }
}
CursorToolkitOne only has two class methods: startWaitCursor(...) and stopWaitCursor(...). You pass a JComponent as parameter, and each method finds the RootPaneContainer (i.e. the uppermost container that contains the component) and then sets the Cursor of the GlassPane of this container to either the default or the wait cursor. It then sets this GlassPane's visibility to true or false. As easy as pie, or is it? What happens if we run the main method?
A JFrame is displayed, with a label and a single button. Pressing the button results in the wait cursor being set on the frame via the startWaitCursor method. This part is still fine, but what happens if you click on the button again? Unfortunately the button's action is performed again (you can see the extra System.out.println). So we have a wait cursor, but is does not actually stop the input.
So let's upgrade to CursorToolkitTwo:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** Basic CursorToolkit that swallows mouseclicks */
public class CursorToolkitTwo implements Cursors {
  private final static MouseAdapter mouseAdapter = 
    new MouseAdapter() {};

  private CursorToolkitTwo() {}

  /** Sets cursor for specified component to Wait cursor */
  public static void startWaitCursor(JComponent component) { 
    RootPaneContainer root =
      ((RootPaneContainer) component.getTopLevelAncestor()); 
    root.getGlassPane().setCursor(WAIT_CURSOR);
    root.getGlassPane().addMouseListener(mouseAdapter);
    root.getGlassPane().setVisible(true);
  }

  /** Sets cursor for specified component to normal cursor */
  public static void stopWaitCursor(JComponent component) { 
    RootPaneContainer root =
      ((RootPaneContainer) component.getTopLevelAncestor()); 
    root.getGlassPane().setCursor(DEFAULT_CURSOR);
    root.getGlassPane().removeMouseListener(mouseAdapter);
    root.getGlassPane().setVisible(false);
  }

  public static void main(String[] args) {
    final JFrame frame = new JFrame("Test App");
    frame.getContentPane().add(
      new JLabel("I'm a Frame"), BorderLayout.NORTH);
    frame.getContentPane().add(
      new JButton(new AbstractAction("Wait Cursor") {
        public void actionPerformed(ActionEvent event) {
          System.out.println("Setting Wait cursor on frame");
          startWaitCursor(frame.getRootPane());
        }
      }));
    frame.setSize(800, 600);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.show();
  }
}
We added a MouseAdapter that does nothing to the GlassPane and this prevents any MouseEvents from getting through to the underlying components. Why does it work this way? Well, that's a topic for another discussion.

Wait Cursors and Modal dialogs...

However, the original question was not only about wait cursors, but their use with modal dialogs, and specifically the parent frame or window of the modal dialog.
The intent of a modal dialog is to deny access to any other part of the application GUI, while retaining access to the dialog. So what happens if the dialog initiates a background operation that takes long to execute (GUI Law 2: never execute a potential long operation from the main GUI thread), and you need to indicate with a wait cursor that the dialog GUI is off limits for the moment? Easy: use CursorToolkitTwo.startWaitCursor to set the wait cursor on the dialog.
And then one day when a client is playing with his mouse while waiting for the wait cursor to disappear (since there is no progress indication because of tight deadlines), he sees that the cursor changes back to the default cursor when he moves the mouse out of the dialog unto the main frame of the application. I can already see the Problem Report: "No wait cursor is shown while the application is busy; only the dialog has a wait cursor, but mouse-clicks have no effect even though there is no wait cursor."
How can we fix this gross enfringement of human rights?

Solution 1

Hey, this should be simple: I only have to get the parent of the modal dialog (in most cases a JFrame), and set the wait cursor on it:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** First attempt at a solution */
public class SolutionOne {
  private static JDialog createDialog(final JFrame frame) {
    final JDialog dialog =new JDialog(frame, "I'm Modal", true);
    dialog.getContentPane().add(
      new JLabel("I'm a busy modal dialog"));
    dialog.getContentPane().add(
      new JButton(new AbstractAction("Wait Cursor") {
        public void actionPerformed(ActionEvent event) {
          setWaitCursor(dialog);
        }
      }));
    dialog.setSize(300, 200);
    return dialog;
  }

  public static void setWaitCursor(JDialog dialog) {
    System.out.println("Setting Wait cursor on frame");
    CursorToolkitTwo.startWaitCursor(
        ((JFrame)dialog.getOwner()).getRootPane());
    System.out.println("Setting Wait cursor on dialog");
    CursorToolkitTwo.startWaitCursor(dialog.getRootPane());
  }

  public static void main(String[] args) {
    final JFrame frame = new JFrame("Solution One");
    frame.getContentPane().add(
      new JLabel("I'm a Frame"), BorderLayout.NORTH);
    frame.getContentPane().add(
      new JButton(new AbstractAction("Show Dialog") {
        public void actionPerformed(ActionEvent event) {
          System.out.println("Showing dialog");
          createDialog(frame).show();
        }
      }));
    frame.setSize(800, 600);
    frame.setVisible(true);
  }
}
I'm not going to discuss all the Swing code; basically a JFrame is created that contains a button. When pressed, this button will show a dialog that contains a button. If this dialog button is pressed, then the setWaitCursors method will be called, which attempts to set the wait cursor on both the dialog and it's parent frame by using our CursorToolkitTwo.
Run it. Press the buttons. It doesn't work :-(. Yes, the wait cursor is set on the dialog, but not on the frame behind it.
Why not?! Well, as soon as a modal dialog is displayed, the current AWT event pump (the mechanism that handles mouse, keyboard and other events) is blocked, and a new event pump is started. As soon as the modal dialog is closed, the previous event pump is unblocked. This means that if the modal dialog in SolutionOne is closed, the wait cursor will suddenly be set on the frame. "Betterlate than never" they say, but this is an example of "better never than late" :-)

Solution 2

There are a few ways around this. If you have access to the code that calls the dialog, but not to the dialog code, you can try to first set the wait cursor on the dialog's parent (the JFrame in our example) before displaying the dialog.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** 
 * This is a second attempt - but native code changes the 
 * cursor back to the default cursor.
 */
public class SolutionTwo {
  private static JDialog createDialog(final JFrame frame) {
    final JDialog dialog =new JDialog(frame, "I'm Modal", true);
    dialog.getContentPane().add(
      new JLabel("I'm a busy modal dialog"));
    dialog.getContentPane().add(
      new JButton(new AbstractAction("Wait Cursor"){
        public void actionPerformed(ActionEvent event) {
          setWaitCursor(dialog);
        }
      }));           
    dialog.setSize(300, 200);
    return dialog;
  }

  private static void setWaitCursor(final JDialog dialog) {
    System.out.println("Setting Wait cursor on dialog");
    CursorToolkitTwo.startWaitCursor(dialog.getRootPane());
  }

  public static void main(String[] args) {
    final JFrame frame = new JFrame("Solution Two");
    frame.getContentPane().add(
      new JLabel("I'm a Frame"), BorderLayout.NORTH);  
    frame.getContentPane().add(
      new JButton(new AbstractAction("Show Dialog") {
        public void actionPerformed(ActionEvent event){
          System.out.println("Setting Wait cursor on frame");
          CursorToolkitTwo.startWaitCursor(frame.getRootPane());
          System.out.println("Showing dialog");
          createDialog(frame).show();
        }
      }));
    frame.setSize(800, 600);
    frame.show();
  }
}
SolutionTwo is similar to SolutionOne, except for the changes listed above. The setWaitCursor method now only sets the wait cursor on the dialog, but in the showDialogAction.actionPerformed method, we set the wait cursor on the frame before showing the dialog.
Run it.
Another failure :-(
Why does this not work? We've set the wait cursor before the main event pump got blocked, and apparently Swing (or AWT) resets the cursor to the default cursor on the rest of the components (i.e. everything except the modal component). You can check the JDC Bug Parade (bug nr 4282540) for their reasons why this is so.

Solution 3

Well, the easy way to fix this (if you have access to the dialog code) is to make the dialog non-modal, but then set the wait cursor on the frame before showing the dialog. It prevents access to the frame, while allowing the wait cursor to be set, and also allows the wait cursor to be set on the dialog. You then basically have SolutionTwo, but a non-modal dialog is now created.
Here is a WaitEnabledDialog that automatically sets the JFrame cursor to an hourglass whenever the dialog is opened and resets it to the default cursor when the dialog closes:
import java.awt.event.*;
import javax.swing.*;

public class WaitEnabledDialog extends JDialog {
  public WaitEnabledDialog(final JFrame owner, String title) {
    super(owner, title, false);
    addWindowListener(new WindowAdapter() {
      public void windowOpened(WindowEvent e) {
        CursorToolkitTwo.startWaitCursor(owner.getRootPane());
      }
      public void windowClosing(WindowEvent e) {
        CursorToolkitTwo.stopWaitCursor(owner.getRootPane());
      }
    });
  }
}
Our third solution now looks like so:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * Here is a solution where we make the modal dialog non-modal.
 * Since we disable mouse clicks on the frame, it is actually
 * the same as a modal dialog.
 */
public class SolutionThree {
  private static JDialog createDialog(final JFrame frame) {
    final WaitEnabledDialog dialog = 
      new WaitEnabledDialog(frame, "I'm not Modal");
    dialog.getContentPane().add(
      new JLabel("I'm a busy non-modal dialog"));
    dialog.getContentPane().add(
      new JButton(new AbstractAction("Wait Cursor") {
        public void actionPerformed(ActionEvent event){
          setWaitCursor(dialog);
        }
      }));
    dialog.setSize(300, 200);
    return dialog;
  }

  public static void setWaitCursor(final JDialog dialog) {
    System.out.println("Setting Wait cursor on dialog");
    CursorToolkitTwo.startWaitCursor(dialog.getRootPane());
  }

  public static void main(String[] args) {
    final JFrame frame = new JFrame("Solution Three");
    frame.getContentPane().add(
      new JLabel("I'm a Frame"), BorderLayout.NORTH);
    frame.getContentPane().add(
      new JButton(new AbstractAction("Show Dialog") {
        public void actionPerformed(ActionEvent event){
          JDialog dialog = createDialog(frame);
          System.out.println("Showing dialog");
          dialog.show();
        }
      }));
    frame.setSize(800, 600);
    frame.show();
  }
}

Semi-modal Dialogs

Talking of non-modal dialogs, I came across the idea of "semi-modal" dialogs on the Web. Basically it's a less strict version of a modal dialog, in that certain specified components of the parent frame/window are still accessible, even though the semi-modal dialog is displayed. I did not fully agree with the situation it was used in: basically the author wanted to give users the option of cancelling the current operation (the semi-modal dialog gets input parameters from the user) by selecting another function on the frame's toolbar. Why not just add a "Cancel" button to the dialog? (GUI Law 3: the GUI should be as simple and intuitive as possible). I actually cannot think of any scenario where you would want to use a semi-modal dialog, however that might be because I wanted to use it for our wait cursor problem, but could not find a way in which it will be easier to use than Solution 3.
I must admit, though, the idea of a semi-modal dialog is an interesting one. You can check out the code as well as a discussion of a class called Blocker at JavaWorld. Blocker extends the java.awt.EventQueue class that handles the queueing and dispatching of AWT events. It allows one to register components that should be "blockable", and then you can enable or disable the blocking (i.e. switch between semi-modal and normal mode).

Threading solutions

Aha! Why not use multiple threads and trick AWT into keeping the wait cursor on the frame, while showing a modal dialog? Because it's a very bad idea to have more than one event pump working, and even if you don't call dialog.setVisible(true) from the main Swing/AWT thread, it will still block the main EventDispatchThread. You can update SolutionTwo to set the wait cursor on the frame, and then start another thread that sleeps a few hundred milliseconds before displaying the dialog. The wait cursor will be visible on the frame while the specified number of milliseconds tick off, and then behold: it is once again a default cursor just as the dialog appears.

And the Keyboard?

You probably noticed that I did not mention keyboard input at all. Well, the above solutions only prevent mouse input. Go have a look at KeyboardFocusManager as an exercise (if you're using JDK 1.4+).
Happy waiting until next time :-)

created by Herman Lintvelt (Polymorph Systems)

0 komentar:

Posting Komentar

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
This Theme Modified by Kapten Andre based on Structure Theme from MIT-style License by Jason J. Jaeger