JFormDesigner / FlatLaf

FlatLaf - Swing Look and Feel (with Darcula/IntelliJ themes support)
https://www.formdev.com/flatlaf/
Apache License 2.0
3.31k stars 265 forks source link

Improving the user friendliness of context menus by closing them less agressively #247

Closed Bios-Marcel closed 2 years ago

Bios-Marcel commented 3 years ago

I've been noticing that context menus are often a source of frustration, as they require a certain level of mouse control if you have submenus present. It's easy to slip one row deeper and accidentally close the submenu that you just meant to click. No matter whether you click the item or just hover over it, the submenu will close as soon as your mouse exits the item that caused the submenu to open.

While you could argue that this makes sense, I don't deem it userfriendly. Looking at Microsoft Excel for example, the submenus are somewhat more nice to work with, since submenus don't close right away, but after a short delay.

Here's a little GIF showing when excel actually closes the menu:

MLOWVMNRko

As you can see, the menu stays open as long as you keep your cursors moving. Personally I'd find this very nice, but it should probably be optional.

DevCharly commented 3 years ago

I fully agree with you. This is a pain.

This happens because when you move the mouse from "Filter" item right-down to the submenu, then the mouse is temporary over "Sortieren", which changes the menu selection and immediately closes the submenu.

There should be some delay before closing a submenu.

Interestingly there is a delay property in JMenu (default is 200ms in FlatLaf). The javadoc says "the suggested delay before the menu's PopupMenu is popped up or down". But it is only used when showing submenus. Not before hiding it.

This stack shows what happens when a submenu is open and the mouse is moved over another menu item:

    at com.formdev.flatlaf.ui.FlatPopupFactory$NonFlashingPopup.hide(FlatPopupFactory.java:294)
    at com.formdev.flatlaf.ui.FlatPopupFactory$DropShadowPopup.hide(FlatPopupFactory.java:435)
    at javax.swing.JPopupMenu.setVisible(JPopupMenu.java:796)
    at javax.swing.JMenu.setPopupMenuVisible(JMenu.java:347)
    at javax.swing.JPopupMenu.menuSelectionChanged(JPopupMenu.java:1480)
    at javax.swing.MenuSelectionManager.setSelectedPath(MenuSelectionManager.java:121)
    at javax.swing.plaf.basic.BasicMenuItemUI$Handler.mouseEntered(BasicMenuItemUI.java:899)
    at java.awt.Component.processMouseEvent(Component.java:6548)
    at javax.swing.JComponent.processMouseEvent(JComponent.java:3324)
    at java.awt.Component.processEvent(Component.java:6304)

The mouseEntered triggers a selection change that closes the submenu. This is done in javax.swing.MenuSelectionManager. Seems that there is no official way to change this.

Anyway, when I add following to FlatMenuItemUI, it is possible to avoid that BasicMenuItemUI$Handler.mouseEntered() is invoked and the submenu stays open. MenuItem selection no longer works.

@Override
protected MouseInputListener createMouseInputListener( JComponent c ) {
    return new BasicMenuItemUI.MouseInputHandler() {
        @Override
        public void mouseEntered( MouseEvent e ) {
            // super.mouseEntered( e );
        }
    };
}

Maybe we can find some clever ideas to fill this...

Bios-Marcel commented 3 years ago

I haven't looked at the code yet, but I am assuming this can't be too hard. I hope I get some time to look at this. But I am glad you are open to this change 👍 Thanks

DevCharly commented 2 years ago

Stumbled across an interesting article regarding submenus, which describes two solutions for this problem: delay and safe triangle

Found out that IntelliJ IDEA has implemented the safe triangle solution since some years: https://github.com/JetBrains/intellij-community/search?q=IDEA-81363&type=commits

It is implemented in class ActionMenu.UsabilityHelper: https://github.com/JetBrains/intellij-community/blob/c8d5c0a960258a7218b841ca844e749a71e7fbea/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.java#L415-L496

But it is difficult to use this solution in FlatLaf because IDEA uses its own event queue, which allows them to process mouse events before they are dispatched to Swing. So while the mouse is moved within the safe triangle, the mouse events are simply not dispatched to Swing and the submenu stays open: https://github.com/JetBrains/intellij-community/blob/c8d5c0a960258a7218b841ca844e749a71e7fbea/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.java#L463-L487

Not sure whether it would be a good idea to replace the event loop in FlatLaf because this would break applications that also use a custom event loop.

Maybe using a temporary custom event loop, while a submenu is shown, could work... See EventQueue.push(EventQueue newEventQueue) and EventQueue.pop().

Bios-Marcel commented 2 years ago

I am unsure what a temporary event loop would look like. Would we hijack the real event loop for a short while?

DevCharly commented 2 years ago

I am unsure what a temporary event loop would look like.

A subclass of java.awt.EventQueue, which overrides dispatchEvent( AWTEvent event ).

Would we hijack the real event loop for a short while?

Yes

eirikbakke commented 2 years ago

It might be worth looking at EventQueue.createSecondaryLoop(), which is the mechanism by which Dialog.setVisible(true) blocks for a modal dialog while keeping the UI responsive. Not sure if it can be used to solve the particular problem mentioned here, but it could potentially be relevant.

DevCharly commented 2 years ago

Implemented in PR #490

Prebuilt JARs are available in the artifacts here: https://github.com/JFormDesigner/FlatLaf/actions/runs/1910500195