Saturday, April 6, 2013

Watching Swing Text Fields for Changes

As I mentioned earlier, I'm currently beating my head against a wall (or several walls) writing a graphical user interface (GUI) for a Java program, using Swing. There are certain dialogs in which I want the user to fill in text fields. In some cases the field content should be a string, with no domain restriction. In other cases the content needs to be a positive integer between specified limits. Either way, I want to listen for changes as they happen.

Listening for changes seems to be a fairly common goal, for a variety of reasons. My motivation is that the inputs are optional (the user is specifying properties of a filter), and each text input is matched with a check box indicating whether or not that particular property should be included in the filter. Although my program is a desktop application, I've filled out a number of similar forms on the web, many of which had what I consider to be a desirable feature: as soon as you start typing something valid in the text field, the check box is automatically selected. I want to do that in my dialog.

I spent considerable time searching for solutions, finding more questions than answers, but I did eventually come up with something that works, which I'll share here. Let's start with listening for changes, which proved to be the stickier bit. The advice that I found for JTextField  consistent involved listening for changes to the value property of the field. Unfortunately, I got a bunch of events where value supposedly changed even though nothing had been typed into the field. The field has oldValue and newValue properties, and it seemed intuitive to me to check for newValue != oldValue, but most of the events I saw returned newValue == null. The problem has to do with when changes are "committed". Hitting the Enter key after typing in the field commits the change, but typing itself does not, nor does typing and then changing focus by tabbing or clicking elsewhere.

The first key is to use JFormattedTextField rather than JTextField. That also buys you the ability to validate inputs and force the user to type approved characters. Just switching to JFormattedTextField is not enough, though. The field requires a formatter factory, and the default factories apparently do not automatically commit changes as soon as they are validated. So I ended up creating my own factory methods for generating formatter factories that immediately commit valid changes. Here's my code:

import java.text.NumberFormat;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import org.jdesktop.swingx.text.NumberFormatExt;
import org.jdesktop.swingx.text.StrictNumberFormatter;

/**
 * FieldFormatter provides a factory method to provide formatter factories for
 * formatted text fields with input limits. The fields allow blank/null entries,
 * and commit immediately upon valid changes.
 * @author Paul A. Rubin <rubin@msu.edu>
 */
public class FieldFormatter {
  
  /**
   * Factory method to generate a formatter factory for integer inputs.
   * @param digits the maximum number of digits to allow (minimum is 0)
   * @param min the minimum legal value
   * @param max the maximum legal value
   * @return a formatter factory
   */
  public static DefaultFormatterFactory integerFormatter(int digits,
                                                         int min, int max) {
    NumberFormatExt f = new NumberFormatExt(NumberFormat.getIntegerInstance());
    f.setParseIntegerOnly(true);
    f.setMaximumIntegerDigits(3);
    f.setMinimumIntegerDigits(0);
    StrictNumberFormatter fmt = new StrictNumberFormatter(f);
    fmt.setAllowsInvalid(true);
    fmt.setCommitsOnValidEdit(true);
    fmt.setMinimum(min);
    fmt.setMaximum(max);
    return new DefaultFormatterFactory(fmt);
  }
  
  /**
   * Factory method to generate a formatter factory for arbitrary string inputs.
   * @return a formatter factory
   */
  public static DefaultFormatterFactory stringFormatter() {
    DefaultFormatter fmt = new DefaultFormatter();
    fmt.setCommitsOnValidEdit(true);
    fmt.setAllowsInvalid(true);
    return new DefaultFormatterFactory(fmt);
  } 
}

A few notes about the code:
  • For the integer fields, I used the NumberFormatExt and StrictNumberFormatter classes from SwingX in order to implement domain restrictions (integer only, maximum and minimum number of digits, upper and lower domain limits). Since the string fields had no domain restrictions, I did not need any SwingX classes for them.
  • The setCommitsOnValidEdit method is the key to getting notifications as soon as the user types something valid in the field.
  • I want to allow the user to delete an entry in a field and leave it empty. That requires setAllowsInvalid(true); otherwise, if the user selects and deletes the field content and then exits the field, the deleted content is automatically restored, at least for the integer fields. (I'm not sure I need it for the string fields, but better safe than sorry.)
Now all you have to do is attach a property change listener to the JFormattedTextField that looks something like the following:
private void listen(ProperChangeEvent evt) {
  if (evt.getPropertyName().equals("value")) {
    // do something
  }
}

No comments:

Post a Comment

Due to intermittent spamming, comments are being moderated. If this is your first time commenting on the blog, please read the Ground Rules for Comments. In particular, if you want to ask an operations research-related question not relevant to this post, consider asking it on Operations Research Stack Exchange.