All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.cuf.model.MultiSelectionInList Maven / Gradle / Ivy

The newest version!
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 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 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 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 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 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 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> 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> 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 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 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 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 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 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 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 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 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 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 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); } } }