ca.odell.glazedlists.ListSelection 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;
// core glazed lists
import ca.odell.glazedlists.event.*;
// access to the volatile implementation classes
import ca.odell.glazedlists.impl.adt.*;
// to store event info for forwarding on the deselected EventList
import java.util.*;
/**
* An {@link EventList} to provide index-based selection features. This
* {@link EventList} does not perform a transformation on the source, but
* instead provides two additional {@link EventList}s:
*
* - {@link #getSelected() Selected} - an {@link EventList} that contains only the selected values.
* - {@link #getDeselected() Deselected} - an {@link EventList} that contains only the deselected values.
*
*
* This design is intended to allow the sharing of selection logic between
* both of our supported GUI toolkits as well as being available for use in
* non-GUI applications and for index-based filtering.
*
*
Warning: This class is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*
* @author Kevin Maltby
*/
public class ListSelection {
/**
* A selection mode where at most one element may be selected at one time.
* For convenience, this value equals {@link javax.swing.ListSelectionModel#SINGLE_SELECTION}.
*/
public static final int SINGLE_SELECTION = 0;
/**
* A selection mode where at most one range of elements may be selected at one time.
* For convenience, this value equals {@link javax.swing.ListSelectionModel#SINGLE_INTERVAL_SELECTION}.
*/
public static final int SINGLE_INTERVAL_SELECTION = 1;
/**
* A selection mode where any element may be selected and elements added
* adjacent to selected elements are selected. For convenience, this value
* equals {@link javax.swing.ListSelectionModel#MULTIPLE_INTERVAL_SELECTION}.
*/
public static final int MULTIPLE_INTERVAL_SELECTION = 2;
/**
* A selection mode where any element may be selected and freshly added elements
* are always deselected. No equivalent policy exists in
* {@link javax.swing.ListSelectionModel}
*/
public static final int MULTIPLE_INTERVAL_SELECTION_DEFENSIVE = 103;
/** the source list */
private final EventList source;
/** the selected view */
private SelectedList selectedList;
/** the deselected view */
private DeselectedList deselectedList;
/** the toggling selected view */
private SelectionToggleList selectedToggleList;
/** the toggling deselected view */
private DeselectionToggleList deselectedToggleList;
/** observe the source list */
private final SourceListener sourceListener = new SourceListener();
/** the selection state */
private Barcode barcode = new Barcode();
/** the lead selection index */
private int leadSelectionIndex = -1;
/** the anchor selection index */
private int anchorSelectionIndex = -1;
/** the selection mode defines characteristics of the selection */
private int selectionMode = MULTIPLE_INTERVAL_SELECTION_DEFENSIVE;
/** the current selection colour */
private Object selected = Barcode.BLACK;
/** the current deselection colour */
private Object deselected = Barcode.WHITE;
/** the registered SelectionListeners */
private List selectionListeners = new ArrayList(1);
/**
* Creates a new ListSelection that listens to changes on the given source.
* When using this constructor, all elements are deselected by default.
*/
public ListSelection(EventList source) {
this.source = source;
barcode.add(0, deselected, source.size());
source.addListEventListener(sourceListener);
}
/**
* Creates a new ListSelection that listens to changes on the given source
* and initializes selection with the given array of indices.
*/
public ListSelection(EventList source, int[] initialSelection) {
this(source);
select(initialSelection);
}
/**
* Handle changes to the source list by adjusting our selection state and
* the contents of the selected and deselected lists.
*/
private class SourceListener implements ListEventListener {
/** {@inheritDoc} */
public void listChanged(ListEvent listChanges) {
// keep track of what used to be selected
int minSelectionIndexBefore = getMinSelectionIndex();
int maxSelectionIndexBefore = getMaxSelectionIndex();
// handle reordering events
if(listChanges.isReordering()) {
// prepare for the reordering event
beginSelected();
int[] sourceReorderMap = listChanges.getReorderMap();
int[] selectReorderMap = new int[barcode.colourSize(selected)];
int[] deselectReorderMap = new int[barcode.colourSize(deselected)];
// adjust the flaglist & construct a reorder map to propagate
Barcode previousBarcode = barcode;
barcode = new Barcode();
for(int c = 0; c < sourceReorderMap.length; c++) {
Object flag = previousBarcode.get(sourceReorderMap[c]);
boolean wasSelected = (flag != deselected);
barcode.add(c, flag, 1);
if(wasSelected) {
int previousIndex = previousBarcode.getColourIndex(sourceReorderMap[c], selected);
int currentIndex = barcode.getColourIndex(c, selected);
selectReorderMap[currentIndex] = previousIndex;
} else {
int previousIndex = previousBarcode.getColourIndex(sourceReorderMap[c], deselected);
int currentIndex = barcode.getColourIndex(c, deselected);
deselectReorderMap[currentIndex] = previousIndex;
}
}
// adjust other internal state
anchorSelectionIndex = -1;
leadSelectionIndex = -1;
// fire the reorder on the selected list
addSelectedReorder(selectReorderMap);
commitSelected();
// fire the reorder on the deselected list
beginDeselected();
addDeselectedReorder(deselectReorderMap);
commitDeselected();
// handle non-reordering events
} else {
// prepare a sequence of changes
beginAll();
// for all changes update the barcode
while(listChanges.next()) {
int index = listChanges.getIndex();
int changeType = listChanges.getType();
// learn about what it was
int previousSelectionIndex = barcode.getColourIndex(index, selected);
boolean previouslySelected = previousSelectionIndex != -1;
// when an element is deleted, blow it away
if(changeType == ListEvent.DELETE) {
// delete selected values
if(previouslySelected) {
barcode.remove(index, 1);
addSelectedChange(ListEvent.DELETE, previousSelectionIndex, previousSelectionIndex);
// delete deselected values
} else {
int deselectedIndex = barcode.getColourIndex(index, deselected);
addDeselectedChange(ListEvent.DELETE, deselectedIndex, deselectedIndex);
barcode.remove(index, 1);
}
// when an element is inserted, it is selected if its index was selected
} else if(changeType == ListEvent.INSERT) {
// when selected, decide based on selection mode
if(previouslySelected) {
// select the inserted for single interval and multiple interval selection
if(selectionMode == SINGLE_INTERVAL_SELECTION
|| selectionMode == MULTIPLE_INTERVAL_SELECTION) {
barcode.add(index, selected, 1);
addSelectedChange(ListEvent.INSERT, previousSelectionIndex, previousSelectionIndex);
// do not select the inserted for single selection and defensive selection
} else {
barcode.add(index, deselected, 1);
int deselectedIndex = barcode.getColourIndex(index, deselected);
addDeselectedChange(ListEvent.INSERT, deselectedIndex, deselectedIndex);
}
// add a deselected value
} else {
barcode.add(index, deselected, 1);
int deselectedIndex = barcode.getColourIndex(index, deselected);
addDeselectedChange(ListEvent.INSERT, deselectedIndex, deselectedIndex);
}
// when an element is changed, assume selection stays the same
} else if(changeType == ListEvent.UPDATE) {
// update a selected value
if(previouslySelected) {
addSelectedChange(ListEvent.UPDATE, previousSelectionIndex, previousSelectionIndex);
// update a deselected value
} else {
int deselectedIndex = barcode.getColourIndex(index, deselected);
addDeselectedChange(ListEvent.UPDATE, deselectedIndex, deselectedIndex);
}
}
// adjust other internal state
anchorSelectionIndex = adjustIndex(anchorSelectionIndex, changeType, index);
leadSelectionIndex = adjustIndex(leadSelectionIndex, changeType, index);
}
commitAll();
}
// notify listeners of selection change
if(minSelectionIndexBefore != -1 && maxSelectionIndexBefore != -1) {
int minSelectionIndexAfter = getMinSelectionIndex();
int maxSelectionIndexAfter = getMaxSelectionIndex();
int changeStart = minSelectionIndexBefore;
int changeFinish = maxSelectionIndexBefore;
if(minSelectionIndexAfter != -1 && minSelectionIndexAfter < changeStart) changeStart = minSelectionIndexAfter;
if(maxSelectionIndexAfter != -1 && maxSelectionIndexAfter > changeFinish) changeFinish = maxSelectionIndexAfter;
fireSelectionChanged(changeStart, changeFinish);
}
}
}
/**
* Adjusts the specified index to the specified change. This is used to adjust
* the anchor and lead selection indices when list changes occur.
*/
private int adjustIndex(int indexBefore, int changeType, int changeIndex) {
if(indexBefore == -1) return -1;
if(changeType == ListEvent.DELETE) {
if(changeIndex < indexBefore) return indexBefore-1;
else if(changeIndex == indexBefore) return -1;
else return indexBefore;
} else if(changeType == ListEvent.UPDATE) {
return indexBefore;
} else if(changeType == ListEvent.INSERT) {
if(changeIndex <= indexBefore) return indexBefore+1;
else return indexBefore;
} else {
throw new IllegalStateException();
}
}
/**
* Gets an {@link EventList} that contains only selected values and modifies
* the source list on mutation.
*
* Adding and removing items from this list performs the same operation on
* the source list.
*/
public EventList getSelected() {
source.getReadWriteLock().writeLock().lock();
try {
if(selectedList == null){
selectedList = new SelectedList(source);
source.getPublisher().setRelatedListener(selectedList, sourceListener);
}
return selectedList;
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* Gets an {@link EventList} that contains only selected values and modifies
* the selection state on mutation.
*
* Adding an item to this list selects it and removing an item deselects it.
* If an item not in the source list is added an
* {@link IllegalArgumentException} is thrown. This list does not support
* the {@link List#set set} method.
*/
public EventList getTogglingSelected() {
source.getReadWriteLock().writeLock().lock();
try {
if(selectedToggleList == null){
selectedToggleList = new SelectionToggleList(source);
source.getPublisher().setRelatedListener(selectedToggleList, sourceListener);
}
return selectedToggleList;
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* Gets an {@link EventList} that contains only deselected values add
* modifies the source list on mutation.
*
* Adding and removing items from this list performs the same operation on
* the source list.
*/
public EventList getDeselected() {
source.getReadWriteLock().writeLock().lock();
try {
if(deselectedList == null){
deselectedList = new DeselectedList(source);
source.getPublisher().setRelatedListener(deselectedList, sourceListener);
}
return deselectedList;
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* Gets an {@link EventList} that contains only deselected values and
* modifies the selection state on mutation.
*
* Adding an item to this list deselects it and removing an item selects it.
* If an item not in the source list is added an
* {@link IllegalArgumentException} is thrown. This list does not support
* the {@link List#set set} method.
*/
public EventList getTogglingDeselected() {
source.getReadWriteLock().writeLock().lock();
try {
if(deselectedToggleList == null) {
deselectedToggleList = new DeselectionToggleList(source);
source.getPublisher().setRelatedListener(deselectedToggleList, sourceListener);
}
return deselectedToggleList;
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* Get the {@link EventList} that selection is being managed for.
*/
public EventList getSource() {
return source;
}
/**
* Inverts the current selection.
*/
public void invertSelection() {
// Switch what colour is considered to be selected
if(selected == Barcode.BLACK) {
selected = Barcode.WHITE;
deselected = Barcode.BLACK;
} else {
selected = Barcode.BLACK;
deselected = Barcode.WHITE;
}
// Clear the anchor and lead
anchorSelectionIndex = -1;
leadSelectionIndex = -1;
// Update the selected list to reflect the selection inversion
beginAll();
addSelectedChange(ListEvent.DELETE, 0, barcode.colourSize(deselected) - 1);
addSelectedChange(ListEvent.INSERT, 0, barcode.colourSize(selected) - 1);
// Update the deselected list to reflect the selection inversion
addDeselectedChange(ListEvent.DELETE, 0, barcode.colourSize(selected) - 1);
addDeselectedChange(ListEvent.INSERT, 0, barcode.colourSize(deselected) - 1);
commitAll();
// notify selection listeners that selection has been inverted
fireSelectionChanged(0, source.size() - 1);
}
/**
* Returns whether or not the item with the given source index
* is selected.
*/
public boolean isSelected(int sourceIndex) {
if(sourceIndex < 0 || sourceIndex >= source.size()) {
return false;
}
return barcode.getColourIndex(sourceIndex, selected) != -1;
}
/**
* Deselects the element at the given index.
*/
public void deselect(int index) {
deselect(index, index);
}
/**
* Deselects all of the elements within the given range.
*/
public void deselect(int start, int end) {
// fast fail if the range is invalid
if(start == -1 || end == -1) {
return;
// use single value deselection in single selection mode
} else if(selectionMode == SINGLE_SELECTION) {
int selectedIndex = getMaxSelectionIndex();
// only change selection if you have to
if(selectedIndex >= start && selectedIndex <= end) {
deselectAll();
}
return;
// adjust the range to prevent the creation of multiple intervals
} else if(selectionMode == SINGLE_INTERVAL_SELECTION && start > getMinSelectionIndex()) {
end = Math.max(end, getMaxSelectionIndex());
}
// update anchor and lead
anchorSelectionIndex = start;
leadSelectionIndex = end;
// alter selection accordingly
setSubRangeOfRange(false, start, end, -1, -1);
}
/**
* Deselects all of the elements in the given array of indices. The
* array must contain indices in sorted, ascending order.
*/
public void deselect(int[] indices) {
// keep track of the range of values that were affected
int firstAffectedIndex = -1;
int lastAffectedIndex = -1;
// iterate through the barcode updating the selected list as you go
beginAll();
int currentIndex = 0;
for(BarcodeIterator i = barcode.iterator(); i.hasNext() && currentIndex != indices.length;) {
Object value = i.next();
if(i.getIndex() == indices[currentIndex]) {
// selection changed
if(value == selected) {
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addDeselectEvent(i);
}
currentIndex++;
}
}
commitAll();
// notify listeners of selection change
if(firstAffectedIndex > -1) fireSelectionChanged(firstAffectedIndex, lastAffectedIndex);
}
/**
* Deselects all elements.
*/
public void deselectAll() {
// keep track of how many selected elements there were
int selectionChangeSize = barcode.colourSize(selected);
// fast fail if there is no change to make
if(selectionChangeSize == 0) return;
// keep track of the range of values that were affected
int firstAffectedIndex = -1;
int lastAffectedIndex = -1;
// update the deselected list while processing the change
beginDeselected();
for(BarcodeIterator i = barcode.iterator(); i.hasNextColour(selected);) {
i.nextColour(selected);
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addDeselectedChange(ListEvent.INSERT, lastAffectedIndex, lastAffectedIndex);
}
// bulk update the barcode to be entirely deselected
barcode.clear();
barcode.add(0, deselected, source.size());
commitDeselected();
beginSelected();
addSelectedChange(ListEvent.DELETE, 0, selectionChangeSize - 1);
commitSelected();
// notify listeners of selection change
fireSelectionChanged(firstAffectedIndex, lastAffectedIndex);
}
/**
* Selects the element at the given index.
*/
public void select(int index) {
select(index, index);
}
/**
* Selects all of the elements within the given range.
*/
public void select(int start, int end) {
// fast fail if the range is an no-op
if(start == -1 || end == -1) {
return;
// use single value deselection in single selection mode
} else if(selectionMode == SINGLE_SELECTION) {
setSelection(start);
return;
// adjust the range to prevent the creation of multiple intervals
} else if(selectionMode == SINGLE_INTERVAL_SELECTION) {
// test if the current and new selection overlap
boolean overlap = false;
int minSelectedIndex = getMinSelectionIndex();
int maxSelectedIndex = getMaxSelectionIndex();
if(minSelectedIndex - 1 <= start && start <= maxSelectedIndex + 1) overlap = true;
if(minSelectedIndex - 1 <= end && end <= maxSelectedIndex + 1) overlap = true;
if(!overlap) {
setSelection(start, end);
return;
}
}
// update anchor and lead
anchorSelectionIndex = start;
leadSelectionIndex = end;
// alter selection accordingly
setSubRangeOfRange(true, start, end, -1, -1);
}
/**
* Selects all of the elements in the given array of indices. The
* array must contain indices in sorted, ascending order.
*/
public void select(int[] indices) {
// keep track of the range of values that were affected
int firstAffectedIndex = -1;
int lastAffectedIndex = -1;
// iterate through the barcode updating the selected list as you go
beginAll();
int currentIndex = 0;
for(BarcodeIterator i = barcode.iterator(); i.hasNext() && currentIndex != indices.length;) {
Object value = i.next();
if(i.getIndex() == indices[currentIndex]) {
// selection changed
if(value != selected) {
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addSelectEvent(i);
}
currentIndex++;
}
}
commitAll();
// notify listeners of selection change
if(firstAffectedIndex > -1) fireSelectionChanged(firstAffectedIndex, lastAffectedIndex);
}
private void addSelectEvent(BarcodeIterator i) {
int deselectedIndex = i.getColourIndex(deselected);
addSelectEvent(i.set(selected), deselectedIndex);
}
private void addSelectEvent(int selectIndex, int deselectIndex){
addDeselectedChange(ListEvent.DELETE, deselectIndex, deselectIndex);
addSelectedChange(ListEvent.INSERT, selectIndex, selectIndex);
}
/**
* Select the specified element, if it exists.
*
* @return the index of the newly selected element, or -1 if no
* element was found.
*/
public int select(E value) {
int index = source.indexOf(value);
if(index != -1) select(index);
return index;
}
/**
* Select all of the specified values.
*
* @return true
if the selection changed as a result of the call.
*/
public boolean select(Collection values) {
// This implementation leaves a lot to be desired. It's inefficient and
// awkward. If possible, we should clean up the entire SelectionList so
// we don't need to worry as much about the deselection list.
// 1. Convert our Collection of values into a SortedSet of indices
SortedSet indicesToSelect = new TreeSet();
for(Iterator v = values.iterator(); v.hasNext(); ) {
E value = v.next();
int index = source.indexOf(value);
if(index == -1) continue;
indicesToSelect.add(new Integer(index));
}
if(indicesToSelect.isEmpty()) return false;
// 2. convert the sorted set of Integers into an int[]
int[] indicesToSelectAsInts = new int[indicesToSelect.size()];
int arrayIndex = 0;
for(Iterator i = indicesToSelect.iterator(); i.hasNext(); ) {
Integer selectIndex = i.next();
indicesToSelectAsInts[arrayIndex] = selectIndex.intValue();
arrayIndex++;
}
// 3. Delegate to the other method, and return true if the selection grew
int selectionSizeBefore = getSelected().size();
select(indicesToSelectAsInts);
int selectionSizeAfter = getSelected().size();
return selectionSizeAfter > selectionSizeBefore;
}
/**
* Selects all elements.
*/
public void selectAll() {
// keep track of how many deselected elements there were
int deselectionChangeSize = barcode.colourSize(deselected);
// fast fail if there is nothing to change
if(deselectionChangeSize == 0) return;
// keep track of the range of values that were affected
int firstAffectedIndex = -1;
int lastAffectedIndex = -1;
// update the selected list while processing the change
beginSelected();
for(BarcodeIterator i = barcode.iterator(); i.hasNextColour(deselected);) {
i.nextColour(deselected);
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addSelectedChange(ListEvent.INSERT, lastAffectedIndex, lastAffectedIndex);
}
// bulk update the barcode to be entirely selected
barcode.clear();
barcode.add(0, selected, source.size());
commitSelected();
// update the deselected list
beginDeselected();
addDeselectedChange(ListEvent.DELETE, 0, deselectionChangeSize - 1);
commitDeselected();
// notify listeners of selection change
fireSelectionChanged(firstAffectedIndex, lastAffectedIndex);
}
/**
* Sets the selection to be only the element at the given index. If the
* given index is -1, the selection will be cleared.
*/
public void setSelection(int index) {
setSelection(index, index);
}
/**
* Sets the selection to be only elements within the given range. If the
* endpoints of the range are -1, the selection will be cleared.
*/
public void setSelection(int start, int end) {
// a range including -1 implies a deselectAll()
if(start == -1 || end == -1) {
deselectAll();
return;
} else if(selectionMode == SINGLE_SELECTION) {
end = start;
}
// update anchor and lead
anchorSelectionIndex = start;
leadSelectionIndex = end;
// alter selection accordingly
setSubRangeOfRange(true, start, end, getMinSelectionIndex(), getMaxSelectionIndex());
}
/**
* Sets the selection to be only the element in the given array of indices.
* Unlike {@link #setSelection(int)} and {@link #setSelection(int,int)},
* providing a value of -1 is an error. The array must contain indices in
* sorted, ascending order.
*/
public void setSelection(int[] indices) {
// fast fail is the selection is empty
if(indices.length == 0) {
deselectAll();
return;
}
// keep track of the range of values that were affected
int firstAffectedIndex = -1;
int lastAffectedIndex = -1;
// iterate through the barcode updating the selected list as you go
beginAll();
int currentIndex = 0;
for(BarcodeIterator i = barcode.iterator(); i.hasNext();) {
Object value = i.next();
// this element should be selected
if(i.getIndex() == indices[currentIndex]) {
// selection changed
if(value != selected) {
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addSelectEvent(i);
}
// look at the next value
if(currentIndex < indices.length - 1) currentIndex++;
// element was selected and isn't within the new selection
} else if(value == selected) {
if(firstAffectedIndex == -1) firstAffectedIndex = i.getIndex();
lastAffectedIndex = i.getIndex();
addDeselectEvent(i);
}
}
commitAll();
// notify listeners of selection change
if(firstAffectedIndex > -1) fireSelectionChanged(firstAffectedIndex, lastAffectedIndex);
}
/**
* Return the anchor of the current selection.
*/
public int getAnchorSelectionIndex() {
return anchorSelectionIndex;
}
/**
* Set the anchor selection index.
*/
public void setAnchorSelectionIndex(int anchorSelectionIndex) {
// update anchor
this.anchorSelectionIndex = anchorSelectionIndex;
// a value of -1 clears selection
if(anchorSelectionIndex == -1 || leadSelectionIndex == -1) {
deselectAll();
// only the anchor should be selected
} else if(selectionMode == SINGLE_SELECTION) {
setSubRangeOfRange(true, anchorSelectionIndex, anchorSelectionIndex, getMinSelectionIndex(), getMaxSelectionIndex());
//setSelection(anchorSelectionIndex);
// select the interval between anchor and lead
} else if(selectionMode == SINGLE_INTERVAL_SELECTION) {
setSubRangeOfRange(true, anchorSelectionIndex, leadSelectionIndex, getMinSelectionIndex(), getMaxSelectionIndex());
//setSelection(anchorSelectionIndex, leadSelectionIndex);
// select the interval between anchor and lead without deselecting anything
} else {
setSubRangeOfRange(true, anchorSelectionIndex, leadSelectionIndex, -1, -1);
//select(anchorSelectionIndex, leadSelectionIndex);
}
}
/**
* Return the lead of the current selection.
*/
public int getLeadSelectionIndex() {
return leadSelectionIndex;
}
/**
* Set the lead selection index.
*/
public void setLeadSelectionIndex(int leadSelectionIndex) {
// update lead
int originalLeadIndex = this.leadSelectionIndex;
this.leadSelectionIndex = leadSelectionIndex;
// a value of -1 clears selection
if(leadSelectionIndex == -1 || anchorSelectionIndex == -1) {
deselectAll();
// select only the lead
} else if(selectionMode == SINGLE_SELECTION) {
setSubRangeOfRange(true, leadSelectionIndex, leadSelectionIndex, getMinSelectionIndex(), getMaxSelectionIndex());
//setSelection(leadSelectionIndex);
// select the interval between anchor and lead
} else if(selectionMode == SINGLE_INTERVAL_SELECTION) {
setSubRangeOfRange(true, anchorSelectionIndex, leadSelectionIndex, getMinSelectionIndex(), getMaxSelectionIndex());
//setSelection(anchorSelectionIndex, leadSelectionIndex);
// select the interval between anchor and lead deselecting as necessary
} else {
setSubRangeOfRange(true, anchorSelectionIndex, leadSelectionIndex, anchorSelectionIndex, originalLeadIndex);
}
}
private void addSelectedReorder(int[] selectReorderMap) {
if(selectedList != null) selectedList.updates().reorder(selectReorderMap);
if(selectedToggleList != null) selectedToggleList.updates().reorder(selectReorderMap);
}
private void addDeselectedReorder(int[] deselectReorderMap) {
if(deselectedList != null) deselectedList.updates().reorder(deselectReorderMap);
if(deselectedToggleList != null) deselectedToggleList.updates().reorder(deselectReorderMap);
}
private void addDeselectEvent(BarcodeIterator i) {
addDeselectEvent(i.getColourIndex(selected), i.set(deselected));
}
private void addDeselectEvent(int selectIndex, int deselectIndex) {
addSelectedChange(ListEvent.DELETE, selectIndex, selectIndex);
addDeselectedChange(ListEvent.INSERT, deselectIndex, deselectIndex);
}
private void addSelectedChange(int type, int start, int end){
if(selectedList != null) selectedList.updates().addChange(type, start, end);
if(selectedToggleList != null) selectedToggleList.updates().addChange(type, start, end);
}
private void addDeselectedChange(int type, int start, int end){
if(deselectedList != null) deselectedList.updates().addChange(type, start, end);
if(deselectedToggleList != null) deselectedToggleList.updates().addChange(type, start, end);
}
private void beginAll() {
beginSelected();
beginDeselected();
}
private void commitAll() {
commitSelected();
commitDeselected();
}
private void beginSelected() {
if(selectedList != null) {
selectedList.updates().beginEvent();
}
if(selectedToggleList != null) {
selectedToggleList.updates().beginEvent();
}
}
private void commitSelected() {
if(selectedList != null) {
selectedList.updates().commitEvent();
}
if(selectedToggleList != null) {
selectedToggleList.updates().commitEvent();
}
}
private void beginDeselected() {
if(deselectedList != null) deselectedList.updates().beginEvent();
if(deselectedToggleList != null) deselectedToggleList.updates().beginEvent();
}
private void commitDeselected() {
if(deselectedList != null) deselectedList.updates().commitEvent();
if(deselectedToggleList != null) deselectedToggleList.updates().commitEvent();
}
/**
* Set the selection mode.
*/
public void setSelectionMode(int selectionMode) {
this.selectionMode = selectionMode;
setSelection(getMinSelectionIndex(), getMaxSelectionIndex());
}
/**
* Returns the current selection mode.
*/
public int getSelectionMode() {
return selectionMode;
}
/**
* Returns the first selected index or -1 if nothing is selected.
*/
public int getMinSelectionIndex() {
if(barcode.colourSize(selected) == 0) return -1;
return barcode.getIndex(0, selected);
}
/**
* Returns the last selected index or -1 if nothing is selected.
*/
public int getMaxSelectionIndex() {
if(barcode.colourSize(selected) == 0) return -1;
return barcode.getIndex(barcode.colourSize(selected) - 1, selected);
}
/**
* Walks through the union of the specified ranges. All values in the
* first range are given the specified selection value. All values in
* the second range but not in the first range are given the opposite
* selection value. In effect, the first range and the intersection of
* the two ranges are given the specified value. The parts that are
* in the second range but not in the first range (ie. everything else)
* is given the opposite selection value.
*
* @param select true to set values in the first range as selected and
* the other values in the second range as not selected. false to
* set values in the first range as not selected and the other
* values in the second range as selected.
* @param changeIndex0 one end of the first range, inclusive.
* @param changeIndex1 the opposite end of the first range, inclusive.
* This may be lower than changeIndex0.
* @param invertIndex0 one end of the second range, inclusive. To
* specify an empty second range, specify -1 for this value.
* @param invertIndex1 the opposite end of the second range, inclusive.
* This may be lower than invertIndex0. To specify an empty second
* range, specify -1 for this value.
*/
private void setSubRangeOfRange(boolean select, int changeIndex0, int changeIndex1, int invertIndex0, int invertIndex1) {
// verify that the first range is legitimate
if(changeIndex0 >= source.size() || changeIndex1 >= source.size()
|| ((changeIndex0 == -1 || changeIndex1 == -1) && changeIndex0 != changeIndex1)) {
throw new IndexOutOfBoundsException("Invalid range for selection: " + changeIndex0 + "-" + changeIndex1 + ", list size is " + source.size());
}
// verify that the second range is legitimate
if(invertIndex0 >= source.size() || invertIndex1 >= source.size()
|| ((invertIndex0 == -1 || invertIndex1 == -1) && invertIndex0 != invertIndex1)) {
throw new IndexOutOfBoundsException("Invalid range for invert selection: " + invertIndex0 + "-" + invertIndex1 + ", list size is " + source.size());
}
// when the first range is empty
if(changeIndex0 == -1 && changeIndex1 == -1) {
// if the second range is empty, we're done
if(invertIndex0 == -1 && invertIndex1 == -1) return;
// otherwise set the first range to the second range and invert the goal
changeIndex0 = invertIndex0;
changeIndex1 = invertIndex1;
select = !select;
}
// when the second range is empty
if(invertIndex0 == -1 && invertIndex1 == -1) {
// make this a subset of the first index which behaves the same as empty
invertIndex0 = changeIndex0;
invertIndex1 = changeIndex1;
}
// now get the change interval and invert interval
int minChangeIndex = Math.min(changeIndex0, changeIndex1);
int maxChangeIndex = Math.max(changeIndex0, changeIndex1);
int minInvertIndex = Math.min(invertIndex0, invertIndex1);
int maxInvertIndex = Math.max(invertIndex0, invertIndex1);
// get the union of the two ranges
int minUnionIndex = Math.min(minChangeIndex, minInvertIndex);
int maxUnionIndex = Math.max(maxChangeIndex, maxInvertIndex);
int minChangedIndex = maxUnionIndex + 1;
int maxChangedIndex = minUnionIndex - 1;
beginAll();
// walk through the affect range updating selection
for(int i = minUnionIndex; i <= maxUnionIndex; i++) {
int selectionIndex = barcode.getColourIndex(i, selected);
boolean selectedBefore = (selectionIndex != -1);
boolean inChangeRange = (i >= minChangeIndex && i <= maxChangeIndex);
boolean selectedAfter = (inChangeRange == select);
// when there's a change
if(selectedBefore != selectedAfter) {
// update change range
if(i < minChangedIndex) minChangedIndex = i;
if(i > maxChangedIndex) maxChangedIndex = i;
// it is being deselected
if(selectedBefore) {
barcode.set(i, deselected, 1);
addDeselectEvent(selectionIndex, i - selectionIndex);
// it is being selected
} else {
barcode.set(i, selected, 1);
int newSelectionIndex = barcode.getColourIndex(i, selected);
addSelectEvent(newSelectionIndex, i - newSelectionIndex);
}
}
}
commitAll();
// notify selection listeners
if(minChangedIndex <= maxChangedIndex) fireSelectionChanged(minChangedIndex, maxChangedIndex);
}
/**
* Register a {@link ca.odell.glazedlists.ListSelection.Listener Listener}
* that will be notified when selection is changed.
*/
public void addSelectionListener(Listener selectionListener) {
selectionListeners.add(selectionListener);
}
/**
* Remove a {@link ca.odell.glazedlists.ListSelection.Listener Listener}
* so that it will no longer be notified when selection changes.
*/
public void removeSelectionListener(Listener selectionListener) {
selectionListeners.remove(selectionListener);
}
/**
* Fire changes in selection to all registered listeners.
*/
private void fireSelectionChanged(int start, int end) {
// notify all
for(Iterator i = selectionListeners.iterator(); i.hasNext(); ) {
Listener listener = i.next();
listener.selectionChanged(start, end);
}
}
/**
* Disposes of this ListSelection freeing up it's resources for
* garbage collection. It is an error to use a ListSelection after
* dispose() has been called.
*/
public void dispose() {
source.removeListEventListener(sourceListener);
selectionListeners.clear();
// detach the publisher dependencies
if(selectedList != null) source.getPublisher().clearRelatedListener(selectedList, sourceListener);
if(deselectedList != null) source.getPublisher().clearRelatedListener(deselectedList, sourceListener);
if(selectedToggleList != null) source.getPublisher().clearRelatedListener(selectedToggleList, sourceListener);
if(deselectedToggleList != null) source.getPublisher().clearRelatedListener(deselectedToggleList, sourceListener);
}
/**
* A generic interface to respond to changes in selection that doesn't
* require including a particular GUI toolkit.
*/
public interface Listener {
/**
* Notifies this SelectionListener of a change in selection.
*
* @param changeStart The first zero-relative index affected by a change in selection.
* @param changeEnd The last zero-relative index affected by a change in selection.
*/
public void selectionChanged(int changeStart, int changeEnd);
}
/**
* The {@link EventList} that contains only values that are currently
* selected.
*/
private class SelectedList extends TransformedList {
/**
* Creates an {@link EventList} that provides a view of the
* selected items in a ListSelection.
*/
SelectedList(EventList source) {
super(source);
}
/** {@inheritDoc} */
public int size() {
return barcode.colourSize(selected);
}
/** {@inheritDoc} */
protected int getSourceIndex(int mutationIndex) {
return barcode.getIndex(mutationIndex, selected);
}
/** {@inheritDoc} */
public void listChanged(ListEvent listChanges) {
// Do nothing as all state changes are handled in ListSelection.listChanged()
}
/**
* This allows access to the EventAssembler for this list.
*/
public ListEventAssembler updates() {
return updates;
}
/** {@inheritDoc} */
protected boolean isWritable() {
return true;
}
/**
* A no-op dispose method to prevent the user from shooting themselves
* in the foot. To dispose a {@link ListSelection}, call
* {@link ListSelection#dispose()} on that class directly.
*/
public void dispose() {
// Do Nothing
}
}
/**
* A SelectedList that mutates the selection instead of the underlying list.
*/
private class SelectionToggleList extends SelectedList{
SelectionToggleList(EventList source) {
super(source);
}
/** @throws UnsupportedOperationException unconditionally */
public E set(int index, E item){
throw new UnsupportedOperationException("Toggling lists don't support setting items");
}
/**
* Select the specified value in the source list, regardless of its
* index. If the given item is found in the source list, it is selected.
*
* @throws IllegalArgumentException if the element isn't found
*/
public void add(int index, E item) {
index = source.indexOf(item);
if(index != -1) {
select(index);
} else {
throw new IllegalArgumentException("Added item " + item + " must be in source list");
}
}
/**
* Deselect the specified index.
*/
public E remove(int index){
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot remove at " + index + " on list of size " + size());
int sourceIndex = getSourceIndex(index);
deselect(sourceIndex);
return source.get(sourceIndex);
}
}
/**
* The {@link EventList} that contains only values that are not currently
* selected.
*/
private class DeselectedList extends TransformedList {
/**
* Creates an {@link EventList} that provides a view of the
* deselected items in a ListSelection.
*/
DeselectedList(EventList source) {
super(source);
}
/** {@inheritDoc} */
public int size() {
return barcode.colourSize(deselected);
}
/** {@inheritDoc} */
protected int getSourceIndex(int mutationIndex) {
return barcode.getIndex(mutationIndex, deselected);
}
/** {@inheritDoc} */
public void listChanged(ListEvent listChanges) {
// Do nothing as all state changes are handled in ListSelection.listChanged()
}
/**
* This allows access to the EventAssembler for this list.
*/
public ListEventAssembler updates() {
return updates;
}
/** {@inheritDoc} */
protected boolean isWritable() {
return true;
}
/**
* A no-op dispose method to prevent the user from shooting themselves
* in the foot. To dispose a {@link ListSelection}, call
* {@link ListSelection#dispose()} on that class directly.
*/
public void dispose() {
// Do Nothing
}
}
/**
* A DeselectedList that mutates the selection instead of the underlying list.
*/
private class DeselectionToggleList extends DeselectedList{
DeselectionToggleList(EventList source) {
super(source);
}
/** @throws UnsupportedOperationException unconditionally */
public E set(int index, E item){
throw new UnsupportedOperationException("Toggling lists don't support setting items");
}
/**
* Deselect the specified value.
*
* @throws IllegalArgumentException if the element isn't found
*/
public void add(int index, E item) {
index = source.indexOf(item);
if(index != -1) {
deselect(index);
} else {
throw new IllegalArgumentException("Added item " + item + " must be in source list");
}
}
/**
* Select the specified index.
*/
public E remove(int index){
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot remove at " + index + " on list of size " + size());
int sourceIndex = getSourceIndex(index);
select(sourceIndex);
return source.get(sourceIndex);
}
}
}