By concatenation I mean obtaining a new list, which listens for changes in all concatenated parts.
What is the purpose of method FXCollections#concat(ObservableList<E>... lists)
? If it just merges several lists, then I see no sense to have separate method for this.
And if regard as doing what I wish then it doesn't work:
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
public class ConcatObservabeList {
public static void main(String[] args) {
ObservableList<Integer> list1 = FXCollections.observableArrayList();
ObservableList<Integer> list2 = FXCollections.observableArrayList();
ObservableList<Integer> concat = FXCollections.concat(list1, list2);
concat.addListener(new ListChangeListener<Integer>() {
public void onChanged(Change<? extends Integer> c) {
System.out.println("changed");
}
});
list1.add(12);
}
}
I was having the same need to concat/aggregate several ObservableLists into one list for a JavaFX LineChart. The examples I found here or on the github posted in another answer always copied all the entries from the sublists into the aggregated List on each change. For a list with many entries this seemed not very elegant.
I decided to implement my on version, which keeps track of the position of the sublists in the aggregated list and when elements in the sublists change, apply the same changes in the aggregated list. There is still room for improvement (not using a delegate List but extending a ObservableList directly, or firing the events from sublists up to the aggregated list and overriding the getters and iterators - help with that would be appreciated), but I thought I post my version here as it is, maybe it helps someone.
Code:
/**
* This class aggregates several other Observed Lists (sublists), observes changes on those sublists and applies those same changes to the
* aggregated list.
* Inspired by:
* - http://stackoverflow.com/questions/25705847/listchangelistener-waspermutated-block
* - http://stackoverflow.com/questions/37524662/how-to-concatenate-observable-lists-in-javafx
* - https://github.com/lestard/advanced-bindings/blob/master/src/main/java/eu/lestard/advanced_bindings/api/CollectionBindings.java
* Posted result on: http://stackoverflow.com/questions/37524662/how-to-concatenate-observable-lists-in-javafx
*/
public class AggregatedObservableArrayList<T> {
protected final List<ObservableList<T>> lists = new ArrayList<>();
final private List<Integer> sizes = new ArrayList<>();
final private List<InternalListModificationListener> listeners = new ArrayList<>();
final protected ObservableList<T> aggregatedList = FXCollections.observableArrayList();
public AggregatedObservableArrayList() {
}
/**
* The Aggregated Observable List. This list is unmodifiable, because sorting this list would mess up the entire bookkeeping we do here.
*
* @return an unmodifiable view of the aggregatedList
*/
public ObservableList<T> getAggregatedList() {
return FXCollections.unmodifiableObservableList(aggregatedList);
}
public void appendList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(list.size());
aggregatedList.addAll(list);
listeners.add(listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void prependList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(0, list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(0, list.size());
aggregatedList.addAll(0, list);
listeners.add(0, listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void removeList(@NotNull ObservableList<T> list) {
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
final int index = lists.indexOf(list);
if (index < 0) {
throw new IllegalArgumentException("Cannot remove a list that is not contained: " + list + " lists=" + lists);
}
final int startIndex = getStartIndex(list);
final int endIndex = getEndIndex(list, startIndex);
// we want to find the start index of this list inside the aggregated List. End index will be start + size - 1.
lists.remove(list);
sizes.remove(index);
final InternalListModificationListener listener = listeners.remove(index);
list.removeListener(listener);
aggregatedList.remove(startIndex, endIndex + 1); // end + 1 because end is exclusive
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
/**
* Get the start index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @return the start index of this list in the aggregated List
*/
private int getStartIndex(@NotNull ObservableList<T> list) {
int startIndex = 0;
//System.out.println("=== searching startIndex of " + list);
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int listIndex = lists.indexOf(list);
for (int i = 0; i < listIndex; i++) {
final Integer size = sizes.get(i);
startIndex += size;
//System.out.println(" startIndex = " + startIndex + " added=" + size);
}
//System.out.println("startIndex = " + startIndex);
return startIndex;
}
/**
* Get the end index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @param startIndex the start of the list (retrieve with {@link #getStartIndex(ObservableList)}
* @return the end index of this list in the aggregated List
*/
private int getEndIndex(@NotNull ObservableList<T> list, int startIndex) {
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int index = lists.indexOf(list);
return startIndex + sizes.get(index) - 1;
}
private class InternalListModificationListener implements ListChangeListener<T> {
@NotNull
private final ObservableList<T> list;
public InternalListModificationListener(@NotNull ObservableList<T> list) {
this.list = list;
}
/**
* Called after a change has been made to an ObservableList.
*
* @param change an object representing the change that was done
* @see Change
*/
@Override
public void onChanged(Change<? extends T> change) {
final ObservableList<? extends T> changedList = change.getList();
final int startIndex = getStartIndex(list);
final int index = lists.indexOf(list);
final int newSize = changedList.size();
//System.out.println("onChanged for list=" + list + " aggregate=" + aggregatedList);
while (change.next()) {
final int from = change.getFrom();
final int to = change.getTo();
//System.out.println(" startIndex=" + startIndex + " from=" + from + " to=" + to);
if (change.wasPermutated()) {
final ArrayList<T> copy = new ArrayList<>(aggregatedList.subList(startIndex + from, startIndex + to));
//System.out.println(" permutating sublist=" + copy);
for (int oldIndex = from; oldIndex < to; oldIndex++) {
int newIndex = change.getPermutation(oldIndex);
copy.set(newIndex - from, aggregatedList.get(startIndex + oldIndex));
}
//System.out.println(" permutating done sublist=" + copy);
aggregatedList.subList(startIndex + from, startIndex + to).clear();
aggregatedList.addAll(startIndex + from, copy);
} else if (change.wasUpdated()) {
// do nothing
} else {
if (change.wasRemoved()) {
List<? extends T> removed = change.getRemoved();
//System.out.println(" removed= " + removed);
// IMPORTANT! FROM == TO when removing items.
aggregatedList.remove(startIndex + from, startIndex + from + removed.size());
}
if (change.wasAdded()) {
List<? extends T> added = change.getAddedSubList();
//System.out.println(" added= " + added);
//add those elements to your data
aggregatedList.addAll(startIndex + from, added);
}
}
}
// update the size of the list in the map
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.set(index, newSize);
//System.out.println("listSizesMap = " + sizes);
}
}
public String dump(Function<T, Object> function) {
StringBuilder sb = new StringBuilder();
sb.append("[");
aggregatedList.forEach(el -> sb.append(function.apply(el)).append(","));
final int length = sb.length();
sb.replace(length - 1, length, "");
sb.append("]");
return sb.toString();
}
}
jUnit Test:
/**
* Testing the AggregatedObservableArrayList
*/
public class AggregatedObservableArrayListTest {
@Test
public void testObservableValue() {
final AggregatedObservableArrayList<IntegerProperty> aggregatedWrapper = new AggregatedObservableArrayList<>();
final ObservableList<IntegerProperty> aggregatedList = aggregatedWrapper.getAggregatedList();
aggregatedList.addListener((Observable observable) -> {
System.out.println("observable = " + observable);
});
final ObservableList<IntegerProperty> list1 = FXCollections.observableArrayList();
final ObservableList<IntegerProperty> list2 = FXCollections.observableArrayList();
final ObservableList<IntegerProperty> list3 = FXCollections.observableArrayList();
list1.addAll(new SimpleIntegerProperty(1), new SimpleIntegerProperty(2), new SimpleIntegerProperty(3), new SimpleIntegerProperty(4),
new SimpleIntegerProperty(5));
list2.addAll(new SimpleIntegerProperty(10), new SimpleIntegerProperty(11), new SimpleIntegerProperty(12), new SimpleIntegerProperty(13),
new SimpleIntegerProperty(14), new SimpleIntegerProperty(15));
list3.addAll(new SimpleIntegerProperty(100), new SimpleIntegerProperty(110), new SimpleIntegerProperty(120), new SimpleIntegerProperty(130),
new SimpleIntegerProperty(140), new SimpleIntegerProperty(150));
// adding list 1 to aggregate
aggregatedWrapper.appendList(list1);
assertEquals("wrong content", "[1,2,3,4,5]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing elems from list1
list1.remove(2, 4);
assertEquals("wrong content", "[1,2,5]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding second List
aggregatedWrapper.appendList(list2);
assertEquals("wrong content", "[1,2,5,10,11,12,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing elems from second List
list2.remove(1, 3);
assertEquals("wrong content", "[1,2,5,10,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// replacing element in first list
list1.set(1, new SimpleIntegerProperty(3));
assertEquals("wrong content", "[1,3,5,10,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding third List
aggregatedWrapper.appendList(list3);
assertEquals("wrong content", "[1,3,5,10,13,14,15,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// emptying second list
list2.clear();
assertEquals("wrong content", "[1,3,5,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding new elements to second list
list2.addAll(new SimpleIntegerProperty(203), new SimpleIntegerProperty(202), new SimpleIntegerProperty(201));
assertEquals("wrong content", "[1,3,5,203,202,201,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// sorting list2. this results in permutation
list2.sort((o1, o2) -> o1.getValue().compareTo(o2.getValue()));
assertEquals("wrong content", "[1,3,5,201,202,203,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing list2 completely
aggregatedWrapper.removeList(list2);
assertEquals("wrong content", "[1,3,5,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// updating one integer value in list 3
SimpleIntegerProperty integer = (SimpleIntegerProperty) list3.get(0);
integer.set(1);
assertEquals("wrong content", "[1,3,5,1,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// prepending list 2 again
aggregatedWrapper.prependList(list2);
assertEquals("wrong content", "[201,202,203,1,3,5,1,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
}
}
A ListChangeListener
added to an ObservableList
sees certain specific changes made to the list as a whole. The overhead of also listening to any ancestor lists is considerable, as seen in the API cited here. Because FXCollections.concat()
simply copies references from the source lists to the destination's backing list, a listener added to concat
will see changes made to concat
; it will not see changes to list1
or list2
.
If you don't need to create a new ObservableList
for some other reason, aggregate the lists in a way that allows you to add the same listener to each.
Console:
changed { [42] added at 0 }
Code:
import java.util.ArrayList;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
/**
* @see https://stackoverflow.com/a/37527245/230513
*/
public class ObservableListAggregate {
public static void main(String[] args) {
ObservableList<Integer> list1 = FXCollections.observableArrayList();
ObservableList<Integer> list2 = FXCollections.observableArrayList();
Aggregate<ObservableList<Integer>> aggregate = new Aggregate(list1, list2);
aggregate.addListener(new ListChangeListener<ObservableList<Integer>>() {
@Override
public void onChanged(ListChangeListener.Change<? extends ObservableList<Integer>> c) {
System.out.println("changed " + c);
}
});
list1.add(42);
}
private static class Aggregate<T> {
List<ObservableList<T>> lists = new ArrayList<>();
public Aggregate(ObservableList<T>... lists) {
for (ObservableList<T> list : lists) {
this.lists.add(list);
}
}
public final void addListener(ListChangeListener<? super T> listener) {
for (ObservableList<T> list : lists) {
list.addListener(listener);
}
}
public final void removeListener(ListChangeListener<? super T> listener) {
for (ObservableList<T> list : lists) {
list.removeListener(listener);
}
}
}
}
To see changes to the individual list elements, use an ObservableList<Observable>
, such as ObservableList<IntegerProperty>
. In the example below, note that ip
, added to list1
, is the same IntegerProperty
later modified in concat
.
Console:
IntegerProperty [value: 42]
concat changed { [IntegerProperty [value: 2147483647]] added at 1 }
Code:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
public class ConcatObservabeList {
public static void main(String[] args) {
ObservableList<IntegerProperty> list1 = FXCollections.observableArrayList();
IntegerProperty ip = new SimpleIntegerProperty(0);
ip.addListener(System.out::println);
list1.add(ip);
ObservableList<IntegerProperty> list2 = FXCollections.observableArrayList();
ObservableList<IntegerProperty> concat = FXCollections.concat(list1, list2);
concat.get(0).setValue(42);
concat.addListener(new ListChangeListener<IntegerProperty>() {
@Override
public void onChanged(ListChangeListener.Change<? extends IntegerProperty> c) {
System.out.println("concat changed " + c);
}
});
concat.add(new SimpleIntegerProperty(Integer.MAX_VALUE));
}
}
I stumbled across the same problem, because the method FXCollections.concat(...)
did not change on changes of the source lists, which was a requirement for my use case.
Since the other answers to this question seem like a little overkill to me, I'll add my own solution, which has two major limitations:
ObservableList
will be read-only to the contents of the source lists.ObservableList
will be a new instance on each change, so that possible UI elements based on them will be newly created on every change.As any ListBinding
computes an ObservableList
as its value and implements (and therefor is a) ObservableList
at the same time, it can be easily used to enhance the default concat
method to update on changes of the source lists:
@SafeVarargs
public static <T> ObservableList<T> concat(ObservableList<T>... sources) {
return new ListBinding<T>() {
{
bind(sources);
}
@Override
protected ObservableList<T> computeValue() {
return FXCollections.concat(sources);
}
};
}
I also had the same problem. Finally I made a list of ObservableList:
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
/**
* Read only view on a list of {@link ObservableList}
*
* @author Marcel Heckel
*/
public class CompositeObservableList<T> extends AbstractList<T> implements ObservableList<T>
{
protected List<ObservableList<T>> lists;
protected List<ListChangeListener<? super T>> listChangeListeners = new ArrayList<>();
protected List<InvalidationListener> invalidationListeners = new ArrayList<>();
protected InvalidationListener internalInvalidationListener = this::invalidated;
protected ListChangeListener<T> internalListChangeListener = this::onChanged;
protected WeakInvalidationListener weakInvalidationListener = new WeakInvalidationListener(
internalInvalidationListener);
protected WeakListChangeListener<T> weakListChangeListener = new WeakListChangeListener<>(
internalListChangeListener);
public CompositeObservableList()
{
this.lists = new ArrayList<>();
}
public CompositeObservableList(List<ObservableList<T>> lists)
{
this.lists = lists;
for (ObservableList<T> l : lists)
{
l.addListener(weakInvalidationListener);
l.addListener(weakListChangeListener);
}
}
public void addObservableList(ObservableList<T> l)
{
lists.add(l);
l.addListener(weakInvalidationListener);
l.addListener(weakListChangeListener);
}
/** remove listeners and clears the internal list */
public void clearLists()
{
for (ObservableList<T> l : lists)
{
l.removeListener(weakInvalidationListener);
l.removeListener(weakListChangeListener);
}
lists.clear();
}
///////////////////////////////////////
// listeners
private void invalidated(Observable observable)
{
for (InvalidationListener l : invalidationListeners)
l.invalidated(CompositeObservableList.this);
}
private void onChanged(ListChangeListener.Change<? extends T> c)
{
int idx = getStartIndexOfListReference(c.getList());
assert (idx >= 0);
if (idx < 0)
return;
c = new IndexOffsetChange<>(CompositeObservableList.this, idx, c);
for (ListChangeListener<? super T> l : listChangeListeners)
{
l.onChanged(c);
}
}
private int getStartIndexOfListReference(ObservableList<?> l)
{
int startIndex = 0;
for (int i = 0; i < lists.size(); i++ )
{
if (l == lists.get(i))
return startIndex;
startIndex += l.size();
}
return -1;
}
////////////////////////////////////////
@Override
public int size()
{
int size = 0;
for (Collection<T> c : lists)
size += c.size();
return size;
}
@Override
public boolean isEmpty()
{
for (Collection<T> c : lists)
if ( !c.isEmpty())
return false;
return true;
}
@Override
public boolean contains(Object obj)
{
for (Collection<T> c : lists)
if (c.contains(obj))
return true;
return false;
}
@Override
public boolean containsAll(Collection<?> c)
{
for (Object ele : c)
{
if ( !this.contains(ele))
return false;
}
return true;
}
@Override
public int indexOf(Object o)
{
int index = 0;
for (List<T> l : lists)
{
int i = l.indexOf(o);
if (i >= 0)
return index + i;
index += l.size();
}
return -1;
}
@Override
public T get(int index)
{
if (index < 0)
throw new IndexOutOfBoundsException("index: " + index + " - size: " + size());
for (List<T> l : lists)
{
if (l.size() > index)
return l.get(index);
index -= l.size();
}
throw new IndexOutOfBoundsException("index: " + index + " - size: " + size());
}
@Override
public Iterator<T> iterator()
{
return new Iterator<T>()
{
Iterator<T> currentIterator = null;
Iterator<ObservableList<T>> listsIterator = lists.iterator();
@Override
public boolean hasNext()
{
while (true)
{
if (currentIterator != null && currentIterator.hasNext())
return true;
if ( !listsIterator.hasNext())
return false;
currentIterator = listsIterator.next().iterator();
}
}
@Override
public T next()
{
if ( !hasNext())
throw new NoSuchElementException();
return currentIterator.next();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
// editing methods
@Override
public boolean add(T obj)
{
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object obj)
{
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(Collection<? extends T> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(final Collection<?> c)
{
throw new UnsupportedOperationException();
}
@Override
public void clear()
{
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(int index, Collection<? extends T> c)
{
throw new UnsupportedOperationException();
}
@Override
public T set(int index, T element)
{
throw new UnsupportedOperationException();
}
@Override
public void add(int index, T element)
{
throw new UnsupportedOperationException();
}
@Override
public T remove(int index)
{
throw new UnsupportedOperationException();
}
// editing methods of ObservableList list
@Override
public boolean addAll(T... elements)
{
throw new UnsupportedOperationException();
}
@Override
public boolean setAll(T... elements)
{
throw new UnsupportedOperationException();
}
@Override
public boolean setAll(Collection<? extends T> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(T... elements)
{
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(T... elements)
{
throw new UnsupportedOperationException();
}
@Override
public void remove(int from, int to)
{
throw new UnsupportedOperationException();
}
/////////////////////////////
@Override
public void addListener(InvalidationListener listener)
{
invalidationListeners.add(listener);
}
@Override
public void removeListener(InvalidationListener listener)
{
invalidationListeners.remove(listener);
}
@Override
public void addListener(ListChangeListener<? super T> listener)
{
listChangeListeners.add(listener);
}
@Override
public void removeListener(ListChangeListener<? super T> listener)
{
listChangeListeners.remove(listener);
}
////////////////
private static class IndexOffsetChange<T> extends ListChangeListener.Change<T>
{
private final int indexOffset;
private final ListChangeListener.Change<? extends T> delegate;
public IndexOffsetChange(ObservableList<T> list, final int indexOffset,
ListChangeListener.Change<? extends T> c)
{
super(list);
this.indexOffset = indexOffset;
this.delegate = c;
}
@Override
public boolean next()
{
return delegate.next();
}
@Override
public void reset()
{
delegate.reset();
}
@Override
public int getFrom()
{
return delegate.getFrom() + indexOffset;
}
@Override
public int getTo()
{
return delegate.getTo() + indexOffset;
}
@Override
public boolean wasPermutated()
{
return delegate.wasPermutated();
}
@Override
public int getPermutation(int i)
{
return indexOffset + super.getPermutation(i - indexOffset);
}
@Override
protected int[] getPermutation()
{
return null;
}
@SuppressWarnings("unchecked")
@Override
public List<T> getAddedSubList()
{
return (List<T>) delegate.getAddedSubList();
}
@Override
public int getAddedSize()
{
return delegate.getAddedSize();
}
@Override
public boolean wasAdded()
{
return delegate.wasAdded();
}
@SuppressWarnings("unchecked")
@Override
public List<T> getRemoved()
{
return (List<T>) delegate.getRemoved();
}
@Override
public int getRemovedSize()
{
return delegate.getRemovedSize();
}
@Override
public boolean wasRemoved()
{
return delegate.wasRemoved();
}
@Override
public boolean wasReplaced()
{
return delegate.wasReplaced();
}
@Override
public boolean wasUpdated()
{
return delegate.wasUpdated();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With