    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    }