javafx.collections.ListChangeBuilder Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.collections;
import com.sun.javafx.collections.ChangeHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import javafx.collections.ListChangeListener.Change;
final class ListChangeBuilder {
private static final int[] EMPTY_PERM = new int[0];
private final ObservableListBase list;
private int changeLock;
private List> addRemoveChanges;
private List> updateChanges;
private SubChange permutationChange;
private void checkAddRemoveList() {
if (addRemoveChanges == null) {
addRemoveChanges = new ArrayList<>();
}
}
private void checkState() {
if (changeLock == 0) {
throw new IllegalStateException("beginChange was not called on this builder");
}
}
private int findSubChange(int idx, final List> list) {
int from = 0;
int to = list.size() - 1;
while (from <= to) {
int changeIdx = (from + to) / 2;
SubChange change = list.get(changeIdx);
if (idx >= change.to) {
from = changeIdx + 1;
} else if (idx < change.from) {
to = changeIdx - 1;
} else {
return changeIdx;
}
}
return ~from;
}
private void insertUpdate(int pos) {
int idx = findSubChange(pos, updateChanges);
if (idx < 0) { //If not found
idx = ~idx;
SubChange change;
if (idx > 0 && (change = updateChanges.get(idx - 1)).to == pos) {
change.to = pos + 1;
} else if (idx < updateChanges.size() && (change = updateChanges.get(idx)).from == pos + 1) {
change.from = pos;
} else {
updateChanges.add(idx, new SubChange(pos, pos + 1, null, EMPTY_PERM, true));
}
} // If found, no need to do another update
}
private void insertRemoved(int pos, final E removed) {
int idx = findSubChange(pos, addRemoveChanges);
if (idx < 0) { // Not found
idx = ~idx;
SubChange change;
if (idx > 0 && (change = addRemoveChanges.get(idx - 1)).to == pos) {
change.removed.add(removed);
--idx; // Idx index will be used as a starting point for update
} else if (idx < addRemoveChanges.size() && (change = addRemoveChanges.get(idx)).from == pos + 1) {
change.from--;
change.to--;
change.removed.add(0, removed);
} else {
ArrayList removedList = new ArrayList<>();
removedList.add(removed);
addRemoveChanges.add(idx, new SubChange<>(pos, pos, removedList, EMPTY_PERM, false));
}
} else {
SubChange change = addRemoveChanges.get(idx);
change.to--; // Removed one element from the previously added list
if (change.from == change.to && (change.removed == null || change.removed.isEmpty())) {
addRemoveChanges.remove(idx);
}
}
for (int i = idx + 1; i < addRemoveChanges.size(); ++i) {
SubChange change = addRemoveChanges.get(i);
change.from--;
change.to--;
}
}
private void insertAdd(int from, int to) {
int idx = findSubChange(from, addRemoveChanges);
final int numberOfAdded = to - from;
if (idx < 0) { // Not found
idx = ~idx;
SubChange change;
if (idx > 0 && (change = addRemoveChanges.get(idx - 1)).to == from) {
change.to = to;
--idx;
} else {
addRemoveChanges.add(idx, new SubChange<>(from, to, new ArrayList(), EMPTY_PERM, false));
}
} else {
SubChange change = addRemoveChanges.get(idx);
change.to += numberOfAdded;
}
for (int i = idx + 1; i < addRemoveChanges.size(); ++i) {
SubChange change = addRemoveChanges.get(i);
change.from += numberOfAdded;
change.to += numberOfAdded;
}
}
private int compress(List> list) {
int removed = 0;
SubChange prev = list.get(0);
for (int i = 1, sz = list.size(); i < sz; ++i) {
SubChange cur = list.get(i);
if (prev.to == cur.from) {
prev.to = cur.to;
if (prev.removed != null || cur.removed != null) {
if (prev.removed == null) {
prev.removed = new ArrayList<>();
}
prev.removed.addAll(cur.removed);
}
list.set(i, null);
++removed;
} else {
prev = cur;
}
}
return removed;
}
private static class SubChange {
int from, to;
List removed;
int[] perm;
boolean updated;
public SubChange(int from, int to, List removed, int[] perm, boolean updated) {
this.from = from;
this.to = to;
this.removed = removed;
this.perm = perm;
this.updated = updated;
}
}
ListChangeBuilder(ObservableListBase list) {
this.list = list;
}
public void nextRemove(int idx, E removed) {
checkState();
checkAddRemoveList();
final SubChange last = addRemoveChanges.isEmpty() ? null
: addRemoveChanges.get(addRemoveChanges.size() - 1);
if (last != null && last.to == idx) {
last.removed.add(removed);
} else if (last != null && last.from == idx + 1) {
last.from--;
last.to--;
last.removed.add(0, removed);
} else {
insertRemoved(idx, removed);
}
if (updateChanges != null && !updateChanges.isEmpty()) {
int uPos = findSubChange(idx, updateChanges);
if (uPos < 0) {
uPos = ~uPos;
} else {
final SubChange change = updateChanges.get(uPos);
if (change.from == change.to - 1) {
updateChanges.remove(uPos);
} else {
change.to--;
++uPos; // Do the update from the next position
}
}
for (int i = uPos; i < updateChanges.size(); ++i) {
updateChanges.get(i).from--;
updateChanges.get(i).to--;
}
}
}
public void nextRemove(int idx, List extends E> removed) {
checkState();
for (int i = 0; i < removed.size(); ++i) {
nextRemove(idx, removed.get(i));
}
}
public void nextAdd(int from, int to) {
checkState();
checkAddRemoveList();
final SubChange last = addRemoveChanges.isEmpty() ? null :
addRemoveChanges.get(addRemoveChanges.size() - 1);
final int numberOfAdded = to - from;
if (last != null && last.to == from) {
last.to = to;
} else if (last != null && from >= last.from && from < last.to) { // Adding to the middle
last.to += numberOfAdded;
} else {
insertAdd(from, to);
}
if (updateChanges != null && !updateChanges.isEmpty()) {
int uPos = findSubChange(from, updateChanges);
if (uPos < 0) {
uPos = ~uPos;
} else {
// We have to split the change into 2
SubChange change = updateChanges.get(uPos);
updateChanges.add(uPos + 1, new SubChange(to, change.to + to - from, null, EMPTY_PERM, true));
change.to = from;
uPos += 2; // skip those 2 for the update
}
for (int i = uPos; i < updateChanges.size(); ++i) {
updateChanges.get(i).from += numberOfAdded;
updateChanges.get(i).to += numberOfAdded;
}
}
}
public void nextPermutation(int from, int to, int[] perm) {
checkState();
int prePermFrom = from;
int prePermTo = to;
int[] prePerm = perm;
if ((addRemoveChanges != null && !addRemoveChanges.isEmpty())) {
//Because there were already some changes to the list, we need
// to "reconstruct" the original list and create a permutation
// as-if there were no changes to the list. We can then
// merge this with the permutation we already did
// This maps elements from current list to the original list.
// -1 means the map was not in the original list.
// Note that for performance reasons, the map is permutated when created
// by the permutation. So it basically contains the order in which the original
// items were permutated by our new permutation.
int[] mapToOriginal = new int[list.size()];
// Marks the original-list indexes that were removed
Set removed = new TreeSet<>();
int last = 0;
int offset = 0;
for (int i = 0, sz = addRemoveChanges.size(); i < sz; ++i) {
SubChange change = addRemoveChanges.get(i);
for (int j = last; j < change.from; ++j) {
mapToOriginal[j < from || j >= to ? j : perm[j - from]] = j + offset;
}
for (int j = change.from; j < change.to; ++j) {
mapToOriginal[j < from || j >= to ? j : perm[j - from]] = -1;
}
last = change.to;
int removedSize = (change.removed != null ? change.removed.size() : 0);
for (int j = change.from + offset, upTo = change.from + offset + removedSize;
j < upTo; ++j) {
removed.add(j);
}
offset += removedSize - (change.to - change.from);
}
// from the last add/remove change to the end of the list
for (int i = last; i < mapToOriginal.length; ++i) {
mapToOriginal[i < from || i >= to ? i : perm[i - from]] = i + offset;
}
int[] newPerm = new int[list.size() + offset];
int mapPtr = 0;
for (int i = 0; i < newPerm.length; ++i) {
if (removed.contains(i)) {
newPerm[i] = i;
} else {
while(mapToOriginal[mapPtr] == -1) {
mapPtr++;
}
newPerm[mapToOriginal[mapPtr++]] = i;
}
}
// We could theoretically find the first and last items such that
// newPerm[i] != i and trim the permutation, but it is not necessary
prePermFrom = 0;
prePermTo = newPerm.length;
prePerm = newPerm;
}
if (permutationChange != null) {
if (prePermFrom == permutationChange.from && prePermTo == permutationChange.to) {
for (int i = 0; i < prePerm.length; ++i) {
permutationChange.perm[i] = prePerm[permutationChange.perm[i] - prePermFrom];
}
} else {
final int newTo = Math.max(permutationChange.to, prePermTo);
final int newFrom = Math.min(permutationChange.from, prePermFrom);
int[] newPerm = new int[newTo - newFrom];
for (int i = newFrom; i < newTo; ++i) {
if (i < permutationChange.from || i >= permutationChange.to) {
newPerm[i - newFrom] = prePerm[i - prePermFrom];
} else {
int p = permutationChange.perm[i - permutationChange.from];
if (p < prePermFrom || p >= prePermTo) {
newPerm[i - newFrom] = p;
} else {
newPerm[i - newFrom] = prePerm[p - prePermFrom];
}
}
}
permutationChange.from = newFrom;
permutationChange.to = newTo;
permutationChange.perm = newPerm;
}
} else {
permutationChange = new SubChange<>(prePermFrom, prePermTo, null, prePerm, false);
}
if ((addRemoveChanges != null && !addRemoveChanges.isEmpty())) {
Set newAdded = new TreeSet<>();
Map> newRemoved = new HashMap<>();
for (int i = 0, sz = addRemoveChanges.size(); i < sz; ++i) {
SubChange change = addRemoveChanges.get(i);
for (int cIndex = change.from; cIndex < change.to; ++cIndex) {
if (cIndex < from || cIndex >= to) {
newAdded.add(cIndex);
} else {
newAdded.add(perm[cIndex - from]);
}
}
if (change.removed != null) {
if (change.from < from || change.from >= to) {
newRemoved.put(change.from, change.removed);
} else {
newRemoved.put(perm[change.from - from], change.removed);
}
}
}
addRemoveChanges.clear();
SubChange lastChange = null;
for (Integer i : newAdded) {
if (lastChange == null || lastChange.to != i) {
lastChange = new SubChange<>(i, i + 1, null, EMPTY_PERM, false);
addRemoveChanges.add(lastChange);
} else {
lastChange.to = i + 1;
}
List removed = newRemoved.remove(i);
if (removed != null) {
if (lastChange.removed != null) {
lastChange.removed.addAll(removed);
} else {
lastChange.removed = removed;
}
}
}
for(Entry> e : newRemoved.entrySet()) {
final Integer at = e.getKey();
int idx = findSubChange(at, addRemoveChanges);
assert(idx < 0);
addRemoveChanges.add(~idx, new SubChange<>(at, at, e.getValue(), new int[0], false));
}
}
if (updateChanges != null && !updateChanges.isEmpty()) {
Set newUpdated = new TreeSet<>();
for (int i = 0, sz = updateChanges.size(); i < sz; ++i) {
SubChange change = updateChanges.get(i);
for (int cIndex = change.from; cIndex < change.to; ++cIndex) {
if (cIndex < from || cIndex >= to) {
newUpdated.add(cIndex);
} else {
newUpdated.add(perm[cIndex - from]);
}
}
}
updateChanges.clear();
SubChange lastUpdateChange = null;
for (Integer i : newUpdated) {
if (lastUpdateChange == null || lastUpdateChange.to != i) {
lastUpdateChange = new SubChange<>(i, i + 1, null, EMPTY_PERM, true);
updateChanges.add(lastUpdateChange);
} else {
lastUpdateChange.to = i + 1;
}
}
}
}
public void nextReplace(int from, int to, List extends E> removed) {
nextRemove(from, removed);
nextAdd(from, to);
}
public void nextSet(int idx, E old) {
nextRemove(idx, old);
nextAdd(idx, idx + 1);
}
public void nextUpdate(int idx) {
checkState();
if (updateChanges == null) {
updateChanges = new ArrayList<>();
}
final SubChange last = updateChanges.isEmpty() ? null : updateChanges.get(updateChanges.size() - 1);
if (last != null && last.to == idx) {
last.to = idx + 1;
} else {
insertUpdate(idx);
}
}
private void commit() {
final boolean addRemoveNotEmpty = addRemoveChanges != null && !addRemoveChanges.isEmpty();
final boolean updateNotEmpty = updateChanges != null && !updateChanges.isEmpty();
if (changeLock == 0
&& (addRemoveNotEmpty
|| updateNotEmpty
|| permutationChange != null)) {
int totalSize = (updateChanges != null ? updateChanges.size() : 0) +
(addRemoveChanges != null ? addRemoveChanges.size() : 0) + (permutationChange != null ? 1 : 0);
if (totalSize == 1) {
if (addRemoveNotEmpty) {
list.fireChange(new SingleChange<>(finalizeSubChange(addRemoveChanges.get(0)), list));
addRemoveChanges.clear();
} else if (updateNotEmpty) {
list.fireChange(new SingleChange<>(finalizeSubChange(updateChanges.get(0)), list));
updateChanges.clear();
} else {
list.fireChange(new SingleChange<>(finalizeSubChange(permutationChange), list));
permutationChange = null;
}
} else {
if (updateNotEmpty) {
int removed = compress(updateChanges);
totalSize -= removed;
}
if (addRemoveNotEmpty) {
int removed = compress(addRemoveChanges);
totalSize -= removed;
}
SubChange[] array = new SubChange[totalSize];
int ptr = 0;
if (permutationChange != null) {
array[ptr++] = permutationChange;
}
if (addRemoveNotEmpty) {
int sz = addRemoveChanges.size();
for (int i = 0; i < sz; ++i) {
final SubChange change = addRemoveChanges.get(i);
if (change != null) {
array[ptr++] = change;
}
}
}
if (updateNotEmpty) {
int sz = updateChanges.size();
for (int i = 0; i < sz; ++i) {
final SubChange change = updateChanges.get(i);
if (change != null) {
array[ptr++] = change;
}
}
}
list.fireChange(new IterableChange<>(finalizeSubChangeArray(array), list));
if (addRemoveChanges != null) addRemoveChanges.clear();
if (updateChanges != null) updateChanges.clear();
permutationChange = null;
}
}
}
public void beginChange() {
changeLock++;
}
public void endChange() {
if (changeLock <= 0) {
throw new IllegalStateException("Called endChange before beginChange");
}
changeLock--;
commit();
}
private static SubChange[] finalizeSubChangeArray(final SubChange[] changes) {
for (SubChange c : changes) {
finalizeSubChange(c);
}
return changes;
}
private static SubChange finalizeSubChange(final SubChange c) {
if (c.perm == null) {
c.perm = EMPTY_PERM;
}
if (c.removed == null) {
c.removed = Collections.emptyList();
} else {
c.removed = Collections.unmodifiableList(c.removed);
}
return c;
}
private static class SingleChange extends Change {
private final SubChange change;
private boolean onChange;
public SingleChange(SubChange change, ObservableListBase list) {
super(list);
this.change = change;
}
@Override
public boolean next() {
if (onChange) {
return false;
}
onChange = true;
return true;
}
@Override
public void reset() {
onChange = false;
}
@Override
public int getFrom() {
checkState();
return change.from;
}
@Override
public int getTo() {
checkState();
return change.to;
}
@Override
public List getRemoved() {
checkState();
return change.removed;
}
@Override
protected int[] getPermutation() {
checkState();
return change.perm;
}
@Override
public boolean wasUpdated() {
checkState();
return change.updated;
}
private void checkState() {
if (!onChange) {
throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change.");
}
}
@Override
public String toString() {
String ret;
if (change.perm.length != 0) {
ret = ChangeHelper.permChangeToString(change.perm);
} else if (change.updated) {
ret = ChangeHelper.updateChangeToString(change.from, change.to);
} else {
ret = ChangeHelper.addRemoveChangeToString(change.from, change.to, getList(), change.removed);
}
return "{ " + ret + " }";
}
}
private static class IterableChange extends Change {
private SubChange[] changes;
private int cursor = -1;
private IterableChange(SubChange[] changes, ObservableList list) {
super(list);
this.changes = changes;
}
@Override
public boolean next() {
if (cursor + 1 < changes.length) {
++cursor;
return true;
}
return false;
}
@Override
public void reset() {
cursor = -1;
}
@Override
public int getFrom() {
checkState();
return changes[cursor].from;
}
@Override
public int getTo() {
checkState();
return changes[cursor].to;
}
@Override
public List getRemoved() {
checkState();
return changes[cursor].removed;
}
@Override
protected int[] getPermutation() {
checkState();
return changes[cursor].perm;
}
@Override
public boolean wasUpdated() {
checkState();
return changes[cursor].updated;
}
private void checkState() {
if (cursor == -1) {
throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change.");
}
}
@Override
public String toString() {
int c = 0;
StringBuilder b = new StringBuilder();
b.append("{ ");
while (c < changes.length) {
if (changes[c].perm.length != 0) {
b.append(ChangeHelper.permChangeToString(changes[c].perm));
} else if (changes[c].updated) {
b.append(ChangeHelper.updateChangeToString(changes[c].from, changes[c].to));
} else {
b.append(ChangeHelper.addRemoveChangeToString(changes[c].from, changes[c].to, getList(), changes[c].removed));
}
if (c != changes.length - 1) {
b.append(", ");
}
++c;
}
b.append(" }");
return b.toString();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy