Question

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);
    }
}
Was it helpful?

Solution 2

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

OTHER TIPS

Call setDelay(delay) on your menu variable, where the delay parameter is the amount of milliseconds to wait for the menu to show, as an int.

This following line of code will set the delay to 1 second, so the user has to mouseover the menu item "Menu n" 1 second, before the submenu is displayed: menu.setDelay(1000);

Here's a snippet of the edited code:

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));
    }
    menu.setDelay(1000);
}

Thank you very much, you saved my day! The solution works as expected but I recommend using the Swing timer to ensure the code is executed by the EDT.

Additionally you should temporary set the menus delay to zero before calling the original stuff. Otherwise the user has to wait twice the delay time.

@Override
public void mouseEntered(MouseEvent e) {
    if (menu.isTopLevelMenu() || menu.getDelay() == 0) {
        originalMouseEnteredStuff(e);
    } else {
        final javax.swing.Timer timer = new javax.swing.Timer(menu.getDelay(), new DelayedMouseEnteredAction(e));
        timer.setRepeats(false);
        timer.start();
    }
}
class DelayedMouseEnteredAction implements ActionListener
{
    private final MouseEvent mouseEnteredEvent;

    private DelayedMouseEnteredAction(MouseEvent mouseEnteredEvent) {
        this.mouseEnteredEvent = mouseEnteredEvent;
    }

    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        if (menu.isShowing()) {
            final Point mouseLocationOnScreen = MouseInfo.getPointerInfo().getLocation();
            final Rectangle menuBoundsOnScreen = new Rectangle(menu.getLocationOnScreen(), menu.getSize());
            if (menuBoundsOnScreen.contains(mouseLocationOnScreen)) {
                /*
                 * forward the mouse event only if the mouse cursor is yet
                 * located in the menus area.
                 */
                int menuDelay = menu.getDelay();
                try {
                    /*
                     * Temporary remove the delay. Otherwise the delegate would wait the
                     * delay a second time e.g. before highlighting the menu item.
                     */
                    menu.setDelay(0);
                    originalMouseEnteredStuff(mouseEnteredEvent);
                } finally {
                    // reset the delay
                    menu.setDelay(menuDelay);
                }
            }
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top