From 6ac33c1e20fe9e0f5199b4a3ff33afd68e2d679f Mon Sep 17 00:00:00 2001 From: weisj Date: Mon, 27 Apr 2020 20:06:32 +0200 Subject: [PATCH] Refactored installation of MouseGrabber. Removed old MouseGrabber from some remaining listener lists it was added to. --- .../com/github/weisj/darklaf/DarkLaf.java | 7 +- .../darklaf/ui/popupmenu/DarkPopupMenuUI.java | 290 +---------------- .../darklaf/ui/popupmenu/MouseGrabber.java | 298 ++++++++++++++++++ .../ui/popupmenu/MouseGrabberUtil.java | 107 +++++++ 4 files changed, 408 insertions(+), 294 deletions(-) create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabber.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabberUtil.java diff --git a/core/src/main/java/com/github/weisj/darklaf/DarkLaf.java b/core/src/main/java/com/github/weisj/darklaf/DarkLaf.java index 37b46c33..26ccb1d3 100644 --- a/core/src/main/java/com/github/weisj/darklaf/DarkLaf.java +++ b/core/src/main/java/com/github/weisj/darklaf/DarkLaf.java @@ -35,7 +35,7 @@ import com.github.weisj.darklaf.platform.DecorationsHandler; import com.github.weisj.darklaf.task.*; import com.github.weisj.darklaf.theme.Theme; import com.github.weisj.darklaf.ui.DarkPopupFactory; -import com.github.weisj.darklaf.ui.popupmenu.DarkPopupMenuUI; +import com.github.weisj.darklaf.ui.popupmenu.MouseGrabberUtil; import com.github.weisj.darklaf.util.SystemInfo; /** @@ -120,10 +120,7 @@ public class DarkLaf extends BasicLookAndFeel { @Override public void uninitialize() { base.uninitialize(); - DarkPopupMenuUI.MouseGrabber mouseGrabber = DarkPopupMenuUI.getMouseGrabber(); - if (mouseGrabber != null) { - mouseGrabber.uninstall(); - } + MouseGrabberUtil.uninstallMouseGrabber(); isInitialized = false; } diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/DarkPopupMenuUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/DarkPopupMenuUI.java index ba06f189..7bb24283 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/DarkPopupMenuUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/DarkPopupMenuUI.java @@ -24,22 +24,15 @@ */ package com.github.weisj.darklaf.ui.popupmenu; -import java.applet.Applet; import java.awt.*; import java.awt.event.*; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.List; import javax.swing.*; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicPopupMenuUI; -import sun.awt.SunToolkit; - -import com.github.weisj.darklaf.components.OverlayScrollPane; import com.github.weisj.darklaf.components.ScrollPopupMenu; import com.github.weisj.darklaf.ui.DarkPopupFactory; import com.github.weisj.darklaf.util.DarkUIUtil; @@ -57,7 +50,6 @@ public class DarkPopupMenuUI extends BasicPopupMenuUI { public static final String KEY_DO_NOT_CANCEL_ON_SCROLL = "doNotCancelOnScroll"; public static final StringBufferWrapper HIDE_POPUP_VALUE = new StringBufferWrapper(new StringBuffer("doNotCancelPopup")); public static final String KEY_DEFAULT_LIGHTWEIGHT_POPUPS = "PopupMenu.defaultLightWeightPopups"; - protected static MouseGrabber mouseGrabber; private PopupMenuContainer popupMenuContainer; public static ComponentUI createUI(final JComponent x) { @@ -88,10 +80,6 @@ public class DarkPopupMenuUI extends BasicPopupMenuUI { return list; } - public static MouseGrabber getMouseGrabber() { - return mouseGrabber; - } - @Override public void installDefaults() { if (!(popupMenu instanceof ScrollPopupMenu)) { @@ -104,10 +92,7 @@ public class DarkPopupMenuUI extends BasicPopupMenuUI { @Override protected void installListeners() { super.installListeners(); - removeOldMouseGrabber(); - if (mouseGrabber == null) { - mouseGrabber = new MouseGrabber(); - } + MouseGrabberUtil.installMouseGrabber(); } @Override @@ -117,279 +102,6 @@ public class DarkPopupMenuUI extends BasicPopupMenuUI { return popupMenuContainer.createPopup(popup, x, y, maxHeight); } - /** - * This Method is responsible for removing the old MouseGrabber from the AppContext, to be able to add our own - * implementation for it that is a bit more generous with closing the popup. - */ - private void removeOldMouseGrabber() { - MenuSelectionManager menuSelectionManager = MenuSelectionManager.defaultManager(); - for (ChangeListener listener : menuSelectionManager.getChangeListeners()) { - if (listener == null) continue; - Class listenerClass = listener.getClass(); - if (listenerClass == null) continue; - Class enclosingClass = listenerClass.getEnclosingClass(); - if (enclosingClass == null) continue; - if (enclosingClass.getName().endsWith("BasicPopupMenuUI")) { - menuSelectionManager.removeChangeListener(listener); - break; - } - } - } - - public static class MouseGrabber implements ChangeListener, AWTEventListener, ComponentListener, WindowListener { - - Window grabbedWindow; - MenuElement[] lastPathSelected; - - public MouseGrabber() { - MenuSelectionManager msm = MenuSelectionManager.defaultManager(); - msm.addChangeListener(this); - this.lastPathSelected = msm.getSelectedPath(); - if (this.lastPathSelected.length != 0) { - grabWindow(this.lastPathSelected); - } - } - - protected void grabWindow(final MenuElement[] newPath) { - // A grab needs to be added - final Toolkit tk = Toolkit.getDefaultToolkit(); - java.security.AccessController.doPrivileged((PrivilegedAction) () -> { - tk.addAWTEventListener(MouseGrabber.this, - AWTEvent.MOUSE_EVENT_MASK - | AWTEvent.MOUSE_MOTION_EVENT_MASK - | AWTEvent.MOUSE_WHEEL_EVENT_MASK - | AWTEvent.WINDOW_EVENT_MASK - | SunToolkit.GRAB_EVENT_MASK); - return null; - }); - - Component invoker = newPath[0].getComponent(); - if (invoker instanceof JPopupMenu) { - invoker = ((JPopupMenu) invoker).getInvoker(); - } - grabbedWindow = (invoker == null) ? null : ((invoker instanceof Window) ? (Window) invoker - : SwingUtilities.getWindowAncestor(invoker)); - if (grabbedWindow != null) { - if (tk instanceof sun.awt.SunToolkit) { - ((sun.awt.SunToolkit) tk).grab(grabbedWindow); - } else { - grabbedWindow.addComponentListener(this); - grabbedWindow.addWindowListener(this); - } - } - } - - public void uninstall() { - MenuSelectionManager.defaultManager().removeChangeListener(this); - ungrabWindow(); - mouseGrabber = null; - } - - protected void ungrabWindow() { - final Toolkit tk = Toolkit.getDefaultToolkit(); - // The grab should be removed - java.security.AccessController.doPrivileged((PrivilegedAction) () -> { - tk.removeAWTEventListener(MouseGrabber.this); - return null; - }); - realUngrabWindow(); - } - - protected void realUngrabWindow() { - Toolkit tk = Toolkit.getDefaultToolkit(); - if (grabbedWindow != null) { - if (tk instanceof sun.awt.SunToolkit) { - ((sun.awt.SunToolkit) tk).ungrab(grabbedWindow); - } else { - grabbedWindow.removeComponentListener(this); - grabbedWindow.removeWindowListener(this); - } - grabbedWindow = null; - } - } - - public void stateChanged(final ChangeEvent e) { - MenuSelectionManager msm = MenuSelectionManager.defaultManager(); - MenuElement[] p = msm.getSelectedPath(); - - if (lastPathSelected.length == 0 && p.length != 0) { - grabWindow(p); - } - - if (lastPathSelected.length != 0 && p.length == 0) { - ungrabWindow(); - } - - lastPathSelected = p; - repaintIfNecessary(e); - } - - protected void repaintIfNecessary(final ChangeEvent e) { - Object source = e.getSource(); - if (source instanceof MenuSelectionManager) { - MenuSelectionManager manager = (MenuSelectionManager) source; - MenuElement[] path = manager.getSelectedPath(); - if (path != null && path.length > 0) { - Component comp = path[0].getComponent(); - if (comp.isVisible()) { - OverlayScrollPane sp = DarkUIUtil.getParentOfType(OverlayScrollPane.class, comp); - if (sp != null) sp.repaint(comp.getBounds()); - } - } - } - } - - public void eventDispatched(final AWTEvent ev) { - if (ev instanceof sun.awt.UngrabEvent) { - // Popup should be canceled in case of ungrab event - cancelPopupMenu(); - return; - } - if (!(ev instanceof MouseEvent)) { - // We are interested in MouseEvents only - return; - } - MouseEvent me = (MouseEvent) ev; - Component src = me.getComponent(); - // If the scroll is done inside a combobox, menuitem, - // or inside a Popup#HeavyWeightWindow or inside a frame - // popup should not close which is the standard behaviour - switch (me.getID()) {/* - * Changed here: Make doNotCancelPopup accessible to all component. - * Allows for more versatile PopupMenus. - */ - case MouseEvent.MOUSE_PRESSED : - if (isInPopup(src) || - (src instanceof JMenu && ((JMenu) src).isSelected())) { - return; - } - if (!(src instanceof JComponent) || - !HIDE_POPUP_VALUE.equals(((JComponent) src).getClientProperty(KEY_DO_NOT_CANCEL_POPUP))) { - // Cancel popup only if this property was not set. - // If this property is set to TRUE component wants - // to deal with this event by himself. - cancelPopupMenu(); - // Ask UIManager about should we consume event that closes - // popup. This made to match native apps behaviour. - boolean consumeEvent = UIManager.getBoolean("PopupMenu.consumeEventOnClose"); - // Consume the event so that normal processing stops. - if (consumeEvent && !(src instanceof MenuElement)) { - me.consume(); - } - } - break; - case MouseEvent.MOUSE_RELEASED : - if (!(src instanceof MenuElement)) { - // Do not forward event to MSM, let component handle it - if (isInPopup(src)) { - break; - } - } - if (src instanceof JMenu || !(src instanceof JMenuItem)) { - MenuSelectionManager.defaultManager().processMouseEvent(me); - } - break; - case MouseEvent.MOUSE_DRAGGED : - if (!(src instanceof MenuElement)) { - // For the MOUSE_DRAGGED event the src is - // the Component in which mouse button was pressed. - // If the src is in popupMenu, - // do not forward event to MSM, let component handle it. - if (isInPopup(src)) { - break; - } - } - MenuSelectionManager.defaultManager().processMouseEvent(me); - break; - case MouseEvent.MOUSE_WHEEL : - if (isInPopup(src) - || ((src instanceof JComboBox) && ((JComboBox) src).isPopupVisible()) - || ((src instanceof JWindow) && src.isVisible()) - || ((src instanceof JMenuItem) && src.isVisible()) - || (src instanceof JFrame) - || (src instanceof JDialog)) { - return; - } - cancelPopupMenu(); - break; - } - } - - protected void cancelPopupMenu() { - // We should ungrab window if a user code throws - // an unexpected runtime exception. See 6495920. - try { - // 4234793: This action should call firePopupMenuCanceled but it's - // a protected method. The real solution could be to make - // firePopupMenuCanceled public and call it directly. - List popups = getPopups(); - for (JPopupMenu popup : popups) { - popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE); - } - MenuSelectionManager.defaultManager().clearSelectedPath(); - } catch (RuntimeException | Error ex) { - realUngrabWindow(); - throw ex; - } - } - - @SuppressWarnings("deprecation") - protected boolean isInPopup(final Component src) { - for (Component c = src; c != null; c = c.getParent()) { - if (c instanceof Applet || c instanceof Window) { - break; - } else if (c instanceof JPopupMenu) { - return true; - } else if (c instanceof JComponent - && Boolean.TRUE.equals(((JComponent) c).getClientProperty(KEY_DO_NOT_CANCEL_ON_SCROLL))) { - /* - * Change here: allows scrollable components that contain the popupMenu. - */ - return true; - } - } - return false; - } - - public void componentResized(final ComponentEvent e) { - cancelPopupMenu(); - } - - public void componentMoved(final ComponentEvent e) { - cancelPopupMenu(); - } - - public void componentShown(final ComponentEvent e) { - cancelPopupMenu(); - } - - public void componentHidden(final ComponentEvent e) { - cancelPopupMenu(); - } - - public void windowOpened(final WindowEvent e) {} - - public void windowClosing(final WindowEvent e) { - cancelPopupMenu(); - } - - public void windowClosed(final WindowEvent e) { - cancelPopupMenu(); - } - - public void windowIconified(final WindowEvent e) { - cancelPopupMenu(); - } - - public void windowDeiconified(final WindowEvent e) {} - - public void windowActivated(final WindowEvent e) {} - - public void windowDeactivated(final WindowEvent e) { - cancelPopupMenu(); - } - } - protected static class StringBufferWrapper { private final StringBuffer buffer; diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabber.java b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabber.java new file mode 100644 index 00000000..3bc0951a --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabber.java @@ -0,0 +1,298 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.popupmenu; + +import java.applet.Applet; +import java.awt.*; +import java.awt.event.*; +import java.security.PrivilegedAction; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import sun.awt.SunToolkit; + +import com.github.weisj.darklaf.components.OverlayScrollPane; +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class MouseGrabber implements ChangeListener, AWTEventListener, ComponentListener, WindowListener { + + Window grabbedWindow; + MenuElement[] lastPathSelected; + + public MouseGrabber() { + install(); + } + + public void install() { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + msm.addChangeListener(this); + this.lastPathSelected = msm.getSelectedPath(); + if (this.lastPathSelected.length != 0) { + grabWindow(this.lastPathSelected); + } + } + + protected void grabWindow(final MenuElement[] newPath) { + // A grab needs to be added + final Toolkit tk = Toolkit.getDefaultToolkit(); + java.security.AccessController.doPrivileged((PrivilegedAction) () -> { + tk.addAWTEventListener(MouseGrabber.this, + AWTEvent.MOUSE_EVENT_MASK + | AWTEvent.MOUSE_MOTION_EVENT_MASK + | AWTEvent.MOUSE_WHEEL_EVENT_MASK + | AWTEvent.WINDOW_EVENT_MASK + | SunToolkit.GRAB_EVENT_MASK); + return null; + }); + + Component invoker = newPath[0].getComponent(); + if (invoker instanceof JPopupMenu) { + invoker = ((JPopupMenu) invoker).getInvoker(); + } + grabbedWindow = DarkUIUtil.getWindow(invoker); + if (grabbedWindow != null) { + if (tk instanceof SunToolkit) { + ((SunToolkit) tk).grab(grabbedWindow); + } else { + grabbedWindow.addComponentListener(this); + grabbedWindow.addWindowListener(this); + } + } + } + + public void uninstall() { + MenuSelectionManager.defaultManager().removeChangeListener(this); + ungrabWindow(); + } + + protected void ungrabWindow() { + final Toolkit tk = Toolkit.getDefaultToolkit(); + // The grab should be removed + java.security.AccessController.doPrivileged((PrivilegedAction) () -> { + tk.removeAWTEventListener(MouseGrabber.this); + return null; + }); + realUngrabWindow(); + } + + protected void realUngrabWindow() { + Toolkit tk = Toolkit.getDefaultToolkit(); + if (grabbedWindow != null) { + if (tk instanceof SunToolkit) { + ((SunToolkit) tk).ungrab(grabbedWindow); + } else { + grabbedWindow.removeComponentListener(this); + grabbedWindow.removeWindowListener(this); + } + grabbedWindow = null; + } + } + + public void stateChanged(final ChangeEvent e) { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement[] p = msm.getSelectedPath(); + + if (lastPathSelected.length == 0 && p.length != 0) { + grabWindow(p); + } + + if (lastPathSelected.length != 0 && p.length == 0) { + ungrabWindow(); + } + + lastPathSelected = p; + repaintIfNecessary(e); + } + + protected void repaintIfNecessary(final ChangeEvent e) { + Object source = e.getSource(); + if (source instanceof MenuSelectionManager) { + MenuSelectionManager manager = (MenuSelectionManager) source; + MenuElement[] path = manager.getSelectedPath(); + if (path != null && path.length > 0) { + Component comp = path[0].getComponent(); + if (comp.isVisible()) { + OverlayScrollPane sp = DarkUIUtil.getParentOfType(OverlayScrollPane.class, comp); + if (sp != null) sp.repaint(comp.getBounds()); + } + } + } + } + + public void eventDispatched(final AWTEvent ev) { + if (ev instanceof sun.awt.UngrabEvent) { + // Popup should be canceled in case of ungrab event + cancelPopupMenu(); + return; + } + if (!(ev instanceof MouseEvent)) { + // We are interested in MouseEvents only + return; + } + MouseEvent me = (MouseEvent) ev; + Component src = me.getComponent(); + // If the scroll is done inside a combobox, menuitem, + // or inside a Popup#HeavyWeightWindow or inside a frame + // popup should not close which is the standard behaviour + switch (me.getID()) {/* + * Changed here: Make doNotCancelPopup accessible to all component. + * Allows for more versatile PopupMenus. + */ + case MouseEvent.MOUSE_PRESSED : + if (isInPopup(src) || + (src instanceof JMenu && ((JMenu) src).isSelected())) { + return; + } + if (!(src instanceof JComponent) || + !DarkPopupMenuUI.HIDE_POPUP_VALUE.equals(((JComponent) src).getClientProperty( + DarkPopupMenuUI.KEY_DO_NOT_CANCEL_POPUP))) { + // Cancel popup only if this property was not set. + // If this property is set to TRUE component wants + // to deal with this event by himself. + cancelPopupMenu(); + // Ask UIManager about should we consume event that closes + // popup. This made to match native apps behaviour. + boolean consumeEvent = UIManager.getBoolean("PopupMenu.consumeEventOnClose"); + // Consume the event so that normal processing stops. + if (consumeEvent && !(src instanceof MenuElement)) { + me.consume(); + } + } + break; + case MouseEvent.MOUSE_RELEASED : + if (!(src instanceof MenuElement)) { + // Do not forward event to MSM, let component handle it + if (isInPopup(src)) { + break; + } + } + if (src instanceof JMenu || !(src instanceof JMenuItem)) { + MenuSelectionManager.defaultManager().processMouseEvent(me); + } + break; + case MouseEvent.MOUSE_DRAGGED : + if (!(src instanceof MenuElement)) { + // For the MOUSE_DRAGGED event the src is + // the Component in which mouse button was pressed. + // If the src is in popupMenu, + // do not forward event to MSM, let component handle it. + if (isInPopup(src)) { + break; + } + } + MenuSelectionManager.defaultManager().processMouseEvent(me); + break; + case MouseEvent.MOUSE_WHEEL : + if (isInPopup(src) + || ((src instanceof JComboBox) && ((JComboBox) src).isPopupVisible()) + || ((src instanceof JWindow) && src.isVisible()) + || ((src instanceof JMenuItem) && src.isVisible()) + || (src instanceof JFrame) + || (src instanceof JDialog)) { + return; + } + cancelPopupMenu(); + break; + } + } + + protected void cancelPopupMenu() { + // We should ungrab window if a user code throws + // an unexpected runtime exception. See 6495920. + try { + // 4234793: This action should call firePopupMenuCanceled but it's + // a protected method. The real solution could be to make + // firePopupMenuCanceled public and call it directly. + List popups = DarkPopupMenuUI.getPopups(); + for (JPopupMenu popup : popups) { + popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE); + } + MenuSelectionManager.defaultManager().clearSelectedPath(); + } catch (RuntimeException | Error ex) { + realUngrabWindow(); + throw ex; + } + } + + @SuppressWarnings("deprecation") + protected boolean isInPopup(final Component src) { + for (Component c = src; c != null; c = c.getParent()) { + if (c instanceof Applet || c instanceof Window) { + break; + } else if (c instanceof JPopupMenu) { + return true; + } else if (c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty( + DarkPopupMenuUI.KEY_DO_NOT_CANCEL_ON_SCROLL))) { + /* + * Change here: allows scrollable components that contain the popupMenu. + */ + return true; + } + } + return false; + } + + public void componentResized(final ComponentEvent e) { + cancelPopupMenu(); + } + + public void componentMoved(final ComponentEvent e) { + cancelPopupMenu(); + } + + public void componentShown(final ComponentEvent e) { + cancelPopupMenu(); + } + + public void componentHidden(final ComponentEvent e) { + cancelPopupMenu(); + } + + public void windowOpened(final WindowEvent e) {} + + public void windowClosing(final WindowEvent e) { + cancelPopupMenu(); + } + + public void windowClosed(final WindowEvent e) { + cancelPopupMenu(); + } + + public void windowIconified(final WindowEvent e) { + cancelPopupMenu(); + } + + public void windowDeiconified(final WindowEvent e) {} + + public void windowActivated(final WindowEvent e) {} + + public void windowDeactivated(final WindowEvent e) { + cancelPopupMenu(); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabberUtil.java b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabberUtil.java new file mode 100644 index 00000000..0cc0e9d1 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/popupmenu/MouseGrabberUtil.java @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.popupmenu; + +import java.awt.*; +import java.awt.event.AWTEventListener; +import java.awt.event.ComponentListener; +import java.awt.event.WindowListener; +import java.security.PrivilegedAction; + +import javax.swing.*; +import javax.swing.event.ChangeListener; + +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class MouseGrabberUtil { + + private MouseGrabberUtil() { + + } + + private static MouseGrabber mouseGrabber; + + public static void uninstallMouseGrabber() { + if (mouseGrabber != null) { + mouseGrabber.uninstall(); + mouseGrabber = null; + } + } + + public static void installMouseGrabber() { + if (mouseGrabber == null) { + uninstallOldMouseGrabber(getOldMouseGrabber()); + mouseGrabber = new MouseGrabber(); + mouseGrabber.install(); + } + } + + public static ChangeListener getOldMouseGrabber() { + MenuSelectionManager menuSelectionManager = MenuSelectionManager.defaultManager(); + for (ChangeListener listener : menuSelectionManager.getChangeListeners()) { + if (listener == null) continue; + Class listenerClass = listener.getClass(); + if (listenerClass == null) continue; + Class enclosingClass = listenerClass.getEnclosingClass(); + if (enclosingClass == null) continue; + if (listenerClass.getName().endsWith("MouseGrabber") + && enclosingClass.getName().endsWith("BasicPopupMenuUI")) { + return listener; + } + } + return null; + } + + /** + * This Method is responsible for removing the old MouseGrabber from the AppContext, to be able to add our own + * implementation for it that is a bit more generous with closing the popup. + */ + public static void uninstallOldMouseGrabber(final ChangeListener oldMouseGrabber) { + if (oldMouseGrabber == null) return; + MenuSelectionManager menuSelectionManager = MenuSelectionManager.defaultManager(); + menuSelectionManager.removeChangeListener(oldMouseGrabber); + if (oldMouseGrabber instanceof AWTEventListener) { + Toolkit tk = Toolkit.getDefaultToolkit(); + java.security.AccessController.doPrivileged((PrivilegedAction) () -> { + tk.removeAWTEventListener((AWTEventListener) oldMouseGrabber); + return null; + }); + } + MenuElement[] path = menuSelectionManager.getSelectedPath(); + if (path.length != 0 && path[0] != null) { + Component invoker = path[0].getComponent(); + if (invoker instanceof JPopupMenu) { + invoker = ((JPopupMenu) invoker).getInvoker(); + } + Window grabbedWindow = DarkUIUtil.getWindow(invoker); + if (oldMouseGrabber instanceof WindowListener) { + grabbedWindow.removeWindowListener((WindowListener) oldMouseGrabber); + } + if (oldMouseGrabber instanceof ComponentListener) { + grabbedWindow.removeComponentListener((ComponentListener) oldMouseGrabber); + } + } + } +}