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.8 2008/09/10 09:26:11 mungady 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 this.key;
099        }
100    
101        /**
102         * Returns the value.
103         *
104         * @return the value for this key.
105         */
106        public Object getValue()
107        {
108          return this.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        this.data = new ArrayList();
147        this.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        this.data.clear();
178        this.data.ensureCapacity(keys.length);
179    
180        for (int i = 0; i < values.length; i++)
181        {
182          add(keys[i], values[i]);
183        }
184    
185        this.selectedItemIndex = -1;
186        final ListDataEvent evt = new ListDataEvent
187            (this, ListDataEvent.CONTENTS_CHANGED, 0, this.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 (this.tempListeners == null)
199        {
200            this.tempListeners = (ListDataListener[]) this.listdatalistener.toArray
201              (new ListDataListener[this.listdatalistener.size()]);
202        }
203    
204        final ListDataListener[] listeners = this.tempListeners;
205        for (int i = 0; i < listeners.length; i++)
206        {
207          final ListDataListener l = listeners[i];
208          l.contentsChanged(evt);
209        }
210      }
211    
212      /**
213       * Returns the selected item.
214       *
215       * @return The selected item or <code>null</code> if there is no selection
216       */
217      public Object getSelectedItem()
218      {
219        return this.selectedItemValue;
220      }
221    
222      /**
223       * Defines the selected key. If the object is not in the list of values, no
224       * item gets selected.
225       *
226       * @param anItem the new selected item.
227       */
228      public void setSelectedKey(final Object anItem)
229      {
230        if (anItem == null)
231        {
232            this.selectedItemIndex = -1;
233            this.selectedItemValue = null;
234        }
235        else
236        {
237          final int newSelectedItem = findDataElementIndex(anItem);
238          if (newSelectedItem == -1)
239          {
240              this.selectedItemIndex = -1;
241              this.selectedItemValue = null;
242          }
243          else
244          {
245              this.selectedItemIndex = newSelectedItem;
246              this.selectedItemValue = getElementAt(this.selectedItemIndex);
247          }
248        }
249        fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
250      }
251    
252      /**
253       * Set the selected item. The implementation of this  method should notify
254       * all registered <code>ListDataListener</code>s that the contents have
255       * changed.
256       *
257       * @param anItem the list object to select or <code>null</code> to clear the
258       *               selection
259       */
260      public void setSelectedItem(final Object anItem)
261      {
262        if (anItem == null)
263        {
264            this.selectedItemIndex = -1;
265            this.selectedItemValue = null;
266        }
267        else
268        {
269          final int newSelectedItem = findElementIndex(anItem);
270          if (newSelectedItem == -1)
271          {
272            if (isAllowOtherValue())
273            {
274                this.selectedItemIndex = -1;
275                this.selectedItemValue = anItem;
276            }
277            else
278            {
279                this.selectedItemIndex = -1;
280              this.selectedItemValue = null;
281            }
282          }
283          else
284          {
285              this.selectedItemIndex = newSelectedItem;
286              this.selectedItemValue = getElementAt(this.selectedItemIndex);
287          }
288        }
289        fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
290      }
291    
292      private boolean isAllowOtherValue()
293      {
294        return this.allowOtherValue;
295      }
296    
297      /**
298       * @param allowOtherValue
299       */
300      public void setAllowOtherValue(final boolean allowOtherValue)
301      {
302        this.allowOtherValue = allowOtherValue;
303      }
304    
305      /**
306       * Adds a listener to the list that's notified each time a change to the data
307       * model occurs.
308       *
309       * @param l the <code>ListDataListener</code> to be added
310       */
311      public synchronized void addListDataListener(final ListDataListener l)
312      {
313        if (l == null)
314        {
315          throw new NullPointerException();
316        }
317        this.listdatalistener.add(l);
318        this.tempListeners = null;
319      }
320    
321      /**
322       * Returns the value at the specified index.
323       *
324       * @param index the requested index
325       * @return the value at <code>index</code>
326       */
327      public Object getElementAt(final int index)
328      {
329        if (index >= this.data.size())
330        {
331          return null;
332        }
333    
334        final ComboBoxItemPair datacon = (ComboBoxItemPair) this.data.get(index);
335        if (datacon == null)
336        {
337          return null;
338        }
339        return datacon.getValue();
340      }
341    
342      /**
343       * Returns the key from the given index.
344       *
345       * @param index the index of the key.
346       * @return the the key at the specified index.
347       */
348      public Object getKeyAt(final int index)
349      {
350        if (index >= this.data.size())
351        {
352          return null;
353        }
354    
355        if (index < 0)
356        {
357          return null;
358        }
359    
360        final ComboBoxItemPair datacon = (ComboBoxItemPair) this.data.get(index);
361        if (datacon == null)
362        {
363          return null;
364        }
365        return datacon.getKey();
366      }
367    
368      /**
369       * Returns the selected data element or null if none is set.
370       *
371       * @return the selected data element.
372       */
373      public Object getSelectedKey()
374      {
375        return getKeyAt(this.selectedItemIndex);
376      }
377    
378      /**
379       * Returns the length of the list.
380       *
381       * @return the length of the list
382       */
383      public int getSize()
384      {
385        return this.data.size();
386      }
387    
388      /**
389       * Removes a listener from the list that's notified each time a change to
390       * the data model occurs.
391       *
392       * @param l the <code>ListDataListener</code> to be removed
393       */
394      public void removeListDataListener(final ListDataListener l)
395      {
396          this.listdatalistener.remove(l);
397          this.tempListeners = null;
398      }
399    
400      /**
401       * Searches an element by its data value. This method is called by the
402       * setSelectedItem method and returns the first occurence of the element.
403       *
404       * @param anItem the item
405       * @return the index of the item or -1 if not found.
406       */
407      private int findDataElementIndex(final Object anItem)
408      {
409        if (anItem == null)
410        {
411          throw new NullPointerException("Item to find must not be null");
412        }
413    
414        for (int i = 0; i < this.data.size(); i++)
415        {
416          final ComboBoxItemPair datacon = (ComboBoxItemPair) this.data.get(i);
417          if (anItem.equals(datacon.getKey()))
418          {
419            return i;
420          }
421        }
422        return -1;
423      }
424    
425      /**
426       * Tries to find the index of element with the given key. The key must not
427       * be null.
428       *
429       * @param key the key for the element to be searched.
430       * @return the index of the key, or -1 if not found.
431       */
432      public int findElementIndex(final Object key)
433      {
434        if (key == null)
435        {
436          throw new NullPointerException("Item to find must not be null");
437        }
438    
439        for (int i = 0; i < this.data.size(); i++)
440        {
441          final ComboBoxItemPair datacon = (ComboBoxItemPair) this.data.get(i);
442          if (key.equals(datacon.getValue()))
443          {
444            return i;
445          }
446        }
447        return -1;
448      }
449    
450      /**
451       * Removes an entry from the model.
452       *
453       * @param key the key
454       */
455      public void removeDataElement(final Object key)
456      {
457        final int idx = findDataElementIndex(key);
458        if (idx == -1)
459        {
460          return;
461        }
462    
463        this.data.remove(idx);
464        final ListDataEvent evt = new ListDataEvent
465            (this, ListDataEvent.INTERVAL_REMOVED, idx, idx);
466        fireListDataEvent(evt);
467      }
468    
469      /**
470       * Adds a new entry to the model.
471       *
472       * @param key    the key
473       * @param cbitem the display value.
474       */
475      public void add(final Object key, final Object cbitem)
476      {
477        final ComboBoxItemPair con = new ComboBoxItemPair(key, cbitem);
478        this.data.add(con);
479        final ListDataEvent evt = new ListDataEvent
480            (this, ListDataEvent.INTERVAL_ADDED, this.data.size() - 2, this.data.size() - 2);
481        fireListDataEvent(evt);
482      }
483    
484      /**
485       * Removes all entries from the model.
486       */
487      public void clear()
488      {
489        final int size = getSize();
490        this.data.clear();
491        final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
492        fireListDataEvent(evt);
493      }
494    
495    }