Abstract: Coding Swing can be tricky. There are a lot of strange edge cases. One of the harder things to get right are the wait cursors, specifically in conjunction with modal dialogs. We will learn how to set the wait cursor on the parent window/frame of a modal dialog.
Welcome to the 65th edition of The Java(tm) Specialists' Newsletter, sent to 6077 Java Experts in almost 100 countries. This week we are privileged that my good friend Herman Lintvelt is again showing us some tricks. This newsletter is on how to handle the wait cursor and modal dialogs. Thanks Herman!
Next week I will again be in Germany, and one of the things I will be doing is an in-house Design Patterns Course at a company in Frankfurt. The timing of this course is fortunate, in that it enables me to visit my dear Grandmother for her 90th birthday as well. A special "thank you" to all those who have made it possible to coordinate the dates.
I recently received a question by Michael Ambrose (forwarded to me by Heinz) about wait cursors in Swing, specifically in conjunction with modal dialogs. One of the questions asked was how to set the wait cursor on the parent window/frame of a modal dialog. But wait, we'll come to that.
Everyone who has developed GUI applications, has been faced with the problem of how to handle those llloooonnggg operations. Especially in the very warm Februaries we have in Worcester [hk: A small village in South Africa, where the tar melts in winter from the heat], operations take very long indeed. This newsletter is not about handling all the issues involved - since that is the stuff of numerous letters - but focus on wait cursors.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
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 static final 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.
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?
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" :-)
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 Java Bug Parade (bug nr 4282540) for their reasons why this is so.
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(); } }
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).
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.
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 :-)
Herman
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.