3

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:

George Z.
  • 6,643
  • 4
  • 27
  • 47

2 Answers2

2

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);
        }
    }
}
Sergiy Medvynskyy
  • 11,160
  • 1
  • 32
  • 48
  • I am aware of that and I mentioned why I don't want this. – George Z. Oct 16 '19 at 12:39
  • Sorry, but you have no another possibility. I've changed my example. Probably this will satisfy you. – Sergiy Medvynskyy Oct 16 '19 at 13:08
  • See my comment in @aterai's answer if you want. I already knew I had to follow this solution. I was just wondering if there is a "cleaner" way. Anyway you will get the mark (and the +1) since I followed this approach. – George Z. Oct 17 '19 at 14:11
  • Also, just one tiny correction: `filter(b -> b.getModel().isArmed() && b.getModel() == model)` should be `.filter(b -> (b.getModel().isArmed() || b.getModel().isSelected()) && b.getModel() == model)` in order to involve sub`JMenu`s. `JMenu`s are considered selected, while `JMenuItems` are considered armed. – George Z. Oct 17 '19 at 14:49
1

Instead of adding a ChangeListener to each JMenuItem, you might be able to add a ChangeListener to the MenuSelectionManager.

MenuSelectionManager.defaultManager().addChangeListener(e -> {
  Object o = e.getSource();
  if (o instanceof MenuSelectionManager) {
    MenuSelectionManager m = (MenuSelectionManager) o;
    printMenuElementArray(m.getSelectedPath());
  }
});

PopupSelection2.java

import java.awt.*;
import javax.swing.*;

public class PopupSelection2 extends JFrame {
  public PopupSelection2() {
    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);

    MenuSelectionManager.defaultManager().addChangeListener(e -> {
      Object o = e.getSource();
      if (o instanceof MenuSelectionManager) {
        MenuSelectionManager m = (MenuSelectionManager) o;
        printMenuElementArray(m.getSelectedPath());
      }
    });

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

  // @see javax/swing/MenuSelectionManager.java
  private static void printMenuElementArray(MenuElement[] path) {
    System.out.println("Path is(");
    for (int i = 0, j = path.length; i < j ; i++) {
      for (int k = 0; k <= i; k++) {
        System.out.print("  ");
      }
      MenuElement me = path[i];
      if (me instanceof JMenuItem) {
        System.out.println(((JMenuItem)me).getText() + ", ");
      } else if (me instanceof JMenuBar) {
        System.out.println("JMenuBar, ");
      } else if (me instanceof JPopupMenu) {
        System.out.println("JPopupMenu, ");
      } else if (me == null) {
        System.out.println("NULL , ");
      } else {
        System.out.println("" + me + ", ");
      }
    }
    System.out.println(")");
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> new PopupSelection2().setVisible(true));
  }
}
aterai
  • 9,658
  • 4
  • 35
  • 44
  • I discovered that `MenuSelectionManager` responds just one minute before I see your answer. However, since I need to know the index of the selected component, and I have already extended `JPopupMenu`, @Sergiy Medvynskyy's answer seems cleaner (which I already knew I had to follow it if there is no way to detect selection with a listener). Anyway, +1 and thanks for your time. – George Z. Oct 17 '19 at 14:08