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}