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 */
042package org.jfree.ui;
043
044import java.util.ArrayList;
045import javax.swing.ComboBoxModel;
046import javax.swing.event.ListDataEvent;
047import 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 */
061public 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}