001: /*
002: * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.binding.adapter;
032:
033: import java.beans.PropertyChangeEvent;
034: import java.beans.PropertyChangeListener;
035: import java.util.Arrays;
036: import java.util.List;
037:
038: import javax.swing.AbstractListModel;
039: import javax.swing.ComboBoxModel;
040: import javax.swing.ListModel;
041: import javax.swing.event.ListDataEvent;
042: import javax.swing.event.ListDataListener;
043:
044: import com.jgoodies.binding.list.SelectionInList;
045: import com.jgoodies.binding.value.ValueModel;
046:
047: /**
048: * A {@link ComboBoxModel} implementation that holds the choice list and a
049: * selection. This adapter has two modes that differ primarily in how
050: * the selection is kept synchronized with the combo's list.
051: * 1) If you construct a ComboBoxAdapter with a {@link SelectionInList},
052: * the selection will be guaranteed to be in the list, and the selection
053: * will reflect changes in the list.
054: * 2) If you construct this adapter with a separate selection holder,
055: * the selection won't be affected by any change in the combo's list.<p>
056: *
057: * In both cases, the combo's list of element will reflect changes in the list,
058: * if it's a ListModel and will ignore content changes, if it's a List.<p>
059: *
060: * <strong>Example:</strong><pre>
061: * String[] countries = new String[] { "USA", "Germany", "France", ... };
062: *
063: * // Using an array and ValueModel
064: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
065: * ComboBoxAdapter adapter = new ComboBoxAdapter(countries, contryModel);
066: * JComboBox countryBox = new JComboBox(adapter);
067: *
068: * // Using a List and ValueModel
069: * List countryList = Arrays.asList(countries);
070: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
071: * ComboBoxAdapter adapter = new ComboBoxAdapter(countryList, contryModel);
072: * JComboBox countryBox = new JComboBox(adapter);
073: *
074: * // Using a ListModel and ValueModel
075: * ListModel countryListModel = new ArrayListModel(Arrays.asList(countries));
076: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
077: * ComboBoxAdapter adapter = new ComboBoxAdapter(countryListModel, contryModel);
078: * JComboBox countryBox = new JComboBox(adapter);
079: *
080: * // Using a SelectionInList - allows only selection of contained elements
081: * ListModel countryListModel = new ArrayListModel(Arrays.asList(countries));
082: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
083: * SelectionInList sil = new SelectionInList(countryListModel, countryModel);
084: * ComboBoxAdapter adapter = new ComboBoxAdapter(sil);
085: * JComboBox countryBox = new JComboBox(adapter);
086: *
087: * // Using ValueModels for the list holder and the selection holder
088: * class Country extends Model {
089: * ListModel getLocales();
090: * Locale getDefaultLocale();
091: * void setDefaultLocale(Locale locale);
092: * }
093: *
094: * BeanAdapter beanAdapter = new BeanAdapter(null, true);
095: * ValueModel localesHolder = beanAdapter.getValueModel("locales");
096: * ValueModel defaultLocaleModel = beanAdapter.getValueModel("defaultLocale");
097: * ComboBoxAdapter adapter = new ComboBoxAdapter(
098: * localesHolder, defaultLocaleModel);
099: * JComboBox localeBox = new JComboBox(adapter);
100: *
101: * beanAdapter.setBean(myCountry);
102: * </pre>
103: *
104: * @author Karsten Lentzsch
105: * @version $Revision: 1.11 $
106: *
107: * @see javax.swing.JComboBox
108: *
109: * @param <E> the type of the combo box items
110: */
111: public final class ComboBoxAdapter<E> extends AbstractListModel
112: implements ComboBoxModel {
113:
114: /**
115: * Holds the list of choices.
116: */
117: private final ListModel listModel;
118:
119: /**
120: * Refers to the ValueModel that holds the current selection.
121: * In case this adapter is constructed for a SelectionInList
122: * or SelectionInListModel, the selection holder will be updated
123: * if the SIL or SILModel changes its selection holder.
124: */
125: private ValueModel selectionHolder;
126:
127: /**
128: * Holds the listener that handles selection changes.
129: */
130: private final PropertyChangeListener selectionChangeHandler;
131:
132: // Instance creation ******************************************************
133:
134: /**
135: * Constructs a ComboBoxAdapter for the specified List of items
136: * and the given selection holder. Structural changes in the list
137: * will be ignored.<p>
138: *
139: * <strong>Example:</strong><pre>
140: * String[] countries = new String[] { "USA", "Germany", "France", ... };
141: * List countryList = Arrays.asList(countries);
142: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
143: * ComboBoxAdapter adapter = new ComboBoxAdapter(countryList, contryModel);
144: * JComboBox countryBox = new JComboBox(adapter);
145: * </pre>
146: *
147: * @param items the list of items
148: * @param selectionHolder holds the selection of the combo
149: * @throws NullPointerException if the list of items or the selection holder
150: * is <code>null</code>
151: */
152: public ComboBoxAdapter(List<E> items, ValueModel selectionHolder) {
153: this (new ListModelAdapter<E>(items), selectionHolder);
154: if (items == null) {
155: throw new NullPointerException("The list must not be null.");
156: }
157: }
158:
159: /**
160: * Constructs a ComboBoxAdapter for the given ListModel and selection
161: * holder. Structural changes in the ListModel will be reflected by
162: * this adapter, but won't affect the selection.<p>
163: *
164: * <strong>Example:</strong><pre>
165: * String[] countries = new String[] { "USA", "Germany", "France", ... };
166: * ListModel countryList = new ArrayListModel(Arrays.asList(countries));
167: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
168: * ComboBoxAdapter adapter = new ComboBoxAdapter(countryList, contryModel);
169: * JComboBox countryBox = new JComboBox(adapter);
170: * </pre>
171: *
172: * @param listModel the initial list model
173: * @param selectionHolder holds the selection of the combo
174: * @throws NullPointerException if the list of items or the selection holder
175: * is <code>null</code>
176: */
177: public ComboBoxAdapter(ListModel listModel,
178: ValueModel selectionHolder) {
179: if (listModel == null) {
180: throw new NullPointerException(
181: "The ListModel must not be null.");
182: }
183: if (selectionHolder == null) {
184: throw new NullPointerException(
185: "The selection holder must not be null.");
186: }
187: this .listModel = listModel;
188: this .selectionHolder = selectionHolder;
189: listModel.addListDataListener(new ListDataChangeHandler());
190: selectionChangeHandler = new SelectionChangeHandler();
191: setSelectionHolder(selectionHolder);
192: }
193:
194: /**
195: * Constructs a ComboBoxAdapter for the specified List of items and the
196: * given selection holder. Structural changes in the list will be ignored.
197: * <p>
198: *
199: * <strong>Example:</strong><pre>
200: * String[] countries = new String[] { "USA", "Germany", "France", ... };
201: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
202: * ComboBoxAdapter adapter = new ComboBoxAdapter(countries, contryModel);
203: * JComboBox countryBox = new JComboBox(adapter);
204: * </pre>
205: *
206: * @param items the list of items
207: * @param selectionHolder holds the selection of the combo
208: * @throws NullPointerException if the list of items or the selection holder
209: * is <code>null</code>
210: */
211: public ComboBoxAdapter(E[] items, ValueModel selectionHolder) {
212: this (new ListModelAdapter<E>(items), selectionHolder);
213: }
214:
215: /**
216: * Constructs a ComboBoxAdapter for the given SelectionInList. Note that
217: * selections which are not elements of the list will be rejected.<p>
218: *
219: * <strong>Example:</strong><pre>
220: * String[] countries = new String[] { "USA", "Germany", "France", ... };
221: * List countryList = Arrays.asList(countries);
222: * ValueModel countryModel = new PropertyAdapter(customer, "country", true);
223: * SelectionInList sil = new SelectionInList(countryList, countryModel);
224: * ComboBoxAdapter adapter = new ComboBoxAdapter(sil);
225: * JComboBox countryBox = new JComboBox(adapter);
226: * </pre>
227: *
228: * @param selectionInList provides the list and selection
229: * @throws NullPointerException if the <code>selectionInList</code> is
230: * <code>null</code>
231: */
232: public ComboBoxAdapter(SelectionInList<E> selectionInList) {
233: this (selectionInList, selectionInList);
234: selectionInList.addPropertyChangeListener(
235: SelectionInList.PROPERTYNAME_SELECTION_HOLDER,
236: new SelectionHolderChangeHandler());
237: }
238:
239: // ComboBoxModel API ****************************************************
240:
241: /**
242: * Returns the selected item by requesting the current value from the
243: * either the selection holder or the SelectionInList's selection.
244: *
245: * @return The selected item or <code>null</code> if there is no selection
246: */
247: public E getSelectedItem() {
248: return (E) selectionHolder.getValue();
249: }
250:
251: /**
252: * Sets the selected item. The implementation of this method should notify
253: * all registered <code>ListDataListener</code>s that the contents has
254: * changed.
255: *
256: * @param object the list object to select or <code>null</code> to clear
257: * the selection
258: */
259: public void setSelectedItem(Object object) {
260: selectionHolder.setValue(object);
261: }
262:
263: /**
264: * Returns the length of the item list.
265: *
266: * @return the length of the list
267: */
268: public int getSize() {
269: return listModel.getSize();
270: }
271:
272: /**
273: * Returns the value at the specified index.
274: *
275: * @param index the requested index
276: * @return the value at <code>index</code>
277: */
278: public E getElementAt(int index) {
279: return (E) listModel.getElementAt(index);
280: }
281:
282: // Helper Code ************************************************************
283:
284: private void setSelectionHolder(ValueModel newSelectionHolder) {
285: ValueModel oldSelectionHolder = selectionHolder;
286: if (oldSelectionHolder != null) {
287: oldSelectionHolder
288: .removeValueChangeListener(selectionChangeHandler);
289: }
290: selectionHolder = newSelectionHolder;
291: if (newSelectionHolder == null) {
292: throw new NullPointerException(
293: "The selection holder must not be null.");
294: }
295: newSelectionHolder
296: .addValueChangeListener(selectionChangeHandler);
297: }
298:
299: // Event Handling *********************************************************
300:
301: private void fireContentsChanged() {
302: fireContentsChanged(this , -1, -1);
303: }
304:
305: /**
306: * Listens to selection changes and fires a contents change event.
307: */
308: private final class SelectionChangeHandler implements
309: PropertyChangeListener {
310:
311: /**
312: * The selection has changed. Notifies all
313: * registered listeners about the change.
314: *
315: * @param evt the property change event to be handled
316: */
317: public void propertyChange(PropertyChangeEvent evt) {
318: fireContentsChanged();
319: }
320:
321: }
322:
323: /**
324: * Handles ListDataEvents in the list model.
325: */
326: private final class ListDataChangeHandler implements
327: ListDataListener {
328:
329: /**
330: * Sent after the indices in the index0, index1 interval have been
331: * inserted in the data model. The new interval includes both index0 and
332: * index1.
333: *
334: * @param evt a <code>ListDataEvent</code> encapsulating the event
335: * information
336: */
337: public void intervalAdded(ListDataEvent evt) {
338: fireIntervalAdded(ComboBoxAdapter.this , evt.getIndex0(),
339: evt.getIndex1());
340: }
341:
342: /**
343: * Sent after the indices in the index0, index1 interval have been
344: * removed from the data model. The interval includes both index0 and
345: * index1.
346: *
347: * @param evt a <code>ListDataEvent</code> encapsulating the event
348: * information
349: */
350: public void intervalRemoved(ListDataEvent evt) {
351: fireIntervalRemoved(ComboBoxAdapter.this , evt.getIndex0(),
352: evt.getIndex1());
353: }
354:
355: /**
356: * Sent when the contents of the list has changed in a way that's too
357: * complex to characterize with the previous methods. For example, this
358: * is sent when an item has been replaced. Index0 and index1 bracket the
359: * change.
360: *
361: * @param evt a <code>ListDataEvent</code> encapsulating the event
362: * information
363: */
364: public void contentsChanged(ListDataEvent evt) {
365: fireContentsChanged(ComboBoxAdapter.this , evt.getIndex0(),
366: evt.getIndex1());
367: }
368: }
369:
370: /**
371: * Listens to changes of the selection holder and updates our internal
372: * reference to it.
373: */
374: private final class SelectionHolderChangeHandler implements
375: PropertyChangeListener {
376:
377: /**
378: * The SelectionInList has changed the selection holder.
379: * Update our internal reference to the new holder and
380: * notify registered listeners about a selection change.
381: *
382: * @param evt the property change event to be handled
383: */
384: public void propertyChange(PropertyChangeEvent evt) {
385: setSelectionHolder((ValueModel) evt.getNewValue());
386: fireContentsChanged();
387: }
388: }
389:
390: // Helper Classes *********************************************************
391:
392: /**
393: * Converts a List to ListModel by wrapping the underlying list.
394: */
395: private static final class ListModelAdapter<E> extends
396: AbstractListModel {
397:
398: private final List<E> aList;
399:
400: ListModelAdapter(List<E> list) {
401: this .aList = list;
402: }
403:
404: ListModelAdapter(E[] elements) {
405: this (Arrays.asList(elements));
406: }
407:
408: /**
409: * Returns the length of the list.
410: * @return the length of the list
411: */
412: public int getSize() {
413: return aList.size();
414: }
415:
416: /**
417: * Returns the value at the specified index.
418: * @param index the requested index
419: * @return the value at <code>index</code>
420: */
421: public E getElementAt(int index) {
422: return aList.get(index);
423: }
424:
425: }
426:
427: }
|