Java Gotcha: JSpinner Preferred size

The Problem

Consider the following “gotcha” code:

package com.implementsblog;

import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;

/**
 * An example of odd JSpinner size.
 */
public class JSpinnerGotcha
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JFrame frame = new JFrame("JSpinner Gotcha");

                frame.getContentPane().add(
                        new JSpinner(
                                new SpinnerNumberModel(
                                        0,                // Initial
                                        0,                // Minimum
                                        Double.MAX_VALUE, // Maximum
                                        1                 // Step
                                )));

                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                frame.pack();
                frame.setVisible(true);
            }
        });
    }
}

On the surface, the output seems fairly simple: a JFrame containing a single component: a JSpinner whose model has been specified. However, when executed, the JFrame is so wide it falls off the side of the screen.

JFrame with wide JSpinner

The PreferredSize of the JSpinner is too large

The JSpinner‘s preferred size is unexpectedly large. But why?

The Investigation

Let’s explore the JSpinner source code. For each implementation of SpinnerModel, there’s a corresponding Editor. For example, the SpinnerDateModel has a DateEditor. Let’s follow line 24 of the gotcha, the call to the JSpinner constructor:

public JSpinner(SpinnerModel model) {
    if (model == null) {
        throw new NullPointerException("model cannot be null");
    }
    this.model = model;
    this.editor = createEditor(model);
    setUIProperty("opaque",true);
    updateUI();
}

From here we’ll follow the createEditor call on line 155:

protected JComponent createEditor(SpinnerModel model) {
    if (model instanceof SpinnerDateModel) {
        return new DateEditor(this);
    }
    else if (model instanceof SpinnerListModel) {
        return new ListEditor(this);
    }
    else if (model instanceof SpinnerNumberModel) {
        return new NumberEditor(this);
    }
    else {
        return new DefaultEditor(this);
    }
}

The gotcha uses SpinnerNumberModel, so we’ll follow line 249. After a few nested constructor calls, we end up at:

private NumberEditor(JSpinner spinner, DecimalFormat format) {
    super(spinner);
    if (!(spinner.getModel() instanceof SpinnerNumberModel)) {
         throw new IllegalArgumentException(
                   "model not a SpinnerNumberModel");
    }

     SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel();
     NumberFormatter formatter = new NumberEditorFormatter(model,
                                                           format);
    DefaultFormatterFactory factory = new DefaultFormatterFactory(
                                          formatter);
    JFormattedTextField ftf = getTextField();
    ftf.setEditable(true);
    ftf.setFormatterFactory(factory);
    ftf.setHorizontalAlignment(JTextField.RIGHT);

    /* TBD - initializing the column width of the text field
     * is imprecise and doing it here is tricky because
     * the developer may configure the formatter later.
     */
    try {
        String maxString = formatter.valueToString(model.getMinimum());
        String minString = formatter.valueToString(model.getMaximum());
        ftf.setColumns(Math.max(maxString.length(),
        minString.length()));
    }
    catch (ParseException e) {
        // TBD should throw a chained error here
    }

}

Our problem lies on lines 1220 and 1221; in the gotcha, the JSpinner‘s model returns Double.MAX_VALUE for model.getMaximum(). Thus, formatter.valueToString(model.getMaximum()) returns the evaluated String of 2-2^{-52} \cdot 2^{1023} , a very long number indeed.

The Workaround

Now that we know the problem, what’s the workaround? One way would be to get the preferred size of the JSpinner before adding the model, and then reset the size after setting the model, as this snippet from the gotcha illustrates:

JFrame frame = new JFrame("JSpinner Gotcha");

JSpinner spinner = new JSpinner();
Dimension size = spinner.getPreferredSize();
spinner.setModel(new SpinnerNumberModel(
        0,                // Initial
        0,                // Minimum
        Double.MAX_VALUE, // Maximum
        1                 // Step
));
spinner.setPreferredSize(size);
frame.getContentPane().add(spinner);

Rather than being a bug in JSpinner, however, this is probably a misuse of JSpinner. Is a user really expected to press the up arrow button until he or she reaches Double.MAX_VALUE?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s