Java Tip 89: Manipulate EventQueues for semimodal dialogs

A technique to give users more freedom when modal dialogs are too restrictive

1 2 Page 2
Page 2 of 2
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.

Carla Miller is currently finishing her third year in the computing science program at Simon Fraser University. She recently completed an eight month co-op term with Plexus Systems Design, a Canadian mortgage software solutions provider. After graduation, Carla hopes to pursue a career in software development.

Learn more about this topic

Related:
1 2 Page 2
Page 2 of 2