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()"); + + } + + +}