001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Iterator; 007import java.util.List; 008import java.util.function.Function; 009 010import javax.swing.AbstractListModel; 011import javax.swing.MutableComboBoxModel; 012 013import org.openstreetmap.josm.data.preferences.ListProperty; 014import org.openstreetmap.josm.spi.preferences.Config; 015 016/** 017 * A data model for the {@link JosmComboBox} 018 * 019 * @author marcello@perathoner.de 020 * @param <E> The element type. 021 * @since 18221 022 */ 023public class JosmComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> { 024 025 /** The maximum number of elements to hold, -1 for no limit. Used for histories. */ 026 private int maxSize = -1; 027 028 /** the elements shown in the dropdown */ 029 protected ArrayList<E> elements = new ArrayList<>(); 030 /** the selected element in the dropdown or null */ 031 protected Object selected; 032 033 /** 034 * Sets the maximum number of elements. 035 * 036 * @param size The maximal number of elements in the model. 037 */ 038 public void setSize(int size) { 039 maxSize = size; 040 } 041 042 /** 043 * Returns a copy of the element list. 044 * @return a copy of the data 045 */ 046 public Collection<E> asCollection() { 047 return new ArrayList<>(elements); 048 } 049 050 /** 051 * Returns the index of the specified element 052 * 053 * Note: This is not part of the {@link javax.swing.ComboBoxModel} interface but is defined in 054 * {@link javax.swing.DefaultComboBoxModel}. 055 * 056 * @param element the element to get the index of 057 * @return the index of the first occurrence of the specified element in this model, 058 * or -1 if this model does not contain the element 059 */ 060 public int getIndexOf(E element) { 061 return elements.indexOf(element); 062 } 063 064 protected void doAddElement(E element) { 065 if (element != null && (maxSize == -1 || getSize() < maxSize)) { 066 elements.add(element); 067 } 068 } 069 070 // 071 // interface java.lang.Iterable 072 // 073 074 @Override 075 public Iterator<E> iterator() { 076 return elements.iterator(); 077 } 078 079 // 080 // interface javax.swing.MutableComboBoxModel 081 // 082 083 /** 084 * Adds an element to the end of the model. Does nothing if max size is already reached. 085 */ 086 @Override 087 public void addElement(E element) { 088 doAddElement(element); 089 fireIntervalAdded(this, elements.size() - 1, elements.size() - 1); 090 } 091 092 @Override 093 public void removeElement(Object elem) { 094 int index = elements.indexOf(elem); 095 if (elem == selected) { 096 if (index == 0) { 097 setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1)); 098 } else { 099 setSelectedItem(getElementAt(index - 1)); 100 } 101 } 102 if (elements.remove(elem)) 103 fireIntervalRemoved(this, index, index); 104 } 105 106 @Override 107 public void removeElementAt(int index) { 108 Object elem = getElementAt(index); 109 if (elem == selected) { 110 if (index == 0) { 111 setSelectedItem(getSize() == 1 ? null : getElementAt(index + 1)); 112 } else { 113 setSelectedItem(getElementAt(index - 1)); 114 } 115 } 116 elements.remove(index); 117 fireIntervalRemoved(this, index, index); 118 } 119 120 /** 121 * Adds an element at a specific index. 122 * 123 * @param element The element to add 124 * @param index Location to add the element 125 */ 126 @Override 127 public void insertElementAt(E element, int index) { 128 if (maxSize != -1 && maxSize <= getSize()) { 129 removeElementAt(getSize() - 1); 130 } 131 elements.add(index, element); 132 fireIntervalAdded(this, index, index); 133 } 134 135 // 136 // javax.swing.ComboBoxModel 137 // 138 139 /** 140 * Set the value of the selected item. The selected item may be null. 141 * 142 * @param elem The combo box value or null for no selection. 143 */ 144 @Override 145 public void setSelectedItem(Object elem) { 146 if ((selected != null && !selected.equals(elem)) || 147 (selected == null && elem != null)) { 148 selected = elem; 149 fireContentsChanged(this, -1, -1); 150 } 151 } 152 153 @Override 154 public Object getSelectedItem() { 155 return selected; 156 } 157 158 // 159 // javax.swing.ListModel 160 // 161 162 @Override 163 public int getSize() { 164 return elements.size(); 165 } 166 167 @Override 168 public E getElementAt(int index) { 169 if (index >= 0 && index < elements.size()) 170 return elements.get(index); 171 else 172 return null; 173 } 174 175 // 176 // end interfaces 177 // 178 179 /** 180 * Adds all elements from the collection. 181 * 182 * @param elems The elements to add. 183 */ 184 public void addAllElements(Collection<E> elems) { 185 int index0 = elements.size(); 186 elems.forEach(e -> doAddElement(e)); 187 int index1 = elements.size() - 1; 188 if (index0 <= index1) 189 fireIntervalAdded(this, index0, index1); 190 } 191 192 /** 193 * Adds all elements from the collection of string representations. 194 * 195 * @param strings The string representation of the elements to add. 196 * @param buildE A {@link java.util.function.Function} that builds an {@code <E>} from a 197 * {@code String}. 198 */ 199 public void addAllElements(Collection<String> strings, Function<String, E> buildE) { 200 int index0 = elements.size(); 201 strings.forEach(s -> doAddElement(buildE.apply(s))); 202 int index1 = elements.size() - 1; 203 if (index0 <= index1) 204 fireIntervalAdded(this, index0, index1); 205 } 206 207 /** 208 * Adds an element to the top of the list. 209 * <p> 210 * If the element is already in the model, moves it to the top. If the model gets too big, 211 * deletes the last element. 212 * 213 * @param newElement the element to add 214 * @return The element that is at the top now. 215 */ 216 public E addTopElement(E newElement) { 217 // if the element is already at the top, do nothing 218 if (newElement.equals(getElementAt(0))) 219 return getElementAt(0); 220 221 removeElement(newElement); 222 insertElementAt(newElement, 0); 223 return newElement; 224 } 225 226 /** 227 * Empties the list. 228 */ 229 public void removeAllElements() { 230 if (!elements.isEmpty()) { 231 int lastIndex = elements.size() - 1; 232 elements.clear(); 233 selected = null; 234 fireIntervalRemoved(this, 0, lastIndex); 235 } else { 236 selected = null; 237 } 238 } 239 240 /** 241 * Finds the item that matches string. 242 * <p> 243 * Looks in the model for an element whose {@code toString()} matches {@code s}. 244 * 245 * @param s The string to match. 246 * @return The item or null 247 */ 248 public E find(String s) { 249 return elements.stream().filter(o -> o.toString().equals(s)).findAny().orElse(null); 250 } 251 252 /** 253 * Gets a preference loader and saver. 254 * 255 * @param readE A {@link Function} that builds an {@code <E>} from a {@link String}. 256 * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String} 257 * @return The {@link Preferences} instance. 258 */ 259 public Preferences prefs(Function<String, E> readE, Function<E, String> writeE) { 260 return new Preferences(readE, writeE); 261 } 262 263 /** 264 * Loads and saves the model to the JOSM preferences. 265 * <p> 266 * Obtainable through {@link #prefs}. 267 */ 268 public final class Preferences { 269 270 /** A {@link Function} that builds an {@code <E>} from a {@code String}. */ 271 private Function<String, E> readE; 272 /** A {@code Function} that serializes {@code <E>} to a {@code String}. */ 273 private Function<E, String> writeE; 274 275 /** 276 * Private constructor 277 * 278 * @param readE A {@link Function} that builds an {@code <E>} from a {@code String}. 279 * @param writeE A {@code Function} that serializes an {@code <E>} to a {@code String} 280 */ 281 private Preferences(Function<String, E> readE, Function<E, String> writeE) { 282 this.readE = readE; 283 this.writeE = writeE; 284 } 285 286 /** 287 * Loads the model from the JOSM preferences. 288 * @param key The preferences key 289 */ 290 public void load(String key) { 291 removeAllElements(); 292 addAllElements(Config.getPref().getList(key), readE); 293 } 294 295 /** 296 * Loads the model from the JOSM preferences. 297 * 298 * @param key The preferences key 299 * @param defaults A list of default values. 300 */ 301 public void load(String key, List<String> defaults) { 302 removeAllElements(); 303 addAllElements(Config.getPref().getList(key, defaults), readE); 304 } 305 306 /** 307 * Loads the model from the JOSM preferences. 308 * 309 * @param prop The property holding the strings. 310 */ 311 public void load(ListProperty prop) { 312 removeAllElements(); 313 addAllElements(prop.get(), readE); 314 } 315 316 /** 317 * Returns the model elements as list of strings. 318 * 319 * @return a list of strings 320 */ 321 public List<String> asStringList() { 322 List<String> list = new ArrayList<>(getSize()); 323 forEach(element -> list.add(writeE.apply(element))); 324 return list; 325 } 326 327 /** 328 * Saves the model to the JOSM preferences. 329 * 330 * @param key The preferences key 331 */ 332 public void save(String key) { 333 Config.getPref().putList(key, asStringList()); 334 } 335 336 /** 337 * Saves the model to the JOSM preferences. 338 * 339 * @param prop The property to write to. 340 */ 341 public void save(ListProperty prop) { 342 prop.put(asStringList()); 343 } 344 } 345}