com.jgoodies.binding.list.SelectionInList Maven / Gradle / Ivy
Show all versions of jgoodies-binding Show documentation
/*
* Copyright (c) 2002-2015 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.binding.list;
import static com.jgoodies.common.base.Preconditions.checkArgument;
import static com.jgoodies.common.base.Preconditions.checkNotNull;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.List;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.beans.BeanAdapter;
import com.jgoodies.binding.value.ValueHolder;
import com.jgoodies.binding.value.ValueModel;
import com.jgoodies.common.base.Objects;
/**
* Represents a selection in a list of objects. Provides bound bean properties
* for the list, the selection, the selection index, and the selection empty
* state. The SelectionInList implements ValueModel with the selection as value.
* Selection changes fire an event only if the old and new value are not equal.
* If you need to compare the identity you can use and observe the selection
* index instead of the selection or value.
*
* The SelectionInList uses three ValueModels to hold the list, the selection
* and selection index and provides bound bean properties for these models.
* You can access, observe and replace these ValueModels. This is useful
* to connect a SelectionInList with other ValueModels; for example you can
* use the SelectionInList's selection holder as bean channel for a
* PresentationModel. Since the SelectionInList is a ValueModel, it is often
* used as bean channel. See the Binding tutorial classes for examples on how
* to connect a SelectionInList with a PresentationModel.
*
* This class also implements the {@link ListModel} interface that allows
* API users to observe fine grained changes in the structure and contents
* of the list. Hence instances of this class can be used directly as model of
* a JList. If you want to use a SelectionInList with a JComboBox or JTable,
* you can convert the SelectionInList to the associated component model
* interfaces using the adapter classes
* {@link com.jgoodies.binding.adapter.ComboBoxAdapter}
* and {@link com.jgoodies.binding.adapter.AbstractTableAdapter} respectively.
* These classes are part of the Binding library too.
*
* The SelectionInList supports two list types as content of its list holder:
* {@code List} and {@code ListModel}. The two modes differ in how
* precise this class can fire events about changes to the content and structure
* of the list. If you use a List, this class can only report
* that the list changes completely; this is done by firing
* a PropertyChangeEvent for the list property.
* Also, a {@code ListDataEvent} is fired that reports a complete change.
* In contrast, if you use a ListModel it will report the same
* PropertyChangeEvent. But fine grained changes in the list
* model will be fired by this class to notify observes about changes in
* the content, added and removed elements.
*
* If the list content doesn't change at all, or if it always changes
* completely, you can work well with both List content and ListModel content.
* But if the list structure or content changes, the ListModel reports more
* fine grained events to registered ListDataListeners, which in turn allows
* list views to chooser better user interface gestures: for example, a table
* with scroll pane may retain the current selection and scroll offset.
*
* An example for using a ListModel in a SelectionInList is the asynchronous
* transport of list elements from a server to a client. Let's say you transport
* the list elements in portions of 10 elements to improve the application's
* responsiveness. The user can then select and work with the SelectionInList
* as soon as the ListModel gets populated. If at a later time more elements
* are added to the list model, the SelectionInList can retain the selection
* index (and selection) and will just report a ListDataEvent about
* the interval added. JList, JTable and JComboBox will then just add
* the new elements at the end of the list presentation.
*
* If you want to combine List operations and the ListModel change reports,
* you may consider using an implementation that combines these two interfaces,
* for example {@link com.jgoodies.common.collect.ArrayListModel}
* or {@link com.jgoodies.common.collect.LinkedListModel}.
*
* Important Note: If you change the ListModel instance,
* either by calling {@code #setListModel(ListModel)} or by setting
* a new value to the underlying list holder, you must ensure that
* the list holder throws a PropertyChangeEvent whenever the instance changes.
* This event is used to remove a ListDataListener from the old ListModel
* instance and is later used to add it to the new ListModel instance.
* It is easy to violate this constraint, just because Java's standard
* PropertyChangeSupport helper class that is used by many beans, checks
* a changed property value via {@code #equals}, not {@code ==}.
* For example, if you change the SelectionInList's list model from an empty
* list {@code L1} to another empty list instance {@code L2},
* the PropertyChangeSupport won't generate a PropertyChangeEvent,
* and so, the SelectionInList won't know about the change, which
* may lead to unexpected behavior.
*
* This binding library provides some help for firing PropertyChangeEvents
* if the old ListModel and new ListModel are equal but not the same.
* Class {@link com.jgoodies.binding.beans.ExtendedPropertyChangeSupport}
* allows to permanently or individually check the identity (using
* {@code ==}) instead of checking the equity (using {@code #equals}).
* Class {@link com.jgoodies.binding.beans.Model} uses this extended
* property change support. And class {@link ValueHolder} uses it too
* and can be configured to always test the identity.
*
* Since version 1.0.2 this class provides public convenience methods
* for firing ListDataEvents, see the methods {@code #fireContentsChanged},
* {@code #fireIntervalAdded}, and {@code #fireIntervalRemoved}.
* These are automatically invoked if the list holder holds a ListModel
* that fires these events. If on the other hand the underlying List or
* ListModel does not fire a required ListDataEvent, you can use these
* methods to notify presentations about a change. It is recommended
* to avoid sending duplicate ListDataEvents; hence check if the underlying
* ListModel fires the necessary events or not. Typically an underlying
* ListModel will fire the add and remove events; but often it'll lack
* an event if the (selected) contents has changed. A convenient way to
* indicate that change is {@code #fireSelectedContentsChanged}. See
* the tutorial's AlbumManagerModel for an example how to use this feature.
*
* The SelectionInList is partially defined for Lists and ListModels
* that contain {@code null}. Setting the selection to {@code null}
* on a SelectionInList that contains {@code null} won't set the selection index
* to the index of the first {@code null} element. For details see the
* {@link #setSelection(Object)} JavaDocs. This is because the current
* implementation interprets a {@code null} selection as unspecified,
* which maps better to a cleared selection than to a concrete selection index.
* Anyway, as long as you work with the selection index and selection index
* holder, such a SelectionInList will work fine. This is the case if you bind
* a SelectionInList to a JList or JTable. Binding such a SelectionInList
* to a JComboBox won't synchronize the selection index if {@code null}
* is selected.
*
* Constraints: The list holder holds instances of {@link List}
* or {@link ListModel}, the selection holder values of type {@code E}
* and the selection index holder of type {@code Integer}. The selection
* index holder must hold non-null index values; however, when firing
* an index value change event, both the old and new value may be null.
* If the ListModel changes, the underlying ValueModel must fire
* a PropertyChangeEvent.
*
* @author Karsten Lentzsch
* @version $Revision: 1.43 $
*
* @see ValueModel
* @see List
* @see ListModel
* @see com.jgoodies.binding.adapter.ComboBoxAdapter
* @see com.jgoodies.binding.adapter.AbstractTableAdapter
* @see com.jgoodies.binding.beans.ExtendedPropertyChangeSupport
* @see com.jgoodies.binding.beans.Model
* @see com.jgoodies.binding.value.ValueHolder
*
* @param the type of the list elements and the selection
*/
public final class SelectionInList extends IndirectListModel
implements ValueModel {
// Property Names *********************************************************
/**
* The name of the bound read-write selection property.
*/
public static final String PROPERTY_SELECTION = "selection";
/**
* The name of the bound read-only selectionEmpty property.
*/
public static final String PROPERTY_SELECTION_EMPTY = "selectionEmpty";
/**
* The name of the bound read-write selection holder property.
*/
public static final String PROPERTY_SELECTION_HOLDER = "selectionHolder";
/**
* The name of the bound read-write selectionIndex property.
*/
public static final String PROPERTY_SELECTION_INDEX = "selectionIndex";
/**
* The name of the bound read-write selection index holder property.
*/
public static final String PROPERTY_SELECTION_INDEX_HOLDER = "selectionIndexHolder";
// ************************************************************************
/**
* A special index that indicates that we have no selection.
*/
private static final int NO_SELECTION_INDEX = -1;
// Instance Fields ********************************************************
/**
* Holds the selection, an instance of {@code Object}.
*/
private ValueModel selectionHolder;
/**
* Holds the selection index, an {@code Integer}.
*/
private ValueModel selectionIndexHolder;
/**
* The {@code PropertyChangeListener} used to handle
* changes of the selection.
*/
private final PropertyChangeListener selectionChangeHandler;
/**
* The {@code PropertyChangeListener} used to handle
* changes of the selection index.
*/
private final PropertyChangeListener selectionIndexChangeHandler;
/**
* Duplicates the value of the selectionHolder.
* Used to provide better old values in PropertyChangeEvents
* fired after selectionIndex changes.
*/
private E oldSelection;
/**
* Duplicates the value of the selectionIndexHolder.
* Used to provide better old values in PropertyChangeEvents
* fired after selectionIndex changes and selection changes.
*/
private int oldSelectionIndex;
// Instance creation ****************************************************
/**
* Constructs a SelectionInList with an empty initial
* {@code ArrayListModel} using defaults for the selection holder
* and selection index holder.
*/
public SelectionInList() {
this((ListModel) new com.jgoodies.common.collect.ArrayListModel());
}
/**
* Constructs a SelectionInList on the given item array
* using defaults for the selection holder and selection index holder.
* The specified array will be converted to a List.
*
* Changes to the list "write through" to the array, and changes
* to the array contents will be reflected in the list.
*
* @param listItems the array of initial items
*
* @throws NullPointerException if {@code listItems} is {@code null}
*/
public SelectionInList(E[] listItems) {
this(Arrays.asList(listItems));
}
/**
* Constructs a SelectionInList on the given item array and
* selection holder using a default selection index holder.
* The specified array will be converted to a List.
*
* Changes to the list "write through" to the array, and changes
* to the array contents will be reflected in the list.
*
* @param listItems the array of initial items
* @param selectionHolder holds the selection
*
* @throws NullPointerException if {@code listItems} or
* {@code selectionHolder} is {@code null}
*/
public SelectionInList(E[] listItems, ValueModel selectionHolder) {
this(Arrays.asList(listItems), selectionHolder);
}
/**
* Constructs a SelectionInList on the given item array and
* selection holder using a default selection index holder.
* The specified array will be converted to a List.
*
* Changes to the list "write through" to the array, and changes
* to the array contents will be reflected in the list.
*
* @param listItems the array of initial items
* @param selectionHolder holds the selection
* @param selectionIndexHolder holds the selection index
*
* @throws NullPointerException if {@code listItems},
* {@code selectionHolder}, or {@code selectionIndexHolder}
* is {@code null}
*/
public SelectionInList(
E[] listItems,
ValueModel selectionHolder,
ValueModel selectionIndexHolder) {
this(Arrays.asList(listItems), selectionHolder, selectionIndexHolder);
}
/**
* Constructs a SelectionInList on the given list
* using defaults for the selection holder and selection index holder.
*
* Note: Favor {@code ListModel} over
* {@code List} when working with the SelectionInList.
* Why? The SelectionInList can work with both types. What's the
* difference? ListModel provides all list access features
* required by the SelectionInList's. In addition it reports more
* fine grained change events, instances of {@code ListDataEvents}.
* In contrast developer often create Lists and operate on them
* and the ListModel may be inconvenient for these operations.
*
* A convenient solution for this situation is to use the
* {@code ArrayListModel} and {@code LinkedListModel} classes.
* These implement both List and ListModel, offer the standard List
* operations and report the fine grained ListDataEvents.
*
* @param list the initial list
*/
public SelectionInList(List list) {
this(new ValueHolder(list, true));
}
/**
* Constructs a SelectionInList on the given list and
* selection holder using a default selection index holder.
*
* Note: Favor {@code ListModel} over
* {@code List} when working with the SelectionInList.
* Why? The SelectionInList can work with both types. What's the
* difference? ListModel provides all list access features
* required by the SelectionInList's. In addition it reports more
* fine grained change events, instances of {@code ListDataEvents}.
* In contrast developer often create Lists and operate on them
* and the ListModel may be inconvenient for these operations.
*
* A convenient solution for this situation is to use the
* {@code ArrayListModel} and {@code LinkedListModel} classes.
* These implement both List and ListModel, offer the standard List
* operations and report the fine grained ListDataEvents.
*
* @param list the initial list
* @param selectionHolder holds the selection
*
* @throws NullPointerException
* if {@code selectionHolder} is {@code null}
*/
public SelectionInList(List list, ValueModel selectionHolder) {
this(new ValueHolder(list, true), selectionHolder);
}
/**
* Constructs a SelectionInList on the given list,
* selection holder, and selection index holder.
*
* Note: Favor {@code ListModel} over
* {@code List} when working with the SelectionInList.
* Why? The SelectionInList can work with both types. What's the
* difference? ListModel provides all list access features
* required by the SelectionInList's. In addition it reports more
* fine grained change events, instances of {@code ListDataEvents}.
* In contrast developer often create Lists and operate on them
* and the ListModel may be inconvenient for these operations.
*
* A convenient solution for this situation is to use the
* {@code ArrayListModel} and {@code LinkedListModel} classes.
* These implement both List and ListModel, offer the standard List
* operations and report the fine grained ListDataEvents.
*
* @param list the initial list
* @param selectionHolder holds the selection
* @param selectionIndexHolder holds the selection index
*
* @throws NullPointerException if {@code selectionHolder},
* or {@code selectionIndexHolder} is {@code null}
*/
public SelectionInList(
List list,
ValueModel selectionHolder,
ValueModel selectionIndexHolder) {
this(new ValueHolder(list, true),
selectionHolder,
selectionIndexHolder);
}
/**
* Constructs a SelectionInList on the given list model
* using defaults for the selection holder and selection index holder.
*
* @param listModel the initial list model
*/
public SelectionInList(ListModel listModel) {
this(new ValueHolder(listModel, true));
}
/**
* Constructs a SelectionInList on the given list model
* and selection holder using a default selection index holder.
*
* @param listModel the initial list model
* @param selectionHolder holds the selection
*
* @throws NullPointerException
* if {@code selectionHolder} is {@code null}
*/
public SelectionInList(ListModel listModel, ValueModel selectionHolder) {
this(new ValueHolder(listModel, true), selectionHolder);
}
/**
* Constructs a SelectionInList on the given list model,
* selection holder, and selection index holder.
*
* @param listModel the initial list model
* @param selectionHolder holds the selection
* @param selectionIndexHolder holds the selection index
*
* @throws NullPointerException if {@code selectionHolder},
* or {@code selectionIndexHolder} is {@code null}
*/
public SelectionInList(
ListModel listModel,
ValueModel selectionHolder,
ValueModel selectionIndexHolder) {
this(new ValueHolder(listModel, true),
selectionHolder,
selectionIndexHolder);
}
/**
* Constructs a SelectionInList on the given list holder
* using defaults for the selection holder and selection index holder.
*
* Constraints:
* 1) The listHolder must hold instances of List or ListModel and
* 2) must report a value change whenever the value's identity changes.
* Note that many bean properties don't fire a PropertyChangeEvent
* if the old and new value are equal - and so would break this constraint.
* If you provide a ValueHolder, enable its identityCheck feature
* during construction. If you provide an adapted bean property from
* a bean that extends the JGoodies {@code Model} class,
* you can enable the identity check feature in the methods
* {@code #firePropertyChange} by setting the trailing boolean
* parameter to {@code true}.
*
* @param listHolder holds the list or list model
*
* @throws NullPointerException
* if {@code listHolder} is {@code null}
*/
public SelectionInList(ValueModel listHolder) {
this(listHolder, new ValueHolder(null, true));
}
/**
* Constructs a SelectionInList on the given list holder,
* selection holder and selection index holder.
*
* Constraints:
* 1) The listHolder must hold instances of List or ListModel and
* 2) must report a value change whenever the value's identity changes.
* Note that many bean properties don't fire a PropertyChangeEvent
* if the old and new value are equal - and so would break this constraint.
* If you provide a ValueHolder, enable its identityCheck feature
* during construction. If you provide an adapted bean property from
* a bean that extends the JGoodies {@code Model} class,
* you can enable the identity check feature in the methods
* {@code #firePropertyChange} by setting the trailing boolean
* parameter to {@code true}.
*
* @param listHolder holds the list or list model
* @param selectionHolder holds the selection
* @throws NullPointerException if {@code listHolder}
* or {@code selectionHolder} is {@code null}
*/
public SelectionInList(ValueModel listHolder, ValueModel selectionHolder) {
this(
listHolder,
selectionHolder,
new ValueHolder(Integer.valueOf(NO_SELECTION_INDEX)));
}
/**
* Constructs a SelectionInList on the given list holder,
* selection holder and selection index holder.
*
* Constraints:
* 1) The listHolder must hold instances of List or ListModel and
* 2) must report a value change whenever the value's identity changes.
* Note that many bean properties don't fire a PropertyChangeEvent
* if the old and new value are equal - and so would break this constraint.
* If you provide a ValueHolder, enable its identityCheck feature
* during construction. If you provide an adapted bean property from
* a bean that extends the JGoodies {@code Model} class,
* you can enable the identity check feature in the methods
* {@code #firePropertyChange} by setting the trailing boolean
* parameter to {@code true}.
*
* @param listHolder holds the list or list model
* @param selectionHolder holds the selection
* @param selectionIndexHolder holds the selection index
*
* @throws NullPointerException if the {@code listModelHolder},
* {@code selectionHolder}, or {@code selectionIndexHolder}
* is {@code null}
* @throws IllegalArgumentException if the listHolder is a ValueHolder
* that doesn't check the identity when changing its value
* @throws ClassCastException if the listModelHolder contents
* is neither a List nor a ListModel
*/
public SelectionInList(
ValueModel listHolder,
ValueModel selectionHolder,
ValueModel selectionIndexHolder) {
super(listHolder);
this.selectionHolder = checkNotNull(selectionHolder, "The selection holder must not be null.");
this.selectionIndexHolder = checkNotNull(selectionIndexHolder, "The selection index holder must not be null.");
selectionChangeHandler = new SelectionChangeHandler();
selectionIndexChangeHandler = new SelectionIndexChangeHandler();
initializeSelectionIndex();
this.selectionHolder.addValueChangeListener(selectionChangeHandler);
this.selectionIndexHolder.addValueChangeListener(selectionIndexChangeHandler);
}
// ListModel Helper Code **************************************************
/**
* Notifies all registered ListDataListeners that the contents
* of the selected list item - if any - has changed.
* Useful to update a presentation after editing the selection.
* See the tutorial's AlbumManagerModel for an example how to use
* this feature.
*
* If the list holder holds a ListModel, this SelectionInList listens
* to ListDataEvents fired by that ListModel, and forwards these events
* by invoking the associated {@code #fireXXX} method, which in turn
* notifies all registered ListDataListeners. Therefore if you fire
* ListDataEvents in an underlying ListModel, you don't need this method
* and should not use it to avoid sending duplicate ListDataEvents.
*
* @see ListModel
* @see ListDataListener
* @see ListDataEvent
*
* @since 1.0.2
*/
public void fireSelectedContentsChanged() {
if (hasSelection()) {
int selectionIndex = getSelectionIndex();
fireContentsChanged(selectionIndex, selectionIndex);
}
}
// Accessing the List, Selection and Index ********************************
/**
* Looks up and returns the current selection using
* the current selection index. Returns {@code null} if
* no object is selected or if the list has no elements.
*
* @return the current selection, {@code null} if none is selected
*/
public E getSelection() {
return getSafeElementAt(getSelectionIndex());
}
/**
* Sets the selection index to the index of the first list element
* that equals {@code newSelection}. If {@code newSelection}
* is {@code null}, it is interpreted as unspecified
* and the selection index is set to -1, and this SelectionInList
* has no selection. Does nothing if the list is empty or {@code null}.
*
* @param newSelection the object to be set as new selection,
* or {@code null} to set the selection index to -1
*/
public void setSelection(E newSelection) {
if (!isEmpty()) {
setSelectionIndex(indexOf(newSelection));
}
}
/**
* Checks and answers if an element is selected.
*
* @return true if an element is selected, false otherwise
*/
public boolean hasSelection() {
return getSelectionIndex() != NO_SELECTION_INDEX;
}
/**
* Checks and answers whether the selection is empty or not.
* Unlike #hasSelection, the underlying property #selectionEmpty
* for this method is bound. I.e. you can observe this property
* using a PropertyChangeListener to update UI state.
*
* @return true if nothing is selected, false if there's a selection
* @see #clearSelection
* @see #hasSelection
*/
public boolean isSelectionEmpty() {
return !hasSelection();
}
/**
* Clears the selection of this SelectionInList - if any.
*/
public void clearSelection() {
setSelectionIndex(NO_SELECTION_INDEX);
}
/**
* Returns the selection index.
*
* @return the selection index
*
* @throws NullPointerException if the selection index holder
* has a null Object set
*/
public int getSelectionIndex() {
return ((Integer) getSelectionIndexHolder().getValue()).intValue();
}
/**
* Sets a new selection index. Does nothing if it is the same as before.
*
* @param newSelectionIndex the selection index to be set
* @throws IndexOutOfBoundsException if the new selection index
* is outside the bounds of the list
*/
public void setSelectionIndex(int newSelectionIndex) {
int upperBound = getSize() - 1;
if (newSelectionIndex < NO_SELECTION_INDEX || newSelectionIndex > upperBound) {
throw new IndexOutOfBoundsException(
"The selection index " + newSelectionIndex + " must be in [-1, " + upperBound + "]");
}
oldSelectionIndex = getSelectionIndex();
if (oldSelectionIndex == newSelectionIndex) {
return;
}
getSelectionIndexHolder().setValue(Integer.valueOf(newSelectionIndex));
}
// Accessing the Holders for: List, Selection and Index *******************
/**
* Returns the selection holder.
*
* @return the selection holder
*/
public ValueModel getSelectionHolder() {
return selectionHolder;
}
/**
* Sets a new selection holder.
* Does nothing if the new is the same as before.
* The selection remains unchanged and is still driven
* by the selection index holder. It's just that future
* index changes will update the new selection holder
* and that future selection holder changes affect the
* selection index.
*
* @param newSelectionHolder the selection holder to set
*
* @throws NullPointerException if the new selection holder is null
*/
public void setSelectionHolder(ValueModel newSelectionHolder) {
checkNotNull(newSelectionHolder, "The new selection holder must not be null.");
ValueModel oldSelectionHolder = getSelectionHolder();
oldSelectionHolder.removeValueChangeListener(selectionChangeHandler);
selectionHolder = newSelectionHolder;
oldSelection = (E) newSelectionHolder.getValue();
newSelectionHolder.addValueChangeListener(selectionChangeHandler);
firePropertyChange(PROPERTY_SELECTION_HOLDER,
oldSelectionHolder,
newSelectionHolder);
}
/**
* Returns the selection index holder.
*
* @return the selection index holder
*/
public ValueModel getSelectionIndexHolder() {
return selectionIndexHolder;
}
/**
* Sets a new selection index holder.
* Does nothing if the new is the same as before.
*
* @param newSelectionIndexHolder the selection index holder to set
*
* @throws NullPointerException if the new selection index holder is null
* @throws IllegalArgumentException if the value of the new selection index
* holder is null
*/
public void setSelectionIndexHolder(ValueModel newSelectionIndexHolder) {
checkNotNull(newSelectionIndexHolder, "The new selection index holder must not be null.");
checkArgument(newSelectionIndexHolder.getValue() != null,
"The value of the new selection index holder must not be null.");
ValueModel oldSelectionIndexHolder = getSelectionIndexHolder();
if (Objects.equals(oldSelectionIndexHolder, newSelectionIndexHolder)) {
return;
}
oldSelectionIndexHolder.removeValueChangeListener(selectionIndexChangeHandler);
selectionIndexHolder = newSelectionIndexHolder;
newSelectionIndexHolder.addValueChangeListener(selectionIndexChangeHandler);
oldSelectionIndex = getSelectionIndex();
oldSelection = getSafeElementAt(oldSelectionIndex);
firePropertyChange(PROPERTY_SELECTION_INDEX_HOLDER,
oldSelectionIndexHolder,
newSelectionIndexHolder);
}
// ValueModel Implementation ********************************************
/**
* Returns the current selection, {@code null} if the selection index
* does not represent a selection in the list.
*
* @return the selected element - if any
*/
@Override
public E getValue() {
return getSelection();
}
/**
* Sets the selection index to the index of the first list element
* that equals {@code newValue}. If {@code newValue}
* is {@code null}, it is interpreted as unspecified
* and the selection index is set to -1, and this SelectionInList
* has no selection. Does nothing if the list is empty or {@code null}.
*
* @param newValue the object to be set as new selection,
* or {@code null} to set the selection index to -1
*/
@Override
public void setValue(Object newValue) {
setSelection((E) newValue);
}
/**
* Registers the given PropertyChangeListener with this model.
* The listener will be notified if the value has changed.
*
* The PropertyChangeEvents delivered to the listener have the name
* set to "value". In other words, the listeners won't get notified
* when a PropertyChangeEvent is fired that has a null object as
* the name to indicate an arbitrary set of the event source's
* properties have changed.
*
* In the rare case, where you want to notify a PropertyChangeListener
* even with PropertyChangeEvents that have no property name set,
* you can register the listener with #addPropertyChangeListener,
* not #addValueChangeListener.
*
* @param l the listener to add
*
* @see ValueModel
*/
@Override
public void addValueChangeListener(PropertyChangeListener l) {
addPropertyChangeListener(PROPERTY_VALUE, l);
}
/**
* Removes the given PropertyChangeListener from the model.
*
* @param l the listener to remove
*/
@Override
public void removeValueChangeListener(PropertyChangeListener l) {
removePropertyChangeListener(PROPERTY_VALUE, l);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param oldValue the value before the change
* @param newValue the value after the change
*
* @see java.beans.PropertyChangeSupport
*/
void fireValueChange(Object oldValue, Object newValue) {
firePropertyChange(PROPERTY_VALUE, oldValue, newValue);
}
// Misc ******************************************************************
/**
* Removes the internal listeners from the list holder, selection holder,
* selection index holder. If the current list is a ListModel, the internal
* ListDataListener is removed from the list model. This SelectionInList
* must not be used after calling {@code #release}.
*
* To avoid memory leaks it is recommended to invoke this method,
* if the list holder, selection holder, or selection index holder
* live much longer than this SelectionInList.
* Instead of releasing the SelectionInList, you typically make
* the list holder, selection holder, and selection index holder
* obsolete by releasing the PresentationModel or BeanAdapter that has
* created them before.
*
* As an alternative you may use ValueModels that in turn use
* event listener lists implemented using {@code WeakReference}.
*
* Basically this release method performs the reverse operation
* performed during the SelectionInList construction.
*
* @see PresentationModel#release()
* @see BeanAdapter#release()
* @see java.lang.ref.WeakReference
*
* @since 1.2
*/
@Override
public void release() {
super.release();
selectionHolder.removeValueChangeListener(selectionChangeHandler);
selectionIndexHolder.removeValueChangeListener(selectionIndexChangeHandler);
selectionHolder = null;
selectionIndexHolder = null;
oldSelection = null;
}
// Helper Code ***********************************************************
private E getSafeElementAt(int index) {
return index < 0 || index >= getSize()
? null
: getElementAt(index);
}
/**
* Returns the index in the list of the first occurrence of the specified
* element, or -1 if the element is {@code null} or the list does not
* contain this element.
*
* {@code null} is mapped to -1, because the current implementation
* interprets a null selection as unspecified.
*
* @param element the element to search for
* @return the index in the list of the first occurrence of the
* given element, or -1 if the element is {@code null} or
* the list does not contain this element.
*/
private int indexOf(Object element) {
return indexOf(getListHolder().getValue(), element);
}
/**
* Returns the index in the list of the first occurrence of the specified
* element, or -1 if the element is {@code null} or the list does not
* contain this element.
*
* {@code null} is mapped to -1, because the current implementation
* interprets a null selection as unspecified.
*
* @param aList the List or ListModel used to look up the element
* @param element the element to search for
* @return the index in the list of the first occurrence of the
* given element, or -1 if the element is {@code null} or
* the list does not contain this element.
*/
private static int indexOf(Object aList, Object element) {
if (element == null) {
return NO_SELECTION_INDEX;
} else if (getSize(aList) == 0) {
return NO_SELECTION_INDEX;
}
if (aList instanceof List) {
return ((List>) aList).indexOf(element);
}
// Search the first occurrence of element in the list model.
ListModel listModel = (ListModel) aList;
int size = listModel.getSize();
for (int index = 0; index < size; index++) {
if (element.equals(listModel.getElementAt(index))) {
return index;
}
}
return NO_SELECTION_INDEX;
}
/**
* Sets the index according to the selection, unless the selection
* is {@code null}.
* Also initializes the copied selection and selection index.
* This method is invoked by the constructors to synchronize
* the selection and index. No listeners are installed yet.
*
* An initial selection of {@code null} may indicate that the selection
* is unspecified. This happens for example, if the selection holder
* adapts a bean property via a PresentationModel, but the bean
* is {@code null}. In this case, the current semantics decides to not
* set the selection index - even if null is a list element.
*
* This leads to an inconsistency. If we construct a SelectionInList
* with {1, 2, 3} and initial selection 1, the selection index is set.
* If we construct {null, 2, 3} and initial selection null, the selection
* index is not set.
*
* TODO: Discuss whether we want to set the selection index if the
* initial selection is {@code null}.
*/
private void initializeSelectionIndex() {
E selectionValue = (E) selectionHolder.getValue();
if (selectionValue != null) {
setSelectionIndex(indexOf(selectionValue));
}
oldSelection = selectionValue;
oldSelectionIndex = getSelectionIndex();
}
// Overriding Superclass Behavior *****************************************
/**
* Creates and returns the ListDataListener used to observe
* changes in the underlying ListModel. It is re-registered
* in {@code #updateListModel}.
*
* @return the ListDataListener that handles changes
* in the underlying ListModel
*/
@Override
protected ListDataListener createListDataChangeHandler() {
return new ListDataChangeHandler();
}
/**
* Removes the list data change handler from the old list in case
* it is a {@code ListModel} and adds it to new one in case
* it is a {@code ListModel}.
* It then fires a property change for the list and a contents change event
* for the list content. Finally it tries to restore the previous selection
* - if any.
*
* Since version 1.1 the selection will be restored after
* the list content change has been indicated. This is because some
* listeners may clear the selection in a side-effect.
* For example a JTable that is bound to this SelectionInList
* via an AbstractTableAdapter and a SingleSelectionAdapter
* will clear the selection if the new list has a size other
* than the old list.
*
* @param oldList the old list content
* @param oldSize the size of the old List content
* @param newList the new list content
*
* @see javax.swing.JTable#tableChanged(javax.swing.event.TableModelEvent)
*/
@Override
protected void updateList(Object oldList, int oldSize, Object newList) {
boolean hadSelection = hasSelection();
Object oldSelectionHolderValue = hadSelection
? getSelectionHolder().getValue()
: null;
super.updateList(oldList, oldSize, newList);
if (hadSelection) {
setSelectionIndex(indexOf(newList, oldSelectionHolderValue));
}
}
// Event Handlers *********************************************************
/**
* Handles ListDataEvents in the list model.
* In addition to the ListDataChangeHandler in IndirectListModel,
* this class also updates the selection index.
*/
private final class ListDataChangeHandler implements ListDataListener {
/**
* Sent after the indices in the index0, index1
* interval have been inserted in the data model.
* The new interval includes both index0 and index1.
*
* @param evt a {@code ListDataEvent} encapsulating the
* event information
*/
@Override
public void intervalAdded(ListDataEvent evt) {
int index0 = evt.getIndex0();
int index1 = evt.getIndex1();
int index = getSelectionIndex();
fireIntervalAdded(index0, index1);
// If the added elements are after the index; do nothing.
if (index >= index0) {
setSelectionIndex(index + index1 - index0 + 1);
}
}
/**
* Sent after the indices in the index0, index1 interval
* have been removed from the data model. The interval
* includes both index0 and index1.
*
* @param evt a {@code ListDataEvent} encapsulating the
* event information
*/
@Override
public void intervalRemoved(ListDataEvent evt) {
int index0 = evt.getIndex0();
int index1 = evt.getIndex1();
int index = getSelectionIndex();
fireIntervalRemoved(index0, index1);
if (index < index0) {
// The removed elements are after the index; do nothing.
} else if (index <= index1) {
setSelectionIndex(NO_SELECTION_INDEX);
} else {
setSelectionIndex(index - (index1 - index0 + 1));
}
}
/**
* Sent when the contents of the list has changed in a way
* that's too complex to characterize with the previous
* methods. For example, this is sent when an item has been
* replaced. Index0 and index1 bracket the change.
*
* @param evt a {@code ListDataEvent} encapsulating the
* event information
*/
@Override
public void contentsChanged(ListDataEvent evt) {
fireContentsChanged(evt.getIndex0(), evt.getIndex1());
updateSelectionContentsChanged(evt.getIndex0(), evt.getIndex1());
}
private void updateSelectionContentsChanged(int first, int last) {
if (first < 0) {
return;
}
int selectionIndex = getSelectionIndex();
if (first <= selectionIndex && selectionIndex <= last) {
// need to synch directly on the holder because the
// usual methods for setting selection/-index check for
// equality
getSelectionHolder().setValue(getElementAt(selectionIndex));
}
}
}
/**
* Listens to changes of the selection.
*/
private final class SelectionChangeHandler implements PropertyChangeListener {
/**
* The selection has been changed. Updates the selection index holder's
* value and notifies registered listeners about the changes - if any -
* in the selection index, selection empty, selection, and value.
*
* Adjusts the selection holder's value and the old selection index
* before any event is fired. This ensures that the event old and
* new values are consistent with the SelectionInList's state.
*
* The current implementation assumes that the event sources
* provides a non-{@code null} new value. An arbitrary selection holder
* may fire change events where the new and/or old value is
* {@code null} to indicate that it is unknown, unspecified,
* or difficult to compute (now).
*
* TODO: Consider getting the new selection safely from the selection
* holder in case the new value is {@code null}. See the commented
* code section below.
*
* @param evt the property change event to be handled
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
E oldValue = (E) evt.getOldValue();
E newSelection = (E) evt.getNewValue();
// if (newSelection == null) {
// newSelection = (E) selectionHolder.getValue();
// }
int newSelectionIndex = indexOf(newSelection);
if (newSelectionIndex != oldSelectionIndex) {
selectionIndexHolder.removeValueChangeListener(selectionIndexChangeHandler);
selectionIndexHolder.setValue(Integer.valueOf(newSelectionIndex));
selectionIndexHolder.addValueChangeListener(selectionIndexChangeHandler);
}
int theOldSelectionIndex = oldSelectionIndex;
oldSelectionIndex = newSelectionIndex;
oldSelection = newSelection;
firePropertyChange(PROPERTY_SELECTION_INDEX,
theOldSelectionIndex,
newSelectionIndex);
firePropertyChange(PROPERTY_SELECTION_EMPTY,
theOldSelectionIndex == NO_SELECTION_INDEX,
newSelectionIndex == NO_SELECTION_INDEX);
/*
* Implementation Note: The following two lines fire the
* PropertyChangeEvents for the 'selection' and 'value' properties.
* If the old and new value are equal, no event is fired.
*
* TODO: Consider using ==, not equals to check for changes.
* That would enable API users to use the selection holder with
* beans that must be checked with ==, not equals.
* However, the SelectionInList's List would still use equals
* to find the index of an element.
*/
firePropertyChange(PROPERTY_SELECTION, oldValue, newSelection);
fireValueChange(oldValue, newSelection);
}
}
/**
* Listens to changes of the selection index.
*/
private final class SelectionIndexChangeHandler implements PropertyChangeListener {
/**
* The selection index has been changed. Updates the selection holder
* value and notifies registered listeners about changes - if any -
* in the selection index, selection empty, selection, and value.
*
* Handles null old values in the index PropertyChangeEvent.
* Ignores null new values in this events, because the selection
* index value must always be a non-null value.
*
* Adjusts the selection holder's value and the old selection index
* before any event is fired. This ensures that the event old and
* new values are consistent with the SelectionInList's state.
*
* @param evt the property change event to be handled
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
int newSelectionIndex = getSelectionIndex();
E theOldSelection = oldSelection;
//E oldSelection = getSafeElementAt(oldSelectionIndex);
E newSelection = getSafeElementAt(newSelectionIndex);
/*
* Implementation Note: The following conditional suppresses
* value change events if the old and new selection are equal.
*
* TODO: Consider using ==, not equals to check for changes.
* That would enable API users to use the selection holder with
* beans that must be checked with ==, not equals.
* However, the SelectionInList's List would still use equals
* to find the index of an element.
*/
if (!Objects.equals(theOldSelection, newSelection)) {
selectionHolder.removeValueChangeListener(selectionChangeHandler);
selectionHolder.setValue(newSelection);
selectionHolder.addValueChangeListener(selectionChangeHandler);
}
int theOldSelectionIndex = oldSelectionIndex;
oldSelectionIndex = newSelectionIndex;
oldSelection = newSelection;
firePropertyChange(PROPERTY_SELECTION_INDEX,
theOldSelectionIndex,
newSelectionIndex);
firePropertyChange(PROPERTY_SELECTION_EMPTY,
theOldSelectionIndex == NO_SELECTION_INDEX,
newSelectionIndex == NO_SELECTION_INDEX);
firePropertyChange(PROPERTY_SELECTION, theOldSelection, newSelection);
fireValueChange(theOldSelection, newSelection);
}
}
}