javaswingjpopupmenu

Is there a way to detect current selection in JPopupMenu (SelectionModel ChangeListener doesn't work)


I want to detect when selection changes inside a JPopupMenu. Not when a menu item is clicked, but when a menu item is selected (armed). With simpler words, I want to detect this:

preview

The thing that should work is to add a ChangeListener to its SelectionModel, but it doesn't seem to respond to selection events:

public class PopupSelection extends JFrame {
    private static final long serialVersionUID = 363879723515243543L;

    public PopupSelection() {
        super("something");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        JLabel label = new JLabel("right click me");

        JPopupMenu menu = new JPopupMenu();
        menu.getSelectionModel().addChangeListener(System.out::println);

        JMenuItem menuItem1 = new JMenuItem("Item1");
        JMenuItem menuItem2 = new JMenuItem("Item2");
        JMenuItem menuItem3 = new JMenuItem("Item3");
        menu.add(menuItem1);
        menu.add(menuItem2);
        menu.add(menuItem3);

        label.setComponentPopupMenu(menu);

        getContentPane().add(label);
        setSize(400, 400);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new PopupSelection().setVisible(true));
    }
}

Second thing I tried is with a PropertyChangeListener, but it does not work (does not detect this specific event) either:

menu.addPropertyChangeListener(System.out::println);

I know there is the alternative of adding a ChangeListener to each JMenuItem and each time iterate all components of the JPopupMenu in order to find which one is selected, but this is not a solution I want to follow since it will add unwanted complexity in my code.

So, is there a way to detect the selection?

In case of a XY problem, my final goal is to increase/decrease this scrollbar properly when user changes menu's selection with arrow buttons: ā†‘ ā†“


Solution

  • Use change listener on button model of your items. Here is the solution:

    import java.awt.Component;
    import java.awt.FlowLayout;
    import java.util.stream.Stream;
    
    import javax.swing.AbstractButton;
    import javax.swing.ButtonModel;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JMenuItem;
    import javax.swing.JPopupMenu;
    import javax.swing.SwingUtilities;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    
    /**
     * <code>PopupSelection</code>.
     */
    public class PopupSelection extends JFrame {
        private static final long serialVersionUID = 363879723515243543L;
    
        public PopupSelection() {
            super("something");
            setDefaultCloseOperation(EXIT_ON_CLOSE);
            setLayout(new FlowLayout());
    
            JLabel label = new JLabel("right click me");
    
            JPopupMenu menu = new MyPopupMenu();
            menu.getSelectionModel().addChangeListener(System.out::println);
    
            JMenuItem menuItem1 = new JMenuItem("Item1");
            JMenuItem menuItem2 = new JMenuItem("Item2");
            JMenuItem menuItem3 = new JMenuItem("Item3");
            menu.add(menuItem1);
            menu.add(menuItem2);
            menu.add(menuItem3);
    
            label.setComponentPopupMenu(menu);
    
            getContentPane().add(label);
            setSize(400, 400);
            setLocationRelativeTo(null);
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> new PopupSelection().setVisible(true));
        }
    
        private static class MyPopupMenu extends JPopupMenu {
    
            private final ChangeListener listener = this::changed;
    
            @Override
            protected void addImpl(Component comp, Object constraints, int index) {
                super.addImpl(comp, constraints, index);
                if (comp instanceof AbstractButton) {
                    ((AbstractButton) comp).getModel().addChangeListener(listener);
                }
            }
    
            @Override
            public void remove(int index) {
                Component comp = getComponent(index);
                if (comp instanceof AbstractButton) {
                    ((AbstractButton) comp).getModel().removeChangeListener(listener);
                }
                super.remove(index);
            }
    
            private void changed(ChangeEvent e) {
                ButtonModel model = (ButtonModel) e.getSource();
                AbstractButton selected = Stream.of(getComponents()).filter(AbstractButton.class::isInstance)
                        .map(AbstractButton.class::cast)
                        .filter(b -> b.getModel().isArmed() && b.getModel() == model).findAny().orElse(null);
                setSelected(selected);
            }
        }
    }