重写 MappedObservableList (#5400)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,124 +17,121 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import javafx.collections.transformation.TransformationList;
|
||||
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 static java.util.stream.Collectors.toCollection;
|
||||
import static javafx.collections.FXCollections.unmodifiableObservableList;
|
||||
/// @author Glavo
|
||||
public final class MappedObservableList<E, F> extends TransformationList<E, F> {
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class MappedObservableList {
|
||||
private MappedObservableList() {
|
||||
/// This method creates a mapping of `source`, using `mapper` as the converter.
|
||||
///
|
||||
/// 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`.
|
||||
/// If an item is removed from `source`, the corresponding item in the returned `ObservableList` will also be removed.
|
||||
/// 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 ObservableList<T> origin;
|
||||
private final ObservableList<U> target;
|
||||
private final Function<T, U> mapper;
|
||||
private final Function<? super F, ? extends E> mapper;
|
||||
private final List<E> elements;
|
||||
|
||||
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
|
||||
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
|
||||
private final List<U> buffer;
|
||||
|
||||
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
|
||||
this.origin = origin;
|
||||
this.target = target;
|
||||
this.mapper = mapper;
|
||||
this.buffer = new ArrayList<>(target);
|
||||
public MappedObservableList(@NotNull ObservableList<? extends F> source, @NotNull Function<? super F, ? extends E> mapper) {
|
||||
super(source);
|
||||
this.mapper = mapper;
|
||||
this.elements = new ArrayList<>(source.size());
|
||||
for (F f : source) {
|
||||
elements.add(mapper.apply(f));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(Change<? extends T> change) {
|
||||
// cache removed elements to reduce calls to mapper
|
||||
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void sourceChanged(ListChangeListener.Change<? extends F> change) {
|
||||
beginChange();
|
||||
while (change.next()) {
|
||||
int from = change.getFrom();
|
||||
int to = change.getTo();
|
||||
|
||||
while (change.next()) {
|
||||
int from = change.getFrom();
|
||||
int to = change.getTo();
|
||||
if (change.wasPermutated()) {
|
||||
Object[] temp = new Object[to - from];
|
||||
int[] permutations = new int[to - from];
|
||||
|
||||
if (change.wasPermutated()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
U[] temp = (U[]) new Object[to - from];
|
||||
for (int i = 0; i < temp.length; i++) {
|
||||
temp[i] = buffer.get(from + i);
|
||||
}
|
||||
for (int i = 0; i < temp.length; i++) {
|
||||
temp[i] = elements.get(from + i);
|
||||
}
|
||||
|
||||
for (int idx = from; idx < to; idx++) {
|
||||
buffer.set(change.getPermutation(idx), temp[idx - from]);
|
||||
}
|
||||
} else {
|
||||
if (change.wasRemoved()) {
|
||||
List<? extends T> originRemoved = change.getRemoved();
|
||||
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
|
||||
for (int i = 0; i < targetRemoved.size(); i++) {
|
||||
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
|
||||
}
|
||||
targetRemoved.clear();
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
U[] toAdd = (U[]) new Object[to - from];
|
||||
for (int i = 0; i < toAdd.length; i++) {
|
||||
toAdd[i] = map(cache, origin.get(from + i));
|
||||
}
|
||||
buffer.addAll(from, Arrays.asList(toAdd));
|
||||
for (int i = from; i < to; i++) {
|
||||
int n = i - from;
|
||||
int permutation = change.getPermutation(i);
|
||||
permutations[n] = permutation;
|
||||
elements.set(permutation, (E) temp[n]);
|
||||
}
|
||||
|
||||
nextPermutation(from, to, permutations);
|
||||
} else if (change.wasUpdated()) {
|
||||
for (int i = from; i < to; i++) {
|
||||
elements.set(i, mapper.apply(getSource().get(i)));
|
||||
nextUpdate(i);
|
||||
}
|
||||
} else {
|
||||
List<E> removed = List.of();
|
||||
if (change.wasRemoved()) {
|
||||
List<E> subList = elements.subList(from, from + change.getRemovedSize());
|
||||
removed = new ArrayList<>(subList);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
|
||||
*
|
||||
* 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 E get(int index) {
|
||||
return elements.get(index);
|
||||
}
|
||||
|
||||
@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