    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}