package net.sf.cuf.model;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* A MultiSelectionInList holds both a list and a selection.
* The selection is both available as an index set as well as a
* list of the selected values from the original list.
*
* The major differences to {@link SelectionInList} are:
*
* We may have multiple selected entries.
* We don't support adding or removing elements from the base list
* by providing specific methods. That is only possible by directly manipulating the
* list and then calling {@link ExternalUpdate#signalExternalUpdate()}.
* Our behavior is undefined if the base list contains the same reference more
* than once. (Maybe we can improve at a later point in time.)
* This implementation still needs some work and lots of testing.
*
*
* The MultiSelectionInList is based on a ValueModel that has a
* {@link List} as value.
* The value of the MultiSelectionInList is the same list as the base list.
*
* Values can be selected and deselected using various methods in this
* class which result in a change to the selection index set and
* the selection values list. Alternatively the
* selected index set or the selected values list can be modified
* to change the selection.
*
* When changes in the selected values list are registered
* (via our special list implementation),
* the content of the selected value list is checked as follows:
*
* Entries that are in our base list are kept and marked as selected.
* For entries that are not in our base list an exception will be thrown.
* All entries that are in our base list but not in our selected list
* are marked not selected.
* The entries of the selected value list are sorted to
* be in the same order as the in the original list.
*
* The selected index set is updated synchronously to the selected values set.
*
* By default, the comparisons are made by reference, not by equals. That means that
* if you have a list of primitive objects, e.g. Integer, you must use the
* same instance of an integer in both the base list and in the
* selected values list, not simply any equal instance.
*
* If you do want a comparison by equals or by any other {@link Comparator},
* you can explicitly set one using #setSelectionComparator
*
* When changes in the selected index set are registered,
* the content of the selected value list is updated to
* reflect the changes. If an index outside the allowed range
* is added, an exception will be thrown.
*
* When changes in the base model are registered,
* we try to keep the selection as similar as possible by value,
* i.e. we try to keep the same selected objects in the list,
* even if we have to change our index values.
*
* Both the selected index set and the selected values list
* may be modified in content but not replaced in their value model.
*/
public class MultiSelectionInList extends AbstractValueModel>
implements ChangeListener
{
/**
* compares two objects and returns 0 when they are {@link Object#equals(Object)}.
* Otherwise -1 is returned (in violation of the {@link Comparator#compare(Object, Object)}
* specification, but it doesn't matter since we use it only as ==0).
* null
values are only equal to other null
values.
*/
public static final Comparator EQUALS_COMPARATOR = (pO1, pO2) -> {
if (pO1==null)
{
if (pO2==null)
{
return 0;
}
else
{
return -1;
}
}
else
{
if (pO2==null)
{
return -1;
}
else if (pO1.equals( pO2))
{
return 0;
}
else
{
return -1;
}
}
};
/**
* compares two objects and returns 0 when they are the same reference.
* Otherwise -1 is returned (in violation of the {@link Comparator#compare(Object, Object)}
* specification, but it doesn't matter since we use it only as ==0).
*/
public static final Comparator IDENTITY_COMPARATOR = (pO1, pO2) -> {
if (pO1 == pO2)
{
return 0;
}
else
{
return -1;
}
};
/**
* the value model that contains our base {@link List}.
* Never null.
*/
private ValueModel> mListHolder;
/**
* the value model that contains a {@link Set} of
* the {@link Integer}s that are the indexes of the
* selected values in the base list.
* The values in the set range from 0 to baselist.size()-1.
* Never null.
*/
private UnmodifiableValueHolder mSelectedIndexSetValueHolder;
/**
* the value model that contains a {@link List}
* of the selected values from the base list.
* Never null.
*/
private UnmodifiableValueHolder mSelectedValuesHolder;
/**
* the internally used set to store the selected indexes
* ({@link Integer}).
* This should never be passed outside of this class
* and all modifications must be consistent with
* {@link #mSelectedValuesList} and the value of
* {@link #mListHolder}.
* The set is ordered by index value.
*/
private SortedSet mSelectedIndexSet;
/**
* the internally used set to store the selected values.
* This should never be passed outside of this class
* and all modifications must be consistent with
* {@link #mSelectedIndexSet} and the value of
* {@link #mListHolder}.
* The List is sorted in the order the elements
* appear in the base list.
* This means that if you iterate through
* {@link #mSelectedIndexSet} and
* {@link #mSelectedValuesList} synchronously, you
* will get the matching index values for the selected objects.
*/
private List mSelectedValuesList;
/**
* the comparator to use when we try to match values in selection
* to values in the base list.
* This value is never null
* (even when {@link #setSelectionComparator(Comparator)} is called with a null parameter).
* When {@link Comparator#compare(Object, Object)} is called, we evaluate the result
* only with respect to ==0 or !=0 since the order is irrelevant.
*/
private Comparator mSelectionComparator;
/**
* Creates a new MultiSelectionInList with the given value model
* as our base. Initially, no entry is selected.
* @param pListHolder the value model, must contain a {@link List} as value
* @throws IllegalArgumentException if pListHolder is null or pListHolder.getValue()
* is not null and not a List
*/
public MultiSelectionInList( final ValueModel> pListHolder)
{
if (pListHolder==null)
throw new IllegalArgumentException("list holder must not be null");
init( pListHolder);
}
/**
* Creates a new MultiSelectionInList with the given list
* as our base. Initially, no entry is selected.
* @param pList the list, may be null
*/
public MultiSelectionInList( final List extends T> pList)
{
init(new ValueHolder<>(pList));
}
/**
* common functionality for our constructors.
* @param pListHolder the value model that contains our base list
*/
private void init( final ValueModel> pListHolder)
{
mListHolder = pListHolder;
mSelectedValuesList = new ArrayList<>();
mSelectedIndexSet = new TreeSet<>();
mSelectedValuesHolder = new UnmodifiableValueHolder<>(new SelectedValuesListAdapter());
mSelectedIndexSetValueHolder = new UnmodifiableValueHolder<>(new SelectedIndexSetAdapter());
mSelectionComparator = IDENTITY_COMPARATOR;
mListHolder.addChangeListener( this);
// note that we don't need observers on
// mSelectedValuesHolder and mSelectedIndexSetValueHolder.
// We've placed there our unique List and Set, respectively,
// that notify us when we have an edit change that would be relevant to us.
// Thus we can safely ignore the signal external change.
}
/**
* sets the selection comparator to use when we try to match
* a selected value to a value in the base list.
* If the parameter is null, we will use the identity comparison
* ({@link #IDENTITY_COMPARATOR}).
* If you want a comparison by {@link Object#equals(Object)},
* you can specify the {@link #EQUALS_COMPARATOR}.
*
* @param pSelectionComparator the comparator to use when matching selection values
* to base list values, may be null
*/
public void setSelectionComparator(final Comparator pSelectionComparator)
{
if (pSelectionComparator==null)
{
mSelectionComparator = IDENTITY_COMPARATOR;
}
else
{
mSelectionComparator = pSelectionComparator;
}
}
/**
* delegates to our base model
* {@inheritDoc}
*/
@Override
public boolean isEditable()
{
return mListHolder.isEditable();
}
/**
* Cleanup all resources: disconnect from any input sources (like
* other ValueModel's ...), and remove all listeners.
*/
@Override
public void dispose()
{
if (mListHolder!=null && !mListHolder.isDisposed())
{
mListHolder.removeChangeListener(this);
}
mSelectedIndexSetValueHolder.dispose();
mSelectedValuesHolder.dispose();
super.dispose();
}
/**
* Set a new value, this will fire a ChangeEvent if the new value
* is different from the old value. The new value must be a List
* or null. We set the list to the value holder.
* @param pValue the new list (null is o.k.)
* @param pIsSetForced true if a forced setValue should be done
* @throws IllegalArgumentException if pValue is neither a List nor null
*/
@Override
public void setValue(final List extends T> pValue, final boolean pIsSetForced)
{
checkDisposed();
if (isInSetValue())
{
return;
}
setInSetValue(true, pIsSetForced);
try
{
mListHolder.setValue(pValue);
updateSelectionForBaseListChange();
fireStateChanged();
}
finally
{
setInSetValue(false, false);
}
}
/**
* Get the current list.
* @return null or our list
*/
@Override
public List extends T> getValue()
{
try
{
checkDisposed();
}
catch (IllegalStateException e)
{
// we consider this as an valid action, otherwise we would force
// to disconnect all cascading value models, returning null in a
// "shutdown" scenario is slightly more graceful than throwing an
// exception
return null;
}
return mListHolder.getValue();
}
/**
* Invoked when the value model holding our list changed its state.
*
* @param pEvent a ChangeEvent object, not used
*/
@Override
public void stateChanged(final ChangeEvent pEvent)
{
checkDisposed();
// we ignore state changes when we just changed the list
if (isInSetValue())
{
return;
}
// check the list/index
updateSelectionForBaseListChange(); // this will notify for the selection value models if they had to be changed
fireStateChanged(); // notify for our own value after the selection has been updated
}
/**
* @return the List value of the {@link #mListHolder} or null if the value is null
* @throws IllegalArgumentException if the value is not null and not a List
*/
protected List extends T> getBaseList()
{
return mListHolder.getValue();
}
/**
* Updates our selection when the base list notifies us with a change.
* We try to keep the selection the same if possible (by value, not by index).
* We signal the selection value models if there has been a change.
*/
private void updateSelectionForBaseListChange()
{
if (mSelectedValuesList.isEmpty())
{
// the very easy case: we have nothing to adjust
return;
}
List extends T> baseList = getBaseList();
boolean selectedValuesChanged = false;
boolean selectedIndexChanged = false;
if (baseList==null || baseList.isEmpty())
{
// the easy case: we can't have any selected values
clearSelection();
return;
}
// Try first the index for faster access
// We take advantage of the fact that our set is sorted by index.
// If we didn't find an element at exactly the same position as before,
// the following variable is the place we store the offset.
// This is very helpful if only an element has been added or removed somewhere in the list.
int lastMatchingOffset = 0;
// first we gather all the old selected index values
int[] oldSelectedIndexes = new int[mSelectedIndexSet.size()];
int i=0;
for (Iterator iter = mSelectedIndexSet.iterator(); iter.hasNext(); i++)
{
oldSelectedIndexes[i] = (Integer) iter.next();
}
// go through the old list and try to find matches
// if the value is still somewhere in the base list, we temporarily store the new index value
// if the value is no longer in the base list, we mark it for removal with -1
int[] newSelectedIndexes = new int[oldSelectedIndexes.length];
for (i = 0; i < oldSelectedIndexes.length; i++)
{
Object selectedValueToFind = mSelectedValuesList.get(i);
newSelectedIndexes[i] = findIndex( selectedValueToFind, baseList, oldSelectedIndexes[i], oldSelectedIndexes[i]+lastMatchingOffset);
// update the reference in the selected values list to reflect the actual object in the base list
// which may have changed even though the index stays the same (needed since mSelectionComparator)
if (newSelectedIndexes[i]>=0)
{
mSelectedValuesList.set(i, baseList.get(newSelectedIndexes[i]));
// otherwise it will be cleared later
}
// now check of change
if (newSelectedIndexes[i]!=oldSelectedIndexes[i])
{
selectedIndexChanged = true;
if (newSelectedIndexes[i]>=0)
{
// still in list but at different index
lastMatchingOffset = newSelectedIndexes[i]-oldSelectedIndexes[i];
}
}
}
// did we have any index changes at all?
if (selectedIndexChanged)
{
// go through the individual changes, first to remove the old index for the changes
// and to the element from the list (reverse order makes that easier)
for (i = oldSelectedIndexes.length-1; i>=0; i--)
{
if (oldSelectedIndexes[i]!=newSelectedIndexes[i])
{
mSelectedIndexSet.remove(oldSelectedIndexes[i]);
if (newSelectedIndexes[i]<0)
{
// not in new list
selectedValuesChanged = true;
mSelectedValuesList.remove( i);
}
}
}
// then to add the new index values (two loops necessary since Set)
for (i = 0; i < oldSelectedIndexes.length; i++)
{
if (oldSelectedIndexes[i]!=newSelectedIndexes[i] && newSelectedIndexes[i]>=0)
{
mSelectedIndexSet.add(newSelectedIndexes[i]);
}
}
// remove the no longer selected element's indexes from the new index list
List newSelectedIndexesList = new ArrayList<>();
for (i = 0; i < newSelectedIndexes.length; i++)
{
if (newSelectedIndexes[i]>=0)
{
newSelectedIndexesList.add(newSelectedIndexes[i]);
}
}
// now newSelectedIndexesList has the same length as our mSelectedValuesList
// and contains the corresponding indexes
if (newSelectedIndexesList.size()!=mSelectedValuesList.size())
{
throw new RuntimeException( "Encountered illegal situation during processing of change event (1): list sizes do not match: "+newSelectedIndexesList.size()+" vs."+mSelectedValuesList.size());
}
if (newSelectedIndexesList.size()!=mSelectedIndexSet.size())
{
throw new RuntimeException( "Encountered illegal situation during processing of change event (2): list sizes do not match: "+newSelectedIndexesList.size()+" vs."+mSelectedIndexSet.size());
}
newSelectedIndexes = new int[newSelectedIndexesList.size()];
for (i = 0; i < newSelectedIndexes.length; i++)
{
newSelectedIndexes[i] = newSelectedIndexesList.get(i);
}
// reorder (if necessary)
boolean reorderNecessary = false;
for (i = 1; i < newSelectedIndexesList.size(); i++)
{
if (newSelectedIndexes[i-1]>=newSelectedIndexes[i])
{
reorderNecessary = true;
break;
}
}
if (reorderNecessary)
{
sortListWithIndex( newSelectedIndexes, mSelectedValuesList);
selectedValuesChanged = true;
}
}
if (selectedIndexChanged)
{
mSelectedIndexSetValueHolder.signalExternalUpdate();
}
if (selectedValuesChanged)
{
mSelectedValuesHolder.signalExternalUpdate();
}
}
/**
* sorts the values in the pList so that the order value at the corresponding index
* of the array is in ascending order.
*
* Package private to allow some separate tests.
*
* @param pReferenceOrder the reference indexes
* @param pList the list, sorted in place
*/
void sortListWithIndex(final int[] pReferenceOrder, final List extends T> pList)
{
if (pReferenceOrder.length!=pList.size())
{
throw new IllegalArgumentException( "pList must have the same size as pReferenceOrder: "+pList.size()+" vs. "+pReferenceOrder.length);
}
// TODO: is there a library method somewhere that sorts a list based on the values in a corresponding (integer) array?
final Map referenceMap = new IdentityHashMap<>();
for (int i = 0; i < pReferenceOrder.length; i++)
{
referenceMap.put( pList.get(i), pReferenceOrder[i]);
}
pList.sort((Comparator) Comparator.comparing(referenceMap::get));
}
/**
* searches the given reference in the list and returns its index
* or -1 if the object could not be found.
*
* To speed up the search two guesses where the object
* may be may be provided.
*
* Note: this compares the values using the {@link #mSelectionComparator}.
*
* @param pValueToFind the object to find
* @param pList the list to find the object in
* @param pFirstIndexHint a hint where the object may be in the list, or -1 if no hint is provided
* @param pSecondIndexHint a hint where the object may be in the list, or -1 if no hint is provided
* @return the index of the object in the list or -1 if the object could not be found
*/
private int findIndex(final Object pValueToFind, final List> pList, final int pFirstIndexHint, final int pSecondIndexHint)
{
if (pFirstIndexHint>=0 && pFirstIndexHint=0 && pSecondIndexHint
* The set contains only {@link Integer}s that are a valid index to the base list.
* Attempts to add other objects or invalid indexes will result in an exception.
*
*/
public ValueModel extends SortedSet> getSelectedIndexSetValueModel()
{
return mSelectedIndexSetValueHolder;
}
/**
* Returns the value model that contains the {@link List}
* with the selected values from the base list.
* The {@link ValueModel} itself is not modifiable,
* but the {@link List} is actively working with
* the {@link MultiSelectionInList} so that a notification
* of changes is not necessary.
* Note that the following restrictions apply:
*
* The List in the value model cannot be replaced by a different instance.
* The List contains the references to the selected elements from the base list.
* Attempts to add objects that are not in the base list (i.e. not found by the given {@link #mSelectionComparator})
* will result in an exception.
* When adding an object that is not the same but still equals using {@link #mSelectionComparator},
* we internally replace the reference in this list by the matching one from the base list.
* The List does not support adding or positioning elements arbitrarily.
* Attempts to do so will be rerouted to reflect the correct order
* of selected elements as defined by the base list.
* The List does not (yet) support all operations.
*
*
* @return the value model that contains the {@link List}
* with the selected values from the base list.
*/
public ValueModel extends List> getSelectedValuesHolder()
{
return mSelectedValuesHolder;
}
/**
* Checks if the same element is in the base list (by using {@link #mSelectionComparator}).
* @param pElement the element to check
* @return the index at which the element can be found in the base list
* or -1 if it is not in the base list
*/
public int determineIndexInBaseList( final Object pElement)
{
List extends T> baseList = getBaseList();
if (baseList==null || baseList.isEmpty())
{
return -1;
}
return findIndex( pElement, baseList, -1, -1);
}
/**
* selects all entries
* and makes the appropriate notifications.
*/
public void selectAll()
{
mSelectedValuesList.clear();
mSelectedIndexSet.clear();
List extends T> baseList = getBaseList();
for (int i = 0; i < baseList.size(); i++)
{
mSelectedIndexSet.add(i);
mSelectedValuesList.add( baseList.get(i));
}
signalSelectionChange();
}
/**
* clears the selection and
* makes the appropriate notifications.
*/
public void clearSelection()
{
if (mSelectedValuesList.isEmpty())
{
return; // nothing to do
}
mSelectedValuesList.clear();
mSelectedIndexSet.clear();
signalSelectionChange();
}
/**
* signals an internal change for the selection value models.
*/
private void signalSelectionChange()
{
mSelectedIndexSetValueHolder.signalExternalUpdate();
mSelectedValuesHolder.signalExternalUpdate();
}
/**
* Sets the selection to the given set of indexes.
*
* @param pSelectedIndexes a collection of {@link Integer}s
* which must be valid indexes for the base list
*/
public void setSelectedIndexes(final Collection pSelectedIndexes)
{
mSelectedIndexSetValueHolder.getValue().setSelectedIndexes(pSelectedIndexes);
}
/**
* Sets the selection to the given set of values.
*
* @param pSelectedValues a collection of Objects, must
* have the same references as the base list or be equal using
* the {@link #mSelectionComparator}.
*/
public void setSelectedElements(final Collection extends T> pSelectedValues)
{
mSelectedValuesHolder.getValue().setSelectedElements(pSelectedValues);
}
/**
* The list of selected values that can be passed to the outside of this class.
* The implementation notices when
* it is modified by adding, removing, or replacing elements.
* It checks that only values that are in our base list
* can be added and that the index set is updated consistently.
* Otherwise it delegates to the internally used list of selected values.
* Instances of this class serialize to an {@link ArrayList}.
*/
private class SelectedValuesListAdapter implements List, Serializable
{
private static final long serialVersionUID = 1250229061027889954L;
/**
* Sets the selection to be exactly as the given values.
*
* @param pSelectedValues a collection of Objects, must
* have the same references as the base list or be equal using
* the {@link MultiSelectionInList#mSelectionComparator}
*/
protected void setSelectedElements(final Collection extends T> pSelectedValues)
{
mSelectedIndexSet.clear();
mSelectedValuesList.clear();
if (pSelectedValues.isEmpty())
{
signalSelectionChange();
}
else
{
addAll( pSelectedValues); // this will signal the change
}
}
/**
* delegates to {@link #addElementInternal(Object)}
* and makes the appropriate notifications
* {@inheritDoc}
*/
@Override
public boolean add(final Object pObject)
{
boolean changed = addElementInternal( pObject);
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* Overwritten to default to {@link #add(Object)}
* since the placement of elements depends on the order in the base list.
* {@inheritDoc}
*/
@Override
public void add(final int pIndex, final Object pElement)
{
add( pElement);
}
/**
* delegates to {@link #addElementInternal(Object)}
* for each element
* and makes the appropriate notifications
* {@inheritDoc}
*/
@Override
public boolean addAll(final Collection extends T> pCollection)
{
boolean changed = false;
for (final Object element : pCollection)
{
changed |= addElementInternal(element);
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* Overwritten to default to {@link #addAll(Collection)}
* since the placement of elements depends on the order in the base list.
* {@inheritDoc}
*/
@Override
public boolean addAll(final int pIndex, final Collection extends T> pCollection)
{
return addAll(pCollection);
}
/**
* adds the element at the appropriate position,
* updates the selection index set, but does not
* make any notifications.
*
* @param pElement the element to add
* @return true if the list has been modified
* @throws IllegalArgumentException if the element is not in the base list
*/
private boolean addElementInternal(final Object pElement)
{
// ensure that it is present in the base list
int indexInBaseList = determineIndexInBaseList( pElement);
if (indexInBaseList<0)
{
throw new IllegalArgumentException( "the new element must be in the base list as reference to be added to the selected list: "+pElement);
}
// ensure it is not a duplicate
if (mSelectedIndexSet.contains(indexInBaseList))
{
return false; // nothing to do
}
T elementInBaseList = getBaseList().get( indexInBaseList);
// find the correct position in our list
// we take advantage of the sorted set of indexes
int indexInOurList = 0;
for (Iterator iter = mSelectedIndexSet.iterator(); iter.hasNext(); indexInOurList++)
{
int index = (Integer) iter.next();
if (index>indexInBaseList)
{
break; // we've found our place (indexInOurList is the correct place)
}
}
// add to our list
mSelectedValuesList.add( indexInOurList, elementInBaseList);
// add to index set
mSelectedIndexSet.add(indexInBaseList);
return true;
}
/**
* clears the selection and triggers the appropriate notifications
* {@inheritDoc}
*/
@Override
public void clear()
{
clearSelection();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public boolean contains(final Object pObject)
{
return mSelectedValuesList.contains(pObject);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public boolean containsAll(final Collection> pCollection)
{
return mSelectedValuesList.containsAll( pCollection);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public T get(final int pIndex)
{
return mSelectedValuesList.get( pIndex);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public int indexOf(final Object pObject)
{
return mSelectedValuesList.indexOf( pObject);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public boolean isEmpty()
{
return mSelectedValuesList.isEmpty();
}
/**
* creates our own iterator based on the one provided by
* {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public Iterator iterator()
{
// Caution: we can't return the iterator of the list
// since that would allow modifications without us being notified.
return new Iterator() {
/**
* the base iterator we use for the basic stuff
*/
private Iterator mSelectedValuesIterator = mSelectedValuesList.iterator();
/**
* the last value a call to {@link #next()} returned.
*/
private Object mLastReturnedValue;
/**
* delegates to the selected values iterator
* {@inheritDoc}
*/
public boolean hasNext()
{
return mSelectedValuesIterator.hasNext();
}
/**
* delegates to the selected values iterator
* but stores the delivered value
* {@inheritDoc}
*/
public Object next()
{
mLastReturnedValue = mSelectedValuesIterator.next();
return mLastReturnedValue;
}
/**
* does not delegate but calls our wrapped remove method for the last
* returned object.
* {@inheritDoc}
*/
public void remove()
{
if (mLastReturnedValue==null)
{
throw new IllegalStateException( "must call next() before remove()");
}
SelectedValuesListAdapter.this.remove( mLastReturnedValue);
mLastReturnedValue = null;
}
};
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public int lastIndexOf(final Object pObject)
{
return mSelectedValuesList.lastIndexOf( pObject);
}
/**
* @throws UnsupportedOperationException we don't support ListIterators (yet)
* {@inheritDoc}
*/
@Override
public ListIterator listIterator()
{
throw new UnsupportedOperationException("ListIterators are not (yet) supported in the selected values list");
}
/**
* @throws UnsupportedOperationException we don't support ListIterators (yet)
* {@inheritDoc}
*/
@Override
public ListIterator listIterator(final int pIndex)
{
throw new UnsupportedOperationException("ListIterators are not (yet) supported in the selected values list");
}
/**
* Removes the object from the selection
* and makes the appropriate notifications
* {@inheritDoc}
*/
@Override
public boolean remove(final Object pObject)
{
boolean changed = removeElementInternal( pObject);
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* Removes the object from the selection
* and makes the appropriate notifications
* {@inheritDoc}
*/
@Override
public T remove(final int pIndex)
{
// TODO: use the extra knowledge about the index
T object = mSelectedValuesList.get( pIndex);
removeElementInternalAtIndex( pIndex);
signalSelectionChange();
return object;
}
/**
* Removes the objects from the selection
* and makes the appropriate notifications.
* {@inheritDoc}
*/
@Override
public boolean removeAll(final Collection> pCollection)
{
boolean changed = false;
for (final Object element : pCollection)
{
changed |= removeElementInternal(element);
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* removes the objects from the selection
* that are not in the given collection
* and makes the appropriate notifications.
* {@inheritDoc}
*/
@Override
public boolean retainAll(final Collection> pCollection)
{
List> collectionAsList;
if (pCollection instanceof List)
{
collectionAsList = (List>) pCollection;
}
else
{
collectionAsList = new ArrayList<>( pCollection);
}
// loop through our existing elements (backwards for convenience)
boolean changed = false;
for( int testIndex = mSelectedValuesList.size()-1; testIndex>=0; testIndex--)
{
if (findIndex( mSelectedValuesList.get(testIndex), collectionAsList, -1, -1)<0)
{
// not in the collection -> remove
changed = true;
removeElementInternalAtIndex( testIndex);
}
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* Redirected to a remove and an add
* since the placement of elements depends on the order in the base list.
* {@inheritDoc}
*/
@Override
public T set(final int pIndex, final Object pElement)
{
T previousObject = mSelectedValuesList.get( pIndex);
if (previousObject==pElement)
{
// nothing to do
return previousObject;
}
removeElementInternalAtIndex( pIndex);
addElementInternal(pElement);
signalSelectionChange();
return previousObject;
}
/**
* removes the element,
* updates the selection index set, but does not
* make any notifications.
*
* @param pElement the element to remove
* @return true if the list has been modified
*/
private boolean removeElementInternal(final Object pElement)
{
// find our index
int ourIndex = findIndex( pElement, mSelectedValuesList, -1, -1);
if (ourIndex<0)
{
return false; // not in list
}
removeElementInternalAtIndex( ourIndex);
return true;
}
/**
* Removes the element at the index, updates the selection index set,
* but does not make any notifications.
* @param pIndex the index to remove.
*/
private void removeElementInternalAtIndex( final int pIndex)
{
if (pIndex<0 || pIndex>=mSelectedValuesList.size())
{
throw new IndexOutOfBoundsException( "index must be in range from 0 to "+(mSelectedValuesList.size()-1)+": "+pIndex);
}
// find the corresponding value in the index set and remove it
Iterator iter = mSelectedIndexSet.iterator();
for (int i = 0; i <= pIndex; i++)
{
iter.next();
}
iter.remove();
mSelectedValuesList.remove( pIndex);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public int size()
{
return mSelectedValuesList.size();
}
/**
* Returns an unmodifiable list.
* Perhaps later we might be able to provide a modifiable list.
* {@inheritDoc}
*/
@Override
public List subList(final int pFromIndex, final int pToIndex)
{
return Collections.unmodifiableList( mSelectedValuesList.subList(pFromIndex, pToIndex));
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public Object[] toArray()
{
return mSelectedValuesList.toArray();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public E[] toArray(final E[] pArray)
{
return mSelectedValuesList.toArray( pArray);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public boolean equals(final Object pObj)
{
return mSelectedValuesList.equals(pObj);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedValuesList}
* {@inheritDoc}
*/
@Override
public int hashCode()
{
return mSelectedValuesList.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return mSelectedValuesList.toString();
}
/**
* Serialize as {@link ArrayList}
* @return the {@link ArrayList} to serialize
*/
private Object writeReplace()
{
return new ArrayList<>(this);
}
}
/**
* The set of selected indexes that can be passed to the outside of this class.
* The implementation notices when
* it is modified by adding or removing elements.
* It checks that only valid indexes can be added
* and that the selected values list is updated consistently.
* Otherwise it delegates to the internally used set of selected indexes.
* Instances of this class serialize to an {@link TreeSet}.
*/
private class SelectedIndexSetAdapter implements SortedSet, Serializable
{
private static final long serialVersionUID = 6728079508990733355L;
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
public Comparator super Integer> comparator()
{
return mSelectedIndexSet.comparator();
}
/**
* Sets the selection to be exactly as the given indexes.
*
* @param pSelectedIndexes a collection of {@link Integer}s
* which must be valid indexes for the base list
*/
protected void setSelectedIndexes(final Collection pSelectedIndexes)
{
mSelectedIndexSet.clear();
mSelectedValuesList.clear();
List extends T> baseList = getBaseList();
for (final Integer element : pSelectedIndexes)
{
if (element == null)
{
throw new IllegalArgumentException("elements in index collection must not be null");
}
int indexInBaseList = element;
if (indexInBaseList < 0 || indexInBaseList >= baseList.size())
{
throw new IndexOutOfBoundsException("index must be in range from 0 to " + (baseList.size() - 1) + ": " + indexInBaseList);
}
mSelectedIndexSet.add(element);
}
// our set is sorted - simply compile the values list
for (final Object aMSelectedIndexSet : mSelectedIndexSet)
{
int indexInBaseList = (Integer) aMSelectedIndexSet;
mSelectedValuesList.add(baseList.get(indexInBaseList));
}
signalSelectionChange();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public Integer first()
{
return mSelectedIndexSet.first();
}
/**
* returns an unmodifiable set.
* Perhaps later we might be able to provide a modifiable set.
* {@inheritDoc}
*/
@Override
public SortedSet headSet(final Integer pToElement)
{
// Caution: we can't return the head set of the index set
// since that would allow modifications without us being notified.
return Collections.unmodifiableSortedSet( mSelectedIndexSet.headSet( pToElement));
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public Integer last()
{
return mSelectedIndexSet.last();
}
/**
* returns an unmodifiable set.
* Perhaps later we might be able to provide a modifiable set.
* {@inheritDoc}
*/
@Override
public SortedSet subSet(final Integer pFromElement, final Integer pToElement)
{
// Caution: we can't return the sub set of the index set
// since that would allow modifications without us being notified.
return Collections.unmodifiableSortedSet( mSelectedIndexSet.subSet( pFromElement, pToElement));
}
/**
* returns an unmodifiable set.
* Perhaps later we might be able to provide a modifiable set.
* {@inheritDoc}
*/
@Override
public SortedSet tailSet(final Integer pFromElement)
{
// Caution: we can't return the tail set of the index set
// since that would allow modifications without us being notified.
return Collections.unmodifiableSortedSet( mSelectedIndexSet.tailSet( pFromElement));
}
/**
* if the object is an Integer and in index range it is added
* and the appropriate notifications are made.
* Otherwise an exception is thrown.
* {@inheritDoc}
*
* @throws IllegalArgumentException if the object is null, not an integer
* @throws IndexOutOfBoundsException if the index is out of range
*/
@Override
public boolean add(final Integer pObject)
{
boolean changed = addIndexInternal( pObject);
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* if the object is an Integer and in index range it is added,
* but no notifications are made.
* Otherwise an exception is thrown.
* @param pIndexInBaseList the object (Integer) to add
* @return true if the set has been modified
* @throws IllegalArgumentException if the object is null, not an integer
* @throws IndexOutOfBoundsException if the index is out of range
*/
private boolean addIndexInternal(final Integer pIndexInBaseList)
{
if (pIndexInBaseList==null)
{
throw new IllegalArgumentException( "pObject must not be null");
}
List extends T> baseList = getBaseList();
if (pIndexInBaseList <0 || pIndexInBaseList >=baseList.size())
{
throw new IndexOutOfBoundsException( "index must be in range from 0 to "+(baseList.size()-1)+": "+ pIndexInBaseList);
}
if (mSelectedIndexSet.contains( pIndexInBaseList))
{
return false; // already in the selection
}
// find the correct position in our selection
// we take advantage of the sorted set of indexes
int indexInOurList = 0;
for (Iterator iter = mSelectedIndexSet.iterator(); iter.hasNext(); indexInOurList++)
{
int index = (Integer) iter.next();
if (index> pIndexInBaseList)
{
break; // we've found our place (indexInOurList is the correct place)
}
}
// add to our list
mSelectedValuesList.add( indexInOurList, baseList.get(pIndexInBaseList));
// add to index set
mSelectedIndexSet.add(pIndexInBaseList);
return true;
}
/**
* if the objects are Integers and in index range they are added
* and the appropriate notifications are made.
* Otherwise an exception is thrown.
* {@inheritDoc}
*
* @throws IllegalArgumentException if a value is null, not an integer
* @throws IndexOutOfBoundsException if an index is out of range
*/
@Override
public boolean addAll(final Collection extends Integer> pCollection)
{
boolean changed = false;
for (final Integer element : pCollection)
{
changed |= addIndexInternal(element);
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* clears the selection and makes the appropriate notifications
* {@inheritDoc}
*/
public void clear()
{
clearSelection();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
public boolean contains(final Object pObject)
{
return mSelectedIndexSet.contains( pObject);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
public boolean containsAll(final Collection> pCollection)
{
return mSelectedIndexSet.containsAll( pCollection);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
public boolean isEmpty()
{
return mSelectedIndexSet.isEmpty();
}
/**
* creates our own iterator based on the one provided by
* {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
public Iterator iterator()
{
// Caution: we can't return the iterator of the index set
// since that would allow modifications without us being notified.
return new Iterator() {
/**
* the base iterator we use for the basic stuff
*/
private Iterator mSelectedIndexIterator = mSelectedIndexSet.iterator();
/**
* the last value a call to {@link #next()} returned.
*/
private Integer mLastReturnedValue;
/**
* delegates to the selected index iterator
* {@inheritDoc}
*/
@Override
public boolean hasNext()
{
return mSelectedIndexIterator.hasNext();
}
/**
* delegates to the selected index iterator
* but stores the delivered value
* {@inheritDoc}
*/
@Override
public Integer next()
{
mLastReturnedValue = mSelectedIndexIterator.next();
return mLastReturnedValue;
}
/**
* does not delegate but calls our wrapped remove method for the last
* returned object.
* {@inheritDoc}
*/
@Override
public void remove()
{
if (mLastReturnedValue==null)
{
throw new IllegalStateException( "must call next() before remove()");
}
SelectedIndexSetAdapter.this.remove( mLastReturnedValue);
mLastReturnedValue = null;
}
};
}
@Override
public boolean remove(final Object pObject)
{
boolean changed = removeElementInternal( pObject);
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* deselects the given index,
* updates the selection values list, but does not
* make any notifications.
*
* @param pElement the index to remove
* @return true if the list has been modified
*/
@SuppressWarnings({"ConstantConditions"})
private boolean removeElementInternal(final Object pElement)
{
if (!mSelectedIndexSet.contains( pElement))
{
return false; // not in selection
}
int indexInBaseList = (Integer) pElement;
// find our index
// we take advantage of the sorted set of indexes
int indexInOurList = 0;
for (Iterator iter = mSelectedIndexSet.iterator(); iter.hasNext(); indexInOurList++)
{
int index = (Integer) iter.next();
if (index==indexInBaseList)
{
break; // we've found our place (indexInOurList is the correct place)
}
}
// remove
mSelectedIndexSet.remove( pElement);
mSelectedValuesList.remove( indexInOurList);
return true;
}
@Override
public boolean removeAll(final Collection> pCollection)
{
boolean changed = false;
for (final Object element : pCollection)
{
changed |= removeElementInternal(element);
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
@Override
public boolean retainAll(final Collection> pCollection)
{
// Note: this could probably be optimized if necessary
// loop through a copy of our existing elements (backwards for convenience)
boolean changed = false;
List indexesToRemove = new ArrayList<>(mSelectedIndexSet);
for (Iterator iter = indexesToRemove.iterator(); iter.hasNext();)
{
Integer index = (Integer) iter.next();
if (pCollection.contains( index))
{
iter.remove(); // we found it in the given collection - don't keep it in our to remove list
}
}
// now remove the identified elements
for (final Object anIndexesToRemove : indexesToRemove)
{
Integer index = (Integer) anIndexesToRemove;
changed |= removeElementInternal(index);
}
if (changed)
{
signalSelectionChange();
}
return changed;
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public int size()
{
return mSelectedIndexSet.size();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public Object[] toArray()
{
return mSelectedIndexSet.toArray();
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public E[] toArray(final E[] pArray)
{
return mSelectedIndexSet.toArray( pArray);
}
/**
* delegates to {@link MultiSelectionInList#mSelectedIndexSet}
* {@inheritDoc}
*/
@Override
public boolean equals(final Object pObj)
{
return mSelectedIndexSet.equals( pObj);
}
@Override
public String toString()
{
return mSelectedIndexSet.toString();
}
/**
* Serialize as {@link TreeSet}
* @return the {@link TreeSet} to serialize
*/
private Object writeReplace()
{
return new TreeSet<>(this);
}
}
}