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 }