io.github.palexdev.mfxcore.collections.TransformableList Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of materialfx-all Show documentation
Show all versions of materialfx-all Show documentation
Material Design/Modern components for JavaFX, now packed as a single Jar
/*
* Copyright (C) 2022 Parisi Alessandro - [email protected]
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
*
* MaterialFX is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the License,
* or (at your option) any later version.
*
* MaterialFX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see .
*/
package io.github.palexdev.mfxcore.collections;
import io.github.palexdev.mfxcore.base.properties.functional.ComparatorProperty;
import io.github.palexdev.mfxcore.base.properties.functional.PredicateProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.collections.transformation.TransformationList;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* A {@code TransformableList} is a particular type of List which wraps another
* List called "source" and allows manipulations such as: filter and sort, retaining
* the original items' index.
*
* Extends {@link TransformationList}, it's basically the same thing of a {@link FilteredList}
* and a {@link SortedList} but combined into one.
*
* A more detailed (and hopefully more clear) explanation about the "indexes retention mentioned above":
*
* Think of this List as a View for the source list. The underlying data provided by the source is
* not guaranteed to be what the user sees but the items' properties are maintained.
* Let's see a brief example:
*
* {@code
* // Let's say I have this ObservableList
* ObservableList source = FXCollections.observableArrayList("A", "B", "C"):
*
* // Now let's say I want to sort this list in reverse order (CBA) and that
* // for some reason I still want A to be the element at index 0, B-1 and C-2
* // This is exactly the purpose of the TransformableList...
* TransformableList transformed = new TransformableList<>(source);
* transformed.setSorter(Comparator.reverseOrder());
*
* // Now that the order is (CBA) let's see how the list behaves:
* transformed.get(0); // Returns C
* transformed.indexOf("C"); // Returns 2, the index is retrieved in the source list
* transformed.viewToSource(0); // Returns 2, it maps an index of the transformed list to the index of the source list, at 0 we have C which is at index 2 in the source list
* transformed.sourceToView(0); // Also returns 2, it maps an index of the source list to the index of the transformed list, at 0 we have C which is at index 2 in the transformed list
*
* // To better see its behavior try to sort and filter the list at the same time.
* // You'll notice that sometimes sourceToView will return a negative index because the item is not in the transformed list (after a filter operation)
* }
*
*
* Check {@link #computeIndexes()} documentation to see how indexes are calculated.
*
* IMPORTANT: If using a reversed comparator please use {@link #setComparator(Comparator, boolean)} with 'true' as argument,
* as {@link #setComparator(Comparator)} will always assume it is a natural order comparator. This is needed to make {@link #sourceToView(int)}
* properly work as it uses a binary search algorithm to find the right index.
*
* @param the items' type
*/
public class TransformableList extends TransformationList {
//================================================================================
// Properties
//================================================================================
private final List indexes = new ArrayList<>();
private boolean reversed = false;
private final PredicateProperty predicate = new PredicateProperty<>() {
@Override
protected void invalidated() {
update();
}
};
private final ComparatorProperty comparator = new ComparatorProperty<>() {
@Override
protected void invalidated() {
update();
}
};
//================================================================================
// Constructors
//================================================================================
public TransformableList(ObservableList source) {
this(source, null);
}
public TransformableList(ObservableList source, Predicate predicate) {
this(source, predicate, null);
}
public TransformableList(ObservableList source, Predicate predicate, Comparator comparator) {
super(source);
setPredicate(predicate);
setComparator(comparator);
update();
}
//================================================================================
// Methods
//================================================================================
/**
* Calls {@link #getSourceIndex(int)}, just with a different name to be more clear.
*
* Maps an index of the transformed list, to the index of the source list.
*/
public int viewToSource(int index) {
return getSourceIndex(index);
}
/**
* Calls {@link #getViewIndex(int)}, just with a different name to be more clear.
*
* Maps an index of the source list, to the index of the transformed list.
*/
public int sourceToView(int index) {
return getViewIndex(index);
}
/**
* Responsible for updating the transformed indexes when the
* predicate or the comparator change.
*/
private void update() {
indexes.clear();
indexes.addAll(computeIndexes());
if (this.hasListeners()) {
this.fireChange(new NonIterableChange.GenericAddRemoveChange<>(0, size(), new ArrayList<>(this), this));
}
}
/**
* Core method of TransformableLists. This is responsible for computing
* the transformed indexes by creating a {@link SortedMap} and mapping every index from 0 to source size
* to its item. Before mapping, items are filtered with the given predicate, {@link #predicateProperty()}.
* Before returning, the map's entry set is sorted by its values with the given comparator, {@link #comparatorProperty()}.
* Finally, returns the map's key set, this set contains the transformed indexes, filtered and sorted.
*/
private Collection computeIndexes() {
Predicate filter = this.getPredicate();
Comparator sorter = this.getComparator();
SortedMap sourceMap;
if (filter != null) {
sourceMap = IntStream.range(0, getSource().size())
.filter((index) -> filter.test(getSource().get(index)))
.collect(TreeMap::new, (map, index) -> map.put(index, getSource().get(index)), TreeMap::putAll);
} else {
sourceMap = IntStream.range(0, getSource().size())
.collect(TreeMap::new, (map, index) -> map.put(index, getSource().get(index)), TreeMap::putAll);
}
return sorter != null ? sourceMap.entrySet().stream()
.sorted((o1, o2) -> sorter.compare(o1.getValue(), o2.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList()) : sourceMap.keySet();
}
public Predicate getPredicate() {
return this.predicate.get();
}
/**
* Specifies the predicate used to filter the source list.
*/
public PredicateProperty predicateProperty() {
return this.predicate;
}
public void setPredicate(Predicate predicate) {
this.predicate.set(predicate);
}
public Comparator getComparator() {
return this.comparator.get();
}
/**
* Specifies the comparator used to sort the source list.
*
* @see #setComparator(Comparator, boolean)
*/
public ComparatorProperty comparatorProperty() {
return this.comparator;
}
public void setComparator(Comparator comparator) {
this.reversed = false;
this.comparator.set(comparator);
}
/**
* This method is NECESSARY if using a reversed comparator,
* a special flag is set to true and {@link #sourceToView(int)} behaves accordingly.
*/
public void setComparator(Comparator comparator, boolean reversed) {
this.reversed = reversed;
this.comparator.set(comparator);
}
/**
* Specifies if a reversed comparator is being used.
*/
public boolean isReversed() {
return reversed;
}
/**
* Communicates to the transformed list, specifically to {@link #getViewIndex(int)},
* if the list is sorted in reversed order.
*/
public void setReversed(boolean reversed) {
this.reversed = reversed;
}
//================================================================================
// Overridden Methods
//================================================================================
/**
* {@inheritDoc}
*
* Calls {@link #update()}.
*/
@Override
protected void sourceChanged(ListChangeListener.Change c) {
beginChange();
update();
endChange();
}
/**
* @return the number of items in the transformable list
*/
@Override
public int size() {
return indexes.size();
}
/**
* Retrieves and return the item at the given index in the transformable list.
* This means transformations due to {@link #predicateProperty()} or {@link #comparatorProperty()}
* are taken into account.
*/
@Override
public T get(int index) {
if (index > size()) {
throw new IndexOutOfBoundsException(index);
} else {
return getSource().get(indexes.get(index));
}
}
@Override
public int getSourceIndex(int index) {
if (index > size()) {
throw new IndexOutOfBoundsException(index);
} else {
return indexes.get(index);
}
}
@Override
public int getViewIndex(int index) {
int viewIndex = reversed ?
Collections.binarySearch(indexes, index, Collections.reverseOrder()) :
Collections.binarySearch(indexes, index);
return viewIndex < 0 ? -1 : viewIndex;
}
}