重写 MappedObservableList (#5400)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Hello Minecraft! Launcher
|
* Hello Minecraft! Launcher
|
||||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -17,124 +17,121 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util.javafx;
|
package org.jackhuang.hmcl.util.javafx;
|
||||||
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.WeakListChangeListener;
|
import javafx.collections.transformation.TransformationList;
|
||||||
import org.jackhuang.hmcl.util.Holder;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toCollection;
|
/// @author Glavo
|
||||||
import static javafx.collections.FXCollections.unmodifiableObservableList;
|
public final class MappedObservableList<E, F> extends TransformationList<E, F> {
|
||||||
|
|
||||||
/**
|
/// This method creates a mapping of `source`, using `mapper` as the converter.
|
||||||
* @author yushijinhun
|
///
|
||||||
*/
|
/// If an item is added to `source`, `mapper` will be invoked to create a corresponding item, which will also be added to the returned `ObservableList`.
|
||||||
public final class MappedObservableList {
|
/// If an item is removed from `source`, the corresponding item in the returned `ObservableList` will also be removed.
|
||||||
private MappedObservableList() {
|
/// If `source` is permutated, the returned `ObservableList` will also be permutated in the same way.
|
||||||
|
///
|
||||||
|
/// The returned `ObservableList` is unmodifiable.
|
||||||
|
public static <T, U> ObservableList<U> create(ObservableList<T> source, Function<T, U> mapper) {
|
||||||
|
return new MappedObservableList<>(source, mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MappedObservableListUpdater<T, U> implements ListChangeListener<T> {
|
private final Function<? super F, ? extends E> mapper;
|
||||||
private final ObservableList<T> origin;
|
private final List<E> elements;
|
||||||
private final ObservableList<U> target;
|
|
||||||
private final Function<T, U> mapper;
|
|
||||||
|
|
||||||
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
|
public MappedObservableList(@NotNull ObservableList<? extends F> source, @NotNull Function<? super F, ? extends E> mapper) {
|
||||||
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
|
super(source);
|
||||||
private final List<U> buffer;
|
this.mapper = mapper;
|
||||||
|
this.elements = new ArrayList<>(source.size());
|
||||||
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
|
for (F f : source) {
|
||||||
this.origin = origin;
|
elements.add(mapper.apply(f));
|
||||||
this.target = target;
|
|
||||||
this.mapper = mapper;
|
|
||||||
this.buffer = new ArrayList<>(target);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(Change<? extends T> change) {
|
@SuppressWarnings("unchecked")
|
||||||
// cache removed elements to reduce calls to mapper
|
protected void sourceChanged(ListChangeListener.Change<? extends F> change) {
|
||||||
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
|
beginChange();
|
||||||
|
while (change.next()) {
|
||||||
|
int from = change.getFrom();
|
||||||
|
int to = change.getTo();
|
||||||
|
|
||||||
while (change.next()) {
|
if (change.wasPermutated()) {
|
||||||
int from = change.getFrom();
|
Object[] temp = new Object[to - from];
|
||||||
int to = change.getTo();
|
int[] permutations = new int[to - from];
|
||||||
|
|
||||||
if (change.wasPermutated()) {
|
for (int i = 0; i < temp.length; i++) {
|
||||||
@SuppressWarnings("unchecked")
|
temp[i] = elements.get(from + i);
|
||||||
U[] temp = (U[]) new Object[to - from];
|
}
|
||||||
for (int i = 0; i < temp.length; i++) {
|
|
||||||
temp[i] = buffer.get(from + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int idx = from; idx < to; idx++) {
|
for (int i = from; i < to; i++) {
|
||||||
buffer.set(change.getPermutation(idx), temp[idx - from]);
|
int n = i - from;
|
||||||
}
|
int permutation = change.getPermutation(i);
|
||||||
} else {
|
permutations[n] = permutation;
|
||||||
if (change.wasRemoved()) {
|
elements.set(permutation, (E) temp[n]);
|
||||||
List<? extends T> originRemoved = change.getRemoved();
|
}
|
||||||
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
|
|
||||||
for (int i = 0; i < targetRemoved.size(); i++) {
|
nextPermutation(from, to, permutations);
|
||||||
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
|
} else if (change.wasUpdated()) {
|
||||||
}
|
for (int i = from; i < to; i++) {
|
||||||
targetRemoved.clear();
|
elements.set(i, mapper.apply(getSource().get(i)));
|
||||||
}
|
nextUpdate(i);
|
||||||
if (change.wasAdded()) {
|
}
|
||||||
@SuppressWarnings("unchecked")
|
} else {
|
||||||
U[] toAdd = (U[]) new Object[to - from];
|
List<E> removed = List.of();
|
||||||
for (int i = 0; i < toAdd.length; i++) {
|
if (change.wasRemoved()) {
|
||||||
toAdd[i] = map(cache, origin.get(from + i));
|
List<E> subList = elements.subList(from, from + change.getRemovedSize());
|
||||||
}
|
removed = new ArrayList<>(subList);
|
||||||
buffer.addAll(from, Arrays.asList(toAdd));
|
subList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.wasAdded()) {
|
||||||
|
Object[] temp = new Object[to - from];
|
||||||
|
List<? extends F> addedSubList = change.getAddedSubList();
|
||||||
|
for (int i = 0; i < addedSubList.size(); i++) {
|
||||||
|
temp[i] = mapper.apply(addedSubList.get(i));
|
||||||
}
|
}
|
||||||
|
elements.addAll(from, (List<E>) Arrays.asList(temp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.wasRemoved() && change.wasAdded()) {
|
||||||
|
nextReplace(from, to, removed);
|
||||||
|
} else if (change.wasRemoved()) {
|
||||||
|
nextRemove(from, removed);
|
||||||
|
} else if (change.wasAdded()) {
|
||||||
|
nextAdd(from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target.setAll(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pushCache(Map<T, LinkedList<U>> cache, T key, U value) {
|
|
||||||
cache.computeIfAbsent(key, any -> new LinkedList<>())
|
|
||||||
.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private U map(Map<T, LinkedList<U>> cache, T key) {
|
|
||||||
LinkedList<U> stack = cache.get(key);
|
|
||||||
if (stack != null && !stack.isEmpty()) {
|
|
||||||
return stack.pop();
|
|
||||||
}
|
|
||||||
return mapper.apply(key);
|
|
||||||
}
|
}
|
||||||
|
endChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
|
public E get(int index) {
|
||||||
*
|
return elements.get(index);
|
||||||
* If an item is added to {@code origin}, {@code mapper} will be invoked to create a corresponding item, which will also be added to the returned {@code ObservableList}.
|
|
||||||
* If an item is removed from {@code origin}, the corresponding item in the returned {@code ObservableList} will also be removed.
|
|
||||||
* If {@code origin} is permutated, the returned {@code ObservableList} will also be permutated in the same way.
|
|
||||||
*
|
|
||||||
* The returned {@code ObservableList} is unmodifiable.
|
|
||||||
*/
|
|
||||||
public static <T, U> ObservableList<U> create(ObservableList<T> origin, Function<T, U> mapper) {
|
|
||||||
// create a already-synchronized target ObservableList<U>
|
|
||||||
ObservableList<U> target = origin.stream()
|
|
||||||
.map(mapper)
|
|
||||||
.collect(toCollection(FXCollections::observableArrayList));
|
|
||||||
|
|
||||||
// then synchronize further changes to target
|
|
||||||
ListChangeListener<T> listener = new MappedObservableListUpdater<>(origin, target, mapper);
|
|
||||||
|
|
||||||
// let target hold a reference to listener to prevent listener being garbage-collected before target is garbage-collected
|
|
||||||
target.addListener(new Holder<>(listener));
|
|
||||||
|
|
||||||
// let origin hold a weak reference to listener, so that target can be garbage-collected when it's no longer used
|
|
||||||
origin.addListener(new WeakListChangeListener<>(listener));
|
|
||||||
|
|
||||||
// ref graph:
|
|
||||||
// target ------> listener <-weak- origin
|
|
||||||
// <------ ------>
|
|
||||||
|
|
||||||
return unmodifiableObservableList(target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return elements.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSourceIndex(int index) {
|
||||||
|
Objects.checkIndex(index, this.size());
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViewIndex(int index) {
|
||||||
|
Objects.checkIndex(index, this.size());
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.util.javafx;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.collections.ObservableListBase;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
public class MappedObservableListTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInitialMapping() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-2", mapped.get(1));
|
||||||
|
assertEquals("Item-3", mapped.get(2));
|
||||||
|
assertEquals("Item-4", mapped.get(3));
|
||||||
|
assertEquals("Item-5", mapped.get(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdd() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.add(4);
|
||||||
|
assertEquals(4, mapped.size());
|
||||||
|
assertEquals("Item-4", mapped.get(3));
|
||||||
|
|
||||||
|
source.add(1, 10);
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-10", mapped.get(1));
|
||||||
|
assertEquals("Item-2", mapped.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemove() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.remove(2);
|
||||||
|
assertEquals(4, mapped.size());
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-2", mapped.get(1));
|
||||||
|
assertEquals("Item-4", mapped.get(2));
|
||||||
|
assertEquals("Item-5", mapped.get(3));
|
||||||
|
|
||||||
|
source.remove(Integer.valueOf(1));
|
||||||
|
assertEquals(3, mapped.size());
|
||||||
|
assertEquals("Item-2", mapped.get(0));
|
||||||
|
assertEquals("Item-4", mapped.get(1));
|
||||||
|
assertEquals("Item-5", mapped.get(2));
|
||||||
|
|
||||||
|
source.remove(1, 3);
|
||||||
|
assertEquals(1, mapped.size());
|
||||||
|
assertEquals("Item-2", mapped.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.set(2, 10);
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
|
||||||
|
// Verify the actual values
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-2", mapped.get(1));
|
||||||
|
assertEquals("Item-10", mapped.get(2));
|
||||||
|
assertEquals("Item-4", mapped.get(3));
|
||||||
|
assertEquals("Item-5", mapped.get(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSort() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(5, 3, 1, 4, 2);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
FXCollections.sort(source);
|
||||||
|
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-2", mapped.get(1));
|
||||||
|
assertEquals("Item-3", mapped.get(2));
|
||||||
|
assertEquals("Item-4", mapped.get(3));
|
||||||
|
assertEquals("Item-5", mapped.get(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClear() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.clear();
|
||||||
|
|
||||||
|
assertEquals(0, mapped.size());
|
||||||
|
assertTrue(mapped.isEmpty());
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, () -> mapped.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddAll() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.addAll(3, 4, 5);
|
||||||
|
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-3", mapped.get(2));
|
||||||
|
assertEquals("Item-4", mapped.get(3));
|
||||||
|
assertEquals("Item-5", mapped.get(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveAll() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.removeAll(2, 4);
|
||||||
|
|
||||||
|
assertEquals(3, mapped.size());
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-3", mapped.get(1));
|
||||||
|
assertEquals("Item-5", mapped.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetAll() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.setAll(10, 20, 30, 40);
|
||||||
|
|
||||||
|
assertEquals(4, mapped.size());
|
||||||
|
assertEquals("Item-10", mapped.get(0));
|
||||||
|
assertEquals("Item-20", mapped.get(1));
|
||||||
|
assertEquals("Item-30", mapped.get(2));
|
||||||
|
assertEquals("Item-40", mapped.get(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSourceIndex() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
MappedObservableList<String, Integer> mapped = new MappedObservableList<>(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
assertEquals(0, mapped.getSourceIndex(0));
|
||||||
|
assertEquals(2, mapped.getSourceIndex(2));
|
||||||
|
assertEquals(4, mapped.getSourceIndex(4));
|
||||||
|
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getSourceIndex(-1));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getSourceIndex(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetViewIndex() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList(1, 2, 3, 4, 5);
|
||||||
|
MappedObservableList<String, Integer> mapped = new MappedObservableList<>(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
assertEquals(0, mapped.getViewIndex(0));
|
||||||
|
assertEquals(2, mapped.getViewIndex(2));
|
||||||
|
assertEquals(4, mapped.getViewIndex(4));
|
||||||
|
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getViewIndex(-1));
|
||||||
|
assertThrows(IndexOutOfBoundsException.class, () -> mapped.getViewIndex(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testComplexOperations() {
|
||||||
|
ObservableList<Integer> source = FXCollections.observableArrayList();
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
// Start with empty
|
||||||
|
assertEquals(0, mapped.size());
|
||||||
|
|
||||||
|
// Add some elements
|
||||||
|
source.addAll(1, 2, 3, 4, 5);
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
|
||||||
|
// Remove middle element
|
||||||
|
source.remove(2);
|
||||||
|
assertEquals(4, mapped.size());
|
||||||
|
assertEquals("Item-4", mapped.get(2));
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
FXCollections.sort(source, Collections.reverseOrder());
|
||||||
|
assertEquals("Item-5", mapped.get(0));
|
||||||
|
assertEquals("Item-4", mapped.get(1));
|
||||||
|
assertEquals("Item-2", mapped.get(2));
|
||||||
|
assertEquals("Item-1", mapped.get(3));
|
||||||
|
|
||||||
|
// Add at specific position
|
||||||
|
source.add(2, 3);
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-3", mapped.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test for [javafx.collections.ListChangeListener.Change#wasUpdated()].
|
||||||
|
@Test
|
||||||
|
public void testUpdate() {
|
||||||
|
class TestUpdateList<T> extends ObservableListBase<T> {
|
||||||
|
private final List<T> backingList;
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public TestUpdateList(T... items) {
|
||||||
|
this.backingList = Arrays.asList(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return backingList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T get(int index) {
|
||||||
|
return backingList.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateItem(int beginIndex, int endIndex, Function<T, T> mapper) {
|
||||||
|
Objects.checkFromToIndex(beginIndex, endIndex, size());
|
||||||
|
beginChange();
|
||||||
|
for (int i = beginIndex; i < endIndex; i++) {
|
||||||
|
backingList.set(i, mapper.apply(backingList.get(i)));
|
||||||
|
nextUpdate(i);
|
||||||
|
}
|
||||||
|
endChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestUpdateList<Integer> source = new TestUpdateList<>(1, 2, 3, 4, 5);
|
||||||
|
ObservableList<String> mapped = MappedObservableList.create(source, i -> "Item-" + i);
|
||||||
|
|
||||||
|
source.updateItem(2, 4, i -> i * 10);
|
||||||
|
assertEquals(5, mapped.size());
|
||||||
|
assertEquals("Item-1", mapped.get(0));
|
||||||
|
assertEquals("Item-2", mapped.get(1));
|
||||||
|
assertEquals("Item-30", mapped.get(2));
|
||||||
|
assertEquals("Item-40", mapped.get(3));
|
||||||
|
assertEquals("Item-5", mapped.get(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user