Skip to content

Commit fb4eb59

Browse files
committed
feat(confluence-plugin): #10 Replace space/page/attachment selection dialogs with autocomplete fields.
1 parent 3371883 commit fb4eb59

File tree

8 files changed

+451
-446
lines changed

8 files changed

+451
-446
lines changed
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
package com.github.vogoltsov.vp.plugins.common.swing;
2+
3+
import lombok.Setter;
4+
5+
import javax.swing.AbstractListModel;
6+
import javax.swing.ComboBoxEditor;
7+
import javax.swing.DefaultListCellRenderer;
8+
import javax.swing.JButton;
9+
import javax.swing.JComboBox;
10+
import javax.swing.JList;
11+
import javax.swing.ListCellRenderer;
12+
import javax.swing.MutableComboBoxModel;
13+
import javax.swing.Timer;
14+
import javax.swing.event.ListDataEvent;
15+
import javax.swing.event.PopupMenuEvent;
16+
import javax.swing.event.PopupMenuListener;
17+
import javax.swing.text.JTextComponent;
18+
import java.awt.Component;
19+
import java.awt.event.ActionEvent;
20+
import java.awt.event.ActionListener;
21+
import java.awt.event.FocusEvent;
22+
import java.awt.event.FocusListener;
23+
import java.io.Serializable;
24+
import java.util.Arrays;
25+
import java.util.Collection;
26+
import java.util.Objects;
27+
import java.util.Optional;
28+
import java.util.Vector;
29+
import java.util.function.Function;
30+
31+
/**
32+
* @author Vitaly Ogoltsov <vitaly.ogoltsov@me.com>
33+
*/
34+
public class AutoCompleteComboBox<T> extends JComboBox<T> {
35+
36+
private static final int DEFAULT_SEARCH_DELAY = 1000;
37+
38+
private static final String PROPERTY_EMPTY_ITEM_LABEL = "emptyItemLabel";
39+
40+
41+
private final Function<String, Collection<T>> searchFunction;
42+
private final Function<T, String> labelFunction;
43+
44+
@Setter
45+
private boolean emptyItemAllowed = false;
46+
private String emptyItemLabel;
47+
48+
private boolean reloadItemsFired = false;
49+
private boolean selectingItem = false;
50+
51+
52+
public AutoCompleteComboBox(Function<String, Collection<T>> searchFunction, Function<T, String> labelFunction) {
53+
super(new AutoCompleteComboBoxModel<>());
54+
55+
Objects.requireNonNull(searchFunction);
56+
Objects.requireNonNull(labelFunction);
57+
this.searchFunction = searchFunction;
58+
this.labelFunction = labelFunction;
59+
60+
this.setEditor(new AutoCompleteComboBoxEditor(this.getEditor()));
61+
this.setRenderer(new AutoCompleteComboBoxRenderer());
62+
63+
// remove show popup button
64+
Arrays.stream(getComponents())
65+
.filter(component -> component instanceof JButton)
66+
.forEach(this::remove);
67+
68+
this.setEditable(true);
69+
}
70+
71+
public void setEmptyItemLabel(String emptyItemLabel) {
72+
if (Objects.equals(this.emptyItemLabel, emptyItemLabel)) {
73+
return;
74+
}
75+
String oldValue = this.emptyItemLabel;
76+
this.emptyItemLabel = emptyItemLabel;
77+
this.firePropertyChange(PROPERTY_EMPTY_ITEM_LABEL, oldValue, this.emptyItemLabel);
78+
}
79+
80+
private void reloadItems() {
81+
//noinspection unchecked
82+
final String searchText = ((AutoCompleteComboBoxEditor) this.getEditor()).getEditorComponent().getText();
83+
final Collection<T> searchResults = this.searchFunction.apply(searchText);
84+
85+
this.reloadItemsFired = true;
86+
setPopupVisible(false);
87+
removeAllItems();
88+
if (emptyItemAllowed) {
89+
addItem(null);
90+
}
91+
searchResults.forEach(this::addItem);
92+
setPopupVisible(getModel().getSize() > 0);
93+
this.reloadItemsFired = false;
94+
}
95+
96+
@Override
97+
public void removeAllItems() {
98+
AutoCompleteComboBoxModel<T> model = (AutoCompleteComboBoxModel<T>) dataModel;
99+
model.removeAllElements();
100+
}
101+
102+
@Override
103+
public AutoCompleteComboBoxModel<T> getModel() {
104+
return (AutoCompleteComboBoxModel<T>) super.getModel();
105+
}
106+
107+
@Override
108+
public T getSelectedItem() {
109+
return getModel().getSelectedItem();
110+
}
111+
112+
@Override
113+
public void setSelectedItem(Object anObject) {
114+
this.selectingItem = true;
115+
super.setSelectedItem(anObject);
116+
this.selectingItem = false;
117+
}
118+
119+
/**
120+
* Override default {@link JComboBox} behavior so selected item is not changed when items list is updated.
121+
*/
122+
@Override
123+
public void contentsChanged(ListDataEvent e) {
124+
/* do nothing */
125+
}
126+
127+
/**
128+
* Override default {@link JComboBox} behavior so selected item is not changed when items list is updated.
129+
*/
130+
@Override
131+
public void intervalAdded(ListDataEvent e) {
132+
/* do nothing */
133+
}
134+
135+
/**
136+
* Override default {@link JComboBox} behavior so selected item is not changed when items list is updated.
137+
*/
138+
@Override
139+
public void intervalRemoved(ListDataEvent e) {
140+
/* do nothing */
141+
}
142+
143+
/**
144+
* Override default {@link JComboBox} behavior so selected item is not changed when items list is updated.
145+
*/
146+
@Override
147+
public void actionPerformed(ActionEvent e) {
148+
/* do nothing */
149+
}
150+
151+
/**
152+
* Override default {@link JComboBox} behavior so selected item is not changed when items list is updated.
153+
*/
154+
@Override
155+
public void configureEditor(ComboBoxEditor anEditor, Object anItem) {
156+
if (this.reloadItemsFired) {
157+
return;
158+
}
159+
super.configureEditor(anEditor, anItem);
160+
}
161+
162+
163+
/**
164+
* Re-implementation of {@link javax.swing.DefaultComboBoxModel} which does not change selected item
165+
* when a list of elements is changed.
166+
*/
167+
private static class AutoCompleteComboBoxModel<T> extends AbstractListModel<T> implements MutableComboBoxModel<T>, Serializable {
168+
169+
private final Vector<T> objects = new Vector<>();
170+
171+
private T selectedObject;
172+
173+
174+
@Override
175+
public void addElement(T item) {
176+
insertElementAt(item, objects.size());
177+
}
178+
179+
@Override
180+
public void removeElement(Object obj) {
181+
//noinspection SuspiciousMethodCalls
182+
int index = objects.indexOf(obj);
183+
if (index != -1) {
184+
removeElementAt(index);
185+
}
186+
}
187+
188+
@Override
189+
public void insertElementAt(T item, int index) {
190+
objects.insertElementAt(item, index);
191+
fireIntervalAdded(this, index, index);
192+
}
193+
194+
@Override
195+
public void removeElementAt(int index) {
196+
objects.removeElementAt(index);
197+
fireIntervalRemoved(this, index, index);
198+
}
199+
200+
void removeAllElements() {
201+
if (objects.size() > 0) {
202+
int firstIndex = 0;
203+
int lastIndex = objects.size() - 1;
204+
objects.removeAllElements();
205+
fireIntervalRemoved(this, firstIndex, lastIndex);
206+
}
207+
}
208+
209+
@Override
210+
public void setSelectedItem(Object anItem) {
211+
if (!Objects.equals(selectedObject, anItem)) {
212+
//noinspection unchecked
213+
this.selectedObject = (T) anItem;
214+
fireContentsChanged(this, -1, -1);
215+
}
216+
}
217+
218+
@Override
219+
public T getSelectedItem() {
220+
return selectedObject;
221+
}
222+
223+
@Override
224+
public int getSize() {
225+
return objects.size();
226+
}
227+
228+
@Override
229+
public T getElementAt(int index) {
230+
if (index >= 0 && index < objects.size()) {
231+
return objects.elementAt(index);
232+
} else {
233+
return null;
234+
}
235+
}
236+
}
237+
238+
239+
/**
240+
* Wrapper around {@link ComboBoxEditor} that adds autocomplete-specific logic like search delay.
241+
*/
242+
private class AutoCompleteComboBoxEditor implements ComboBoxEditor, FocusListener, PopupMenuListener {
243+
244+
private final ComboBoxEditor comboBoxEditorDelegate;
245+
private final Timer searchTimer;
246+
247+
248+
private boolean settingText = false;
249+
250+
251+
AutoCompleteComboBoxEditor(ComboBoxEditor delegate) {
252+
Objects.requireNonNull(delegate);
253+
this.comboBoxEditorDelegate = delegate;
254+
255+
// create timer which reloads combo box items on user input delay
256+
this.searchTimer = new Timer(DEFAULT_SEARCH_DELAY, (timerEvent) -> AutoCompleteComboBox.this.reloadItems());
257+
this.searchTimer.setRepeats(false);
258+
259+
// restart search timer when text in editor is changed
260+
getEditorComponent().getDocument().addDocumentListener((DocumentListenerAdapter) e -> {
261+
if (!reloadItemsFired && !selectingItem && !settingText) {
262+
this.searchTimer.restart();
263+
}
264+
});
265+
// register as focus listener on editor component
266+
getEditorComponent().addFocusListener(this);
267+
// register as combobox popup menu listener
268+
AutoCompleteComboBox.this.addPopupMenuListener(this);
269+
// listen on empty label changes
270+
AutoCompleteComboBox.this.addPropertyChangeListener(PROPERTY_EMPTY_ITEM_LABEL, e -> updateItem());
271+
// set default value
272+
setItem(null);
273+
}
274+
275+
@Override
276+
public JTextComponent getEditorComponent() {
277+
return (JTextComponent) comboBoxEditorDelegate.getEditorComponent();
278+
}
279+
280+
private void updateItem() {
281+
setItem(AutoCompleteComboBox.this.getSelectedItem());
282+
}
283+
284+
@Override
285+
public void setItem(Object anObject) {
286+
//noinspection unchecked
287+
String label = Optional.ofNullable((T) anObject)
288+
.map(AutoCompleteComboBox.this.labelFunction)
289+
.orElse(getEditorComponent().hasFocus() ? null : AutoCompleteComboBox.this.emptyItemLabel);
290+
this.settingText = true;
291+
this.comboBoxEditorDelegate.setItem(label);
292+
this.settingText = false;
293+
}
294+
295+
@Override
296+
public Object getItem() {
297+
return comboBoxEditorDelegate.getItem();
298+
}
299+
300+
@Override
301+
public void selectAll() {
302+
comboBoxEditorDelegate.selectAll();
303+
}
304+
305+
void deselect() {
306+
getEditorComponent().select(0, 0);
307+
}
308+
309+
@Override
310+
public void addActionListener(ActionListener l) {
311+
comboBoxEditorDelegate.addActionListener(l);
312+
}
313+
314+
@Override
315+
public void removeActionListener(ActionListener l) {
316+
comboBoxEditorDelegate.removeActionListener(l);
317+
}
318+
319+
@Override
320+
public void focusGained(FocusEvent e) {
321+
updateItem();
322+
if (AutoCompleteComboBox.this.getSelectedItem() == null) {
323+
AutoCompleteComboBox.this.reloadItems();
324+
} else {
325+
selectAll();
326+
}
327+
}
328+
329+
@Override
330+
public void focusLost(FocusEvent e) {
331+
updateItem();
332+
deselect();
333+
}
334+
335+
@Override
336+
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
337+
/* do nothing */
338+
}
339+
340+
@Override
341+
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
342+
// don't update editor text on items reload
343+
if (reloadItemsFired) {
344+
return;
345+
}
346+
updateItem();
347+
}
348+
349+
@Override
350+
public void popupMenuCanceled(PopupMenuEvent e) {
351+
/* do nothing */
352+
}
353+
354+
}
355+
356+
357+
/**
358+
* Wrapper around {@link ListCellRenderer} that uses {@link AutoCompleteComboBox#labelFunction}
359+
* to convert items to strings.
360+
*/
361+
private class AutoCompleteComboBoxRenderer implements ListCellRenderer<T> {
362+
363+
private final ListCellRenderer<Object> delegate;
364+
365+
AutoCompleteComboBoxRenderer() {
366+
this.delegate = new DefaultListCellRenderer();
367+
}
368+
369+
@Override
370+
public Component getListCellRendererComponent(JList<? extends T> list, T value, int index, boolean isSelected, boolean cellHasFocus) {
371+
String label = Optional.ofNullable(value)
372+
.map(AutoCompleteComboBox.this.labelFunction)
373+
.orElse(AutoCompleteComboBox.this.emptyItemLabel);
374+
return delegate.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus);
375+
}
376+
377+
}
378+
379+
380+
}

0 commit comments

Comments
 (0)