Question

I was trying to achieve the following.

I want to use a JSpinner that uses numbers for its data, but I want to have it rendered like this: "6 / 10", that is, "value / maximum", where the default JSpinner only displays "value". Of course, I'm using a SpinnerNumberModel to only work with integers as data.

I was looking for analogue behavior compared to a JCombobox renderer and/or editor, a JTable renderer and/or editor, ... but it seems a JSpinner doesn't work this way. I found this example, for custom content inside the spinner. The example uses a JLabel to display icons, but in my case I have to be able to edit the contents and check the input after hitting Enter.

So this is what I basically did:

JSpinner spinner = new JSpinner(new SpinnerNumberModel(1,1,10,1));
spinner.setEditor(new CustomSpinnerEditor(spinner));

where CustomSpinnerEditor is extending JTextField. I unleashed some inputmap/actionmap things on it (for Enter behavior), added a ChangeListener to the spinner to be able to refresh the textfield content when the user clicks on the arrows, added a FocusListener to select all text on focusGained and verify the input on focusLost, ... and it works.

So the problem is not to get it to work. The problem is, that this is way too much work for something really simple, and that I possibly lose some features by not using the default editor. If the component just provided the ability to set a renderer on it, I would have had a hook where I simply returned a JLabel with the correct String, like I would do with a JComboBox.

Is there a simpler way to achieve what I want?

Was it helpful?

Solution

You can avoid creating a dedicated editor, but I don't know if it becomes simpler: retrieve the default editor's text field using

JFormattedTextField f = ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField();

and then set a formatter factory on that text field:

f.setFormatterFactory(new AbstractFormatterFactory() {
    @Override
    public AbstractFormatter getFormatter(JFormattedTextField tf) {
        // return your formatter
    }
});

The formatter returned by the getFormatter method must then convert between your value and a string representation of it, using its stringToValue and valueToString methods.

OTHER TIPS

I don't know if you would consider this simpler.

JSpinner Test

I created a Renderer class that holds the values and the value string that you want to display.

In the custom spinner model, I created a List of Renderer instances, and navigated the list.

I put the classes together so I could copy and paste one block of code. These 3 classes should be kept in different source modules.

Here's one way to do what you want.

import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractSpinnerModel;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SpinnerTesting implements Runnable {

    @Override
    public void run() {
        JFrame frame = new JFrame("JSpinner Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel mainPanel = new JPanel();
        final JSpinner spinner = new JSpinner(new CustomSpinnerModel(4, 1, 10,
                1));
        setBorder(spinner);
        spinner.addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent event) {
                Renderer renderer = (Renderer) spinner.getValue();
                System.out.println(renderer.getValue());
            }
        });
        mainPanel.add(spinner);

        frame.add(mainPanel);
        frame.setSize(new Dimension(300, 100));
        frame.setVisible(true);
    }

    private void setBorder(JSpinner spinner) {
        Border lineBorder = BorderFactory.createLineBorder(Color.BLACK);
        Border insetBorder = BorderFactory.createEmptyBorder(0, 10, 0, 10);
        spinner.setBorder(BorderFactory.createCompoundBorder(lineBorder,
                insetBorder));
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new SpinnerTesting());
    }

    public class CustomSpinnerModel extends AbstractSpinnerModel {

        private int             value;
        private int             minimum;
        private int             maximum;
        private int             stepSize;

        private int             listIndex;

        private List<Renderer>  spinnerList;

        public CustomSpinnerModel(int value, int minimum, int maximum,
                int stepSize) throws IllegalArgumentException {
            if (!((minimum <= value) && (value <= maximum))) {
                throw new IllegalArgumentException(
                        "(minimum <= value <= maximum) is false");
            }

            this.value = value;
            this.minimum = minimum;
            this.maximum = maximum;
            this.stepSize = stepSize;

            this.spinnerList = new ArrayList<Renderer>();
            setSpinnerList();
        }

        private void setSpinnerList() {
            int index = 0;
            for (int i = minimum; i <= maximum; i += stepSize) {
                Renderer renderer = new Renderer(i, maximum);
                if (i == value) {
                    listIndex = index;
                }
                spinnerList.add(renderer);
                index++;
            }
        }

        @Override
        public Object getNextValue() {
            listIndex = Math.min(++listIndex, (spinnerList.size() - 1));
            fireStateChanged();
            return spinnerList.get(listIndex);
        }

        @Override
        public Object getPreviousValue() {
            listIndex = Math.max(--listIndex, 0);
            fireStateChanged();
            return spinnerList.get(listIndex);
        }

        @Override
        public Object getValue() {
            return spinnerList.get(listIndex);
        }

        @Override
        public void setValue(Object object) {

        }

    }

    public class Renderer {

        private int     value;

        private String  valueString;

        public Renderer(int value, int maximum) {
            this.value = value;
            this.valueString = value + " / " + maximum;
        }

        public int getValue() {
            return value;
        }

        public String getValueString() {
            return valueString;
        }

        @Override
        public String toString() {
            return valueString + " ";
        }

    }

}

Yes, there is a simpler way...

Why populating the actionmap of a custom editor and not just editing the actionmap of the DefaultEditor?

Let's supply the DefaultEditor's JFormattedTextField with a custom action for the input notify event:

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.text.TextAction;

public class OnEnterSpinner extends JPanel {
    private final JSpinner spin;
    private final JSpinner.DefaultEditor editor;
    private final JFormattedTextField field;

    private final class OnEnterAction extends TextAction {
        private OnEnterAction() {
            super(JTextField.notifyAction); //The name of the TextAction. Let's say it is its key.
        }

        @Override
        public void actionPerformed(final ActionEvent actevt) {
            if (getFocusedComponent() == field)
                System.out.println("The user pressed ENTER for: \"" + field.getText() + "\".");
        }
    }

    private OnEnterSpinner() {
        spin = new JSpinner(new SpinnerNumberModel(0, 0, 10000, 1)); //Your values here. Let's say for now this is a spinner for integers.
        spin.setPreferredSize(new Dimension(100, 20));

        //We need a DefaultEditor (to be able to obtain the JFormattedTextField and customize it).
        editor = new JSpinner.DefaultEditor(spin);

        field = editor.getTextField();
        field.setEditable(true); //Allow user input (obvious reasons).

        //And here, you register the custom action!
        field.getActionMap().put(JTextField.notifyAction, new OnEnterAction()); //I found the proper key for the map: "notifyAction".

        spin.setEditor(editor);

        add(spin);
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("OnEnterSpinner demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new OnEnterSpinner());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

If, furthermore, you need to check the validity of the entered value via a custom AbstractFormatter which you supplied to the JFormattedTextField, then just call JSpinner#commitEdit() upon ENTER:

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.text.ParseException;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.text.TextAction;

public class CommitOnEnterSpinner extends JPanel {
    private final JSpinner spin;
    private final JSpinner.DefaultEditor editor;
    private final JFormattedTextField field;

    private final class OnEnterAction extends TextAction {
        private OnEnterAction() {
            super(JTextField.notifyAction);
        }

        @Override
        public void actionPerformed(final ActionEvent actevt) {
            if (getFocusedComponent() == field) { //This check may be redundant in this case.
                System.out.println("The user pressed ENTER for: \"" + field.getText() + "\".");
                try {
                    //Commit the edits, upon carriage return:
                    spin.commitEdit();

                    //If successfull, then handle it as desired:
                    JOptionPane.showMessageDialog(null, "Yes! This is a valid value.", "OK", JOptionPane.PLAIN_MESSAGE);
                }
                catch (final ParseException pe) {
                    //If failed, then handle the unaccepted value:
                    JOptionPane.showMessageDialog(null, "Sorry, \"" + field.getText() + "\" is not a valid value.", "Oups!", JOptionPane.INFORMATION_MESSAGE);
                }
            }
        }
    }

    private CommitOnEnterSpinner() {
        spin = new JSpinner(new SpinnerNumberModel(0, 0, 10000, 1)); //Your values here. Let's say for now this is a spinner for integers.
        spin.setPreferredSize(new Dimension(100, 20));

        //We need a DefaultEditor (to be able to obtain the JFormattedTextField and customize it).
        editor = new JSpinner.DefaultEditor(spin);

        field = editor.getTextField();
        field.setEditable(true); //Allow user input (obvious reasons).

        //And here, you register the custom action!
        field.getActionMap().put(JTextField.notifyAction, new OnEnterAction());

        spin.setEditor(editor);

        add(spin);
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("CommitOnEnterSpinner demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new CommitOnEnterSpinner());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top