3

I have a form asking some details to a user. In top of that form, there are 2 controls : a JSpinner and a JToggleButton.

If the user use the JSpinner, he can select from form number 1 to 4, if he clic on the JToggleButton, the spinner is disabled an the form number 0 is displayed (if this button is toggled back, the spinner is enabled back and the form load with the number in the spinner).

So far no problems.

But I would like now, to display a popup when a form is edited, not saved and that the user use one of the 2 controls discrebed.

No problem for the popup, but I don't know how to undo modification on the control that fired the display of the popup.

Because I am using ChangeListener for the spinner and ActionListener for the button, I am displaying the popup AFTER the modification of the control.

In fact I am searching a way to be notified of an action on the controls but with the possibility to stop the modification (something like a notification listener where we need to return true or false to validate the modification).

How would you do that ?

Thanks.

Here is a sample :

package test;

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SwingTest extends JFrame
{
    private static final long   serialVersionUID    = 1L;

    private JToggleButton       btnRecueilPermanent;
    private JSpinner            spinner;
    private JTextField          tf;
    private boolean             formChanged         = false;

    public SwingTest()
    {
        super();

        setLayout(new GridBagLayout());
        initComponents();

        loadForm(1);
    }

    private void initComponents()
    {
        JPanel panelGeneral = new JPanel();
        GridBagConstraints gbc_panelGeneral = new GridBagConstraints();
        gbc_panelGeneral.fill = GridBagConstraints.BOTH;
        gbc_panelGeneral.anchor = java.awt.GridBagConstraints.CENTER;
        gbc_panelGeneral.weightx = 100.0;
        gbc_panelGeneral.weighty = 100.0;
        gbc_panelGeneral.gridx = 0;
        gbc_panelGeneral.gridy = 0;
        add(panelGeneral, gbc_panelGeneral);
        panelGeneral.setLayout(new BorderLayout(0, 0));

        JPanel panelSelecteur = new JPanel();
        panelGeneral.add(panelSelecteur, BorderLayout.NORTH);

        JLabel lblChoixDuFormulaire = new JLabel("Choose form :");
        panelSelecteur.add(lblChoixDuFormulaire);

        spinner = new JSpinner(new SpinnerNumberModel(1, 1, 4, 1));
        spinner.addChangeListener(new ChangeListener()
        {
            public void stateChanged(final ChangeEvent e)
            {
                loadForm((Integer) spinner.getValue());
            }
        });
        panelSelecteur.add(spinner);

        btnRecueilPermanent = new JToggleButton("Permanent form");
        btnRecueilPermanent.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if(btnRecueilPermanent.isSelected())
                {
                    loadForm(0);
                }
                else
                {
                    loadForm((Integer) spinner.getValue());
                }
            }
        });
        panelSelecteur.add(btnRecueilPermanent);

        final JPanel formPanel = new JPanel();
        tf = new JTextField(20);
        tf.addKeyListener(new KeyListener()
        {
            @Override
            public void keyTyped(KeyEvent e)
            {
                formChanged = true;
            }

            @Override
            public void keyPressed(KeyEvent e)
            {
            }

            @Override
            public void keyReleased(KeyEvent e)
            {
            }
        });
        formPanel.add(tf);
        panelGeneral.add(formPanel, BorderLayout.CENTER);
    }

    protected void loadForm(final int nbForm)
    {
        if(formChanged)
        {
            Object[] options =
            {
                "Continue", "Discard changes"
            };

            final int result = JOptionPane.showOptionDialog(this, "You have unsaved modifivations", "Beware !", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
            if(result == 0)
            {
                // HERE WE DISCARD THE FORM CHANGE, BUT THE TOGGLEBUTTON or THE JSPINNER HAVED CHANGED
                return;
            }
        }

        if(nbForm == 0)
        {
            btnRecueilPermanent.setText("Normal form");
        }
        else
        {
            btnRecueilPermanent.setText("Permanent form");
        }

        tf.setText(String.valueOf(nbForm));

        spinner.setEnabled(nbForm != 0);

        formChanged = false;
    }

    public static void main(String[] args)
    {
        final SwingTest jf = new SwingTest();

        jf.pack();
        jf.setVisible(true);
    }
}
iXô
  • 1,133
  • 1
  • 16
  • 39

1 Answers1

1

In fact I am searching a way to be notified of an action on the controls but with the possibility to stop the modification (something like a notification listener where we need to return true or false to validate the modification).

Note: thanks to @mKorbel comment/feedback below I have fully understood your problem.

Let's start with JSpinner. As suggested you could use PropertyChangeListener and/or PropertyChangeSupport to listen to "value" property changes. In fact I would say that the simplest/best approach is actually attach a PropertyChangeListener to the text field that is part of spinner's editor, just because this component can be updated either by pressing up/down buttons or entering a value manually and is updated (by contract) on model value change events.

That said, please consider this snippet:

final JSpinner spinner = new JSpinner(new SpinnerNumberModel(1, 1, 4, 1));
JSpinner.NumberEditor editor = (JSpinner.NumberEditor) spinner.getEditor();
JFormattedTextField textField = editor.getTextField();
textField.addPropertyChangeListener("value", new PropertyChangeListener() {...});

Now, you need a mechanism to keep the last valid value to rollback changes if the user regrets the edition (i.e.: a simple backup variable with scope enough to be accessed from within propertyChange(PropertyChangeEvent evt) method). In addition you have now the old and new values available through PropertyChangeEvent API to do the validation and check if the new value is exactly the same that was stored in the backup variable. See the example below to a better understanding.

Let's focus on JToggleButton now. In this case I would replace ActionListener by ItemListener in order to count with the new button's state (selected/deselected) directly from the ItemEvent. Something like this:

final JToggleButton button = new JToggleButton("Toggle");
button.addItemListener(new ItemListener() {...});

Once again, you will need a mechanism to keep the last valid value of this toggle button and be able to rollback users changes if they consider those are not appropriate. Beware that item changes are fired twice: first time to notify the old state and second time to notify new state. So you'll have to ignore the first call just because is the very current state. See the example below to a better understanding.

Example

Try this full example I think it satisfies your requirements.

import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;

/**
 * @author dic19
 */
public class Demo {

    private Object backupSpinnerValue;
    private Boolean backupButtonState;

    public void createAndShowGUI() {

        final JSpinner spinner = new JSpinner(new SpinnerNumberModel(1, 1, 4, 1));
        JSpinner.NumberEditor editor = (JSpinner.NumberEditor) spinner.getEditor();
        JFormattedTextField textField = editor.getTextField();
        textField.addPropertyChangeListener("value", new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                Object oldValue = evt.getOldValue();
                Object newValue = evt.getNewValue();
                if (!evt.getNewValue().equals(backupSpinnerValue)) { // Just ignore if they're the same
                    if (Demo.this.confirmChanges(oldValue, newValue)) {
                        backupSpinnerValue = newValue;
                    } else {
                        spinner.setValue(backupSpinnerValue);
                    }
                }
            }
        });

        final JToggleButton button = new JToggleButton("Toggle");
        button.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                Boolean isSelected = e.getStateChange() == ItemEvent.SELECTED;
                if (!isSelected.equals(backupButtonState)) { // Just ignore if they're the same
                    if (Demo.this.confirmChanges(backupButtonState, isSelected)) {
                        backupButtonState = isSelected;
                    }
                    button.setSelected(backupButtonState);
                }
            }
        });

        backupSpinnerValue = spinner.getValue();
        backupButtonState = button.isSelected();

        JPanel content = new JPanel();
        content.add(spinner);
        content.add(button);

        JFrame frame = new JFrame("Demo");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(content);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    private boolean confirmChanges(Object oldValue, Object newValue) {
        String message = String.format("Do you want to confirm changes from value '%1s' to '%2s'?", oldValue, newValue);
        int option = JOptionPane.showConfirmDialog(null, message, "Confirm changes", JOptionPane.OK_CANCEL_OPTION);
        return option == JOptionPane.OK_OPTION;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Demo().createAndShowGUI();
            }
        });
    }
}

Other comments

1) At these lines:

tf = new JTextField(20);
tf.addKeyListener(new KeyListener(){...});

Note that KeyListener is not the right choice when you want to listen for a text field's change. You should use DocumentListener instead. See How to Write a Document Listener

2) Swing components are intended to be used "as are" so it's preferable composition over inheritance. So you should consider remove extends JFrame on your class declaration and add all your components to a local frame instead. See also this topic: Extends JFrame vs. creating it inside the the program

3) Always include a default close operation to your top-level containers (windows):

public class SwingTest extends JFrame {
   ...
   private void initComponents() {
       ...
       setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
   }
}

4) If you are interested in a more elegant/sophisticated approach than simple variables to keep states take a look to Memento pattern. This pattern allows to keep not just a single previous state but the whole state changes history.

Community
  • 1
  • 1
dic19
  • 17,821
  • 6
  • 40
  • 69
  • 1
    ButtonModel (JToggleButton) listened together with JSpinner by using ChangeListener is basics, there should be abother option by using PropertyChangeSupport/Listener, then/but should not be in one code block (ChangeListener), and logics for verification/validation must be separated to the pieces – mKorbel Sep 22 '14 at 12:56
  • Thanks dic19 !, this wasn't exactly compatible with my code, but a minor tweak makes it works like I needed. (Note : the points you higlight in your 'other comments' haven't been used in my sample to makes it light, but real code is well coded, I hope). – iXô Sep 23 '14 at 15:49
  • You are welcome :) Yes, I suspected your code was just an example but I like pointing out that kind of details mostly to help future visitors.@iXô – dic19 Sep 23 '14 at 15:54