/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.impl.Grouper;
import ca.odell.glazedlists.impl.adt.Barcode;
import ca.odell.glazedlists.impl.adt.BarcodeIterator;
import ca.odell.glazedlists.impl.adt.barcode2.Element;
import ca.odell.glazedlists.impl.adt.barcode2.SimpleTree;
import ca.odell.glazedlists.impl.adt.barcode2.SimpleTreeIterator;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* A list that adds separator objects before each group of elements.
*
* <p>SeparatorList is writable, however, attempts to write over separators
* will always produce an {@link IllegalArgumentException}. For example,
* calling {@link #add(int, Object)}, {@link #set(int, Object)} or
* {@link #remove(int)} with a an index that actually corresponds to a
* separator in this list will produce an {@link IllegalArgumentException}.
* This is because there is no corresponding index for separators in the source
* list; separators are added by this SeparatorList. All index-based write
* operations must be performed on indexes known to correspond to non-separator
* elements.
*
* <p><strong>Warning:</strong> this class won't work very well with generics
* because separators are mixed in, which will be a different class than the
* other list elements.
*
* <p><strong><font color="#FF0000">Warning:</font></strong> This class is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*
* <p><strong>Developer Preview</strong> this class is still under heavy development
* and subject to API changes. It's also really slow at the moment and won't scale
* to lists of size larger than a hundred or so efficiently.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class SeparatorList<E> extends TransformedList<E, E> {
/** delegate to an inner class to insert the separators */
private SeparatorInjectorList<E> separatorSource;
private static final Object SEPARATOR = Barcode.BLACK;
private static final Object SOURCE_ELEMENT = Barcode.WHITE;
/** how many elements before we get a separator, such as 1 or 2 */
private final int minimumSizeForSeparator;
/** manage collapsed elements */
private Barcode collapsedElements;
/**
* Construct a SeparatorList overtop of the <code>source</code> list by
* using the given <code>comparator</code> to compute groups of similar
* source items. For each group a single separator will be present in this
* SeparatorList provided the group contains at least the
* <code>minimumSizeForSeparator</code> number of items (otherwise they are
* left without a separator). In addition this SeparatorList will never
* show more than the <code>defaultLimit</code> number of group elements
* from any given group.
*
* @param source the list containing the raw items to be grouped
* @param comparator the Comparator which defines the grouping logic
* @param minimumSizeForSeparator the number of elements which must exist
* in a group in order for a separator to be created
* @param defaultLimit the maximum number of element to display for a group;
* extra elements are truncated
*/
public SeparatorList(EventList<E> source, Comparator<? super E> comparator, int minimumSizeForSeparator, int defaultLimit) {
super(new SeparatorInjectorList<E>(new SortedList<E>(source, comparator), defaultLimit));
this.separatorSource = (SeparatorInjectorList<E>)super.source;
this.minimumSizeForSeparator = minimumSizeForSeparator;
// prepare the collapsed elements
rebuildCollapsedElements();
// handle changes to the separators list
this.separatorSource.addListEventListener(this);
}
/**
* Rebuild the entire collapsed elements barcode.
*/
private void rebuildCollapsedElements() {
collapsedElements = new Barcode();
collapsedElements.addBlack(0, separatorSource.size());
int groupCount = separatorSource.insertedSeparators.colourSize(SEPARATOR);
for(int i = 0; i < groupCount; i++) {
updateGroup(i, groupCount, false);
}
}
/** {@inheritDoc} */
public int size() {
return collapsedElements.colourSize(Barcode.BLACK);
}
/** {@inheritDoc} */
protected int getSourceIndex(int mutationIndex) {
return collapsedElements.getIndex(mutationIndex, Barcode.BLACK);
}
/** {@inheritDoc} */
protected boolean isWritable() {
return true;
}
/**
* Set the {@link Comparator} used to determine how elements are split
* into groups.
*
* <p>Performance Note: sorting will take <code>O(N * Log N)</code> time.
*
* <p><strong><font color="#FF0000">Warning:</font></strong> This method is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*/
public void setComparator(Comparator<E> comparator) {
final boolean isEmpty = isEmpty();
if (!isEmpty) {
// this implementation loses selection, but that's the best we can do
// with the current limitations of the Glazed Lists ListEventAssembler.
// What we really need here is the ability to fire an event that contains
// both reordering and structure change information.
updates.beginEvent();
// remove all
updates.addDelete(0, size() - 1);
}
// make the change to the sorted source, the grouper will respond but
// the {@link SeparatorInjectorList} doesn't fire any events forward when
// its main Comparator is changed
SortedList<E> sortedList = (SortedList<E>)separatorSource.source;
sortedList.setComparator(comparator);
if (!isEmpty) {
// rebuild which elements are collapsed out
rebuildCollapsedElements();
// insert all again
updates.addInsert(0, size() - 1);
updates.commitEvent();
}
}
/**
* Go from the current group (assumed to be black) to the next black group
* to follow. This works by finding a white follower, then a black follower
* of that one.
*
* @return <code>true</code> if the next group was found, or <code>false</code>
* if there was no such group and the iterator is now in an unspecified
* location, not necessarily the end of the barcode.
*/
private static boolean nextBlackGroup(BarcodeIterator iterator) {
// step to an intermediate white group
if(!iterator.hasNextWhite()) return false;
iterator.nextWhite();
// then to the following black group to get a completely different group
if(!iterator.hasNextBlack()) return false;
iterator.nextBlack();
// success!
return true;
}
/** {@inheritDoc} */
public void listChanged(ListEvent<E> listChanges) {
updates.beginEvent(true);
// when the source changes order, forward a reordering if no elements
// go from being outside the limit filter to inside it
if(listChanges.isReordering()) {
boolean canReorder = true;
for(SimpleTreeIterator<SeparatorInjectorList<E>.GroupSeparator> i = new SimpleTreeIterator<SeparatorInjectorList<E>.GroupSeparator>(separatorSource.separators); i.hasNext(); ) {
i.next();
Element<SeparatorInjectorList<E>.GroupSeparator> node = i.node();
int limit = node.get().getLimit();
if(limit == 0) continue;
if(limit >= separatorSource.size()) continue;
if(limit >= node.get().size()) continue;
canReorder = false;
break;
}
// forward the reorder event, this requires a lot of rework because
// we're mapping backwards and fowards unnecessarily
if(canReorder) {
int[] previousIndices = listChanges.getReorderMap();
int[] reorderMap = new int[collapsedElements.colourSize(Barcode.BLACK)];
// walk through the unfiltered elements, adjusting the indices
// for each group of unfiltered (black) elements
BarcodeIterator i = collapsedElements.iterator();
int groupStartSourceIndex = 0;
while(true) {
// we already know where this group starts, now we calculate
// where it ends, and how many indices it's offset by in the view
boolean newGroupFound;
int groupEndSourceIndex;
int leadingCollapsedElements;
if(i.hasNextWhite()) {
i.nextWhite();
groupEndSourceIndex = i.getIndex();
newGroupFound = true;
leadingCollapsedElements = i.getWhiteIndex();
} else {
newGroupFound = false;
groupEndSourceIndex = collapsedElements.size();
leadingCollapsedElements = collapsedElements.whiteSize();
}
// update the reorder map for each element in this group
for(int j = groupStartSourceIndex; j < groupEndSourceIndex; j++) {
reorderMap[j - leadingCollapsedElements] = previousIndices[j] - leadingCollapsedElements;
}
// prepare the next iteration: find the start of the next group
if(newGroupFound && i.hasNextBlack()) {
i.nextBlack();
groupStartSourceIndex = i.getIndex();
} else {
break;
}
}
updates.reorder(reorderMap);
// fire insert/delete pairs. This loses selection because currently
// Glazed Lists lacks the ability to fire a mix of move and insert/update
// events
} else {
int size = collapsedElements.colourSize(Barcode.BLACK);
if(size > 0) {
updates.addDelete(0, size - 1);
updates.addInsert(0, size - 1);
}
}
// handle other changes by adjusting the limits as necessary
} else {
// keep this around, it's handy
int groupCount = separatorSource.insertedSeparators.colourSize(SEPARATOR);
// first update the barcode, optimistically
while(listChanges.next()) {
int changeIndex = listChanges.getIndex();
int changeType = listChanges.getType();
// if we're inserting something new, always fire an insert event,
// even if we need to revoke it later
if(changeType == ListEvent.INSERT) {
collapsedElements.add(changeIndex, Barcode.BLACK, 1);
int viewIndex = collapsedElements.getColourIndex(changeIndex, Barcode.BLACK);
updates.addInsert(viewIndex);
// updates are probably already accurate, don't change the state
} else if(changeType == ListEvent.UPDATE) {
// if its visible, fire an update event
if(collapsedElements.get(changeIndex) == Barcode.BLACK) {
int viewIndex = collapsedElements.getColourIndex(changeIndex, Barcode.BLACK);
updates.addUpdate(viewIndex);
}
// fire a delete event if this is a visible element being deleted
} else if(changeType == ListEvent.DELETE) {
Object oldColor = collapsedElements.get(changeIndex);
if(oldColor == Barcode.BLACK) {
int viewIndex = collapsedElements.getColourIndex(changeIndex, Barcode.BLACK);
updates.addDelete(viewIndex);
}
collapsedElements.remove(changeIndex, 1);
}
}
// Now make sure our limits are correct, which they may not be
// due to the fact that we made a lot of guesses in the first pass.
// Note that this is really slow and needs some work for performance
// reasons
listChanges.reset();
while(listChanges.next()) {
int changeIndex = listChanges.getIndex();
int changeType = listChanges.getType();
if(changeType == ListEvent.INSERT) {
int group = separatorSource.insertedSeparators.getColourIndex(changeIndex, true, SEPARATOR);
updateGroup(group, groupCount, true);
} else if(changeType == ListEvent.UPDATE) {
int group = separatorSource.insertedSeparators.getColourIndex(changeIndex, true, SEPARATOR);
// it's possible that this impacts the previous group!
if(group > 0) updateGroup(group - 1, groupCount, true);
updateGroup(group, groupCount, true);
// it's possible that this impacts the next group
if(group < groupCount - 1) updateGroup(group + 1, groupCount, true);
} else if(changeType == ListEvent.DELETE) {
// if there is a group that this came from, update it
if(changeIndex < separatorSource.insertedSeparators.size()) {
int group = separatorSource.insertedSeparators.getColourIndex(changeIndex, true, SEPARATOR);
updateGroup(group, groupCount, true);
}
}
}
}
updates.commitEvent();
}
/**
* Update all elements in the specified group. We need to refine this method
* since currently it does a linear scan through the group's elements, and
* that just won't do for performance requirements.
*/
private void updateGroup(int group, int groupCount, boolean fireEvents) {
Separator separator = separatorSource.separators.get(group).get();
int limit = separator.getLimit();
// fix up this separator
int separatorStart = separatorSource.insertedSeparators.getIndex(group, SEPARATOR);
int nextGroup = group + 1;
int separatorEnd = nextGroup == groupCount ? separatorSource.insertedSeparators.size() : separatorSource.insertedSeparators.getIndex(nextGroup, SEPARATOR);
int size = separatorEnd - separatorStart - 1;
// if this is too small to show a separator
if(size < minimumSizeForSeparator) {
// remove the separator
setVisible(separatorStart, Barcode.WHITE, fireEvents);
// everything else must be visible
for(int i = separatorStart + 1; i < separatorEnd; i++) {
setVisible(i, Barcode.BLACK, fireEvents);
}
// if this is different than the limit
} else {
// show the separator
setVisible(separatorStart, Barcode.BLACK, fireEvents);
// show everything up to the limit and nothing after
for(int i = separatorStart + 1; i < separatorEnd; i++) {
boolean withinLimit = i - separatorStart <= limit;
setVisible(i, withinLimit ? Barcode.BLACK : Barcode.WHITE, fireEvents);
}
}
}
/**
* Update the visible state of the specified element.
*/
private void setVisible(int index, Object colour, boolean fireEvents) {
Object previousColour = collapsedElements.get(index);
// no change
if(colour == previousColour) {
return;
// hide this element
} else if(colour == Barcode.WHITE) {
int viewIndex = collapsedElements.getColourIndex(index, Barcode.BLACK);
if(fireEvents) updates.addDelete(viewIndex);
collapsedElements.set(index, Barcode.WHITE, 1);
// show this element
} else if(colour == Barcode.BLACK) {
collapsedElements.set(index, Barcode.BLACK, 1);
int viewIndex = collapsedElements.getColourIndex(index, Barcode.BLACK);
if(fireEvents) updates.addInsert(viewIndex);
} else {
throw new IllegalArgumentException();
}
}
/**
* A separator heading the elements of a group.
*/
public interface Separator<E> {
/**
* Get the maximum number of elements in this group to show.
*/
public int getLimit();
/**
* Set the maximum number of elements in this group to show. This is
* useful to collapse a group (limit of 0), cap the elements of a group
* (limit of 5) or reverse those actions.
*
* <p>This method requires the write lock of the {@link SeparatorList} to be
* held during invocation.
*/
public void setLimit(int limit);
/**
* Get the {@link List} of all elements in this group.
*
* <p>This method requires the read lock of the {@link SeparatorList}
* to be held during invocation.
*/
public List<E> getGroup();
/**
* A convenience method to get the first element from this group. This
* is useful to render the separator's name.
*/
public E first();
/**
* A convenience method to get the number of elements in this group. This
* is useful to render the separator.
*/
public int size();
}
/**
* This inner class handles the insertion of separators
* as a separate transformation from the hiding of separators
* and collapsed elements.
*/
static class SeparatorInjectorList<E> extends TransformedList<E, E> {
/** the grouping service manages finding where to insert groups */
private final Grouper<E> grouper;
/**
* The separators list is black for separators, white for
* everything else.
*
* <p>The following demonstrates the layout of the barcode for the
* given source list:
* <pre><code>
* INDICES 0 1 2
* 012345678901234567890
* SOURCE LIST AAAABBBCCCDEFF
* GROUPER BARCODE X___X__X__XXX_
* SEPARATOR BARCODE X____X___X___X_X_X__
* </pre></code>
*
* <p>To read this structure:
* <li>the grouper barcode is an "X" for the first element in each
* group (called uniques), and an "_" for the following
* elements (called duplicates).
* <li>the separator barcode is very similar to the grouper barcode.
* In this barcode, there is an "X" for each separator and an "_"
* for each element in the source list. We use the structure of the
* grouper barcode to derive and maintain the separator barcode.
*
* <p>When accessing elements, the separator barcode is queried. If it
* holds an "X", the element is a separator and that separator is returned.
* Otherwise if it is an "_", the corresponding source index is obtained
* (by removing the number of preceding "X" elements) and the element is
* retrieved from the source list.
*/
private Barcode insertedSeparators;
/** a list of {@link Separator}s, one for each separator in the list */
private SimpleTree<GroupSeparator> separators;
/** the number of elements to show in each group, such as 0, 5, or {@link Integer#MAX_VALUE} */
private int defaultLimit;
/**
* Create a new {@link SeparatorInjectorList} that groups together like
* items using the Comparator from the <code>source</code>. Elements
* that the {@link Comparator} determines are equal will share a common
* separator.
*
* @param source the sorted list of items with a Comparator that defines
* the group boundaries
* @param defaultLimit the maximum number of items to include in a
* group; all remaining items will be truncated
*/
public SeparatorInjectorList(SortedList<E> source, int defaultLimit) {
super(source);
this.defaultLimit = defaultLimit;
// prepare the groups
GrouperClient grouperClient = new GrouperClient();
this.grouper = new Grouper<E>(source, grouperClient);
// initialize separators state
rebuildSeparators();
// handle changes via the grouper
source.addListEventListener(this);
}
/**
* Statically build the separators data structures.
*/
private void rebuildSeparators() {
// clear the initial state of these separators
insertedSeparators = new Barcode();
separators = new SimpleTree<GroupSeparator>();
// prepare the separator list
insertedSeparators.add(0, SOURCE_ELEMENT, source.size());
for(BarcodeIterator i = grouper.getBarcode().iterator(); i.hasNextColour(Grouper.UNIQUE); ) {
i.nextColour(Grouper.UNIQUE);
int groupIndex = i.getColourIndex(Grouper.UNIQUE);
int sourceIndex = i.getIndex();
insertedSeparators.add(groupIndex + sourceIndex, SEPARATOR, 1);
Element<GroupSeparator> node = separators.add(groupIndex, new GroupSeparator(), 1);
node.get().setNode(node);
node.get().setLimit(defaultLimit);
}
// update the cached values in all separators
for(int i = 0; i < separators.size(); i++) {
separators.get(i).get().updateCachedValues();
}
}
/** {@inheritDoc} */
public E get(int index) {
Object type = insertedSeparators.get(index);
if(type == SEPARATOR) return (E)separators.get(getSeparatorIndex(index)).get();
else if(type == SOURCE_ELEMENT) return source.get(getSourceIndex(index));
else throw new IllegalStateException();
}
/** {@inheritDoc} */
protected int getSourceIndex(int mutationIndex) {
Object type = insertedSeparators.get(mutationIndex);
if(type == SEPARATOR) throw new IllegalArgumentException("No source index exists for the separator located at index " + mutationIndex);
else if(type == SOURCE_ELEMENT) return insertedSeparators.getColourIndex(mutationIndex, SOURCE_ELEMENT);
else throw new IllegalStateException();
}
protected int getSeparatorIndex(int mutationIndex) {
Object type = insertedSeparators.get(mutationIndex);
if(type == SEPARATOR) return insertedSeparators.getColourIndex(mutationIndex, SEPARATOR);
else if(type == SOURCE_ELEMENT) return -1;
else throw new IllegalStateException();
}
/** {@inheritDoc} */
protected boolean isWritable() {
return true;
}
/** {@inheritDoc} */
public int size() {
return insertedSeparators.size();
}
/** {@inheritDoc} */
public void listChanged(ListEvent<E> listChanges) {
// when the separator comparator is changed in the source list, let
// the grouper know so we can rebuild our groups, then bail since
// the {@link SeparatorList} already knows about this event
SortedList<E> sortedSource = (SortedList<E>) source;
Comparator<? super E> sourceComparator = sortedSource.getComparator();
if(sourceComparator != grouper.getComparator()) {
grouper.setComparator(sourceComparator);
rebuildSeparators();
return;
}
updates.beginEvent(true);
// reorderings should be contained within the existing groups, we
// need to send these reorderings forward
if(listChanges.isReordering()) {
int[] previousIndices = listChanges.getReorderMap();
int[] reorderMap = new int[insertedSeparators.size()];
// walk through each group, adjusting indices in the forward
// reorder map to notify listeners
int groupStartIndex = -1; // inclusive
int groupEndIndex = 0; // exclusive
int group = -1;
for(int i = 0; i < previousIndices.length; i++) {
// if this is the start of a new group, add that to the reorder map
if(i == groupEndIndex) {
group++;
reorderMap[i + group] = i + group;
groupStartIndex = groupEndIndex;
int nextGroup = group + 1;
groupEndIndex = nextGroup < separators.size() ? separators.get(nextGroup).get().start() : insertedSeparators.size();
}
// make sure the move doesn't leave the group
int previousIndex = previousIndices[i];
if(previousIndex < groupStartIndex || previousIndex >= groupEndIndex) {
throw new IllegalStateException();
}
// adjust this change within the current group
reorderMap[i + group + 1] = previousIndex + group + 1;
}
updates.reorder(reorderMap);
// handle regular changes by adjusting the separators via our grouper
} else {
grouper.listChanged(listChanges);
}
// update the cached values in all separators
for(int i = 0; i < separators.size(); i++) {
separators.get(i).get().updateCachedValues();
}
updates.commitEvent();
}
/**
* Fire two events, one for the group (the separator) and another for the
* actual list element.
*/
private class GrouperClient implements Grouper.Client<E> {
public void groupChanged(int index, int groupIndex, int groupChangeType, boolean primary, int elementChangeType, E oldValue, E newValue) {
boolean fixSeparatorForInsertGroupUpdateElement = false;
// handle the group change first
if(groupChangeType == ListEvent.INSERT) {
int expandedIndex = index + groupIndex;
insertedSeparators.add(expandedIndex, SEPARATOR, 1);
updates.addInsert(expandedIndex);
// add the separator and link the separator to its node
Element<GroupSeparator> node = separators.add(groupIndex, new GroupSeparator(), 1);
node.get().setNode(node);
node.get().setLimit(defaultLimit);
} else if(groupChangeType == ListEvent.UPDATE) {
int expandedIndex = insertedSeparators.getIndex(groupIndex, SEPARATOR);
updates.addUpdate(expandedIndex);
} else if(groupChangeType == ListEvent.DELETE) {
int expandedIndex = insertedSeparators.getIndex(groupIndex, SEPARATOR);
insertedSeparators.remove(expandedIndex, 1);
updates.addDelete(expandedIndex);
// invalidate the node
Element<GroupSeparator> node = separators.get(groupIndex);
separators.remove(node);
node.get().setNode(null);
node.get().updateCachedValues();
groupIndex--;
}
// then handle the element change
if(elementChangeType == ListEvent.INSERT) {
int expandedIndex = index + groupIndex + 1;
insertedSeparators.add(expandedIndex, SOURCE_ELEMENT, 1);
updates.addInsert(expandedIndex);
} else if(elementChangeType == ListEvent.UPDATE) {
int expandedIndex = index + groupIndex + 1;
// if we inserted a separator directly before an existing separator,
// we must increase update index for element by one
if (groupChangeType == ListEvent.INSERT) {
int separatorCount = insertedSeparators.colourSize(SEPARATOR);
if (groupIndex + 1 < separatorCount) {
// separator at groupIndex is not the last one...
int nextSeparatorsIndex = insertedSeparators.getIndex(groupIndex + 1, SEPARATOR);
if (nextSeparatorsIndex == expandedIndex) {
// separator at groupIndex + 1 is located at the element update position,
// so increment update position by one...
expandedIndex++;
// ...and fix location of separator at groupIndex + 1
fixSeparatorForInsertGroupUpdateElement = true;
}
}
}
updates.addUpdate(expandedIndex);
} else if(elementChangeType == ListEvent.DELETE) {
int expandedIndex = index + groupIndex + 1;
insertedSeparators.remove(expandedIndex, 1);
updates.addDelete(expandedIndex);
}
if (fixSeparatorForInsertGroupUpdateElement) {
// fix special case:
// the location of separator at groupIndex + 1 must be increased by one
int wrongSeparatorIndex = index + groupIndex + 1;
assert wrongSeparatorIndex == insertedSeparators.getIndex(groupIndex + 1, SEPARATOR);
insertedSeparators.remove(wrongSeparatorIndex, 1);
updates.addDelete(wrongSeparatorIndex);
insertedSeparators.add(wrongSeparatorIndex + 1, SEPARATOR, 1);
updates.addInsert(wrongSeparatorIndex + 1);
}
// Special case out the shift operation. The Grouper automatically
// handles shifts, but SeparatorList needs to manage them independently.
// Here we respond to the grouper and the separators barcode getting
// out of sync, and resolve that problem.
// If this fix poses a problem, we might want to change the way
// Grouper works to fire a special flag called 'shift' with the
// value true whenever the group joined is a RIGHT_GROUP
int shiftGroupIndex = groupIndex + 1;
if(groupChangeType == ListEvent.DELETE && elementChangeType != ListEvent.INSERT
&& shiftGroupIndex < insertedSeparators.colourSize(SEPARATOR)
&& shiftGroupIndex < grouper.getBarcode().colourSize(Grouper.UNIQUE)) {
int collapsedGroupStartIndex = grouper.getBarcode().getIndex(shiftGroupIndex, Grouper.UNIQUE);
int separatorsIndex = insertedSeparators.getIndex(shiftGroupIndex , SEPARATOR);
//String was = insertedSeparators.toString();
if(collapsedGroupStartIndex + shiftGroupIndex < separatorsIndex) {
insertedSeparators.remove(separatorsIndex, 1);
updates.addDelete(separatorsIndex);
insertedSeparators.add(collapsedGroupStartIndex + shiftGroupIndex, SEPARATOR, 1);
updates.addInsert(collapsedGroupStartIndex + shiftGroupIndex);
//String now = insertedSeparators.toString();
//System.out.println("Changed from " + was + " to " + now);
}
}
}
}
/**
* Implement the {@link Separator} interface in the most natural way.
*/
class GroupSeparator implements Separator<E> {
private int limit = Integer.MAX_VALUE;
private int size;
private E first;
/**
* The node allows the separator to figure out which
* group in the overall list its representing.
*/
private Element<GroupSeparator> node = null;
/** {@inheritDoc} */
public int getLimit() {
return limit;
}
/** {@inheritDoc} */
public void setLimit(int limit) {
if(this.limit == limit) return;
// fail gracefully if the node is null, that means this separator
// has been removed from the list but its still visible to an editor
if(node == null) {
return;
}
this.limit = limit;
// notify the world of this separator change
updates.beginEvent();
int groupIndex = separators.indexOfNode(node, (byte)1);
int separatorIndex = insertedSeparators.getIndex(groupIndex, SEPARATOR);
updates.addUpdate(separatorIndex);
updates.commitEvent();
}
/** {@inheritDoc} */
public List<E> getGroup() {
if(node == null) return Collections.EMPTY_LIST;
return source.subList(start(), end());
}
/** {@inheritDoc} */
public E first() {
return first;
}
/** {@inheritDoc} */
public int size() {
return size;
}
/**
* Set the {@link Element} that this {@link Separator} can
* use to find its index in the overall list of {@link Separator}s;
*/
public void setNode(Element<GroupSeparator> node) {
this.node = node;
}
/**
* The first index in the source containing an element from this group.
*/
private int start() {
if(this.node == null) throw new IllegalStateException();
int separatorIndex = separators.indexOfNode(node, (byte)1);
if(separatorIndex == -1) throw new IllegalStateException();
int groupStartIndex = insertedSeparators.getIndex(separatorIndex, SEPARATOR);
return groupStartIndex - separatorIndex;
}
/**
* The last index in the source containing an element from this group.
*/
private int end() {
if(this.node == null) throw new IllegalStateException();
int nextSeparatorIndex = separators.indexOfNode(node, (byte)1) + 1;
if(nextSeparatorIndex == 0) throw new IllegalStateException();
int nextGroupStartIndex = nextSeparatorIndex == insertedSeparators.colourSize(SEPARATOR) ? insertedSeparators.size() : insertedSeparators.getIndex(nextSeparatorIndex, SEPARATOR);
return nextGroupStartIndex - nextSeparatorIndex;
}
/**
* Update the cached {@link #first()} and {@link #size()} values, so that they
* can be retrieved without the {@link SeparatorList}'s lock.
*/
public void updateCachedValues() {
if(node != null) {
int start = start();
int end = end();
this.first = source.get(start);
this.size = end - start;
} else {
this.first = null;
this.size = 0;
}
}
/** {@inheritDoc} */
public String toString() {
return "" + size() + " elements starting with \"" + first() + "\"";
}
}
}
}