I'm programming a JToggleButton
to load to/discard from memory the configuration of an element (a telescope config), so I've added a JComboBox
in a JFrame
and near it the button to load the selected item. When the JToggleButton
is selected, an hard disk icon is displayed, another icon if otherwise. I'm using the IntelliJ IDEA GUI editor for that. Of course, I've added an ItemListener
(as suggested from the web) to that button:
loadTelescopeButton.setSelected(true);
System.out.println(loadTelescopeButton.isSelected());
loadTelescopeButton.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
System.out.println("LAODACTION " + loadTelescopeButton.isSelected());
try {
if (e.getStateChange() == ItemEvent.SELECTED) {
String selected = telescopesList.getSelectedItem().toString();
if ((selected != null) && (!selected.equals("")) && (ObjUtils.isAlphaNumeric(selected))) {
//...
} else {
showErrorMessage("Invalid id selected!");
}
} else if (e.getStateChange() == ItemEvent.DESELECTED) {
if ((configurationActivity != null) && (configurationActivity.getManager() != null) &&
(configurationActivity.getTelescope() != null) && (configurationActivity.getTelescope().isConnected())) {
//...
} else {
//...
}
}
} catch (Exception e1) {
e1.printStackTrace();
}
}
});
Output:
true
-> When the window is displayed
LAOD_ACTION false
-> When I click the button
I've made some tests with some new toggle buttons and they gave me same error: the code inside itemStateChanged(ItemEvent e) {...}
is repeated forever, without stopping! In that piece of code there are no for
and while
loops! The result is a great number of message dialogs (only one dialog should be displayed), and if I focus another window in my desktop the screen behind the dialogs becomes black (the area of the parent window). I changed the listener to ActionListener
and now everything is executed one time/click.
Why this error? I've copied that code from https://stackoverflow.com/a/7524627/6267019, as you can see.
Full code on GitHub Here, I've highlighted the code for that toggle button. The same error happens with other JToggleButton
s in my MainActivity.java
file, and also when debugging IntelliJ lets me see that the code in the listener is repeated forever. After some thousand of dialogs Windows shows me a message and closes Java Platform Binary with an error.
EDIT:
The same problem in a new class:
import javax.swing.*;
import java.awt.*;
public class ErrorGUI extends JFrame {
public ErrorGUI() throws HeadlessException {
super("ciao");
JPanel panel1 = new JPanel();
setContentPane(panel1);
JToggleButton ciaoToggleButton = new JToggleButton("cajs");
ciaoToggleButton.setSelected(true);
ciaoToggleButton.addItemListener(e -> {
System.out.println("caiooasfsdvn");
try {
JOptionPane.showMessageDialog(panel1, "skjngksfnb");
} catch (Exception e2) {
e2.printStackTrace();
}
});
panel1.add(ciaoToggleButton);
pack();
setVisible(true);
}
public static void main(String[] args) {
new ErrorGUI();
}
}
Whenever you open a modal dialog, the opening method call will return only after the dialog has been closed. This is crucial for the dialogs that return an entered value or choice.
This implies that while the dialog is open, a new event handling loop has to be started to react on the input in the dialog.
So when you open a modal dialog from a listener, you are stopping the handling of the current event and start processing of subsequent events, which can disturb the handling of the current event significantly. Most notably, the button will suddenly loose the focus when the new dialog is opened.
The nested event handling can be easily demonstrated by changing the listener to
ciaoToggleButton.addItemListener(e -> {
System.out.println("entering");
JOptionPane.showMessageDialog(panel1,
e.getStateChange()==ItemEvent.SELECTED? "selected": "deselected");
System.out.println("leaving");
});
which will print sequences of
entering
entering
leaving
leaving
showing how the contradicting event is generated while the processing of the old one hasn’t completed.
As said by others, you can fix this by opening the dialog after the completion of the even handling, like
ciaoToggleButton.addItemListener(e -> {
System.out.println("entering");
EventQueue.invokeLater(() -> JOptionPane.showMessageDialog(panel1,
e.getStateChange()==ItemEvent.SELECTED? "selected": "deselected"));
System.out.println("leaving");
});
or you enforce a non-modal dialog:
ciaoToggleButton.addItemListener(e -> {
System.out.println("entering");
JDialog d = new JOptionPane(
e.getStateChange()==ItemEvent.SELECTED? "selected": "deselected",
JOptionPane.INFORMATION_MESSAGE)
.createDialog(panel1, UIManager.getString("OptionPane.messageDialogTitle"));
d.setModal(false);
d.setVisible(true);
System.out.println("leaving");
});
(in a real application you would either keep the dialog for later reuse or call dispose
after use)
Unfortunately, the danger of opening modal dialogs (or doing anything else that creates a secondary event loop) hasn’t been emphasized enough in the documentation. You can read everywhere that accessing Swing components from other threads can create inconsistencies, but starting new event handling loop while there are incompletely processed events can have a similar impact.