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.7 2007/10/19 13:05:51 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        
204        final ListDataListener[] listeners = 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 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          selectedItemIndex = -1;
233          selectedItemValue = null;
234        }
235        else
236        {
237          final int newSelectedItem = findDataElementIndex(anItem);
238          if (newSelectedItem == -1)
239          {
240            selectedItemIndex = -1;
241            selectedItemValue = null;
242          }
243          else
244          {
245            selectedItemIndex = newSelectedItem;
246            selectedItemValue = getElementAt(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          selectedItemIndex = -1;
265          selectedItemValue = null;
266        }
267        else
268        {
269          final int newSelectedItem = findElementIndex(anItem);
270          if (newSelectedItem == -1)
271          {
272            if (isAllowOtherValue())
273            {
274              selectedItemIndex = -1;
275              selectedItemValue = anItem;
276            }
277            else
278            {
279              selectedItemIndex = -1;
280              selectedItemValue = null;
281            }
282          }
283          else
284          {
285            selectedItemIndex = newSelectedItem;
286            selectedItemValue = getElementAt(selectedItemIndex);
287          }
288        }
289        fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
290      }
291    
292      private boolean isAllowOtherValue()
293      {
294        return allowOtherValue;
295      }
296    
297      public void setAllowOtherValue(final boolean allowOtherValue)
298      {
299        this.allowOtherValue = allowOtherValue;
300      }
301    
302      /**
303       * Adds a listener to the list that's notified each time a change to the data
304       * model occurs.
305       *
306       * @param l the <code>ListDataListener</code> to be added
307       */
308      public synchronized void addListDataListener(final ListDataListener l)
309      {
310        if (l == null)
311        {
312          throw new NullPointerException();
313        }
314        listdatalistener.add(l);
315        tempListeners = null;
316      }
317    
318      /**
319       * Returns the value at the specified index.
320       *
321       * @param index the requested index
322       * @return the value at <code>index</code>
323       */
324      public Object getElementAt(final int index)
325      {
326        if (index >= data.size())
327        {
328          return null;
329        }
330    
331        final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(index);
332        if (datacon == null)
333        {
334          return null;
335        }
336        return datacon.getValue();
337      }
338    
339      /**
340       * Returns the key from the given index.
341       *
342       * @param index the index of the key.
343       * @return the the key at the specified index.
344       */
345      public Object getKeyAt(final int index)
346      {
347        if (index >= data.size())
348        {
349          return null;
350        }
351    
352        if (index < 0)
353        {
354          return null;
355        }
356    
357        final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(index);
358        if (datacon == null)
359        {
360          return null;
361        }
362        return datacon.getKey();
363      }
364    
365      /**
366       * Returns the selected data element or null if none is set.
367       *
368       * @return the selected data element.
369       */
370      public Object getSelectedKey()
371      {
372        return getKeyAt(selectedItemIndex);
373      }
374    
375      /**
376       * Returns the length of the list.
377       *
378       * @return the length of the list
379       */
380      public int getSize()
381      {
382        return data.size();
383      }
384    
385      /**
386       * Removes a listener from the list that's notified each time a change to
387       * the data model occurs.
388       *
389       * @param l the <code>ListDataListener</code> to be removed
390       */
391      public void removeListDataListener(final ListDataListener l)
392      {
393        listdatalistener.remove(l);
394        tempListeners = null;
395      }
396    
397      /**
398       * Searches an element by its data value. This method is called by the
399       * setSelectedItem method and returns the first occurence of the element.
400       *
401       * @param anItem the item
402       * @return the index of the item or -1 if not found.
403       */
404      private int findDataElementIndex(final Object anItem)
405      {
406        if (anItem == null)
407        {
408          throw new NullPointerException("Item to find must not be null");
409        }
410    
411        for (int i = 0; i < data.size(); i++)
412        {
413          final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(i);
414          if (anItem.equals(datacon.getKey()))
415          {
416            return i;
417          }
418        }
419        return -1;
420      }
421    
422      /**
423       * Tries to find the index of element with the given key. The key must not
424       * be null.
425       *
426       * @param key the key for the element to be searched.
427       * @return the index of the key, or -1 if not found.
428       */
429      public int findElementIndex(final Object key)
430      {
431        if (key == null)
432        {
433          throw new NullPointerException("Item to find must not be null");
434        }
435    
436        for (int i = 0; i < data.size(); i++)
437        {
438          final ComboBoxItemPair datacon = (ComboBoxItemPair) data.get(i);
439          if (key.equals(datacon.getValue()))
440          {
441            return i;
442          }
443        }
444        return -1;
445      }
446    
447      /**
448       * Removes an entry from the model.
449       *
450       * @param key the key
451       */
452      public void removeDataElement(final Object key)
453      {
454        final int idx = findDataElementIndex(key);
455        if (idx == -1)
456        {
457          return;
458        }
459    
460        data.remove(idx);
461        final ListDataEvent evt = new ListDataEvent
462            (this, ListDataEvent.INTERVAL_REMOVED, idx, idx);
463        fireListDataEvent(evt);
464      }
465    
466      /**
467       * Adds a new entry to the model.
468       *
469       * @param key    the key
470       * @param cbitem the display value.
471       */
472      public void add(final Object key, final Object cbitem)
473      {
474        final ComboBoxItemPair con = new ComboBoxItemPair(key, cbitem);
475        data.add(con);
476        final ListDataEvent evt = new ListDataEvent
477            (this, ListDataEvent.INTERVAL_ADDED, data.size() - 2, data.size() - 2);
478        fireListDataEvent(evt);
479      }
480    
481      /**
482       * Removes all entries from the model.
483       */
484      public void clear()
485      {
486        final int size = getSize();
487        data.clear();
488        final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
489        fireListDataEvent(evt);
490      }
491    
492    }