ca.odell.glazedlists.SeparatorList Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* 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. * *
* *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. * *
Warning: 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. * *
Warning: This class is * thread ready but not thread safe. See {@link EventList} for an example * of thread safe code. * *
Developer Preview 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 Jesse Wilson */ public class SeparatorList
extends TransformedList { /** delegate to an inner class to insert the separators */ private SeparatorInjectorList 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 source
list by * using the givencomparator
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 *minimumSizeForSeparator
number of items (otherwise they are * left without a separator). In addition this SeparatorList will never * show more than thedefaultLimit
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(EventListsource, Comparator super E> comparator, int minimumSizeForSeparator, int defaultLimit) { super(new SeparatorInjectorList (new SortedList (source, comparator), defaultLimit)); this.separatorSource = (SeparatorInjectorList )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} */ @Override public int size() { return collapsedElements.colourSize(Barcode.BLACK); } /** {@inheritDoc} */ @Override protected int getSourceIndex(int mutationIndex) { return collapsedElements.getIndex(mutationIndex, Barcode.BLACK); } /** {@inheritDoc} */ @Override protected boolean isWritable() { return true; } /** * Set the {@link Comparator} used to determine how elements are split * into groups. * * Performance Note: sorting will take
O(N * Log N)
time. * *Warning: This method is * thread ready but not thread safe. See {@link EventList} for an example * of thread safe code. */ public void setComparator(Comparator
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 sortedList = (SortedList )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 true
if the next group was found, orfalse
* 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} */ @Override public void listChanged(ListEventlistChanges) { 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 .GroupSeparator> i = new SimpleTreeIterator .GroupSeparator>(separatorSource.separators); i.hasNext(); ) { i.next(); Element .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(); } } /** {@inheritDoc} */ @Override public void dispose() { // dispose internal SeparatorInjectorList and SortedList separatorSource.dispose(); separatorSource.source.dispose(); super.dispose(); } /** * A separator heading the elements of a group. */ public interface Separator { /** * 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. * * 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. * *
This method requires the read lock of the {@link SeparatorList} * to be held during invocation. */ public List
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 extends TransformedList { /** the grouping service manages finding where to insert groups */ private final Grouper grouper; /** * The separators list is black for separators, white for * everything else. * * The following demonstrates the layout of the barcode for the * given source list: *
* INDICES 0 1 2 * 012345678901234567890 * SOURCE LIST AAAABBBCCCDEFF * GROUPER BARCODE X___X__X__XXX_ * SEPARATOR BARCODE X____X___X___X_X_X__ *
To read this structure: *
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 SimpleTreesource
. 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