source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java@ 18173

Last change on this file since 18173 was 18173, checked in by Don-vip, 3 years ago

fix #20690 - fix #21240 - Refactoring of UploadDialog, HistoryComboBox and AutoCompletingComboBox (patch by marcello)

  • Property svn:eol-style set to native
File size: 10.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.ac;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.Comparator;
7import java.util.Iterator;
8import java.util.List;
9import java.util.Objects;
10import java.util.function.Function;
11
12import javax.swing.AbstractListModel;
13import javax.swing.MutableComboBoxModel;
14
15import org.openstreetmap.josm.data.preferences.ListProperty;
16import org.openstreetmap.josm.spi.preferences.Config;
17
18/**
19 * A data model for the {@link AutoCompComboBox}
20 *
21 * @author marcello@perathoner.de
22 * @param <E> The element type.
23 * @since 18173
24 */
25public class AutoCompComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> {
26
27 /**
28 * The comparator used by {@link #findBestCandidate}
29 * <p>
30 * The comparator is used exclusively for autocompleting, and not for sorting the combobox
31 * entries. The default comparator sorts elements in alphabetical order according to
32 * {@code E::toString}.
33 */
34 private Comparator<E> comparator;
35 /** The maximum number of elements to hold, -1 for no limit. Used for histories. */
36 private int maxSize = -1;
37
38 /** the elements shown in the dropdown */
39 protected ArrayList<E> elements = new ArrayList<>();
40 /** the selected element in the dropdown or null */
41 protected Object selected;
42
43 /**
44 * Constructs a new empty model with a default {@link #comparator}.
45 */
46 public AutoCompComboBoxModel() {
47 setComparator(Comparator.comparing(E::toString));
48 }
49
50 /**
51 * Constructs a new empty model with a custom {@link #comparator}.
52 *
53 * @param comparator A custom {@link #comparator}.
54 */
55 public AutoCompComboBoxModel(Comparator<E> comparator) {
56 setComparator(comparator);
57 }
58
59 /**
60 * Sets a custom {@link #comparator}.
61 * <p>
62 * Example:
63 * {@code setComparator(Comparator.comparing(E::getPriority).thenComparing(E::toString));}
64 * <p>
65 * If {@code <E>} implements {@link java.lang.Comparable Comparable} you can automagically create a
66 * comparator with {@code setComparator(Comparator.naturalOrder());}.
67 *
68 * @param comparator A custom comparator.
69 */
70 public void setComparator(Comparator<E> comparator) {
71 Objects.requireNonNull(comparator, "A comparator cannot be null.");
72 this.comparator = comparator;
73 }
74
75 /**
76 * Sets the maximum number of elements.
77 *
78 * @param size The maximal number of elements in the model.
79 */
80 public void setSize(int size) {
81 maxSize = size;
82 }
83
84 /**
85 * Returns a copy of the element list.
86 * @return a copy of the data
87 */
88 public Collection<E> asCollection() {
89 return new ArrayList<>(elements);
90 }
91
92 //
93 // interface java.lang.Iterable
94 //
95
96 @Override
97 public Iterator<E> iterator() {
98 return elements.iterator();
99 }
100
101 //
102 // interface javax.swing.MutableComboBoxModel
103 //
104
105 /**
106 * Adds an element to the end of the model. Does nothing if max size is already reached.
107 */
108 @Override
109 public void addElement(E element) {
110 if (element != null && (maxSize == -1 || getSize() < maxSize)) {
111 elements.add(element);
112 }
113 }
114
115 @Override
116 public void removeElement(Object elem) {
117 elements.remove(elem);
118 }
119
120 @Override
121 public void removeElementAt(int index) {
122 Object elem = getElementAt(index);
123 if (elem == selected) {
124 if (index == 0) {
125 setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1));
126 } else {
127 setSelectedItem(getElementAt(index - 1));
128 }
129 }
130 elements.remove(index);
131 fireIntervalRemoved(this, index, index);
132 }
133
134 /**
135 * Adds an element at a specific index.
136 *
137 * @param element The element to add
138 * @param index Location to add the element
139 */
140 @Override
141 public void insertElementAt(E element, int index) {
142 if (maxSize != -1 && maxSize <= getSize()) {
143 removeElementAt(getSize() - 1);
144 }
145 elements.add(index, element);
146 }
147
148 //
149 // javax.swing.ComboBoxModel
150 //
151
152 /**
153 * Set the value of the selected item. The selected item may be null.
154 *
155 * @param elem The combo box value or null for no selection.
156 */
157 @Override
158 public void setSelectedItem(Object elem) {
159 if ((selected != null && !selected.equals(elem)) ||
160 (selected == null && elem != null)) {
161 selected = elem;
162 fireContentsChanged(this, -1, -1);
163 }
164 }
165
166 @Override
167 public Object getSelectedItem() {
168 return selected;
169 }
170
171 //
172 // javax.swing.ListModel
173 //
174
175 @Override
176 public int getSize() {
177 return elements.size();
178 }
179
180 @Override
181 public E getElementAt(int index) {
182 if (index >= 0 && index < elements.size())
183 return elements.get(index);
184 else
185 return null;
186 }
187
188 //
189 // end interfaces
190 //
191
192 /**
193 * Adds all elements from the collection.
194 *
195 * @param elems The elements to add.
196 */
197 public void addAllElements(Collection<E> elems) {
198 elems.forEach(e -> addElement(e));
199 }
200
201 /**
202 * Adds all elements from the collection of string representations.
203 *
204 * @param strings The string representation of the elements to add.
205 * @param buildE A {@link java.util.function.Function} that builds an {@code <E>} from a
206 * {@code String}.
207 */
208 public void addAllElements(Collection<String> strings, Function<String, E> buildE) {
209 strings.forEach(s -> addElement(buildE.apply(s)));
210 }
211
212 /**
213 * Adds an element to the top of the list.
214 * <p>
215 * If the element is already in the model, moves it to the top. If the model gets too big,
216 * deletes the last element.
217 *
218 * @param newElement the element to add
219 * @return The element that is at the top now.
220 */
221 public E addTopElement(E newElement) {
222 // if the element is already at the top, do nothing
223 if (newElement.equals(getElementAt(0)))
224 return getElementAt(0);
225
226 removeElement(newElement);
227 insertElementAt(newElement, 0);
228 return newElement;
229 }
230
231 /**
232 * Empties the list.
233 */
234 public void removeAllElements() {
235 if (!elements.isEmpty()) {
236 int firstIndex = 0;
237 int lastIndex = elements.size() - 1;
238 elements.clear();
239 selected = null;
240 fireIntervalRemoved(this, firstIndex, lastIndex);
241 } else {
242 selected = null;
243 }
244 }
245
246 /**
247 * Finds the best candidate for autocompletion.
248 * <p>
249 * Looks in the model for an element whose prefix matches {@code prefix}. If more than one
250 * element matches {@code prefix}, returns the first of the matching elements (first according
251 * to {@link #comparator}). An element that is equal to {@code prefix} is always preferred.
252 *
253 * @param prefix The prefix to match.
254 * @return The best candidate (may be null)
255 */
256 public E findBestCandidate(String prefix) {
257 return elements.stream()
258 .filter(o -> o.toString().startsWith(prefix))
259 // an element equal to the prefix is always the best candidate
260 .min((x, y) -> x.toString().equals(prefix) ? -1 :
261 y.toString().equals(prefix) ? 1 :
262 comparator.compare(x, y))
263 .orElse(null);
264 }
265
266 /**
267 * Gets a preference loader and saver.
268 *
269 * @param readE A {@link Function} that builds an {@code <E>} from a {@link String}.
270 * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
271 * @return The {@link Preferences} instance.
272 */
273 public Preferences prefs(Function<String, E> readE, Function<E, String> writeE) {
274 return new Preferences(readE, writeE);
275 }
276
277 /**
278 * Loads and saves the model to the JOSM preferences.
279 * <p>
280 * Obtainable through {@link #prefs}.
281 */
282 public final class Preferences {
283
284 /** A {@link Function} that builds an {@code <E>} from a {@code String}. */
285 private Function<String, E> readE;
286 /** A {@code Function} that serializes {@code <E>} to a {@code String}. */
287 private Function<E, String> writeE;
288
289 /**
290 * Private constructor
291 *
292 * @param readE A {@link Function} that builds an {@code <E>} from a {@code String}.
293 * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String}
294 */
295 private Preferences(Function<String, E> readE, Function<E, String> writeE) {
296 this.readE = readE;
297 this.writeE = writeE;
298 }
299
300 /**
301 * Loads the model from the JOSM preferences.
302 * @param key The preferences key
303 */
304 public void load(String key) {
305 removeAllElements();
306 addAllElements(Config.getPref().getList(key), readE);
307 }
308
309 /**
310 * Loads the model from the JOSM preferences.
311 *
312 * @param key The preferences key
313 * @param defaults A list of default values.
314 */
315 public void load(String key, List<String> defaults) {
316 removeAllElements();
317 addAllElements(Config.getPref().getList(key, defaults), readE);
318 }
319
320 /**
321 * Loads the model from the JOSM preferences.
322 *
323 * @param prop The property holding the strings.
324 */
325 public void load(ListProperty prop) {
326 removeAllElements();
327 addAllElements(prop.get(), readE);
328 }
329
330 /**
331 * Returns the model elements as list of strings.
332 *
333 * @return a list of strings
334 */
335 public List<String> asStringList() {
336 List<String> list = new ArrayList<>(getSize());
337 forEach(element -> list.add(writeE.apply(element)));
338 return list;
339 }
340
341 /**
342 * Saves the model to the JOSM preferences.
343 *
344 * @param key The preferences key
345 */
346 public void save(String key) {
347 Config.getPref().putList(key, asStringList());
348 }
349
350 /**
351 * Saves the model to the JOSM preferences.
352 *
353 * @param prop The property to write to.
354 */
355 public void save(ListProperty prop) {
356 prop.put(asStringList());
357 }
358 }
359}
Note: See TracBrowser for help on using the repository browser.