001    /* ========================================================================
002     * JCommon : a free general purpose class library for the Java(tm) platform
003     * ========================================================================
004     *
005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jcommon/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it
010     * under the terms of the GNU Lesser General Public License as published by
011     * the Free Software Foundation; either version 2.1 of the License, or
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022     * USA.
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * ------------------
028     * KeyedComboBoxModel.java
029     * ------------------
030     * (C) Copyright 2004, by Thomas Morgner and Contributors.
031     *
032     * Original Author:  Thomas Morgner;
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * $Id: KeyedComboBoxModel.java,v 1.6 2006/12/03 15:33:33 taqua Exp $
036     *
037     * Changes
038     * -------
039     * 07-Jun-2004 : Added JCommon header (DG);
040     *
041     */
042    package org.jfree.ui;
043    
044    import java.util.ArrayList;
045    import javax.swing.ComboBoxModel;
046    import javax.swing.event.ListDataEvent;
047    import javax.swing.event.ListDataListener;
048    
049    /**
050     * The KeyedComboBox model allows to define an internal key (the data element)
051     * for every entry in the model.
052     * <p/>
053     * This class is usefull in all cases, where the public text differs from the
054     * internal view on the data. A separation between presentation data and
055     * processing data is a prequesite for localizing combobox entries. This model
056     * does not allow selected elements, which are not in the list of valid
057     * elements.
058     *
059     * @author Thomas Morgner
060     */
061    public class KeyedComboBoxModel implements ComboBoxModel
062    {
063    
064      /**
065       * The internal data carrier to map keys to values and vice versa.
066       */
067      private static class ComboBoxItemPair
068      {
069        /**
070         * The key.
071         */
072        private Object key;
073        /**
074         * The value for the key.
075         */
076        private Object value;
077    
078        /**
079         * Creates a new item pair for the given key and value. The value can be
080         * changed later, if needed.
081         *
082         * @param key   the key
083         * @param value the value
084         */
085        public ComboBoxItemPair(final Object key, final Object value)
086        {
087          this.key = key;
088          this.value = value;
089        }
090    
091        /**
092         * Returns the key.
093         *
094         * @return the key.
095         */
096        public Object getKey()
097        {
098          return key;
099        }
100    
101        /**
102         * Returns the value.
103         *
104         * @return the value for this key.
105         */
106        public Object getValue()
107        {
108          return value;
109        }
110    
111        /**
112         * Redefines the value stored for that key.
113         *
114         * @param value the new value.
115         */
116        public void setValue(final Object value)
117        {
118          this.value = value;
119        }
120      }
121    
122      /**
123       * The index of the selected item.
124       */
125      private int selectedItemIndex;
126      private Object selectedItemValue;
127      /**
128       * The data (contains ComboBoxItemPairs).
129       */
130      private ArrayList data;
131      /**
132       * The listeners.
133       */
134      private ArrayList listdatalistener;
135      /**
136       * The cached listeners as array.
137       */
138      private transient ListDataListener[] tempListeners;
139      private boolean allowOtherValue;
140    
141      /**
142       * Creates a new keyed combobox model.
143       */
144      public KeyedComboBoxModel()
145      {
146        data = new ArrayList();
147        listdatalistener = new ArrayList();
148      }
149    
150      /**
151       * Creates a new keyed combobox model for the given keys and values. Keys
152       * and values must have the same number of items.
153       *
154       * @param keys   the keys
155       * @param values the values
156       */
157      public KeyedComboBoxModel(final Object[] keys, final Object[] values)
158      {
159        this();
160        setData(keys, values);
161      }
162    
163      /**
164       * Replaces the data in this combobox model. The number of keys must be
165       * equals to the number of values.
166       *
167       * @param keys   the keys
168       * @param values the values
169       */
170      public void setData(final Object[] keys, final Object[] values)
171      {
172        if (values.length != keys.length)
173        {
174          throw new IllegalArgumentException("Values and text must have the same length.");
175        }
176    
177        data.clear();
178        data.ensureCapacity(keys.length);
179    
180        for (int i = 0; i < values.length; i++)
181        {
182          add(keys[i], values[i]);
183        }
184    
185        selectedItemIndex = -1;
186        final ListDataEvent evt = new ListDataEvent
187            (this, ListDataEvent.CONTENTS_CHANGED, 0, data.size() - 1);
188        fireListDataEvent(evt);
189      }
190    
191      /**
192       * Notifies all registered list data listener of the given event.
193       *
194       * @param evt the event.
195       */
196      protected synchronized void fireListDataEvent(final ListDataEvent evt)
197      {
198        if (tempListeners == null)
199        {
200          tempListeners = (ListDataListener[]) listdatalistener.toArray
201              (new ListDataListener[listdatalistener.size()]);
202        }
203        for (int i = 0; i < tempListeners.length; i++)
204        {
205          final ListDataListener l = tempListeners[i];
206          l.contentsChanged(evt);
207        }
208      }
209    
210      /**
211       * Returns the selected item.
212       *
213       * @return The selected item or <code>null</code> if there is no selection
214       */
215      public Object getSelectedItem()
216      {
217        return selectedItemValue;
218      }
219    
220      /**
221       * Defines the selected key. If the object is not in the list of values, no
222       * item gets selected.
223       *
224       * @param anItem the new selected item.
225       */
226      public void setSelectedKey(final Object anItem)
227      {
228        if (anItem == null)
229        {
230          selectedItemIndex = -1;
231          selectedItemValue = null;
232        }
233        else
234        {
235          final int newSelectedItem = findDataElementIndex(anItem);
236          if (newSelectedItem == -1)
237          {
238            selectedItemIndex = -1;
239            selectedItemValue = null;
240          }
241          else
242          {
243            selectedItemIndex = newSelectedItem;
244            selectedItemValue = getElementAt(selectedItemIndex);
245          }
246        }
247        fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
248      }
249    
250      /**
251       * Set the selected item. The implementation of this  method should notify
252       * all registered <code>ListDataListener</code>s that the contents have
253       * changed.
254       *
255       * @param anItem the list object to select or <code>null</code> to clear the
256       *               selection
257       */
258      public void setSelectedItem(final Object anItem)
259      {
260        if (anItem == null)
261        {
262          selectedItemIndex = -1;
263          selectedItemValue = null;
264        }
265        else
266        {
267          final int newSelectedItem = findElementIndex(anItem);
268          if (newSelectedItem == -1)
269          {
270            if (isAllowOtherValue())
271            {
272              selectedItemIndex = -1;
273              selectedItemValue = anItem;
274            }
275            else
276            {
277              selectedItemIndex = -1;
278              selectedItemValue = null;
279            }
280          }
281          else
282          {
283            selectedItemIndex = newSelectedItem;
284            selectedItemValue = getElementAt(selectedItemIndex);
285          }
286        }
287        fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
288      }
289    
290      private boolean isAllowOtherValue()
291      {
292        return allowOtherValue;
293      }
294    
295      public void setAllowOtherValue(final boolean allowOtherValue)
296      {
297        this.allowOtherValue = allowOtherValue;
298      }
299    
300      /**
301       * Adds a listener to the list that's notified each time a change to the data
302       * model occurs.
303       *
304       * @param l the <code>ListDataListener</code> to be added
305       */
306      public synchronized void addListDataListener(final ListDataListener l)
307      {
308        listdatalistener.add(l);
309        tempListeners = null;
310      }
311    
312      /**
313       * Returns the value at the specified index.
314       *
315       * @param index the requested index
316       * @return the value at <code>index</code>
317       */
318      public Object getElementAt(final int index)
319      {
320        if (index >= data.size())
321        {
322          return null;
323        }
324    
325        final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(index);
326        if (datacon == null)
327        {
328          return null;
329        }
330        return datacon.getValue();
331      }
332    
333      /**
334       * Returns the key from the given index.
335       *
336       * @param index the index of the key.
337       * @return the the key at the specified index.
338       */
339      public Object getKeyAt(final int index)
340      {
341        if (index >= data.size())
342        {
343          return null;
344        }
345    
346        if (index < 0)
347        {
348          return null;
349        }
350    
351        final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(index);
352        if (datacon == null)
353        {
354          return null;
355        }
356        return datacon.getKey();
357      }
358    
359      /**
360       * Returns the selected data element or null if none is set.
361       *
362       * @return the selected data element.
363       */
364      public Object getSelectedKey()
365      {
366        return getKeyAt(selectedItemIndex);
367      }
368    
369      /**
370       * Returns the length of the list.
371       *
372       * @return the length of the list
373       */
374      public int getSize()
375      {
376        return data.size();
377      }
378    
379      /**
380       * Removes a listener from the list that's notified each time a change to
381       * the data model occurs.
382       *
383       * @param l the <code>ListDataListener</code> to be removed
384       */
385      public void removeListDataListener(final ListDataListener l)
386      {
387        listdatalistener.remove(l);
388        tempListeners = null;
389      }
390    
391      /**
392       * Searches an element by its data value. This method is called by the
393       * setSelectedItem method and returns the first occurence of the element.
394       *
395       * @param anItem the item
396       * @return the index of the item or -1 if not found.
397       */
398      private int findDataElementIndex(final Object anItem)
399      {
400        if (anItem == null)
401        {
402          throw new NullPointerException("Item to find must not be null");
403        }
404    
405        for (int i = 0; i < data.size(); i++)
406        {
407          final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(i);
408          if (anItem.equals(datacon.getKey()))
409          {
410            return i;
411          }
412        }
413        return -1;
414      }
415    
416      /**
417       * Tries to find the index of element with the given key. The key must not
418       * be null.
419       *
420       * @param key the key for the element to be searched.
421       * @return the index of the key, or -1 if not found.
422       */
423      public int findElementIndex(final Object key)
424      {
425        if (key == null)
426        {
427          throw new NullPointerException("Item to find must not be null");
428        }
429    
430        for (int i = 0; i < data.size(); i++)
431        {
432          final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(i);
433          if (key.equals(datacon.getValue()))
434          {
435            return i;
436          }
437        }
438        return -1;
439      }
440    
441      /**
442       * Removes an entry from the model.
443       *
444       * @param key the key
445       */
446      public void removeDataElement(final Object key)
447      {
448        final int idx = findDataElementIndex(key);
449        if (idx == -1)
450        {
451          return;
452        }
453    
454        data.remove(idx);
455        final ListDataEvent evt = new ListDataEvent
456            (this, ListDataEvent.INTERVAL_REMOVED, idx, idx);
457        fireListDataEvent(evt);
458      }
459    
460      /**
461       * Adds a new entry to the model.
462       *
463       * @param key    the key
464       * @param cbitem the display value.
465       */
466      public void add(final Object key, final Object cbitem)
467      {
468        final ComboBoxItemPair con = new ComboBoxItemPair(key, cbitem);
469        data.add(con);
470        final ListDataEvent evt = new ListDataEvent
471            (this, ListDataEvent.INTERVAL_ADDED, data.size() - 2, data.size() - 2);
472        fireListDataEvent(evt);
473      }
474    
475      /**
476       * Removes all entries from the model.
477       */
478      public void clear()
479      {
480        final int size = getSize();
481        data.clear();
482        final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
483        fireListDataEvent(evt);
484      }
485    
486    }