/* ========================================================================
* JCommon : a free general purpose class library for the Java(tm) platform
* ========================================================================
*
* (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jcommon/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* ------------------
* KeyedComboBoxModel.java
* ------------------
* (C) Copyright 2004, by Thomas Morgner and Contributors.
*
* Original Author: Thomas Morgner;
* Contributor(s): David Gilbert (for Object Refinery Limited);
*
* $Id: KeyedComboBoxModel.java,v 1.6 2006/12/03 15:33:33 taqua Exp $
*
* Changes
* -------
* 07-Jun-2004 : Added JCommon header (DG);
* 10-Dec-2015 : Implemented generics support
*
*/
package net.pms.util;
import java.util.ArrayList;
import javax.swing.ComboBoxModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
/**
* The KeyedComboBox model allows to define an internal key (the data element)
* for every entry in the model.
* <p/>
* This class is useful in all cases, where the public text differs from the
* internal view on the data. A separation between presentation data and
* processing data is a prerequisite for localizing combobox entries. This model
* does not allow selected elements, which are not in the list of valid
* elements.
*
* @author Thomas Morgner
* @author mail@tcox.org (Added generics)
* @author Nadahar (Implemented generics)
*/
public class KeyedComboBoxModel<K, V> implements ComboBoxModel<V> {
/**
* The internal data carrier to map keys to values and vice versa.
*/
private class ComboBoxItemPair {
/**
* The key.
*/
private K key;
/**
* The value for the key.
*/
private V value;
/**
* Creates a new item pair for the given key and value. The value can be
* changed later, if needed.
*
* @param key the key
* @param value the value
*/
public ComboBoxItemPair(final K key, final V value) {
this.key = key;
this.value = value;
}
/**
* Returns the key.
*
* @return the key.
*/
public K getKey() {
return key;
}
/**
* Returns the value.
*
* @return the value for this key.
*/
public V getValue() {
return value;
}
/**
* Redefines the value stored for that key.
*
* @param value the new value.
*/
@SuppressWarnings("unused")
public void setValue(final V value) {
this.value = value;
}
}
/**
* The index of the selected item.
*/
protected int selectedItemIndex;
protected K selectedItemKey;
protected V selectedItemValue;
/**
* The data (contains ComboBoxItemPairs).
*/
private ArrayList<ComboBoxItemPair> data;
/**
* The listeners.
*/
private ArrayList<ListDataListener> listdatalistener;
/**
* The cached listeners as array.
*/
private transient ListDataListener[] tempListeners;
private boolean allowOtherValue;
/**
* Creates a new keyed {@link ComboBoxModel}.
*/
public KeyedComboBoxModel() {
data = new ArrayList<>();
listdatalistener = new ArrayList<>();
}
/**
* Creates a new keyed {@link ComboBoxModel} for the given keys and values. Keys
* and values must have the same number of items.
*
* @param keys the keys
* @param values the values
*/
public KeyedComboBoxModel(final K[] keys, final V[] values) {
this();
setData(keys, values);
}
/**
* Replaces the data in this combobox model. The number of keys must be
* equals to the number of values.
*
* @param keys the keys
* @param values the values
*/
public void setData(final K[] keys, final V[] values) {
if (values.length != keys.length) {
throw new IllegalArgumentException("Keys and values must have the same length.");
}
data.clear();
data.ensureCapacity(keys.length);
for (int i = 0; i < values.length; i++) {
add(keys[i], values[i]);
}
selectedItemIndex = -1;
final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, data.size() - 1);
fireListDataEvent(evt);
}
/**
* Notifies all registered list data listener of the given event.
*
* @param event the event.
*/
protected synchronized void fireListDataEvent(final ListDataEvent event) {
if (tempListeners == null) {
tempListeners = listdatalistener.toArray(new ListDataListener[listdatalistener.size()]);
}
for (ListDataListener listener : tempListeners) {
if (listener != null && event != null) {
listener.contentsChanged(event);
}
}
}
/**
* Returns the selected item.
*
* @return The selected item or <code>null</code> if nothing is selected.
*
* @deprecated Inherited method. Use {@link #getSelectedKey()} or
* {@link #getSelectedValue()} instead.
*/
@Override
public Object getSelectedItem() {
return selectedItemValue;
}
/**
* Returns the selected key.
*
* @return The selected key or <code>null</code> if nothing is selected.
*/
public K getSelectedKey() {
return selectedItemKey;
}
/**
* Returns the selected value.
*
* @return The selected value or <code>null</code> if nothing is selected.
*/
public V getSelectedValue() {
return selectedItemValue;
}
/**
* Sets the selected key. If the object is not in the list of keys, no
* item gets selected.
*
* @param aKey the new selected item.
*/
public void setSelectedKey(final K aKey) {
if (aKey == null) {
selectedItemIndex = -1;
selectedItemKey = null;
selectedItemValue = null;
} else {
final int newSelectedItem = findKeyIndex(aKey);
if (newSelectedItem == -1) {
selectedItemIndex = -1;
selectedItemKey = null;
selectedItemValue = null;
} else {
selectedItemIndex = newSelectedItem;
selectedItemKey = getKeyAt(selectedItemIndex);
selectedItemValue = getValueAt(selectedItemIndex);
}
}
fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
}
/**
* Set the selected item. The implementation of this method should notify
* all registered <code>ListDataListener</code>s that the contents have
* changed.
*
* @param anItem the list object to select or <code>null</code> to clear the
* selection
*
* @deprecated Inherited method. Use {@link #setSelectedKey(K)} or
* {@link #setSelectedValue(V)} instead.
*/
@SuppressWarnings("unchecked")
@Override
public void setSelectedItem(final Object anItem) {
setSelectedValue((V) anItem);
}
/**
* Sets the selected value. If the object is not in the list of values, no
* item gets selected.
*
* @param aValue the new selected item.
*/
public void setSelectedValue(final V aValue) {
if (aValue == null) {
selectedItemIndex = -1;
selectedItemKey = null;
selectedItemValue = null;
} else {
int newSelectedIndex = findValueIndex(aValue);
if (newSelectedIndex == -1) {
if (isAllowOtherValue()) {
selectedItemIndex = -1;
selectedItemKey = null;
selectedItemValue = aValue;
} else {
selectedItemIndex = -1;
selectedItemKey = null;
selectedItemValue = null;
}
} else {
selectedItemIndex = newSelectedIndex;
selectedItemKey = getKeyAt(selectedItemIndex);
selectedItemValue = getValueAt(selectedItemIndex);
}
}
fireListDataEvent(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1));
}
private boolean isAllowOtherValue() {
return allowOtherValue;
}
public void setAllowOtherValue(final boolean allowOtherValue) {
this.allowOtherValue = allowOtherValue;
}
/**
* Adds a listener to the list that's notified each time a change to the data
* model occurs.
*
* @param listener the <code>ListDataListener</code> to be added
*/
@Override
public synchronized void addListDataListener(final ListDataListener listener) {
listdatalistener.add(listener);
tempListeners = null;
}
/**
* Returns the key at the specified index.
*
* @param index the requested index
* @return the key at <code>index</code>
*
* @deprecated Inherited method. Use {@link #getValueAt(int)} or
* {@link #getKeyAt(int)} instead.
*/
@Override
public V getElementAt(final int index) {
if (index >= data.size()) {
return null;
}
final ComboBoxItemPair datacon = data.get(index);
if (datacon == null) {
return null;
}
return datacon.getValue();
}
/**
* Returns the value at the specified index.
*
* @param index the requested index
* @return the value at <code>index</code>
*/
public V getValueAt(final int index) {
if (index >= data.size()) {
return null;
}
final ComboBoxItemPair datacon = data.get(index);
if (datacon == null) {
return null;
}
return datacon.getValue();
}
/**
* Returns the key from the given index.
*
* @param index the index of the key.
* @return the the key at the specified index.
*/
public K getKeyAt(final int index) {
if (index >= data.size()) {
return null;
}
if (index < 0) {
return null;
}
final ComboBoxItemPair datacon = data.get(index);
if (datacon == null) {
return null;
}
return datacon.getKey();
}
/**
* Returns the length of the list.
*
* @return the length of the list
*/
@Override
public int getSize() {
return data.size();
}
/**
* Removes a listener from the list that's notified each time a change to
* the data model occurs.
*
* @param l the <code>ListDataListener</code> to be removed
*/
@Override
public synchronized void removeListDataListener(final ListDataListener l) {
listdatalistener.remove(l);
tempListeners = null;
}
/**
* Tries to find the index of element with the given key. The key must not
* be null.
*
* @param key the key for the element to be searched.
* @return the index of the key, or -1 if not found.
*/
public int findKeyIndex(final Object key) {
if (key == null) {
throw new NullPointerException("Search key can not be null");
}
for (int i = 0; i < data.size(); i++) {
final ComboBoxItemPair datacon = data.get(i);
if (key.equals(datacon.getKey())) {
return i;
}
}
return -1;
}
/**
* Tries to find the index of element with the given value. The value must
* not be null.
*
* @param value the value for the element to be searched.
* @return the index of the value, or -1 if not found.
*/
public int findValueIndex(final Object value) {
if (value == null) {
throw new NullPointerException("Search value can not be null");
}
for (int i = 0; i < data.size(); i++) {
final ComboBoxItemPair datacon = data.get(i);
if (value.equals(datacon.getValue())) {
return i;
}
}
return -1;
}
/**
* Removes an entry from the model.
*
* @param key the key
*/
public void removeDataElement(final K key) {
final int idx = findKeyIndex(key);
if (idx == -1) {
return;
}
data.remove(idx);
final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, idx, idx);
fireListDataEvent(evt);
}
/**
* Adds a new entry to the model.
*
* @param key the key
* @param value the display value.
*/
public void add(final K key, final V value) {
final ComboBoxItemPair con = new ComboBoxItemPair(key, value);
data.add(con);
final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, data.size() - 2, data.size() - 2);
fireListDataEvent(evt);
}
/**
* Removes all entries from the model.
*/
public void clear() {
final int size = getSize();
data.clear();
final ListDataEvent evt = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
fireListDataEvent(evt);
}
}