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}