javaswingjmenujmenuitem

Java Swing - Add leniency when selecting items in submenus


When attempting to click on an item in a submenu, it is natural to quickly draw your mouse across the menu items below it. Both Windows and Mac natively handle this by putting a small delay before the a menu is opened. Swing JMenus do not handle this, and the menu the mouse briefly hovers over would be opened before the mouse reaches the intended menu item.

For example, in the image below, if I tried to select Item 3, but in the process my mouse briefly slid across Menu 2, the Menu 1 submenu would disappear before I got to it.

Does anyone have any tips or suggestions for getting around this? My idea was to define a custom MenuUI that added a timer to its mouse handler.

a screen

Here is some simple example code that illustrates my problem:

public class Thing extends JFrame {
    public Thing()
    {
        super();
        this.setSize(new Dimension(500, 500));
        final JPopupMenu pMenu = new JPopupMenu();
        for (int i = 0; i < 5; i++)
        {
            JMenu menu = new JMenu("Menu " + i);
            pMenu.add(menu);
            for (int j = 0; j < 10; j++)
            {
                menu.add(new JMenuItem("Item " + j));
            }
        }

        this.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                pMenu.show(Thing.this, e.getX(), e.getY());
            }
        });
    }

    public static void main(String[] args)
    {
        Thing t = new Thing();
        t.setVisible(true);
    }
}

Solution

  • I came up with a very hacky solution.

    I made a UI class that extends BasicMenuUI. I override the createMouseInputListener method to return a custom MouseInputListener instead of the private handler object inside BasicMenuUI.

    I then got the code for the MouseInputListener implementation in handler from GrepCode[1], and copied it into my custom listener. I made one change, putting a timer in mouseEntered. My final code for mouseEntered looks like this:

    public void mouseEntered(MouseEvent e) {
            timer.schedule(new TimerTask() {
    
                @Override
                public void run() {
                    if (menuItem.isShowing())
                    {
                        Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
                        Point menuLoc = menuItem.getLocationOnScreen();
                        if (mouseLoc.x >= menuLoc.x && mouseLoc.x <= menuLoc.x + menuItem.getWidth() &&
                                mouseLoc.y >= menuLoc.y && mouseLoc.y <= menuLoc.y + menuItem.getHeight())
                        {
                            originalMouseEnteredStuff();
                        }
                    }
                }
            }, 100);
        }
    

    Before calling the the original code that was in mouseEntered, I check to make sure the mouse is still within this menu's area. I don't want all the menus my mouse brushes over to pop up after 100 ms.

    Please let me know if anyone has discovered a better solution for this.

    [1] http://www.grepcode.com/file_/repository.grepcode.com/java/root/jdk/openjdk/7-b147/javax/swing/plaf/basic/BasicMenuUI.java/?v=source