Skip to content

Commit

Permalink
Concat lists (#6)
Browse files Browse the repository at this point in the history
* Add EasyBind.concat(List<ObservableList<T>>)

Creates a new list that combines the values of the given
lists. Unlike FXCollections.concat(), updates to the source
lists propagate to the combined list.

* Fix compiler errors

* add some tests for concatenating lists

* Fix bugs in the ConcatList implementation

* Duplicated List values will break the implementation's change events
* Added better tests that test different combinations.

This commit closes comments in #8.

* Add .idea to .gitignore, clean up test case

* Added support for an "ObservableList" input

* Adds an ObservableList input
* Tracks changes to the List, firing changes if the 2D list changes,
  this eases up the work when bigger changes happen
* Change `concat` -> `flattenList`, it's a flatten operation after all

* Rename flatten to concat

Co-authored-by: maul.esel <maul.esel@go4more.de>
Co-authored-by: Kevin Brightwell <kevin.brightwell2@gmail.com>
  • Loading branch information
3 people committed May 5, 2020
1 parent d0a2d99 commit 01ece20
Show file tree
Hide file tree
Showing 3 changed files with 401 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/main/java/org/fxmisc/easybind/EasyBind.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

Expand Down Expand Up @@ -116,6 +117,17 @@ public static <T, U> ObservableList<U> map(
return new MappedList<>(sourceList, f);
}

public static <T> ObservableList<T> flatten(
ObservableList<ObservableList<? extends T>> sources) {
return new FlattenedList<>(sources);
}

@SafeVarargs
public static <T> ObservableList<T> concat(
ObservableList<? extends T>... sources) {
return new FlattenedList<>(FXCollections.observableArrayList(sources));
}

/**
* Creates a new list in which each element is converted using the provided mapping.
* All changes to the underlying list are propagated to the converted list.
Expand Down
181 changes: 181 additions & 0 deletions src/main/java/org/fxmisc/easybind/FlattenedList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.fxmisc.easybind;

import java.util.*;
import java.util.function.Consumer;

import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableListBase;

class FlattenedList<E> extends ObservableListBase<E> {
private final ObservableList<ObservableList<? extends E>> sourceLists;

FlattenedList(ObservableList<ObservableList<? extends E>> sourceLists) {
if (sourceLists == null) {
throw new NullPointerException("sourceLists = null");
}

this.sourceLists = sourceLists;

// We make a Unique set of source lists, otherwise the event gets called multiple
// times if there are duplicate lists.
Set<ObservableList<? extends E>> sourcesSet = new HashSet<>(sourceLists);
sourcesSet.forEach(source -> source.addListener(this::onSourceChanged));

sourceLists.addListener(this::onSourcesListChanged);
}

private void onSourcesListChanged(ListChangeListener.Change<? extends ObservableList<? extends E>> change) {

beginChange();

while (change.next()) {
int fromIdx = 0; // Flattened start idx
for (int i = 0; i < change.getFrom(); ++i) {
fromIdx += sourceLists.get(i).size();
}

int toIdx = fromIdx; // Flattened end idx
for (int i = change.getFrom(); i < change.getTo(); ++i) {
toIdx += sourceLists.get(i).size();
}

final int rangeSize = toIdx - fromIdx;

if (change.wasPermutated()) {

// build up a set of permutations based on the offsets AND the actual permutation
int[] permutation = new int[rangeSize];
int fIdx = fromIdx;
for (int parentIdx = change.getFrom(); parentIdx < change.getTo(); ++parentIdx) {
for (int i = 0; i < sourceLists.get(i).size(); ++i, fIdx++) {
permutation[fIdx] = change.getPermutation(parentIdx) + i;
}
}

nextPermutation(fromIdx, toIdx, permutation);
} else if (change.wasUpdated()) {
// Just iterate over the fromIdx..toIdx
for (int i = fromIdx; i < toIdx; ++i) {
nextUpdate(i);
}
} else if (change.wasAdded()) {
nextAdd(fromIdx, toIdx);
} else {
// Each remove is indexed
List<E> itemsToRemove = new ArrayList<>(rangeSize);

change.getRemoved().forEach(itemsToRemove::addAll);

nextRemove(fromIdx, itemsToRemove);
}
}

endChange();
}

private void onSourceChanged(ListChangeListener.Change<? extends E> change) {
ObservableList<? extends E> source = change.getList();

List<Integer> offsets = new ArrayList<>();
int calcOffset = 0;
for (ObservableList<? extends E> currList : sourceLists) {
if (currList == source) {
offsets.add(calcOffset);
}

calcOffset += currList.size();
}

// Because a List could be duplicated, we have to do the change for EVERY offset.
// Annoying, but it's needed.

beginChange();
while (change.next()) {
if (change.wasPermutated()) {
int rangeSize = change.getTo() - change.getFrom();

// build up a set of permutations based on the offsets AND the actual permutation
int[] permutation = new int[rangeSize * offsets.size()];
for (int offsetIdx = 0; offsetIdx < offsets.size(); ++offsetIdx) {
int indexOffset = offsets.get(offsetIdx);
for (int i = 0; i < rangeSize; ++i) {
permutation[i + offsetIdx * rangeSize] =
change.getPermutation(i + change.getFrom()) + indexOffset;
}
}

for (int indexOffset: offsets) {
nextPermutation(change.getFrom() + indexOffset, change.getTo() + indexOffset, permutation);
}
} else if (change.wasUpdated()) {

// For each update, it's just the index from getFrom()..getTo() + indexOffset
for (int indexOffset: offsets) {
for (int i = change.getFrom(); i < change.getTo(); ++i) {
nextUpdate(i + indexOffset);
}
}
} else if (change.wasAdded()) {

// Each Add is just from() + the offset
for (int indexOffset: offsets) {
nextAdd(change.getFrom() + indexOffset, change.getTo() + indexOffset);
}

} else {
// Each remove is indexed
for (int indexOffset: offsets) {
nextRemove(change.getFrom() + indexOffset, change.getRemoved());
}
}
}
endChange();
}

@Override
public E get(int index) {
if (index < 0)
throw new IndexOutOfBoundsException("List index must be >= 0. Was " + index);

for (ObservableList<? extends E> source : sourceLists) {
if (index < source.size())
return source.get(index);
index -= source.size();
}
throw new IndexOutOfBoundsException("Index too large.");
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Iterator<ObservableList<? extends E>> sourceIterator = sourceLists.iterator();
Iterator<? extends E> currentIterator = null;

@Override
public boolean hasNext() {
while (currentIterator == null || !currentIterator.hasNext())
if (sourceIterator.hasNext())
currentIterator = sourceIterator.next().iterator();
else
return false;
return true;
}

@Override
public E next() {
while (currentIterator == null || !currentIterator.hasNext())
if (sourceIterator.hasNext())
currentIterator = sourceIterator.next().iterator();
else
throw new NoSuchElementException();
return currentIterator.next();
}
};
}

@Override
public int size() {
return sourceLists.stream().mapToInt(ObservableList::size).sum();
}
}
Loading

0 comments on commit 01ece20

Please sign in to comment.