diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index cdf3d9c47..0c38b2b10 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -26,6 +26,7 @@ import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -37,6 +38,7 @@ import javafx.stage.Stage; import org.apache.commons.lang3.mutable.MutableObject; import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.util.CircularArrayList; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -134,7 +136,6 @@ public final class LogWindow extends Stage { while (logs.size() > config().getLogLines()) { Log removedLog = logs.removeFirst(); if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) { - // TODO: fix O(n) impl.listView.getItems().remove(0); } } @@ -162,6 +163,8 @@ public final class LogWindow extends Stage { LogWindowImpl() { getStyleClass().add("log-window"); + listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1))); + boolean flag = false; cboLines.getItems().setAll("10000", "5000", "2000", "500"); for (String i : cboLines.getItems()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CircularArrayList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CircularArrayList.java new file mode 100644 index 000000000..ad086256e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CircularArrayList.java @@ -0,0 +1,348 @@ +package org.jackhuang.hmcl.util; + +import org.jetbrains.annotations.Contract; + +import java.util.*; + +/** + * @author Glavo + */ +@SuppressWarnings("unchecked") +public final class CircularArrayList extends AbstractList implements RandomAccess { + + private static final int DEFAULT_CAPACITY = 10; + private static final Object[] EMPTY_ARRAY = new Object[0]; + + private Object[] elements; + private int begin = -1; + private int end = 0; + + public CircularArrayList() { + this.elements = EMPTY_ARRAY; + } + + public CircularArrayList(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("illegal initialCapacity: " + initialCapacity); + } + + this.elements = initialCapacity == 0 ? EMPTY_ARRAY : new Object[initialCapacity]; + } + + private static int inc(int i, int capacity) { + return i + 1 >= capacity ? 0 : i + 1; + } + + private static int inc(int i, int distance, int capacity) { + if ((i += distance) - capacity >= 0) { + i -= capacity; + } + + return i; + } + + private static int dec(int i, int capacity) { + return i - 1 < 0 ? capacity - 1 : i - 1; + } + + private static int sub(int i, int distance, int capacity) { + if ((i -= distance) < 0) { + i += capacity; + } + return i; + } + + private void grow() { + grow(elements.length + 1); + } + + private void grow(int minCapacity) { + final int oldCapacity = elements.length; + final int size = size(); + final int newCapacity = newCapacity(oldCapacity, minCapacity); + + final Object[] newElements; + if (size == 0) { + newElements = new Object[newCapacity]; + } else if (begin < end) { + newElements = Arrays.copyOf(elements, newCapacity, Object[].class); + } else { + newElements = new Object[newCapacity]; + System.arraycopy(elements, begin, newElements, 0, elements.length - begin); + System.arraycopy(elements, 0, newElements, elements.length - begin, end); + begin = 0; + end = size; + } + this.elements = newElements; + } + + private static int newCapacity(int oldCapacity, int minCapacity) { + return oldCapacity == 0 + ? Math.max(DEFAULT_CAPACITY, minCapacity) + : Math.max(Math.max(oldCapacity, minCapacity), oldCapacity + (oldCapacity >> 1)); + } + + private static void checkElementIndex(int index, int size) throws IndexOutOfBoundsException { + if (index < 0 || index >= size) { + // Optimized for execution by hotspot + checkElementIndexFailed(index, size); + } + } + + @Contract("_, _ -> fail") + private static void checkElementIndexFailed(int index, int size) { + if (size < 0) { + throw new IllegalArgumentException("size(" + size + ") < 0"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index(" + index + ") < 0"); + } + if (index >= size) { + throw new IndexOutOfBoundsException("index(" + index + ") >= size(" + size + ")"); + } + throw new AssertionError(); + } + + private static void checkPositionIndex(int index, int size) throws IndexOutOfBoundsException { + if (index < 0 || index > size) { + // Optimized for execution by hotspot + checkPositionIndexFailed(index, size); + } + } + + @Contract("_, _ -> fail") + private static void checkPositionIndexFailed(int index, int size) { + if (size < 0) { + throw new IllegalArgumentException("size(" + size + ") < 0"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index(" + index + ") < 0"); + } + if (index > size) { + throw new IndexOutOfBoundsException("index(" + index + ") > size(" + size + ")"); + } + throw new AssertionError(); + } + + @Override + public boolean isEmpty() { + return begin == -1; + } + + @Override + public int size() { + if (isEmpty()) { + return 0; + } else if (begin < end) { + return end - begin; + } else { + return elements.length - begin + end; + } + } + + @Override + public E get(int index) { + if (isEmpty()) { + throw new IndexOutOfBoundsException("Index out of range: " + index); + } else if (begin < end) { + checkElementIndex(index, end - begin); + return (E) elements[begin + index]; + } else { + checkElementIndex(index, elements.length - begin + end); + return (E) elements[inc(begin, index, elements.length)]; + } + } + + @Override + public E set(int index, E element) { + int arrayIndex; + if (isEmpty()) { + throw new IndexOutOfBoundsException(); + } else if (begin < end) { + checkElementIndex(index, end - begin); + arrayIndex = begin + index; + } else { + final int size = elements.length - begin + end; + checkElementIndex(index, size); + arrayIndex = inc(begin, index, elements.length); + } + + E oldValue = (E) elements[arrayIndex]; + elements[arrayIndex] = element; + return oldValue; + } + + @Override + public void add(int index, E element) { + if (index == 0) { + addFirst(element); + return; + } + + final int oldSize = size(); + if (index == oldSize) { + addLast(element); + return; + } + + checkPositionIndex(index, oldSize); + + if (oldSize == elements.length) { + grow(); + } + + if (begin < end) { + final int targetIndex = begin + index; + if (end < elements.length) { + System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex); + end++; + } else { + System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin + 1); + begin--; + } + elements[targetIndex] = element; + } else { + int targetIndex = inc(begin, index, elements.length); + if (targetIndex <= end) { + System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex); + elements[targetIndex] = element; + end++; + } else { + System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin); + elements[targetIndex - 1] = element; + begin--; + } + } + } + + @Override + public E remove(int index) { + final int oldSize = size(); + checkElementIndex(index, oldSize); + + if (index == 0) { + return removeFirst(); + } + + if (index == oldSize - 1) { + return removeLast(); + } + + final Object res; + + if (begin < end) { + final int targetIndex = begin + index; + res = elements[targetIndex]; + System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1); + end--; + } else { + final int targetIndex = inc(begin, index, elements.length); + res = elements[targetIndex]; + if (targetIndex < end) { + System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1); + end--; + } else { + System.arraycopy(elements, begin, elements, begin + 1, targetIndex - begin); + begin = inc(begin, elements.length); + } + } + + return (E) res; + } + + @Override + public void clear() { + if (isEmpty()) { + return; + } + + if (begin < end) { + Arrays.fill(elements, begin, end, null); + } else { + Arrays.fill(elements, 0, end, null); + Arrays.fill(elements, begin, elements.length, null); + } + + begin = -1; + end = 0; + } + + // Deque + + public void addFirst(E e) { + final int oldSize = size(); + if (oldSize == elements.length) { + grow(); + } + + if (oldSize == 0) { + begin = elements.length - 1; + } else { + begin = dec(begin, elements.length); + } + elements[begin] = e; + } + + public void addLast(E e) { + final int oldSize = size(); + if (oldSize == elements.length) { + grow(); + } + elements[end] = e; + end = inc(end, elements.length); + + if (oldSize == 0) { + begin = 0; + } + } + + public E removeFirst() { + final int oldSize = size(); + if (oldSize == 0) { + throw new NoSuchElementException(); + } + + Object res = elements[begin]; + elements[begin] = null; + + if (oldSize == 1) { + begin = -1; + end = 0; + } else { + begin = inc(begin, elements.length); + } + return (E) res; + } + + public E removeLast() { + final int oldSize = size(); + if (oldSize == 0) { + throw new NoSuchElementException(); + } + final int lastIdx = dec(end, elements.length); + E res = (E) elements[lastIdx]; + elements[lastIdx] = null; + + if (oldSize == 1) { + begin = -1; + end = 0; + } else { + end = lastIdx; + } + return res; + } + + public E getFirst() { + if (isEmpty()) + throw new NoSuchElementException(); + + return get(0); + } + + public E getLast() { + if (isEmpty()) + throw new NoSuchElementException(); + + return get(size() - 1); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/CircularArrayListTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/CircularArrayListTest.java new file mode 100644 index 000000000..79aeb7f0d --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/CircularArrayListTest.java @@ -0,0 +1,240 @@ +package org.jackhuang.hmcl.util; + +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +public class CircularArrayListTest { + + private static void assertEmpty(CircularArrayList list) { + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + assertThrows(NoSuchElementException.class, () -> list.getFirst()); + assertThrows(NoSuchElementException.class, () -> list.getLast()); + assertThrows(NoSuchElementException.class, () -> list.removeFirst()); + assertThrows(NoSuchElementException.class, () -> list.removeLast()); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(0)); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(10)); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + } + + private static void assertListEquals(List expected, CircularArrayList actual) { + assertIterableEquals(expected, actual); + if (expected.isEmpty()) { + assertEmpty(actual); + } else { + assertEquals(expected.get(0), actual.getFirst()); + assertEquals(expected.get(expected.size() - 1), actual.getLast()); + assertThrows(IndexOutOfBoundsException.class, () -> actual.get(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> actual.get(actual.size())); + } + } + + @Test + public void testEmpty() { + CircularArrayList list = new CircularArrayList<>(); + assertEmpty(list); + } + + @Test + public void testSequential() { + Helper helper = new Helper<>(); + + helper.addAll("str0", "str1", "str2"); + helper.add("str3"); + helper.add(2, "str4"); + helper.remove(1); + helper.remove(0); + helper.removeFirst(); + helper.removeLast(); + helper.remove(0); + assertEmpty(helper.list); + } + + @Test + public void testSequentialExpansion() { + Helper helper = new Helper<>(); + Random random = new Random(0); + for (int i = 0; i < 5; i++) { + helper.add("str" + i); + } + + for (int i = 5; i < 100; i++) { + helper.add(random.nextInt(helper.size()) + 1, "str" + i); + } + + for (int i = 0; i < 100; i++) { + helper.set(random.nextInt(helper.size()), "new str " + i); + } + + for (int i = 0; i < 20; i++) { + helper.remove(random.nextInt(helper.size())); + } + + for (int i = 0; i < 20; i++) { + helper.removeFirst(); + helper.removeLast(); + } + + int remaining = helper.size(); + for (int i = 0; i < remaining; i++) { + helper.removeLast(); + } + } + + @Test + public void testLoopback() { + Helper helper = new Helper<>(); + + helper.addAll("str3", "str4", "str5"); + helper.addAll(0, "str0", "str1", "str2"); + helper.remove(1); + helper.remove(4); + helper.removeFirst(); + helper.removeLast(); + helper.remove(1); + helper.remove(0); + assertEmpty(helper.list); + } + + @Test + public void testLoopbackExpansion() { + Helper helper = new Helper<>(); + Random random = new Random(0); + + for (int i = 5; i < 10; i++) { + helper.add("str" + i); + } + for (int i = 4; i >= 0; i--) { + helper.add(0, "str" + i); + } + + for (int i = 10; i < 100; i++) { + helper.add(random.nextInt(helper.size() + 1), "str" + i); + } + + for (int i = 0; i < 100; i++) { + helper.set(random.nextInt(helper.size()), "new str " + i); + } + + for (int i = 0; i < 20; i++) { + helper.remove(random.nextInt(helper.size())); + } + + for (int i = 0; i < 20; i++) { + helper.removeFirst(); + helper.removeLast(); + } + + int remaining = helper.size(); + for (int i = 0; i < remaining; i++) { + helper.removeLast(); + } + } + + @Test + public void testClear() { + CircularArrayList list = new CircularArrayList<>(); + list.clear(); + assertEmpty(list); + + for (int i = 0; i < 20; i++) { + list.add("str" + i); + } + list.clear(); + assertEmpty(list); + + for (int i = 10; i < 20; i++) { + list.add("str" + i); + } + for (int i = 9; i >= 0; i--) { + list.addFirst("str" + i); + } + list.clear(); + assertEmpty(list); + } + + private static final class Helper { + final List expected; + final CircularArrayList list; + + Helper() { + this.expected = new ArrayList<>(); + this.list = new CircularArrayList<>(); + + assertStatus(); + } + + Helper(List expected, CircularArrayList list) { + this.expected = expected; + this.list = list; + + assertStatus(); + } + + void assertStatus() { + assertListEquals(expected, list); + } + + int size() { + return expected.size(); + } + + void set(int i, E e) { + assertEquals(expected.set(i, e), list.set(i, e)); + assertStatus(); + } + + void add(E e) { + expected.add(e); + list.add(e); + assertStatus(); + } + + void add(int i, E e) { + expected.add(i, e); + list.add(i, e); + assertStatus(); + } + + @SafeVarargs + final void addAll(E... values) { + Collections.addAll(expected, values); + Collections.addAll(list, values); + assertStatus(); + } + + @SafeVarargs + final void addAll(int i, E... values) { + List valuesList = Arrays.asList(values); + assertEquals(expected.addAll(i, valuesList), list.addAll(i, valuesList)); + assertStatus(); + } + + void remove(int idx) { + assertEquals(expected.remove(idx), list.remove(idx)); + assertStatus(); + } + + void removeFirst() { + assertEquals(expected.remove(0), list.removeFirst()); + assertStatus(); + } + + void removeLast() { + assertEquals(expected.remove(expected.size() - 1), list.removeLast()); + assertStatus(); + } + + void clear() { + expected.clear(); + list.clear(); + assertEmpty(list); + } + } +}