diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d9561b..db86cd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ FlatLaf Change Log - FlatLaf window decorations: Minimize and maximize icons were not shown for custom scale factors less than 100% (e.g. `-Dflatlaf.uiScale=75%`). (issue #951) +- Linux: Popups (menus and combobox lists) were not hidden when window is moved, + resized, maximized, restored, iconified or switched to another window. (issue + #962) ## 3.5.4 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index 1fd3aef9..bba4d829 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -111,6 +111,7 @@ public abstract class FlatLaf private PopupFactory oldPopupFactory; private MnemonicHandler mnemonicHandler; private boolean subMenuUsabilityHelperInstalled; + private LinuxPopupMenuCanceler linuxPopupMenuCanceler; private Consumer postInitialization; private List> uiDefaultsGetters; @@ -305,6 +306,10 @@ public abstract class FlatLaf // install submenu usability helper subMenuUsabilityHelperInstalled = SubMenuUsabilityHelper.install(); + // install Linux popup menu canceler + if( SystemInfo.isLinux ) + linuxPopupMenuCanceler = new LinuxPopupMenuCanceler(); + // listen to desktop property changes to update UI if system font or scaling changes if( SystemInfo.isWindows ) { // Windows 10 allows increasing font size independent of scaling: @@ -397,6 +402,12 @@ public abstract class FlatLaf subMenuUsabilityHelperInstalled = false; } + // uninstall Linux popup menu canceler + if( linuxPopupMenuCanceler != null ) { + linuxPopupMenuCanceler.uninstall(); + linuxPopupMenuCanceler = null; + } + // restore default link color new HTMLEditorKit().getStyleSheet().addRule( "a, address { color: blue; }" ); postInitialization = null; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxPopupMenuCanceler.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxPopupMenuCanceler.java new file mode 100644 index 00000000..0c097781 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxPopupMenuCanceler.java @@ -0,0 +1,164 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf; + +import java.awt.Component; +import java.awt.Window; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JPopupMenu; +import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Cancels (hides) popup menus on Linux. + *

+ * On Linux, popups are not hidden under following conditions, which results in + * misplaced popups: + *

+ * + * On Windows and macOS, popups are automatically hidden. + *

+ * The implementation is similar to what's done in + * {@code javax.swing.plaf.basic.BasicPopupMenuUI.MouseGrabber}, + * but only hides popup in some conditions. + * + * @author Karl Tauber + */ +class LinuxPopupMenuCanceler + extends WindowAdapter + implements ChangeListener, ComponentListener +{ + private MenuElement[] lastPathSelectedPath; + private Window window; + + LinuxPopupMenuCanceler() { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + msm.addChangeListener( this ); + + lastPathSelectedPath = msm.getSelectedPath(); + if( lastPathSelectedPath.length > 0 ) + addWindowListeners( lastPathSelectedPath[0] ); + } + + void uninstall() { + MenuSelectionManager.defaultManager().removeChangeListener( this ); + } + + private void addWindowListeners( MenuElement selected ) { + // see BasicPopupMenuUI.MouseGrabber.grabWindow() + Component invoker = selected.getComponent(); + if( invoker instanceof JPopupMenu ) + invoker = ((JPopupMenu)invoker).getInvoker(); + window = (invoker instanceof Window) + ? (Window) invoker + : SwingUtilities.windowForComponent( invoker ); + + if( window != null ) { + window.addWindowListener( this ); + window.addComponentListener( this ); + } + } + + private void removeWindowListeners() { + if( window != null ) { + window.removeWindowListener( this ); + window.removeComponentListener( this ); + window = null; + } + } + + private void cancelPopupMenu() { + try { + MenuSelectionManager msm = MenuSelectionManager.defaultManager(); + MenuElement[] selectedPath = msm.getSelectedPath(); + for( MenuElement e : selectedPath ) { + if( e instanceof JPopupMenu ) + ((JPopupMenu)e).putClientProperty( "JPopupMenu.firePopupMenuCanceled", true ); + } + msm.clearSelectedPath(); + } catch( RuntimeException ex ) { + removeWindowListeners(); + throw ex; + } catch( Error ex ) { + removeWindowListeners(); + throw ex; + } + } + + //---- ChangeListener ---- + + @Override + public void stateChanged( ChangeEvent e ) { + MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath(); + + if( selectedPath.length == 0 ) + removeWindowListeners(); + else if( lastPathSelectedPath.length == 0 ) + addWindowListeners( selectedPath[0] ); + + lastPathSelectedPath = selectedPath; + } + + //---- WindowListener ---- + + @Override + public void windowIconified( WindowEvent e ) { + cancelPopupMenu(); + } + + @Override + public void windowDeactivated( WindowEvent e ) { + cancelPopupMenu(); + } + + @Override + public void windowClosing( WindowEvent e ) { + cancelPopupMenu(); + } + + //---- ComponentListener ---- + + @Override + public void componentResized( ComponentEvent e ) { + cancelPopupMenu(); + } + + @Override + public void componentMoved( ComponentEvent e ) { + cancelPopupMenu(); + } + + @Override + public void componentShown( ComponentEvent e ) { + } + + @Override + public void componentHidden( ComponentEvent e ) { + cancelPopupMenu(); + } +}