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 */
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 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}