Skip to content

Commit 98140e6

Browse files
bobbylightclaude
andcommitted
Add DescWindowVisibility (ALWAYS/ON_DEMAND/NEVER) for the description popup
Replaces the boolean setShowDescWindow()/getShowDescWindow() API with a DescWindowVisibility enum, adding an ON_DEMAND mode where the description window is only shown when explicitly toggled. The toggle keystroke is customizable via the new setDescWindowToggleKey()/getDescWindowToggleKey() wrapper around the text component's InputMap/ActionMap. Also fixes a bug where a disposed description window could be silently resurrected by the JDK: java.awt.Window#hide() cascades to owned windows still visible at the moment their owner is hidden, hiding them too and flagging them (via a package-private showWithParent field) to be automatically re-shown the next time the owner is shown again - even if that owned window was dispose()d in the meantime. Since AutoCompletePopupWindow hid itself before explicitly hiding the description window, a description window that was visible when the choices popup lost focus would get this flag set; disposing it afterward (e.g. by switching to NEVER) didn't clear it, so the next time the choices popup reopened, the JDK silently recreated the disposed window's native peer and showed it again with stale content, bypassing all of our own visibility logic. Fixed by hiding the description window before hiding the choices popup, so it's already invisible by the time the JDK's cascade runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5cc2d2a commit 98140e6

7 files changed

Lines changed: 514 additions & 27 deletions

File tree

AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ class AutoCompletePopupWindow extends JWindow implements CaretListener,
7474
*/
7575
private AutoCompleteDescWindow descWindow;
7676

77+
/**
78+
* Whether the description window is currently toggled "on" when
79+
* {@link AutoCompletion#getDescWindowVisibility()} is
80+
* {@link DescWindowVisibility#ON_DEMAND}. Ignored for other visibility
81+
* settings.
82+
*/
83+
private boolean descWindowVisibleOnDemand;
84+
7785
/**
7886
* The preferred size of the optional description window. This field
7987
* only exists because the user may (and usually will) set the size of
@@ -271,6 +279,115 @@ protected void doAutocomplete() {
271279
}
272280

273281

282+
/**
283+
* Returns whether the description window should currently be displayed,
284+
* per {@link AutoCompletion#getDescWindowVisibility()}.
285+
*
286+
* @return Whether the description window should currently be displayed.
287+
* @see #toggleDescriptionWindow()
288+
*/
289+
private boolean shouldShowDescWindow() {
290+
switch (ac.getDescWindowVisibility()) {
291+
case ALWAYS:
292+
return true;
293+
case ON_DEMAND:
294+
return descWindowVisibleOnDemand;
295+
case NEVER:
296+
default:
297+
return false;
298+
}
299+
}
300+
301+
302+
/**
303+
* Toggles whether the description window is displayed, when
304+
* {@link AutoCompletion#getDescWindowVisibility()} is
305+
* {@link DescWindowVisibility#ON_DEMAND}. Does nothing otherwise, or if
306+
* this popup window is not currently visible.
307+
*/
308+
void toggleDescriptionWindow() {
309+
310+
if (ac.getDescWindowVisibility() != DescWindowVisibility.ON_DEMAND || !isVisible()) {
311+
return;
312+
}
313+
314+
descWindowVisibleOnDemand = !descWindowVisibleOnDemand;
315+
316+
if (descWindowVisibleOnDemand) {
317+
if (descWindow == null) {
318+
descWindow = createDescriptionWindow();
319+
}
320+
Completion c = list.getSelectedValue();
321+
if (c != null) {
322+
descWindow.setDescriptionFor(c);
323+
}
324+
positionDescWindow();
325+
descWindow.setVisible(true);
326+
}
327+
else if (descWindow != null) {
328+
descWindow.setVisible(false);
329+
}
330+
331+
}
332+
333+
334+
/**
335+
* The key used in the input map for the description window toggle action.
336+
*/
337+
private static final String DESC_WINDOW_TOGGLE_KEY = "AutoCompletion.ToggleDescWindow";
338+
339+
340+
/**
341+
* Installs a "description window toggle key" action onto a text component.
342+
*
343+
* @param ac The auto-completion instance the text component is installed on.
344+
* @param tc The text component.
345+
* @param ks The keystroke that should toggle the description window's visibility.
346+
* @see #uninstallDescWindowToggleKey(JTextComponent, KeyStroke)
347+
*/
348+
static void installDescWindowToggleKey(AutoCompletion ac, JTextComponent tc, KeyStroke ks) {
349+
InputMap im = tc.getInputMap();
350+
im.put(ks, DESC_WINDOW_TOGGLE_KEY);
351+
ActionMap am = tc.getActionMap();
352+
am.put(DESC_WINDOW_TOGGLE_KEY, new ToggleDescWindowAction(ac));
353+
}
354+
355+
356+
/**
357+
* Removes a previously-installed "description window toggle key" action from a text component.
358+
*
359+
* @param tc The text component.
360+
* @param ks The keystroke previously passed to {@link #installDescWindowToggleKey}.
361+
*/
362+
static void uninstallDescWindowToggleKey(JTextComponent tc, KeyStroke ks) {
363+
tc.getInputMap().remove(ks);
364+
tc.getActionMap().remove(DESC_WINDOW_TOGGLE_KEY);
365+
}
366+
367+
368+
/**
369+
* Toggles the description window's visibility when triggered while {@link DescWindowVisibility#ON_DEMAND}
370+
* is active; a no-op otherwise.
371+
*/
372+
private static final class ToggleDescWindowAction extends AbstractAction {
373+
374+
private final AutoCompletion ac;
375+
376+
ToggleDescWindowAction(AutoCompletion ac) {
377+
this.ac = ac;
378+
}
379+
380+
@Override
381+
public void actionPerformed(ActionEvent e) {
382+
AutoCompletePopupWindow popupWindow = ac.getPopupWindow();
383+
if (popupWindow != null) {
384+
popupWindow.toggleDescriptionWindow();
385+
}
386+
}
387+
388+
}
389+
390+
274391
/**
275392
* Returns the copy keystroke to use for this platform.
276393
*
@@ -316,7 +433,7 @@ AutoCompleteDescWindow getDescWindow() {
316433
* that never gets un-mapped or repainted. Disposing of the native peer
317434
* avoids that.
318435
*
319-
* @see AutoCompletion#setShowDescWindow(boolean)
436+
* @see AutoCompletion#setDescWindowVisibility(DescWindowVisibility)
320437
*/
321438
void disposeDescWindow() {
322439
if (descWindow != null) {
@@ -452,7 +569,7 @@ public void mouseReleased(MouseEvent e) {
452569
*/
453570
private void positionDescWindow() {
454571

455-
boolean showDescWindow = descWindow!=null && ac.getShowDescWindow();
572+
boolean showDescWindow = descWindow!=null && shouldShowDescWindow();
456573
if (!showDescWindow) {
457574
return;
458575
}
@@ -703,7 +820,7 @@ public void setLocationRelativeTo(Rectangle r) {
703820
Rectangle screenBounds = Util.getScreenBoundsForPoint(r.x, r.y);
704821
//Dimension screenSize = getToolkit().getScreenSize();
705822

706-
boolean showDescWindow = descWindow!=null && ac.getShowDescWindow();
823+
boolean showDescWindow = descWindow!=null && shouldShowDescWindow();
707824
int totalH = getHeight();
708825
if (showDescWindow) {
709826
totalH = Math.max(totalH, descWindow.getHeight());
@@ -755,7 +872,9 @@ public void setVisible(boolean visible) {
755872
installKeyBindings();
756873
lastLine = ac.getLineOfCaret();
757874
selectFirstItem();
758-
if (descWindow==null && ac.getShowDescWindow()) {
875+
// ON_DEMAND starts back off each time the popup is (re)shown.
876+
descWindowVisibleOnDemand = false;
877+
if (descWindow==null && shouldShowDescWindow()) {
759878
descWindow = createDescriptionWindow();
760879
positionDescWindow();
761880
}
@@ -771,6 +890,19 @@ public void setVisible(boolean visible) {
771890
}
772891
else {
773892
uninstallKeyBindings();
893+
// Explicitly hide the desc window *before* hiding ourselves.
894+
// java.awt.Window#hide() cascades to any owned window that is
895+
// still visible at that moment, hiding it too and flagging it
896+
// to be automatically re-shown (via Window#show()'s internal
897+
// "showWithParent" bookkeeping) the next time we're shown
898+
// again - even if that desc window gets disposed in the
899+
// meantime. Hiding it first ensures it's already invisible
900+
// when our own super.setVisible(false) cascades below, so the
901+
// JDK never sets that flag and can't resurrect a disposed
902+
// desc window behind our back.
903+
if (descWindow != null) {
904+
descWindow.setVisible(false);
905+
}
774906
}
775907

776908
super.setVisible(visible);
@@ -797,7 +929,7 @@ public void setVisible(boolean visible) {
797929
// because of the way child JWindows' visibility is handled - in
798930
// some ways it's dependent on the parent, in other ways it's not.
799931
if (descWindow!=null) {
800-
descWindow.setVisible(visible && ac.getShowDescWindow());
932+
descWindow.setVisible(visible && shouldShowDescWindow());
801933
}
802934

803935
}

AutoComplete/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ public class AutoCompletion {
107107
private static LinkRedirector linkRedirector;
108108

109109
/**
110-
* Whether the description window should be displayed along with the
110+
* Whether/when the description window should be displayed along with the
111111
* completion choice window.
112112
*/
113-
private boolean showDescWindow;
113+
private DescWindowVisibility descWindowVisibility;
114114

115115
/**
116116
* Whether auto-complete is enabled.
@@ -176,6 +176,12 @@ public class AutoCompletion {
176176
*/
177177
private Action oldParenAction;
178178

179+
/**
180+
* The keystroke that toggles the description window's visibility in
181+
* {@link DescWindowVisibility#ON_DEMAND} mode, or <code>null</code>.
182+
*/
183+
private KeyStroke descWindowToggleKey;
184+
179185
/**
180186
* Listens for events in the parent window that affect the visibility of the
181187
* popup windows.
@@ -268,7 +274,8 @@ public AutoCompletion(CompletionProvider provider) {
268274
setAutoCompleteEnabled(true);
269275
setAutoCompleteSingleChoices(true);
270276
setAutoActivationEnabled(false);
271-
setShowDescWindow(false);
277+
setDescWindowVisibility(DescWindowVisibility.NEVER);
278+
setDescWindowToggleKey(getDefaultDescWindowToggleKey());
272279
setHideOnCompletionProviderChange(true);
273280
setHideOnNoText(true);
274281
setParameterDescriptionTruncateThreshold(300);
@@ -384,6 +391,18 @@ public static KeyStroke getDefaultTriggerKey() {
384391
}
385392

386393

394+
/**
395+
* Returns the default desc window toggle keystroke ({@link DescWindowVisibility#ON_DEMAND}).
396+
*
397+
* @return The default keystroke.
398+
* @see #setDescWindowToggleKey(KeyStroke)
399+
*/
400+
public static KeyStroke getDefaultDescWindowToggleKey() {
401+
int mask = InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK;
402+
return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, mask);
403+
}
404+
405+
387406
/**
388407
* Returns the handler to use when an external URL is clicked in the
389408
* description window.
@@ -462,14 +481,24 @@ protected String getReplacementText(Completion c, Document doc, int start,
462481

463482

464483
/**
465-
* Returns whether the "description window" should be shown alongside the
466-
* completion window.
484+
* Returns whether/when the "description window" should be shown alongside the completion choices window.
467485
*
468-
* @return Whether the description window should be shown.
469-
* @see #setShowDescWindow(boolean)
486+
* @return Whether/when the description window should be shown.
487+
* @see #setDescWindowVisibility(DescWindowVisibility)
470488
*/
471-
public boolean getShowDescWindow() {
472-
return showDescWindow;
489+
public DescWindowVisibility getDescWindowVisibility() {
490+
return descWindowVisibility;
491+
}
492+
493+
494+
/**
495+
* Returns the desc window toggle keystroke ({@link DescWindowVisibility#ON_DEMAND}).
496+
*
497+
* @return The keystroke, or <code>null</code> if none is installed.
498+
* @see #setDescWindowToggleKey(KeyStroke)
499+
*/
500+
public KeyStroke getDescWindowToggleKey() {
501+
return descWindowToggleKey;
473502
}
474503

475504

@@ -665,6 +694,9 @@ public void install(JTextComponent c) {
665694

666695
this.textComponent = c;
667696
installTriggerKey(getTriggerKey());
697+
if (descWindowToggleKey != null) {
698+
AutoCompletePopupWindow.installDescWindowToggleKey(this, textComponent, descWindowToggleKey);
699+
}
668700

669701
// Install the function completion key, if there is one.
670702
// NOTE: We cannot do this if the start char is ' ' (e.g. just a space
@@ -1148,20 +1180,20 @@ protected void setPopupVisible(boolean visible) {
11481180

11491181

11501182
/**
1151-
* Sets whether the "description window" should be shown beside the
1152-
* completion window.
1183+
* Sets whether/when the "description window" should be shown beside the completion choices window.
11531184
*
1154-
* @param show Whether to show the description window.
1155-
* @see #getShowDescWindow()
1185+
* @param visibility Whether/when to show the description window.
1186+
* @see #getDescWindowVisibility()
11561187
*/
1157-
public void setShowDescWindow(boolean show) {
1188+
public void setDescWindowVisibility(DescWindowVisibility visibility) {
1189+
Objects.requireNonNull(visibility, "visibility cannot be null");
11581190
hidePopupWindow(); // Needed to force it to take effect
1159-
if (!show && popupWindow != null) {
1191+
if (visibility != DescWindowVisibility.ALWAYS && popupWindow != null) {
11601192
// Dispose (rather than hide) the desc window on toggle-off, to avoid a
11611193
// Linux/X11 "ghost" window bug when hiding it instead; see issue #84.
11621194
popupWindow.disposeDescWindow();
11631195
}
1164-
showDescWindow = show;
1196+
descWindowVisibility = visibility;
11651197
}
11661198

11671199

@@ -1186,6 +1218,27 @@ public void setTriggerKey(KeyStroke ks) {
11861218
}
11871219

11881220

1221+
/**
1222+
* Sets the desc window toggle keystroke ({@link DescWindowVisibility#ON_DEMAND}).
1223+
*
1224+
* @param ks The keystroke, or {@code null} to remove any previously installed toggle keystroke.
1225+
* @see #getDescWindowToggleKey()
1226+
*/
1227+
public void setDescWindowToggleKey(KeyStroke ks) {
1228+
if (!Objects.equals(ks, descWindowToggleKey)) {
1229+
if (textComponent != null) {
1230+
if (descWindowToggleKey != null) {
1231+
AutoCompletePopupWindow.uninstallDescWindowToggleKey(textComponent, descWindowToggleKey);
1232+
}
1233+
if (ks != null) {
1234+
AutoCompletePopupWindow.installDescWindowToggleKey(this, textComponent, ks);
1235+
}
1236+
}
1237+
descWindowToggleKey = ks;
1238+
}
1239+
}
1240+
1241+
11891242
/**
11901243
* Displays a "tool tip" detailing the inputs to the function just entered.
11911244
*
@@ -1238,6 +1291,9 @@ public void uninstall() {
12381291
hidePopupWindow(); // Unregisters listeners, actions, etc.
12391292

12401293
uninstallTriggerKey();
1294+
if (descWindowToggleKey != null) {
1295+
AutoCompletePopupWindow.uninstallDescWindowToggleKey(textComponent, descWindowToggleKey);
1296+
}
12411297

12421298
// Uninstall the function completion key.
12431299
char start = provider.getParameterListStart();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* This library is distributed under a modified BSD license. See the included
3+
* LICENSE.md file for details.
4+
*/
5+
package org.fife.ui.autocomplete;
6+
7+
8+
/**
9+
* Controls when the "description" window (the popup that shows documentation
10+
* for the currently selected completion choice) is displayed alongside the
11+
* completion choices window.
12+
*
13+
* @author Robert Futrell
14+
* @version 1.0
15+
* @see AutoCompletion#setDescWindowVisibility(DescWindowVisibility)
16+
*/
17+
public enum DescWindowVisibility {
18+
19+
/**
20+
* The description window is shown automatically whenever the completion
21+
* choices window is showing and a description is available. This is the
22+
* default (legacy) behavior.
23+
*/
24+
ALWAYS,
25+
26+
/**
27+
* The description window is only shown when the user explicitly requests
28+
* it, via the keystroke configured by
29+
* {@link AutoCompletion#setDescWindowToggleKey(javax.swing.KeyStroke)}.
30+
*/
31+
ON_DEMAND,
32+
33+
/**
34+
* The description window is never shown.
35+
*/
36+
NEVER
37+
38+
}

0 commit comments

Comments
 (0)