A technique to give users more freedom when modal dialogs are too restrictive
When designing graphical user interfaces (GUIs), you might come across a few situations that require the use of semimodal dialogs. This Java Tip outlines how to create them. With the construction of just one additional class, you can use this technique to customize your own GUIs.
What are semimodal dialogs, and when do you need them?
While building a new GUI at Plexus Systems Design, the programming team on which I worked discovered the need for semimodal dialogs. The GUI we were building masked an existing green-screen application that our customers used. The new GUI needed to look just like the old text-based UI, so customers could use it immediately. Unfortunately, this meant synchronizing the new GUI with the old application, which led to some interesting situations. Consider the following:
In our green-screen application, users were prompted via a small overlay screen to enter additional data, as shown in the figure below. Typically, this happened when they were performing a search or sending customer information.
The user could either enter the requested data or press special function keys to navigate back to a particular menu. In the new GUI, portrayed in the figure above, all the navigation keys were replaced with a toolbar on the user’s main screen. When the dialog box popped up, we still wanted the user to have access to the toolbar, which would be blocked if the overlay dialog was modal.
At the same time, we didn’t want the rest of the main GUI to be accessible while the overlay window was up. A modal dialog box blocked access to all of the underlying main UI. A modeless dialog permitted access to all of the underlying main UI. How could we duplicate the behavior of the old application?
We could have presented the overlay screen as a modal dialog box, forcing the user to enter the requested data before proceeding. But remember, our navigational toolbar was on the window underneath the overlay. The user wouldn’t have had the option of canceling and backing up to a previous menu.
So then what? Well, if modal dialogs were too restricting, then we had to use nonmodal dialogs. So assume the overlay was nonmodal and the user could select anything from the underlying frame. But the information requested in the overlay was required before we could receive any other function requests or input from the user. Obviously, modeless dialogs were not restricting enough.
The answer? Semimodal dialogs were exactly what we needed. We wanted the ability to present an overlay window and to let the user access only certain components on the underlying frame. Here’s how we did it.
Possible (but dead-end) solutions
At first, the glassPane
attribute seemed like the perfect solution. The glassPane
is an attribute of all JFrame
objects. When the glassPane
is set to be visible, it intercepts all MouseEvents
occurring within that JFrame
. The programmer becomes responsible for catching and executing the events. By default, events occurring over the glassPane
are ignored.
Here’s how the glassPane
could be useful: Suppose we set the overlay dialog to be modeless. Then the user has full access to the underlying main UI. So, we determined which components on the main UI should be blocked and then set their glassPane
components to be visible. Then, any MouseEvents
sent to those blocked components would be ignored. Wonderful! But what could we do about the keyboard? We could block all the mouse clicks we wanted, but we still needed to prevent blocked components from becoming focused via tab presses.
Of course, we could have come up with some sort of solution for dealing with keyboard events, but the project was already starting to become quite complicated. Why implement one solution for dealing with mouse events and another one for dealing with key events? Wouldn’t it be easier to come up with one solution that worked for both, and that could easily be extended to handle other event types?
OK, here was our next idea: How about enabling and disabling the components using the setEnabled
method of all Component
and JComponent
objects? That would have been a great solution if it hadn’t affected the appearance of the specified components; but, as it happened, this technique would have grayed them out. It would look strange for the appearance of the parent frame to change as soon as an overlay was presented.
So, what did we do?
Semimodal dialogs via EventQueue manipulation
To solve our problem, we developed a Blocker
class. The Blocker
class extends the Java EventQueue
class, and replaces the default SystemQueue
.
In essence, it catches all events and checks whether or not they are instances of MouseEvents
or KeyEvents
. If the event is not an instance of a mouse click or key event, then we allow the event to pass through and be handled by the dispatchEvent
method of the Blocker
‘s superclass (EventQueue
).
We wanted a dispatchEvent
method, which overrides the same method in the EventQueue
, within our Blocker
class. This method performs the event-type checking and deals with all incoming events accordingly. The following code segment illustrates how to implement the Blocker
‘s dispatchEvent
method, along with the private isSourceBlocked
helper method. The isSourceBlocked
method performs a preliminary check to see whether or not the source of an event is blocked, before that particular event is examined any further.
private boolean isSourceBlocked( Component source ) {
boolean blocked = false;
if( ( restrictedComponents != null ) && ( source != null ) ) {
int i = 0;
while( i < restrictedComponents.length &&
( restrictedComponents[i].equals( source ) == false ) )
i++;
blocked = i < restrictedComponents.length;
}
return blocked;
}
protected void dispatchEvent( AWTEvent event ) {
boolean blocked = false;
if( inBlockedState ) {
// getSource is a private helper method
blocked = isSourceBlocked( getSource( event ) );
}
if( blocked && ( event.getID() == MouseEvent.MOUSE_CLICKED
|| event.getID() == MouseEvent.MOUSE_PRESSED ) ) {
Toolkit.getDefaultToolkit().beep();
}
else if( blocked && event instanceof KeyEvent
&& event.getSource() instanceof Component ) {
DefaultFocusManager dfm = new DefaultFocusManager();
dfm.getCurrentManager();
Component currentFocusOwner = getSource( event );
boolean focusNotFound = true;
do {
dfm.focusNextComponent( currentFocusOwner );
currentFocusOwner = SwingUtilities.findFocusOwner(
( Component )event.getSource() );
if( currentFocusOwner instanceof JComponent ) {
focusNotFound =
( ( ( JComponent )currentFocusOwner ).isRequestFocusEnabled() == false );
}
} while( focusNotFound );
}
else {
super.dispatchEvent( event );
}
}
How did we know which components to block and which to keep available? Within the Blocker
class, we maintained an array of components, access to which is blocked when the Blocker
object is in a blocked state. The components stored in this array can be specified, cleared, and respecified by the user at any point in time by calling the Blocker
‘s setRestrictedComponents
method (which I will discuss momentarily).
If the event that is caught is a mouse click or keypress, then the Blocker
checks the source component of the event. When a mouse click comes from a blocked component, the code sounds a system beep and disregards the actual event. When a key event comes from a blocked component, the code transfers focus to the next focusable component. If there are no other focusable components, the focus returns to the source component.
Therefore, you need to set the focusEnabled
flag for each blocked component to false
. You also check all blocked components for instances of AbstractButtons
. These should have their setFocusPainted
flags set to false
as well. This prevents focus transfers from being visible. When you are trying to find the next focusable component, the focus may land on a blocked component, from which it is immediately transferred. If a component’s focus paint is visible, you will see the split second when this blocked component has focus before relinquishing it to the next component in line. Thus, when we turn off the focus paint, it is simply for aesthetic purposes. Once the focus finds a component that isn’t blocked, the traversal process stops.
Again, you know that this loop must come to an end since it can only be invoked from a focused component. Therefore, you know that at least one component — the component that invoked it — is focusable. Even if this is the only one that is available, the loop will find it after jumping over all other blocked components, and then the loop will terminate.
Finding the source
The Blocker
is my team’s version of an EventQueue
, with the added ability to intercept events, determine their source, and dispatch or discard them accordingly.
The Blocker
created in this Java Tip catches mouse clicks and keyboard events. To block other types of events, simply register the event type in the extended dispatchEvent
method of Blocker
. This will also require that we specify a way to find the source of this new type of event in the getSource
method.
Finding the source of events can be tricky. For mouse events, using event.getSource()
may not return the individual button or checkbox from where the event derived. Instead, it will likely return the parent container to which the individual component belongs. To get around this problem, use:
event.getSource().getDeepestComponentAt(event.getX(), event.getY() )
For key events, use the following to find the event source:
SwingUtilities.findFocusOwner( ( Component )( event.getSource() ) )
But to avoid ClassCastExceptions
, make sure that event.getSource()
does return a Component
object first.
Putting this all together, the getSource
method looks like this:
private Component getSource( AWTEvent event ) {
Component source = null;
// each of these five MouseEvents will still be valid (regardless
// of their source), so we still want to process them.
if( ( event instanceof MouseEvent )
&& ( event.getID() != MouseEvent.MOUSE_DRAGGED )
&& ( event.getID() != MouseEvent.MOUSE_ENTERED )
&& ( event.getID() != MouseEvent.MOUSE_EXITED )
&& ( event.getID() != MouseEvent.MOUSE_MOVED )
&& ( event.getID() != MouseEvent.MOUSE_RELEASED ) ) {
MouseEvent mouseEvent = ( MouseEvent )event;
source = SwingUtilities.getDeepestComponentAt(
mouseEvent.getComponent(),
mouseEvent.getX(),
mouseEvent.getY() );
}
else if( event instanceof KeyEvent
&& event.getSource() instanceof Component ) {
source = SwingUtilities.findFocusOwner(
( Component )( event.getSource() ) );
}
return source;
}
Toggling between blocked and unblocked states
Obviously, we only want components to remain blocked while the overlay dialog is still active on the screen. Therefore, once the overlay is closed, the previously blocked components should be unblocked and made readily accessible again. In order to provide this functionality, we maintained an array of components that can be blocked and unblocked, called restrictedComponents
.
When you are going into a blocked state, each of the elements in the restrictedComponents
array must have its setFocusEnabled
and setFocusPainted
flags set to false
. To move into an unblocked state, simply reset these two flags to true
.
You must also keep track of which state you are currently in (i.e., whether you are blocked or unblocked). This is important, because if you are already in a blocked state, you do not want to execute all of the blocking steps again. Similarly, you do not want to repeat all of the unblocking steps if you are already in an unblocked state. But most important, you need to know what state you are in so you can decide whether to treat your restricted components as blocked components or accessible components. Hence, you need to maintain an inBlockedState
flag.
The following method is used to place the Blocker
in a blocked or unblocked state, depending on the boolean value passed in as the parameter. The first time you go into a blocked state, you push your Blocker
object onto the SystemQueue
. From this point on, the SystemQueue
will be a singleton instance of the Blocker
class. Since you extended the EventQueue
class with your Blocker
, the SystemQueue
will maintain all of its original functionality. But now, with the adjustments introduced in the Blocker
class, you will be able to tell the SystemQueue
to ignore certain events.
public void setBlockingEnabled( boolean block ) {
// this methods must be called from the AWT thread to avoid
// toggling between states while events are being processed
if( block && !inBlockedState
&& restrictedComponents != null ) {
adjustFocusCapabilities( true );
// "adjustFocusCapabilities" is a private helper function that
// sets the focusEnabled & focusPainted flags for the
// appropriate components. Its boolean parameter signifies
// whether we are going into a blocked or unblocked state
// (true = blocked, false = unblocked)
if( !alreadyBlockedOnce ) {
// here is where we replace the SystemQueue
sysQ.push( this );
alreadyBlockedOnce = true;
}
inBlockedState = true;
}
else if( !block && inBlockedState ) {
adjustFocusCapabilities( false );
inBlockedState = false;
}
}
Singleton insurance
It is important to ensure that only one instance of the Blocker
class is created. Multiple instances meddle dangerously with the SystemQueue
, and also create confusion as to which components (under Blocker
) are currently being blocked. The technique for creating singleton classes has been worked into a design pattern (see the Resources section below for more information).
Therefore, when a Blocker
is instantiated, you maintain a reference to this Blocker
so that it can be returned whenever a user attempts to create additional Blockers
. This way, the user may create numerous Blocker
references, which must all point to the same Blocker
object.
You can achieve this functionality by making the Blocker
class’s constructor method private, and implementing the following method for creating a new instance:
public static synchronized Blocker Instance() {
if( instance == null ) {
// instance is a static global variable of type Blocker
instance = new Blocker();
}
return instance;
}
Setting restricted components
For our GUI, we needed a mechanism for telling the Blocker
object components to which access should be restricted when we were in a blocked state. And since we had limited the Blocker
class to being a singleton, we also needed a mechanism for changing the components that will be restricted. Basically, for different frames, we needed to block different components.
To handle this situation, the method setRestrictedComponents
should receive an array of components to be registered with the Blocker
.
Note: When the Blocker
receives an array of components to register, it is important that it registers not only each component, but also each of that component’s child components — components can be instances of containers. Don’t forget to check for children recursively; children can have children of their own.
The following method receives an array of components as its only parameter, and sets the global helperVector
variable to contain every individual component in that array.
private void extractAllComponents( Component[] array ) {
for( int i = 0; i < array.length; i++ ) {
if( array[i] != null ) {
helperVector.addElement( array[i] );
if( ( ( Container )array[i] ).getComponentCount() != 0 ) {
extractAllComponents( ( ( Container )array[i] ).getComponents() );
}
}
}
}
Each time you call the setRestrictedComponents
method and pass it an array of components, the previous array of restrictedComponents
is cleared. Only those components specified in the method parameter are stored as elements of the restrictedComponents
array. The following code represents the setRestrictedComponents
method. The reset method that is referenced from setRestrictedComponents
puts the Blocker
into an unblocked state, and clears the restrictedComponents
array.
public void setRestrictedComponents( Component[] restrictedComponents ) {
reset(); // puts the Blocker into an unblocked state, and clears the
// restrictedComponents array (see private method below)
helperVector = new Vector();
// global Vector variable
if( restrictedComponents != null ) {
extractAllComponents( restrictedComponents );
// see previous "extractAllComponents" method description
}
// builds the blockedComponent array
if( helperVector.size() >= 1 ) {
this.restrictedComponents = new Component[ helperVector.size() ];
for( int k = 0; k < helperVector.size(); k++ ) {
this.restrictedComponents[k] = ( Component )helperVector.elementAt( k );
}
}
else {
this.restrictedComponents = null;
}
}
private void reset() {
if( inBlockedState ) {
setBlockingEnabled( false );
}
restrictedComponents = null;
}
Complete working example
The following code is a complete working example of the semimodal dialog technique outlined in this Java Tip. The first class defines the Blocker
object, while the second one provides a small Test
application that uses the Blocker
.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
public class Blocker extends EventQueue {
private Component[] restrictedComponents;
private Vector helperVector;
private boolean inBlockedState = false;
private EventQueue sysQ =
Toolkit.getDefaultToolkit().getSystemEventQueue();
private boolean alreadyBlockedOnce = false;
private static Blocker instance = null;
public static synchronized Blocker Instance() {
if( instance == null ) {
instance = new Blocker();
}
return instance;
}
private Blocker() {
restrictedComponents = null;
}
private void reset() {
if( inBlockedState ) {
setBlockingEnabled( false );
}
restrictedComponents = null;
}
public void setRestrictedComponents( Component[] restrictedComponents ) {
reset(); // puts the Blocker into an unblocked state, and clears the
// restrictedComponents array (see private method below)
helperVector = new Vector();
// global Vector variable
if( restrictedComponents != null ) {
extractAllComponents( restrictedComponents );
}
// builds the blockedComponent array
if( helperVector.size() >= 1 ) {
this.restrictedComponents = new Component[ helperVector.size() ];
for( int k = 0; k < helperVector.size(); k++ ) {
this.restrictedComponents[k] = ( Component )helperVector.elementAt( k );
}
}
else {
this.restrictedComponents = null;
}
}
private void extractAllComponents( Component[] array ) {
for( int i = 0; i < array.length; i++ ) {
if( array[i] != null ) {
helperVector.addElement( array[i] );
if( ( ( Container )array[i] ).getComponentCount() != 0 ) {
extractAllComponents( ( ( Container )array[i] ).getComponents() );
}
}
}
}
private void adjustFocusCapabilities( boolean blocked ) {
if( blocked ) {
for( int i = 0; i < restrictedComponents.length; i++ ) {
if( restrictedComponents[i] instanceof JComponent ) {
( ( JComponent )restrictedComponents[i] ).setRequestFocusEnabled( false );
}
// removes the focus indicator from all components that are capable
// of painting their focus
if( restrictedComponents[i] instanceof AbstractButton ) {
( ( AbstractButton )restrictedComponents[i] ).setFocusPainted( false );
}
}
}
else {
for( int k = 0; k < restrictedComponents.length; k++ ) {
if( restrictedComponents[k] instanceof JComponent ) {
( ( JComponent )restrictedComponents[k] ).setRequestFocusEnabled( true );
}
if( restrictedComponents[k] instanceof AbstractButton ) {
( ( AbstractButton )restrictedComponents[k] ).setFocusPainted( true );
}
}
}
}
private Component getSource( AWTEvent event ) {
Component source = null;
// each of these five MouseEvents will still be valid (regardless
// of their source), so we still want to process them.
if( ( event instanceof MouseEvent )
&& ( event.getID() != MouseEvent.MOUSE_DRAGGED )
&& ( event.getID() != MouseEvent.MOUSE_ENTERED )
&& ( event.getID() != MouseEvent.MOUSE_EXITED )
&& ( event.getID() != MouseEvent.MOUSE_MOVED )
&& ( event.getID() != MouseEvent.MOUSE_RELEASED ) ) {
MouseEvent mouseEvent = ( MouseEvent )event;
source = SwingUtilities.getDeepestComponentAt(
mouseEvent.getComponent(),
mouseEvent.getX(),
mouseEvent.getY() );
}
else if( event instanceof KeyEvent
&& event.getSource() instanceof Component ) {
source = SwingUtilities.findFocusOwner(
( Component )( event.getSource() ) );
}
return source;
}
private boolean isSourceBlocked( Component source ) {
boolean blocked = false;
if( ( restrictedComponents != null ) && ( source != null ) ) {
int i = 0;
while( i < restrictedComponents.length &&
( restrictedComponents[i].equals( source ) == false ) )
i++;
blocked = i < restrictedComponents.length;
}
return blocked;
}
protected void dispatchEvent( AWTEvent event ) {
boolean blocked = false;
if( inBlockedState ) {
// getSource is a private helper method
blocked = isSourceBlocked( getSource( event ) );
}
if( blocked && ( event.getID() == MouseEvent.MOUSE_CLICKED
|| event.getID() == MouseEvent.MOUSE_PRESSED ) ) {
Toolkit.getDefaultToolkit().beep();
}
else if( blocked && event instanceof KeyEvent
&& event.getSource() instanceof Component ) {
DefaultFocusManager dfm = new DefaultFocusManager();
dfm.getCurrentManager();
Component currentFocusOwner = getSource( event );
boolean focusNotFound = true;
do {
dfm.focusNextComponent( currentFocusOwner );
currentFocusOwner = SwingUtilities.findFocusOwner(
( Component )event.getSource() );
if( currentFocusOwner instanceof JComponent ) {
focusNotFound =
( ( ( JComponent )currentFocusOwner ).isRequestFocusEnabled() == false );
}
} while( focusNotFound );
}
else {
super.dispatchEvent( event );
}
}
public void setBlockingEnabled( boolean block ) {
// this methods must be called from the AWT thread to avoid
// toggling between states while events are being processed
if( block && !inBlockedState
&& restrictedComponents != null ) {
adjustFocusCapabilities( true );
// "adjustFocusCapabilities" is a private helper function that
// sets the focusEnabled & focusPainted flags for the
// appropriate components. Its boolean parameter signifies
// whether we are going into a blocked or unblocked state
// (true = blocked, false = unblocked)
if( !alreadyBlockedOnce ) {
// here is where we replace the SystemQueue
sysQ.push( this );
alreadyBlockedOnce = true;
}
inBlockedState = true;
}
else if( !block && inBlockedState ) {
adjustFocusCapabilities( false );
inBlockedState = false;
}
}
}
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import Blocker;
/**
* This Test class demonstrates how the Blocker is used.
* The Test opens a new JFrame object containing five different
* components. The button labeled "Block" will block "Button"
* and "CheckBox". The button labeled "Unblock" will make "Button"
* and "CheckBox" accessible to the user.
*
* "Button" and "CheckBox" have no attached functionality.
*/
public class Test {
public static void main( String[] argv ) {
JFrame frame = new JFrame();
frame.setTitle( "Blocker Test" );
frame.addWindowListener( new WindowAdapter() {
public void windowClosing( WindowEvent e ) {
System.exit(0);
}
} );
frame.setSize( 200, 200 );
JButton blockButton = new JButton( " Block " );
JButton unblockButton = new JButton( "Unblock " );
JButton button = new JButton( " Button " );
JCheckBox checkBox = new JCheckBox( "CheckBox" );
JButton exitButton = new JButton( " Exit " );
final Blocker blocker = Blocker.Instance();
// this line sets "Button" and "CheckBox" as restricted
// components...to change which components are restricted
// simply call this method again, passing in a different
// array of components.
blocker.setRestrictedComponents( new Component[] {
button, checkBox } );
blockButton.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
blocker.setBlockingEnabled( true );
}
} );
unblockButton.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
blocker.setBlockingEnabled( false );
}
} );
exitButton.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
System.exit(0);
}
} );
Container contentPane = frame.getContentPane();
contentPane.setLayout( new FlowLayout() );
contentPane.add( blockButton );
contentPane.add( unblockButton );
contentPane.add( button );
contentPane.add( checkBox );
contentPane.add( exitButton );
frame.setVisible( true );
}
}
Conclusion
So now you have a versatile Blocker
class that you can easily modify to perform a wide variety of event-handling procedures. The Blocker
can handle MouseEvents
and KeyEvents
; you can also program it to deal with other event types. With this degree of control, you can create a new variation on the standard dialog window. The semimodal dialog — not modal, but not modeless — is an interesting concept and useful technique.