From d985b31cbb5646c526e1a68a7547f26f56d37607 Mon Sep 17 00:00:00 2001 From: Alexander Zvegintsev Date: Wed, 29 Jan 2025 22:09:01 +0000 Subject: [PATCH] 8342096: Popup menus that request focus are not shown on Linux with Wayland Reviewed-by: aivanov, honkar --- .../unix/classes/sun/awt/UNIXToolkit.java | 38 +++- .../JPopupMenu/FocusablePopupDismissTest.java | 62 ++++-- .../JPopupMenu/NestedFocusablePopupTest.java | 187 ++++++++++++++++++ 3 files changed, 265 insertions(+), 22 deletions(-) create mode 100644 test/jdk/javax/swing/JPopupMenu/NestedFocusablePopupTest.java diff --git a/src/java.desktop/unix/classes/sun/awt/UNIXToolkit.java b/src/java.desktop/unix/classes/sun/awt/UNIXToolkit.java index 5881ba55ef3..4c6b451b7e4 100644 --- a/src/java.desktop/unix/classes/sun/awt/UNIXToolkit.java +++ b/src/java.desktop/unix/classes/sun/awt/UNIXToolkit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,7 +51,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.util.Arrays; import sun.awt.X11.XBaseWindow; import com.sun.java.swing.plaf.gtk.GTKConstants.TextDirection; @@ -521,6 +520,20 @@ public boolean isRunningOnWayland() { // application icons). private static final WindowFocusListener waylandWindowFocusListener; + private static boolean containsWaylandWindowFocusListener(Window window) { + if (window == null) { + return false; + } + + for (WindowFocusListener focusListener : window.getWindowFocusListeners()) { + if (focusListener == waylandWindowFocusListener) { + return true; + } + } + + return false; + } + static { if (isOnWayland()) { waylandWindowFocusListener = new WindowAdapter() { @@ -530,13 +543,22 @@ public void windowLostFocus(WindowEvent e) { Window oppositeWindow = e.getOppositeWindow(); // The focus can move between the window calling the popup, - // and the popup window itself. + // and the popup window itself or its children. // We only dismiss the popup in other cases. if (oppositeWindow != null) { - if (window == oppositeWindow.getParent() ) { + if (containsWaylandWindowFocusListener(oppositeWindow.getOwner())) { addWaylandWindowFocusListenerToWindow(oppositeWindow); return; } + + Window owner = window.getOwner(); + while (owner != null) { + if (owner == oppositeWindow) { + return; + } + owner = owner.getOwner(); + } + if (window.getParent() == oppositeWindow) { return; } @@ -557,11 +579,11 @@ public void windowLostFocus(WindowEvent e) { } private static void addWaylandWindowFocusListenerToWindow(Window window) { - if (!Arrays - .asList(window.getWindowFocusListeners()) - .contains(waylandWindowFocusListener) - ) { + if (!containsWaylandWindowFocusListener(window)) { window.addWindowFocusListener(waylandWindowFocusListener); + for (Window ownedWindow : window.getOwnedWindows()) { + addWaylandWindowFocusListenerToWindow(ownedWindow); + } } } diff --git a/test/jdk/javax/swing/JPopupMenu/FocusablePopupDismissTest.java b/test/jdk/javax/swing/JPopupMenu/FocusablePopupDismissTest.java index 2704c9789e3..cb3811265dc 100644 --- a/test/jdk/javax/swing/JPopupMenu/FocusablePopupDismissTest.java +++ b/test/jdk/javax/swing/JPopupMenu/FocusablePopupDismissTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,22 +24,28 @@ /* * @test * @key headful - * @bug 8319103 + * @bug 8319103 8342096 * @requires (os.family == "linux") - * @library /java/awt/regtesthelpers - * @build PassFailJFrame + * @library /java/awt/regtesthelpers /test/lib + * @build PassFailJFrame jtreg.SkippedException * @summary Tests if the focusable popup can be dismissed when the parent * window or the popup itself loses focus in Wayland. * @run main/manual FocusablePopupDismissTest */ +import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; import java.awt.Window; import java.util.List; +import jtreg.SkippedException; + public class FocusablePopupDismissTest { private static final String INSTRUCTIONS = """ A frame with a "Click me" button should appear next to the window @@ -47,44 +53,72 @@ public class FocusablePopupDismissTest { Click on the "Click me" button. - If the JTextField popup with "Some text" is not showing on the screen, - click Fail. + A menu should appear next to the window. If you move the cursor over + the first menu, the JTextField popup should appear on the screen. + If it doesn't, click Fail. The following steps require some focusable system window to be displayed on the screen. This could be a system settings window, file manager, etc. Click on the "Click me" button if the popup is not displayed - on the screen. + on the screen, move the mouse pointer over the menu. While the popup is displayed, click on some other window on the desktop. - If the popup has disappeared, click Pass, otherwise click Fail. + If the popup does not disappear, click Fail. + + Open the menu again, move the mouse cursor over the following: + "Focusable 1" -> "Focusable 2" -> "Editor Focusable 2" + Move the mouse to the focusable system window + (keeping the "Editor Focusable 2" JTextField open) and click on it. + + If the popup does not disappear, click Fail, otherwise click Pass. """; public static void main(String[] args) throws Exception { if (System.getenv("WAYLAND_DISPLAY") == null) { - //test is valid only when running on Wayland. - return; + throw new SkippedException("XWayland only test"); } PassFailJFrame.builder() .title("FocusablePopupDismissTest") .instructions(INSTRUCTIONS) - .rows(20) .columns(45) .testUI(FocusablePopupDismissTest::createTestUI) .build() .awaitAndCheck(); } + static JMenu getMenuWithMenuItem(boolean isSubmenuItemFocusable, String text) { + JMenu menu = new JMenu(text); + menu.add(isSubmenuItemFocusable + ? new JTextField("Editor " + text, 11) + : new JMenuItem("Menu item" + text) + ); + return menu; + } + static List createTestUI() { JFrame frame = new JFrame("FocusablePopupDismissTest"); JButton button = new JButton("Click me"); - frame.add(button); + + JPanel wrapper = new JPanel(); + wrapper.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); + wrapper.add(button); + + frame.add(wrapper); button.addActionListener(e -> { JPopupMenu popupMenu = new JPopupMenu(); - JTextField textField = new JTextField("Some text", 10); - popupMenu.add(textField); + + JMenu menu1 = new JMenu("Menu 1"); + menu1.add(new JTextField("Some text", 10)); + JMenu menu2 = new JMenu("Menu 2"); + menu2.add(new JTextField("Some text", 10)); + + popupMenu.add(getMenuWithMenuItem(true, "Focusable 1")); + popupMenu.add(getMenuWithMenuItem(true, "Focusable 2")); + popupMenu.add(getMenuWithMenuItem(false, "Non-Focusable 1")); + popupMenu.add(getMenuWithMenuItem(false, "Non-Focusable 2")); popupMenu.show(button, 0, button.getHeight()); }); frame.pack(); diff --git a/test/jdk/javax/swing/JPopupMenu/NestedFocusablePopupTest.java b/test/jdk/javax/swing/JPopupMenu/NestedFocusablePopupTest.java new file mode 100644 index 00000000000..55963b081a5 --- /dev/null +++ b/test/jdk/javax/swing/JPopupMenu/NestedFocusablePopupTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary tests if nested menu is displayed on Wayland + * @requires (os.family == "linux") + * @key headful + * @bug 8342096 + * @library /test/lib + * @build jtreg.SkippedException + * @run main NestedFocusablePopupTest + */ + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.IllegalComponentStateException; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.event.InputEvent; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import jtreg.SkippedException; + +public class NestedFocusablePopupTest { + + static volatile JMenu menuWithFocusableItem; + static volatile JMenu menuWithNonFocusableItem; + static volatile JPopupMenu popupMenu; + static volatile JFrame frame; + static volatile Robot robot; + + public static void main(String[] args) throws Exception { + if (System.getenv("WAYLAND_DISPLAY") == null) { + throw new SkippedException("XWayland only test"); + } + + robot = new Robot(); + robot.setAutoDelay(50); + + try { + SwingUtilities.invokeAndWait(NestedFocusablePopupTest::initAndShowGui); + test0(); + test1(); + } finally { + SwingUtilities.invokeAndWait(frame::dispose); + } + } + + public static void waitTillShown(final Component component, long msTimeout) + throws InterruptedException, TimeoutException { + long startTime = System.currentTimeMillis(); + + while (true) { + try { + Thread.sleep(50); + component.getLocationOnScreen(); + break; + } catch (IllegalComponentStateException e) { + if (System.currentTimeMillis() - startTime > msTimeout) { + throw new TimeoutException("Component not shown within the specified timeout"); + } + } + } + } + + static Rectangle waitAndGetOnScreenBoundsOnEDT(Component component) + throws InterruptedException, TimeoutException, ExecutionException { + waitTillShown(component, 500); + robot.waitForIdle(); + + FutureTask task = new FutureTask<>(() + -> new Rectangle(component.getLocationOnScreen(), component.getSize())); + SwingUtilities.invokeLater(task); + return task.get(500, TimeUnit.MILLISECONDS); + } + + static void test0() throws Exception { + Rectangle frameBounds = waitAndGetOnScreenBoundsOnEDT(frame); + robot.mouseMove(frameBounds.x + frameBounds.width / 2, + frameBounds.y + frameBounds.height / 2); + + robot.mousePress(InputEvent.BUTTON3_DOWN_MASK); + robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK); + + Rectangle menuBounds = waitAndGetOnScreenBoundsOnEDT(menuWithFocusableItem); + robot.mouseMove(menuBounds.x + 5, menuBounds.y + 5); + + // Give popup some time to disappear (in case of failure) + robot.waitForIdle(); + robot.delay(200); + + try { + waitTillShown(popupMenu, 500); + } catch (TimeoutException e) { + throw new RuntimeException("The popupMenu disappeared when it shouldn't have."); + } + } + + static void test1() throws Exception { + Rectangle frameBounds = waitAndGetOnScreenBoundsOnEDT(frame); + robot.mouseMove(frameBounds.x + frameBounds.width / 2, + frameBounds.y + frameBounds.height / 2); + + robot.mousePress(InputEvent.BUTTON3_DOWN_MASK); + robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK); + + Rectangle menuBounds = waitAndGetOnScreenBoundsOnEDT(menuWithFocusableItem); + robot.mouseMove(menuBounds.x + 5, menuBounds.y + 5); + robot.waitForIdle(); + robot.delay(200); + + menuBounds = waitAndGetOnScreenBoundsOnEDT(menuWithNonFocusableItem); + robot.mouseMove(menuBounds.x + 5, menuBounds.y + 5); + + // Give popup some time to disappear (in case of failure) + robot.waitForIdle(); + robot.delay(200); + + try { + waitTillShown(popupMenu, 500); + } catch (TimeoutException e) { + throw new RuntimeException("The popupMenu disappeared when it shouldn't have."); + } + } + + static JMenu getMenuWithMenuItem(boolean isSubmenuItemFocusable, String text) { + JMenu menu = new JMenu(text); + menu.add(isSubmenuItemFocusable + ? new JButton(text) + : new JMenuItem(text) + ); + return menu; + } + + private static void initAndShowGui() { + frame = new JFrame("NestedFocusablePopupTest"); + JPanel panel = new JPanel(); + panel.setPreferredSize(new Dimension(200, 180)); + + + popupMenu = new JPopupMenu(); + menuWithFocusableItem = + getMenuWithMenuItem(true, "focusable subitem"); + menuWithNonFocusableItem = + getMenuWithMenuItem(false, "non-focusable subitem"); + + popupMenu.add(menuWithFocusableItem); + popupMenu.add(menuWithNonFocusableItem); + + panel.setComponentPopupMenu(popupMenu); + frame.add(panel); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } +}