ca.odell.glazedlists.CollectionList Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glazedlists_java15 Show documentation
Show all versions of glazedlists_java15 Show documentation
Event-driven lists for dynamically filtered and sorted tables
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;
import java.util.*;
// the core Glazed Lists package
import ca.odell.glazedlists.event.*;
// volatile implementation support
import ca.odell.glazedlists.impl.adt.*;
import ca.odell.glazedlists.impl.adt.barcode2.SimpleTree;
import ca.odell.glazedlists.impl.adt.barcode2.Element;
import ca.odell.glazedlists.impl.adt.barcode2.SimpleTreeIterator;
/**
* A list that acts like a tree in that it contains child elements to nodes contained in
* another list. An example usage would be to wrap a parent list containing record albums
* and use the CollectionList to display the songs on the album.
*
* The actual mapping from the parent list to the child list (record to songs in the
* above example) is done by a {@link CollectionList.Model} that is provided to the
* constructor.
*
*
* EventList Overview
* Writable: only {@link #set(int,Object)} and {@link #remove(int)}
* Concurrency: thread ready, not thread safe
* Performance: reads: O(log N), writes O(log N)
* Memory: 96 bytes per element
* Unit Tests: N/A
* Issues:
* 96
* 162
* 257
* 265
*
*
*
* @see CollectionList.Model
*
* @author Rob Eden
* @author Jesse Wilson
*/
public class CollectionList extends TransformedList implements ListEventListener {
/** This is a hack - we need a temporary value when inserting into IndexedTrees, and this is the one we use. */
private final ChildElement EMPTY_CHILD_ELEMENT = new SimpleChildElement(Collections.EMPTY_LIST, null);
/** used to extract children */
private final Model model;
/**
* Barcode containing the node mappings. There is a black node for each parent
* followed by a white node for each of its children.
*/
private final Barcode barcode = new Barcode();
/** the Lists and EventLists that this is composed of */
private final SimpleTree> childElements = new SimpleTree>();
/**
* Create a {@link CollectionList} with its contents being the children of
* the elements in the specified source {@link EventList}.
*/
public CollectionList(EventList source, Model model) {
super(source);
if(model == null) throw new IllegalArgumentException("Collection map cannot be null");
this.model = model;
// Sync the current size and indexes
for(int i = 0; i < source.size(); i++) {
List children = model.getChildren(source.get(i));
// update the list of child lists
Element> node = childElements.add(i, EMPTY_CHILD_ELEMENT, 1);
node.set(createChildElementForList(children, node));
// update the barcode
barcode.addBlack(barcode.size(), 1);
if(!children.isEmpty()) barcode.addWhite(barcode.size(), children.size());
}
// Listen for events
source.addListEventListener(this);
}
/** {@inheritDoc} */
public int size() {
// Size of the child nodes only
return barcode.whiteSize();
}
/** {@inheritDoc} */
public E get(int index) {
// get the child
ChildElement childElement = getChildElement(index);
int childIndexInParent = barcode.getWhiteSequenceIndex(index);
return childElement.get(childIndexInParent);
}
/** {@inheritDoc} */
public E set(int index, E value) {
// set on the child
ChildElement childElement = getChildElement(index);
int childIndexInParent = barcode.getWhiteSequenceIndex(index);
return childElement.set(childIndexInParent, value);
}
/** {@inheritDoc} */
public E remove(int index) {
// remove from the child
ChildElement childElement = getChildElement(index);
int childIndexInParent = barcode.getWhiteSequenceIndex(index);
return childElement.remove(childIndexInParent);
}
/**
* Return the index of the first child in the CollectionList for the given parent
* index. This can be very useful for things like selecting the children in a
* CollectionList when the parent is selected in another list.
*
* @see #childEndingIndex
*/
public int childStartingIndex(int parentIndex) {
if(parentIndex < 0) throw new IndexOutOfBoundsException("Invalid index: " + parentIndex);
if(parentIndex >= source.size()) throw new IndexOutOfBoundsException("Invalid index: " + parentIndex);
// Get the index of the next node
// Find the index of the black node with that index
int parentFullIndex = barcode.getIndex(parentIndex, Barcode.BLACK);
int childFullIndex = parentFullIndex + 1;
// If this node has no children, the next node index will be past the size or black
if(childFullIndex >= barcode.size()) return -1;
if(barcode.get(childFullIndex) != Barcode.WHITE) return -1;
// return the child index
int childIndex = childFullIndex - (parentIndex+1);
assert(barcode.getWhiteIndex(childFullIndex) == childIndex);
return childIndex;
}
/**
* Return the index of the last child in the CollectionList for the given parent
* index. This can be very useful for things like selecting the children in a
* CollectionList when the parent is selected in another list.
*
* @see #childStartingIndex
*/
public int childEndingIndex(int parentIndex) {
if(parentIndex < 0) throw new IndexOutOfBoundsException("Invalid index: " + parentIndex);
if(parentIndex >= source.size()) throw new IndexOutOfBoundsException("Invalid index: " + parentIndex);
// Get the index of the next node
// Find the index of the black node with that index
int nextParentFullIndex = (parentIndex == barcode.blackSize() - 1) ? barcode.size() : barcode.getIndex(parentIndex + 1, Barcode.BLACK);
int lastWhiteBeforeNextParent = nextParentFullIndex - 1;
// If this node has no children, the next node index will be past the size or black
if(barcode.get(lastWhiteBeforeNextParent) == Barcode.BLACK) return -1;
// return the child index
int childIndex = lastWhiteBeforeNextParent - (parentIndex+1);
assert(barcode.getWhiteIndex(lastWhiteBeforeNextParent) == childIndex);
return childIndex;
}
/**
* Handle changes in the parent list. We'll need to update our node list sizes.
*/
public void listChanged(ListEvent listChanges) {
// Need to process the changes so that our size caches are up to date.
updates.beginEvent();
while(listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
// Insert means we'll need to insert a new node in the array
if(type == ListEvent.INSERT) {
handleInsert(index);
} else if(type == ListEvent.DELETE) {
handleDelete(index);
// Treat like a delete and then an add:
} else if(type == ListEvent.UPDATE) {
handleDelete(index);
handleInsert(index);
}
}
updates.commitEvent();
}
/** @inheritDoc */
public void dispose() {
super.dispose();
// iterate over all child elements and dispose them
final SimpleTreeIterator> treeIterator =
new SimpleTreeIterator>(childElements);
while(treeIterator.hasNext()) {
treeIterator.next();
treeIterator.value().dispose();
}
}
/**
* Helper for {@link #listChanged(ListEvent)} when inserting.
*/
private void handleInsert(int parentIndex) {
// Find the index of the black node with that index
int absoluteIndex = getAbsoluteIndex(parentIndex);
// Find the size of the new node and add it to the total
S parent = source.get(parentIndex);
List children = model.getChildren(parent);
// update the list of child lists
Element> node = childElements.add(parentIndex, EMPTY_CHILD_ELEMENT, 1);
node.set(createChildElementForList(children, node));
// update the barcode
barcode.addBlack(absoluteIndex, 1);
if(!children.isEmpty()) barcode.addWhite(absoluteIndex + 1, children.size());
// add events
if(!children.isEmpty()) {
int childIndex = absoluteIndex - parentIndex;
updates.addInsert(childIndex, childIndex + children.size() - 1);
}
}
/**
* Helper for {@link #listChanged(ListEvent)} when deleting.
*/
private void handleDelete(int sourceIndex) {
// Find the index of the black node with that index
int parentIndex = getAbsoluteIndex(sourceIndex);
int nextParentIndex = getAbsoluteIndex(sourceIndex + 1);
// update the list of child lists
Element> removedChildElement = childElements.get(sourceIndex);
childElements.remove(removedChildElement);
removedChildElement.get().dispose();
// update the barcode
int childCount = nextParentIndex - parentIndex - 1; // subtract one for the parent
barcode.remove(parentIndex, 1 + childCount); // delete the parent and all children
// fire events
if(childCount > 0) {
int firstDeletedChildIndex = parentIndex - sourceIndex;
int firstNotDeletedChildIndex = firstDeletedChildIndex + childCount;
updates.addDelete(firstDeletedChildIndex, firstNotDeletedChildIndex - 1); // inclusive ranges
}
}
/**
* Get the child element for the specified child index.
*/
private ChildElement getChildElement(int childIndex) {
if(childIndex < 0) throw new IndexOutOfBoundsException("Invalid index: " + childIndex);
if(childIndex >= size()) throw new IndexOutOfBoundsException("Index: " + childIndex + ", Size: " + size());
// get the child element
int parentIndex = barcode.getBlackBeforeWhite(childIndex);
return childElements.get(parentIndex).get();
}
/**
* Create a {@link ChildElement} for the specified List.
*/
private ChildElement createChildElementForList(List children, Element> node) {
if(children instanceof EventList) return new EventChildElement((EventList)children, node);
else return new SimpleChildElement(children, node);
}
/**
* Get the absolute index for the specified parent index. This may be virtual
* if the parent index is one greater than the last element. This is useful
* for calculating the size of a range by using the location of its follower.
*/
private int getAbsoluteIndex(int parentIndex) {
if(parentIndex < barcode.blackSize()) {
return barcode.getIndex(parentIndex, Barcode.BLACK);
} else if(parentIndex == barcode.blackSize()) {
return barcode.size();
} else {
throw new IndexOutOfBoundsException();
}
}
/**
* Models a list held by the CollectionList.
*/
private interface ChildElement {
public E get(int index);
public E remove(int index);
public E set(int index, E element);
public void dispose();
}
/**
* Provides the logic to map a parent record (e.g., a records album) to its children
* (e.g., the songs on the record). Serves basically the same purpose as
* {@link javax.swing.tree.TreeModel} does to a JTree in Swing.
*
* @see CollectionList
* @see GlazedLists#listCollectionListModel()
*/
public interface Model {
/**
* Return a list of the child nodes for a parent node.
*
* @param parent The parent node.
* @return A List containing the child nodes.
*/
public List getChildren(E parent);
}
/**
* Manages a standard List that does not implement {@link EventList}.
*/
private class SimpleChildElement implements ChildElement {
private List children;
private Element node;
public SimpleChildElement(List children, Element node) {
this.children = children;
this.node = node;
}
public E get(int index) {
return children.get(index);
}
public E remove(int index) {
E removed = children.remove(index);
// update the barcode
int parentIndex = childElements.indexOfNode(node, (byte)0);
int absoluteIndex = getAbsoluteIndex(parentIndex);
int firstChildIndex = absoluteIndex + 1;
barcode.remove(firstChildIndex + index, 1);
// forward the offset event
int childOffset = absoluteIndex - parentIndex;
updates.beginEvent();
updates.addDelete(index + childOffset);
updates.commitEvent();
// all done
return removed;
}
public E set(int index, E element) {
E replaced = children.set(index, element);
// forward the offset event
int parentIndex = childElements.indexOfNode(node, (byte)0);
int absoluteIndex = getAbsoluteIndex(parentIndex);
int childOffset = absoluteIndex - parentIndex;
updates.beginEvent();
updates.addUpdate(index + childOffset);
updates.commitEvent();
// all done
return replaced;
}
public void dispose() {
// do nothing
}
}
/**
* Monitors changes to a member EventList and forwards changes to all listeners
* of the CollectionList.
*/
private class EventChildElement implements ChildElement, ListEventListener {
private EventList children;
private Element> node;
public EventChildElement(EventList children, Element> node) {
this.children = children;
this.node = node;
children.getPublisher().setRelatedSubject(this, CollectionList.this);
children.addListEventListener(this);
}
public E get(int index) {
return children.get(index);
}
public E remove(int index) {
// events will be fired from this call
return children.remove(index);
}
public E set(int index, E element) {
// events will be fired from this call
return children.set(index, element);
}
public void listChanged(ListEvent listChanges) {
int parentIndex = childElements.indexOfNode(node, (byte)1);
int absoluteIndex = getAbsoluteIndex(parentIndex);
int nextNodeIndex = getAbsoluteIndex(parentIndex+1);
// update the barcode
int firstChildIndex = absoluteIndex + 1;
int previousChildrenCount = nextNodeIndex - firstChildIndex;
if(previousChildrenCount > 0) barcode.remove(firstChildIndex, previousChildrenCount);
if(!children.isEmpty()) barcode.addWhite(firstChildIndex, children.size());
// get the offset of this child list
int childOffset = absoluteIndex - parentIndex;
// forward the offset event
updates.beginEvent();
while(listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
updates.addChange(type, index + childOffset);
}
updates.commitEvent();
}
public void dispose() {
children.removeListEventListener(this);
children.getPublisher().clearRelatedSubject(this);
}
public String toString() {
return "[" + childElements.indexOfNode(node, (byte)0) + ":" + children + "]";
}
}
}