Skip to main content

Swing in a better world: Listeners

Posted by alexfromsun on June 15, 2011 at 9:37 AM PDT

In the list of the things that needs improving in Swing, the implementation of listeners takes the first place. The problem is the fact that the order in which listeners are notified is not specified and it is not even guaranteed that your listeners will be notified after the Swing system listeners. Actually all listeners can be mixed together with any possible combination and it leads to two main problems:

Examining the problem

 Let's write some code and check if a JButton is in the pressed state when MouseListener.mousePressed() is notified:

import javax.swing.*;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ListenersOrder extends JFrame {

    public ListenersOrder() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        final JButton button = new JButton("JButton");
       
        button.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                System.out.println("button.getModel().isPressed() = "
                        + button.getModel().isPressed());
            }
        });

        add(button);
        pack();
    }

    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new ListenersOrder().setVisible(true);
            }
        });
    }
}

When you press the button you can see that it is in the pressed state, as we may expect.
button.getModel().isPressed() = true
Let's add some more code and change the system Look and Feel after the button is created:

import javax.swing.*;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ListenersOrder extends JFrame {

    public ListenersOrder() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        final JButton button = new JButton("JButton");
       
        button.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                System.out.println("button.getModel().isPressed() = "
                        + button.getModel().isPressed());
            }
        });

        add(button);
        pack();
       
        changeLaF();
    }

    private void changeLaF() {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Exception e) {
            e.printStackTrace();
        }
       
        // we don't forget to update all the components
        SwingUtilities.updateComponentTreeUI(this);
    }
   
    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new ListenersOrder().setVisible(true);
            }
        });
    }
}

This simple update changes the mouse listener's order and now, surprisingly, when you press the button you'll see:
button.getModel().isPressed() = false

How to explain it?

Let's examine the mouse listeners' added to the button, corresponding to every relevant line:

Line of code MouseListener added to the button
JButton button = new JButton("JButton"); Listener from MetalLaF
button.addMouseListener(new MouseAdapter() {}); user's listener
changeLaF(); Listener from MetalLaF
changeLaF(); Listener from NimbusLaF

So you can see that in the first test the ui listener sets the pressed flag before the user's listener, when in the second test the listener's order is reversed.
It means that the actual state of the button seeing from a user's mouseListener is unspecified and you shouldn't rely on any particular order.

Modal dialogs

Showing a modal dialog from a listener is the best way to observe your component in a transition state. If you run the following test and click an unselected list's item the modal dialog will come up and you'll be able to see the two selected items for a list with the SINGLE_SELECTION mode.

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.FlowLayout;

public class ModalityTest extends JFrame {

    public ModalityTest() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        String[] data = {"One", "Two", "Three", "Four"};
        JList list = new JList(data);
        list.setSelectionMode(
                ListSelectionModel.SINGLE_SELECTION);
        list.setSelectedIndex(0);

        list.addListSelectionListener(
                new ListSelectionListener() {
                    public void valueChanged(ListSelectionEvent e) {
                       
                        // Showing a modal dialog blocks the listener's notification
                        JOptionPane.showConfirmDialog(null,
                                "Do you see two selected lines ?");
                    }
                });

        add(list);
        setSize(200, 300);
    }

    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new ModalityTest().setVisible(true);
            }
        });
    }
}

 

 

This example shows why you shoudn't stop the listener's notification somewhere in the middle with a modal dialog and should always show it after all the listeners are notified. The recommended pattern is to always use SwingUtilities.invokeLater() when you need to show a modal dialog from your listener:

list.addListSelectionListener(
                new ListSelectionListener() {
                    public void valueChanged(ListSelectionEvent e) {

                        SwingUtilities.invokeLater(new Runnable() {
                            public void run() {
                                // Showing a modal dialog with invokeLater
                                // doesn't leave the GUI in a transition state
                                JOptionPane.showConfirmDialog(null,
                                        "Do you see two selected lines ?");
                            }
                        });
                    }
                });

How should it have been implemented?

     We seriously thought about improving the listeners notification several years ago but after heated disputes we didn't come up to one proposal. I offered to mark the Swing UI listeners with the existing UIResource interface and gurantee their notification before the user's listeners. The other propsal was to add a conception of listeners "weight" which will control the order in which a listener will be notified. So you would use the new add*Listener() methods with the second parameter - button.addMouseListener(listener, weight);

As a matter of fact the multiple listeners approach brings more problems than solutions. It would have been better to make the users override methods like processMouseEvent and automatically notify the same kind of method in the ComponentUI.

This was an entry from the Swing in a better world series
Thanks alexp

Related Topics >>