From 82e054963ccb4be836879f76cbbc1bae231598bf Mon Sep 17 00:00:00 2001 From: bobbylight Date: Sun, 2 Aug 2026 23:08:46 -0400 Subject: [PATCH] fix: dispose description window when toggled off, not just hide it On some Linux/X11 window managers, quickly hiding a JWindow shortly after showing it can leave a blank "ghost" window on screen that is never un-mapped or repainted. Disposing of the description window's native peer when it's toggled off, instead of merely calling setVisible(false), avoids this. The window is lazily recreated the next time it's needed. Fixes #84 Co-Authored-By: Claude --- .../autocomplete/AutoCompletePopupWindow.java | 30 +++++++ .../fife/ui/autocomplete/AutoCompletion.java | 10 +++ .../ui/autocomplete/AutoCompletionTest.java | 88 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 AutoComplete/src/test/java/org/fife/ui/autocomplete/AutoCompletionTest.java diff --git a/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java b/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java index ca0c9e0..3876266 100644 --- a/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java +++ b/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java @@ -296,6 +296,36 @@ public Color getDescriptionWindowColor() { } + /** + * Returns the description window, if it has been created. + * + * @return The description window, or {@code null} if it has not been + * created yet. + */ + AutoCompleteDescWindow getDescWindow() { + return descWindow; + } + + + /** + * Disposes of the description window, if it has been created, and + * discards our reference to it so it is lazily recreated the next time + * it's needed. Simply hiding the description window is not enough here; + * on some Linux/X11 window managers, quickly toggling a {@code JWindow} + * visible and then invisible can leave a blank "ghost" window on screen + * that never gets un-mapped or repainted. Disposing of the native peer + * avoids that. + * + * @see AutoCompletion#setShowDescWindow(boolean) + */ + void disposeDescWindow() { + if (descWindow != null) { + descWindow.dispose(); + descWindow = null; + } + } + + /** * Returns the default list cell renderer used when a completion provider * does not supply its own. diff --git a/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java b/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java index 5425929..36eb615 100644 --- a/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java +++ b/AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java @@ -802,6 +802,11 @@ public boolean isPopupVisible() { return popupWindow != null && popupWindow.isVisible(); } + // Returns the completion popup window, or null if not created yet. + AutoCompletePopupWindow getPopupWindow() { + return popupWindow; + } + /** * Refreshes the popup window. First, this method gets the possible @@ -1151,6 +1156,11 @@ protected void setPopupVisible(boolean visible) { */ public void setShowDescWindow(boolean show) { hidePopupWindow(); // Needed to force it to take effect + if (!show && popupWindow != null) { + // Dispose (rather than hide) the desc window on toggle-off, to avoid a + // Linux/X11 "ghost" window bug when hiding it instead; see issue #84. + popupWindow.disposeDescWindow(); + } showDescWindow = show; } diff --git a/AutoComplete/src/test/java/org/fife/ui/autocomplete/AutoCompletionTest.java b/AutoComplete/src/test/java/org/fife/ui/autocomplete/AutoCompletionTest.java new file mode 100644 index 0000000..01082a3 --- /dev/null +++ b/AutoComplete/src/test/java/org/fife/ui/autocomplete/AutoCompletionTest.java @@ -0,0 +1,88 @@ +/* + * This library is distributed under a modified BSD license. See the included + * LICENSE.md file for details. + */ +package org.fife.ui.autocomplete; + +import java.awt.GraphicsEnvironment; +import javax.swing.JFrame; +import javax.swing.JTextArea; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +class AutoCompletionTest { + + private JFrame frame; + + + @BeforeEach + void setUp() { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + } + + + @AfterEach + void tearDown() { + if (frame != null) { + frame.dispose(); + frame = null; + } + } + + + /** + * Regression test for https://github.com/bobbylight/AutoComplete/issues/84 - + * merely hiding the description window when it's toggled off could leave + * a blank "ghost" window on screen on some Linux/X11 window managers. + * The fix disposes of the description window's native peer instead of + * just hiding it, so it must be lazily recreated the next time it's + * shown. + */ + @Test + void setShowDescWindow_false_disposesExistingDescWindow() { + + DefaultCompletionProvider provider = new DefaultCompletionProvider(); + provider.addCompletion(new BasicCompletion(provider, "foo", "foo's summary")); + provider.addCompletion(new BasicCompletion(provider, "foobar", "foobar's summary")); + + AutoCompletion ac = new AutoCompletion(provider); + ac.setShowDescWindow(true); + ac.setAutoCompleteEnabled(true); + + JTextArea textArea = new JTextArea(); + ac.install(textArea); + + frame = new JFrame(); + frame.add(textArea); + frame.pack(); + frame.setVisible(true); + + textArea.setText("foo"); + textArea.setCaretPosition(textArea.getText().length()); + ac.doCompletion(); + + AutoCompletePopupWindow popupWindow = ac.getPopupWindow(); + Assertions.assertNotNull(popupWindow, + "Popup window should have been created by doCompletion()"); + AutoCompleteDescWindow descWindow = popupWindow.getDescWindow(); + Assertions.assertNotNull(descWindow, + "Description window should have been created since showDescWindow was true"); + Assertions.assertTrue(descWindow.isDisplayable(), + "Description window's native peer should exist while showing"); + + ac.setShowDescWindow(false); + + Assertions.assertNull(popupWindow.getDescWindow(), + "Description window should be disposed and discarded, not just hidden"); + Assertions.assertFalse(descWindow.isDisplayable(), + "Description window's native peer should be destroyed by dispose()"); + + } + + +}