Vine Vine - 17 days ago 7
Java Question

Dynamically create Java Swing submenus

I know how to create Java Swing submenus using JMenu. When we hover the mouse over a JMenu object, it displays a JPopupMenu showing the submenu items, like this:

Submenu using JMenu

https://i.stack.imgur.com/K6yz8.png

My problem is that in my application, determining which menu elements will have a submenu is expensive. I don't want to have to determine in advance whether a particular menu element should be a JMenu or just a JMenuItem. I want to make every element a JMenuItem and display a submenu for it only if the user requests it by, e.g., hovering the mouse over a menu item. Like this:

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

public class Menu2 extends JFrame
{
public Menu2()
{
super("Menu2");
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu mItems = new JMenu("Items");
menuBar.add(mItems);
mItems.add(new JMI("A"));
mItems.add(new JMI("B"));
mItems.add(new JMI("C"));
JLabel stuff = new JLabel("Other stuff");
stuff.setPreferredSize(new Dimension(200,200));
getContentPane().add(stuff);
pack();
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
}

private class JMI extends JMenuItem
implements MouseListener
{
public JPopupMenu childrenPopup = null;

public JMI(String label)
{
super(label);
addMouseListener(this);
}

// MouseListener

public void mouseClicked(MouseEvent ev) {}
public void mouseEntered(MouseEvent ev)
{
// Show a submenu for item "B" only.
// In real life we'd want a Timer to delay showing the submenu
// until we are sure the user is hovering the mouse.
// For simplicity I've omitted it.

if (getText().equals("B")) {
if (childrenPopup == null) {
childrenPopup = new JPopupMenu();
// Expensive processing to determine submenu elements...
childrenPopup.add("D");
childrenPopup.add("E");
}
// Display the submenu
childrenPopup.show(this,getWidth(),0);
}
}
public void mouseExited(MouseEvent ev) {}
public void mousePressed(MouseEvent ev) {}
public void mouseReleased(MouseEvent ev) {}
}


public static void main(String[] args)
throws Exception
{
new Menu2().setVisible(true);
}
}


The only problem is that when my manually created JPopupMenu is displayed, the rest of the menu gets closed. The resulting display does not look like the earlier one, but rather like this:

Submenu displayed manually

https://i.stack.imgur.com/cqV7q.png

Note that I did not click on the "B" menu item, only moved the mouse into it. The menu did not close due to a mouse click.

How can I do what JMenu does -- display a JPopupMenu without closing the rest of the menu?

Answer

The approach I've tentatively decided upon is to extend JMenu instead of JMenuItem and use this type for all of my menu elements. But I won't populate these elements (the expensive step) until the user requests it by hovering the mouse.

To avoid cluttering up the menu with the arrow icons that JMenu normally displays (potentially misleading in this case), I use a technique described by Stackoverflow's MadProgrammer to instantiate an arrowless JMenu in a static factory method. Since I restore the arrow icon property after creating the arrowless JMenu, normal JMenu instances created elsewhere will still show the arrow.

Some menu elements will need to execute actions and close the menu, like a JMenuItem does. A JMenu doesn't normally respond to mouse clicks, so I execute click actions in my MouseListener.

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

public class Menu3 extends JFrame
{
  public Menu3()
  {
    super("Menu3");
    JMenuBar menuBar = new JMenuBar();
    setJMenuBar(menuBar);
    JMenu mItems = new JMenu("Items");
    menuBar.add(mItems);
    mItems.add(JM.create("A"));
    mItems.add(JM.create("B"));
    mItems.add(JM.create("C"));
    JLabel stuff = new JLabel("Other stuff");
    stuff.setPreferredSize(new Dimension(200,200));
    getContentPane().add(stuff);
    pack();
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
  }

  private static class JM extends JMenu
    implements MouseListener
  {
    private static final String ARROW_ICON_KEY = "Menu.arrowIcon";

    private boolean populated = false; // Submenu already populated?

    protected JM(String label)
    {
      super(label);
      addMouseListener(this);
    }

    // This static factory method returns a JM without an arrow icon.

    public static JM create(String label)
    {
      UIDefaults uiDefaults = UIManager.getLookAndFeelDefaults();
      Object savedArrowIcon = uiDefaults.get(ARROW_ICON_KEY);
      uiDefaults.put(ARROW_ICON_KEY,null);
      JM newJM = new JM(label);
      uiDefaults.put(ARROW_ICON_KEY,savedArrowIcon);
      return newJM;
    }

    // MouseListener

    public void mouseClicked(MouseEvent ev)
    {
      // Since some menu elements need to execute actions and a JMenu
      // doesn't normally respond to mouse clicks, we execute click
      // actions here.  In real life we'll probably fire some event
      // for which an EventListener can listen.  For illustrative
      // purposes we'll just write out a trace message.

      System.err.println("Executing "+getText());
      MenuSelectionManager.defaultManager().clearSelectedPath();
    }
    public void mouseEntered(MouseEvent ev)
    {
      // In real life we'd want a Timer to delay showing the submenu
      // until we are sure the user is "hovering" the mouse.
      // For simplicity I've omitted it.

      // Populate this submenu only once
      if (!populated) {
        // For purposes of example, show a submenu for item "B" only.
        if (getText().equals("B")) {
          // Expensive processing...
          add(create("D"));
          add(create("E"));
        }
        populated = true;
      }
    }
    public void mouseExited(MouseEvent ev) {}
    public void mousePressed(MouseEvent ev) {}
    public void mouseReleased(MouseEvent ev) {}
  }

  public static void main(String[] args)
    throws Exception
  {
    new Menu3().setVisible(true);
  }
}

The result works the way I want:

Menu3 with open menu