Skip to main content

The best laid plans

Posted by castelaz on August 10, 2004 at 7:47 AM PDT

I recently started a new assignment at work to improve field validation in our primary Swing application. Like many older GUI based applications, our product relies heavily upon the lost focus event to trigger field validation. In other words, we verify the value of a component, such as a JTextField, inside its lost focus event. The problem, as you might guess, is that the verfication occurs too late. We've already lost focus. While it is easy enough to request that focus return to the offending component if it contains invalid data, the new focus owner may be unable to relinquish it for some reason. In addition, lost focus events will frequently cascade which makes managing the validation process difficult. For instance, there may be times when we need to ignore validation errors. The best example of this is the Cancel button. Since we’re about to throw away all the changes the user made, it is safe to disregard any validation errors. Correctly turning on and off the validation flag can become a real challenge, especially in a blizzard of lost focus events.

As I investigated alternatives, I quickly came upon the InputVerifier class. The basic idea behind the class is to validate the component before relinquishing focus. If the component is valid, then focus may move. If it isn't, then focus remains on the invalid component. Not only is the basic idea quite simple, but using the class is extremely easy. For each type of validation you wish to perform, you extend the InputVerifier class and implement its verify() method. Then, create an instance of the validation subclass and register it with the component(s) you wish to validate using the setInputVerifier method. The verify() method is automatically called as needed. There is even a way to suppress validation when desired, as in the case of the Cancel button. I was very pleased with finding the class, and it looked like this was the way to go. However, as I was all set to proceed, a co-worker told me about some bug reports she had found about the class.

Without belaboring the point, InputVerifier has some problems. A good summary can be found at Make Buttons Respect InputVerifier. In addition to describing the problem and linking to the specific Java bug report #4533820, the author provides a solution. Unfortunately, the solution involves extending the Metal Look and Feel, and a future requirement for our product is to allow users to select from several Look and Feels. Extending all of them isn't practical.

Although I couldn't use the solution directly, it did give me another idea. Basically, the problem is that the button gets the "click" before it calls the other component's verify() method. I recalled Joshua Marinacci's Swing Hack 4 and thought about using the GlassPane to monitor "clicks" in terms of mouse events. By determining the current and target components within the mouse event handler, I can assure that the verify() method is called before focus is shifted. In the case where the current component is not verified, the MouseEvent is simply eaten by the GlassPane. In most all other cases, the MouseEvent is redirected to the target component. Essentially, the idea is to insure that the verify() method is always called before any focus event is fired. Standard keyboard focus traversal appears to handle verify correctly on its own, and the GlassPane does not need to listen for keyboard events.

I already know that this technique does not work with JInternalFrames, which maintain their own GlassPane components. In addition, I have not tested either default button processing or mnemonic traversal. While the solution may not address everyone's needs, it does appear to work in plain vanilla JFrames and JDialogs.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class GlassPaneDemo extends JComponent implements MouseListener {

    Container contentPane;

    public GlassPaneDemo(Container contentPane) {
addMouseListener(this);
this.contentPane = contentPane;
    }

    public void mousePressed(MouseEvent me) {
redispatchMouseEvent(me, false);
    }

    public void mouseReleased(MouseEvent me) {
//buttons appear depressed until they receive mouse release
redispatchMouseEvent(me, false);
    }

    public void mouseClicked(MouseEvent me) {
    }

    public void mouseEntered(MouseEvent me) {
    }

    public void mouseExited(MouseEvent me) {
    }

    private void redispatchMouseEvent(MouseEvent me, boolean repaint) {
Point containerPoint = SwingUtilities.convertPoint(this, me.getPoint(), contentPane);
JComponent targetComponent =
            (JComponent)SwingUtilities.getDeepestComponentAt(contentPane, containerPoint.x, containerPoint.y);
if (targetComponent == null) {
    return;
}
if (targetComponent.isFocusOwner()) {
    redispatchEvent(targetComponent, me);
    return;
}

else {
    JComponent currentComponent =
                  (JComponent)KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
    if ((currentComponent != null)
                && (currentComponent.getInputVerifier() != null)
                && (targetComponent.getVerifyInputWhenFocusTarget())) {
if (! currentComponent.getInputVerifier().verify(currentComponent)) {
    // not verified, so eat the mouse event

}
else {
    redispatchEvent(targetComponent, me);
}
    }
    else {
redispatchEvent(targetComponent, me);
    }
}
    }

    private void redispatchEvent(Component component, MouseEvent me) {
Point componentPoint = SwingUtilities.convertPoint(this, me.getPoint(), component);
component.dispatchEvent(new MouseEvent(component,
            me.getID(),
            me.getWhen(), 
            me.getModifiers(),
            componentPoint.x,
            componentPoint.y,
            me.getClickCount(),
            me.isPopupTrigger()));
return;
    }

    public static void main(String[] args) {
JFrame frame = new JFrame("GlassPane Demo");
JTextField textField = new JTextField("this is a textfield");
textField.setInputVerifier(new TextVerifier());
JButton cancelButton = new JButton("Cancel");
cancelButton.setVerifyInputWhenFocusTarget(false);
JButton saveButton = new JButton("Save");
JPanel mainPanel = new JPanel();
mainPanel.add(textField);
mainPanel.add(cancelButton);
mainPanel.add(saveButton);
frame.getContentPane().add(mainPanel);
GlassPaneDemo gpd = new GlassPaneDemo(frame.getContentPane());
frame.setGlassPane(gpd);
gpd.setVisible(true);
frame.pack();
frame.setSize(400, 200);
frame.show();
    }
}

class TextVerifier extends InputVerifier {
    public boolean verify(JComponent component) {
if (component instanceof JTextField) {
    return ((JTextField)component).getText().equalsIgnoreCase("pass");
}
return true;
    }
}