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 = ((Map<?, ?>) tableField.get(appContextClass.getMethod("getAppContext").invoke(appContextClass)))
059                        .entrySet().stream()
060                        .filter(e -> MOUSE_GRABBER_KEY.equals(Objects.toString(e.getKey())))
061                        .findFirst()
062                        .map(Entry::getValue)
063                        .orElse(null);
064                final ChangeListener changeListener = (ChangeListener) mouseGrabber;
065                final AWTEventListener awtEventListener = (AWTEventListener) mouseGrabber;
066                final MenuSelectionManager msm = MenuSelectionManager.defaultManager();
067                final Toolkit tk = Toolkit.getDefaultToolkit();
068                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
069                    if (b)
070                        msm.removeChangeListener(changeListener);
071                    else
072                        msm.addChangeListener(changeListener);
073                    tk.removeAWTEventListener(awtEventListener);
074                    tk.addAWTEventListener(awtEventListener,
075                            AWTEvent.MOUSE_EVENT_MASK |
076                            AWTEvent.MOUSE_MOTION_EVENT_MASK |
077                            AWTEvent.MOUSE_WHEEL_EVENT_MASK |
078                            AWTEvent.WINDOW_EVENT_MASK | (b ? 0 : GRAB_EVENT_MASK));
079                    return null;
080                });
081            } catch (ReflectiveOperationException | RuntimeException e) {
082                Logging.error(e);
083            }
084        }
085        super.setVisible(b);
086    }
087}