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}