mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-01-24 14:34:15 +08:00
改进 LogWindow (#1971)
* Add CircularArrayList * fix test * update * update
This commit is contained in:
parent
94b54f3dce
commit
461cde3282
@ -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())
|
||||
|
@ -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<E> extends AbstractList<E> 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);
|
||||
}
|
||||
}
|
@ -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<String> list = new CircularArrayList<>();
|
||||
assertEmpty(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSequential() {
|
||||
Helper<String> 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<String> 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<String> 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<String> 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<String> 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<E> {
|
||||
final List<E> expected;
|
||||
final CircularArrayList<E> list;
|
||||
|
||||
Helper() {
|
||||
this.expected = new ArrayList<>();
|
||||
this.list = new CircularArrayList<>();
|
||||
|
||||
assertStatus();
|
||||
}
|
||||
|
||||
Helper(List<E> expected, CircularArrayList<E> 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<E> 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user