George Z.
George Z.

Reputation: 6808

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:

Upvotes: 3

Views: 470

Answers (2)

aterai
aterai

Reputation: 9808

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));
  }
}

Upvotes: 1

Sergiy Medvynskyy
Sergiy Medvynskyy

Reputation: 11327

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);
        }
    }
}

Upvotes: 2

Related Questions