Search code examples
javaswingbuttongroupjtogglebutton

Prevent switch from a specifc toggle button in button group unless a condition is met


I'd like to create a group of two buttons (A and B), where switching from B to A is always allowed, but switching from A to B is dependent on a condition. The condition may only be checked in the latter case (A to B) and the check may only be done once per switch attempt. If the switch is prevented, there must be no ItemEvent.SELECTED events generated for either of the buttons.

Seems pretty straightforward, so I am baffled as to why I haven't been able to do this in a simple and concise way. I thought extending ButtonGroup was the way to do this but now I'm not sure anymore.

import java.awt.FlowLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;

public class ToggleGroup extends JFrame {

    private ButtonGroup group = new MyButtonGroup();
    private JToggleButton buttonA = new JToggleButton("A");
    private JToggleButton buttonB = new JToggleButton("B");

    public ToggleGroup() {
        setLayout(new FlowLayout());
        add(buttonA);
        add(buttonB);
        group.add(buttonA);
        group.add(buttonB);
        group.setSelected(buttonA.getModel(), true);
        pack();
        setLocationRelativeTo(null);

        ItemListener itemListener = new ItemListener() {
            public void itemStateChanged(ItemEvent e) {                
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    System.out.println("-> " + (e.getSource() == buttonA ? "A" : "B") + " selected");
                }
            }
        };
        buttonA.addItemListener(itemListener);
        buttonB.addItemListener(itemListener);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                ToggleGroup test = new ToggleGroup();
                test.setVisible(true);
            }
        });
    }

    private class MyButtonGroup extends ButtonGroup {

        private boolean check() {
            int result = JOptionPane.showConfirmDialog(
                    ToggleGroup.this, "May I switch to B?",
                    "Important question", JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE);
            return result == JOptionPane.YES_OPTION;
        }

        @Override
        public void setSelected(ButtonModel m, boolean b) {      
            if (!b) {
                return;
            }
            if (m == buttonA.getModel() || m == buttonB.getModel() && check()) {
                super.setSelected(m, b);
            }
        }

    }
}

The problem with my code is obvious. The condition is checked multiple times, therefore the dialog is also shown multiple times.

So how can I "consume" a switch attempt when the condition fails?

EDIT:

The context in which I'm using these buttons is an implementation of switching between different modes of an application. One of the modes enables data to be changed and later committed. If uncommitted changes exist, switching modes might imply data loss. I'd like to make sure that the switch was intentional. Disabling either of the buttons until the condition is met is not an option.


Solution

  • Interesting ... digging a bit turns out that the interaction of selected/armed/pressed is somewhat confused by the interrupted check (faintly remember some bug, but can't find it right now). The main issue is to not allow the re-entry into the setSelected method. A dirty (read: didn't to dig further) way out is to have toggle a flag, something like

    private boolean isChecking;
    @Override
    public void setSelected(final ButtonModel m, boolean b) {   
        if (isChecking) return;
        isChecking = false;
        if (!b) {
            return;
        }
        if (m == buttonB.getModel()) {
            isChecking = true;
            final boolean select = check();
            if (select) {
                superSetSelected(m, select);
            }
            isChecking = false;
            return;
        } else {
            superSetSelected(m, b);
        }
    }
    
    protected void superSetSelected(ButtonModel m, boolean b) {
        super.setSelected(m, b);
    }