001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.util;
003
004import java.awt.AWTEvent;
005import java.awt.Toolkit;
006import java.awt.event.AWTEventListener;
007import java.lang.reflect.Field;
008import java.security.AccessController;
009import java.security.PrivilegedAction;
010import java.util.Map;
011import java.util.Map.Entry;
012import java.util.Objects;
013
014import javax.swing.JPopupMenu;
015import javax.swing.MenuSelectionManager;
016import javax.swing.event.ChangeListener;
017
018import org.openstreetmap.josm.tools.Logging;
019import org.openstreetmap.josm.tools.PlatformManager;
020import org.openstreetmap.josm.tools.ReflectionUtils;
021
022/**
023 * A {@link JPopupMenu} that can stay open on all platforms when containing {@code StayOpen*} items.
024 * @since 15492
025 */
026public class StayOpenPopupMenu extends JPopupMenu {
027
028    private static final String MOUSE_GRABBER_KEY = "javax.swing.plaf.basic.BasicPopupMenuUI.MouseGrabber";
029
030    /**
031     * Special mask for the UngrabEvent events, in addition to the public masks defined in AWTEvent.
032     */
033    private static final int GRAB_EVENT_MASK = 0x80000000;
034
035    /**
036     * Constructs a new {@code StayOpenPopupMenu}.
037     */
038    public StayOpenPopupMenu() {
039    }
040
041    /**
042     * Constructs a new {@code StayOpenPopupMenu} with the specified title.
043     * @param label  the string that a UI may use to display as a title for the popup menu.
044     */
045    public StayOpenPopupMenu(String label) {
046        super(label);
047    }
048
049    @Override
050    public void setVisible(boolean b) {
051        // macOS triggers a spurious UngrabEvent that is catched by BasicPopupMenuUI.MouseGrabber
052        // and makes the popup menu disappear. Probably related to https://bugs.openjdk.java.net/browse/JDK-8225698
053        if (PlatformManager.isPlatformOsx()) {
054            try {
055                Class<?> appContextClass = Class.forName("sun.awt.AppContext");
056                Field tableField = appContextClass.getDeclaredField("table");
057                ReflectionUtils.setObjectsAccessible(tableField);
058                Object mouseGrabber = null;
059                for (Entry<?, ?> e : ((Map<?, ?>)
060                        tableField.get(appContextClass.getMethod("getAppContext").invoke(appContextClass))).entrySet()) {
061                    if (MOUSE_GRABBER_KEY.equals(Objects.toString(e.getKey()))) {
062                        mouseGrabber = e.getValue();
063                        break;
064                    }
065                }
066                final ChangeListener changeListener = (ChangeListener) mouseGrabber;
067                final AWTEventListener awtEventListener = (AWTEventListener) mouseGrabber;
068                final MenuSelectionManager msm = MenuSelectionManager.defaultManager();
069                final Toolkit tk = Toolkit.getDefaultToolkit();
070                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
071                    if (b)
072                        msm.removeChangeListener(changeListener);
073                    else
074                        msm.addChangeListener(changeListener);
075                    tk.removeAWTEventListener(awtEventListener);
076                    tk.addAWTEventListener(awtEventListener,
077                            AWTEvent.MOUSE_EVENT_MASK |
078                            AWTEvent.MOUSE_MOTION_EVENT_MASK |
079                            AWTEvent.MOUSE_WHEEL_EVENT_MASK |
080                            AWTEvent.WINDOW_EVENT_MASK | (b ? 0 : GRAB_EVENT_MASK));
081                    return null;
082                });
083            } catch (ReflectiveOperationException | RuntimeException e) {
084                Logging.error(e);
085            }
086        }
087        super.setVisible(b);
088    }
089}