Search code examples
javaswinglistenerjspinner

How to change the background color of a JSpinner dependent on the current edited content of the field?


I have a GUI with a JSpinner using a SpinnerNumberModel using double values.

As soon as I change the content of the Editor of the JSpinner, I want the background to change to yellow (to show that the currently displayed value is not the one "saved" in the JSpinner respectively its Model.

If that content is not valid (e.g. out of the allowed range specified by my SpinnerNumberModel or a text as "abc") the background should change to red.

I tried to achieve what I want with a FocusListener already but yet have not been successful, also I am not sure if It could work anyway, as I need to check the content somewhere between focussing and defocussing.

I checked Tutorials for all Listeners that exist for Swing components, but could not find a right one that suits the job. (here I informed myself)

I am new to the concept of Listeners and would really appreciate any help that gets me closer to solving the problem but also helps generally understanding Listeners and how to use them in this context better!

My really basic code example with the mentioned poor attempt using a focus listener:

public class test implements FocusListener{

JFrame frame;

SpinnerNumberModel model;
JSpinner spinner;
JComponent comp;
JFormattedTextField field;

public test() {
    JFrame frame = new JFrame("frame");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

    model = new SpinnerNumberModel(0., 0., 100., 0.1);
    spinner = new JSpinner(model);
    comp = spinner.getEditor();
    field = (JFormattedTextField) comp.getComponent(0);
    field.addFocusListener(this);

    frame.getContentPane().add(spinner);
    frame.getContentPane().add(new JButton("defocus spinner")); //to have something to defocus when testing :)
    frame.pack();
    frame.setVisible(true);
}

@Override
public void focusGained(FocusEvent e) {
    // TODO Auto-generated method stub
    //when the values of the field and the spinner don't match, the field should get yellow
    if(!field.getValue().equals(spinner.getModel().getValue())) {
        field.setBackground(Color.YELLOW);
    }
}

@Override
public void focusLost(FocusEvent e) {
    // TODO Auto-generated method stub
    //if they match again, reset to white
            if(!field.getValue().equals(spinner.getModel().getValue())) {
                field.setBackground(Color.RED);
            }
}
}

Solution

  • I was able to fulfill the task with a combination of a KeyListener, a DocumentListener and a FocusListener. The solution might not be the easiest, but finally I coded sth. that works. Comments in the file appended should explain how I dealt with the problem.

    I expanded the original task with a CommaReplacingNumericDocumentFilter expands DocumentFilter class that was not written by me, I got the code from my professor and edited it to my needs only. Now only digits, minus and e, E are accepted as entries in the JSpinner. Commas are replaced with dots also.

    Code:

    import java.awt.*;
    import java.awt.event.*;
    import java.util.Locale;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.text.*;
    
    
    public class test implements DocumentListener, ChangeListener, KeyListener{
        boolean keyPressed;
        JFrame frame;   
        SpinnerNumberModel model;
        JSpinner spinner;
        JComponent comp;
        JFormattedTextField field;
    
    public test() {
        JFrame frame = new JFrame("frame");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));
    
        model = new SpinnerNumberModel(0., 0., 100000., .1);
        spinner = new JSpinner(model);
    
        //disable grouping for spinner
        JSpinner.NumberEditor editor = new JSpinner.NumberEditor(spinner);
        editor.getFormat().setGroupingUsed(false);
        spinner.setEditor(editor);
    
        comp = spinner.getEditor();
        field = (JFormattedTextField) comp.getComponent(0);
        field.getDocument().addDocumentListener(this);
        field.addKeyListener(this);
        spinner.addChangeListener(this);
    
        frame.getContentPane().add(spinner);
        frame.pack();
        frame.setVisible(true);
    }
    
    @Override
    public void insertUpdate(DocumentEvent e) {
        DocumentEventHandler(e);
    }
    
    @Override
    public void removeUpdate(DocumentEvent e) {
        DocumentEventHandler(e);
    }
    
    @Override
    public void changedUpdate(DocumentEvent e) {
        DocumentEventHandler(e);
    }
    
    public static boolean isNumeric(String str)  
    {  
      try  
      {  
        double d = Double.parseDouble(str);  
      }  
      catch(NumberFormatException nfe)  
      {  
        return false;  
      }  
      return true;  
    }
    
    public static void main(String[] args) {
        //to get the right format for double precision numbers
        Locale.setDefault(Locale.US);
        test test = new test();
    }
    
    @Override
    public void stateChanged(ChangeEvent e) {
        System.out.println("valuechanged: " + spinner.getValue().toString());
        if(keyPressed) {
            field.setBackground(Color.WHITE);
        }
        keyPressed = false;
    }
    
    public void DocumentEventHandler(DocumentEvent e) {
        //as soon as update is inserted, set background to yellow
        if (keyPressed) {
            field.setBackground(Color.YELLOW);  
    
            //check if input is numeric and in bounds
            String text = field.getText();
            if (isNumeric(text)) {
                double value = Double.parseDouble(text);
                if (value < (Double)model.getMinimum() || value > (Double)model.getMaximum()) {
                    field.setBackground(Color.RED);
                }
            }
            else { //set background to red
                field.setBackground(Color.RED);
            } 
        }
    
        keyPressed = false;
    
        //System.out.println(e.toString());
        //System.out.println("Text: " + field.getText());
    }
    
    @Override
    public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub
    
    }
    
    @Override
    public void keyReleased(KeyEvent e) {
        // TODO Auto-generated method stub
    
    }
    
     /** If not done yet, replaces the DocumentFilter with one replacing commas by decimal points.
        *  This can't be done at the very beginning because the DocumentFilter would be changed to a
        *  javax.swing.text.DefaultFormatter$DefaultDocumentFilter when setting up the JSpinner GUI. */
       public void keyPressed(KeyEvent e) {
           PlainDocument document = (PlainDocument)(field.getDocument());
           if(!(document.getDocumentFilter() instanceof CommaReplacingNumericDocumentFilter))
               document.setDocumentFilter(new CommaReplacingNumericDocumentFilter());
           /*Tell the other handlers that a key has been pressed and the change in the document does
            * not come from using the JSpinner buttons or the MouseWheel.
            */
           keyPressed = true;
       }
    
    
    
    }
    
    /** A javax.swing.text.DocumentFilter that replaces commas to decimal points
     *  and ignores non-numeric characters except 'e' and 'E'. This is called before
     *  modi */
    class CommaReplacingNumericDocumentFilter extends DocumentFilter {
        @Override
        public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr)
                throws BadLocationException {
        text = filter(text);
        if (text.length() > 0)
            super.insertString(fb, offset, text, attr);
    }
    
    @Override
    public void replace(FilterBypass fb, int offset, int length, String text,
        AttributeSet attrs) throws BadLocationException {
        text = filter(text);
        if (text.length() > 0)
            super.replace(fb, offset, length, text, attrs);
    }
    
    String filter(String text) {
        return text.replace(',', '.').replaceAll("[^0-9eE.-]","");
    }
    }