优化 TaskListPane (#4292)
This commit is contained in:
@@ -17,15 +17,20 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.construct;
|
package org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXListView;
|
||||||
import com.jfoenix.controls.JFXProgressBar;
|
import com.jfoenix.controls.JFXProgressBar;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.WeakListener;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.DoubleBinding;
|
import javafx.beans.binding.DoubleBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||||
@@ -64,9 +69,9 @@ import org.jackhuang.hmcl.task.TaskListener;
|
|||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.util.FXThread;
|
import org.jackhuang.hmcl.util.FXThread;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -80,15 +85,17 @@ public final class TaskListPane extends StackPane {
|
|||||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
||||||
|
|
||||||
private TaskExecutor executor;
|
private TaskExecutor executor;
|
||||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
private final JFXListView<Node> listView = new JFXListView<>();
|
||||||
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
|
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
|
||||||
private final Map<String, StageNode> stageNodes = new HashMap<>();
|
private final Map<String, StageNode> stageNodes = new HashMap<>();
|
||||||
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
||||||
|
|
||||||
public TaskListPane() {
|
private Cell lastCell;
|
||||||
listBox.setSpacing(0);
|
|
||||||
|
|
||||||
getChildren().setAll(listBox);
|
public TaskListPane() {
|
||||||
|
listView.setPadding(new Insets(12, 0, 0, 0));
|
||||||
|
listView.setCellFactory(l -> new Cell());
|
||||||
|
getChildren().setAll(listView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXThread
|
@FXThread
|
||||||
@@ -96,7 +103,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
for (String stage : stages) {
|
for (String stage : stages) {
|
||||||
stageNodes.computeIfAbsent(stage, s -> {
|
stageNodes.computeIfAbsent(stage, s -> {
|
||||||
StageNode node = new StageNode(stage);
|
StageNode node = new StageNode(stage);
|
||||||
listBox.add(node);
|
listView.getItems().add(node);
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,7 +121,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stageNodes.clear();
|
stageNodes.clear();
|
||||||
listBox.clear();
|
listView.getItems().clear();
|
||||||
addStages(executor.getStages());
|
addStages(executor.getStages());
|
||||||
updateProgressNodePadding();
|
updateProgressNodePadding();
|
||||||
});
|
});
|
||||||
@@ -193,7 +200,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
ProgressListNode node = new ProgressListNode(task);
|
ProgressListNode node = new ProgressListNode(task);
|
||||||
nodes.put(task, node);
|
nodes.put(task, node);
|
||||||
StageNode stageNode = stageNodes.get(task.getInheritedStage());
|
StageNode stageNode = stageNodes.get(task.getInheritedStage());
|
||||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
listView.getItems().add(listView.getItems().indexOf(stageNode) + 1, node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +214,10 @@ public final class TaskListPane extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProgressListNode node = nodes.remove(task);
|
ProgressListNode node = nodes.remove(task);
|
||||||
if (node == null)
|
if (node != null) {
|
||||||
return;
|
node.unbind();
|
||||||
node.unbind();
|
listView.getItems().remove(node);
|
||||||
listBox.remove(node);
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,57 +259,199 @@ public final class TaskListPane extends StackPane {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class StageNode extends BorderPane {
|
private final class Cell extends ListCell<Node> {
|
||||||
private final String stage;
|
private static final double STATUS_ICON_SIZE = 14;
|
||||||
|
|
||||||
|
private final BorderPane pane = new BorderPane();
|
||||||
|
private final StackPane left = new StackPane();
|
||||||
private final Label title = new Label();
|
private final Label title = new Label();
|
||||||
|
private final Label message = new Label();
|
||||||
|
private final JFXProgressBar bar = new JFXProgressBar();
|
||||||
|
|
||||||
|
private WeakReference<StageNode> prevStageNodeRef;
|
||||||
|
private ChangeListener<StageNode.Status> statusChangeListener;
|
||||||
|
|
||||||
|
private Cell() {
|
||||||
|
setPadding(Insets.EMPTY);
|
||||||
|
|
||||||
|
FXUtils.setLimitHeight(left, STATUS_ICON_SIZE);
|
||||||
|
FXUtils.setLimitWidth(left, STATUS_ICON_SIZE);
|
||||||
|
|
||||||
|
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||||
|
BorderPane.setMargin(left, new Insets(0, 12, 0, 0));
|
||||||
|
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||||
|
pane.setCenter(title);
|
||||||
|
|
||||||
|
DoubleBinding barWidth = Bindings.createDoubleBinding(() -> {
|
||||||
|
Insets padding = pane.getPadding();
|
||||||
|
Insets insets = pane.getInsets();
|
||||||
|
return pane.getWidth() - padding.getLeft() - padding.getRight() - insets.getLeft() - insets.getRight();
|
||||||
|
}, pane.paddingProperty(), pane.widthProperty());
|
||||||
|
bar.minWidthProperty().bind(barWidth);
|
||||||
|
bar.prefWidthProperty().bind(barWidth);
|
||||||
|
bar.maxWidthProperty().bind(barWidth);
|
||||||
|
|
||||||
|
setGraphic(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLeftIcon(StageNode.Status status) {
|
||||||
|
left.getChildren().setAll(status.svg.createIcon(Theme.blackFill(), STATUS_ICON_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Node item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||||
|
if (this == lastCell && !isVisible())
|
||||||
|
return;
|
||||||
|
lastCell = this;
|
||||||
|
|
||||||
|
pane.paddingProperty().unbind();
|
||||||
|
title.textProperty().unbind();
|
||||||
|
message.textProperty().unbind();
|
||||||
|
bar.progressProperty().unbind();
|
||||||
|
|
||||||
|
StageNode prevStageNode;
|
||||||
|
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||||
|
prevStageNode.status.removeListener(statusChangeListener);
|
||||||
|
|
||||||
|
if (item instanceof ProgressListNode) {
|
||||||
|
var progressListNode = (ProgressListNode) item;
|
||||||
|
title.setText(progressListNode.title);
|
||||||
|
message.textProperty().bind(progressListNode.message);
|
||||||
|
bar.progressProperty().bind(progressListNode.progress);
|
||||||
|
|
||||||
|
pane.paddingProperty().bind(progressNodePadding);
|
||||||
|
pane.setLeft(null);
|
||||||
|
pane.setRight(message);
|
||||||
|
pane.setBottom(bar);
|
||||||
|
} else if (item instanceof StageNode) {
|
||||||
|
var stageNode = (StageNode) item;
|
||||||
|
title.textProperty().bind(stageNode.title);
|
||||||
|
message.setText("");
|
||||||
|
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||||
|
|
||||||
|
pane.setPadding(Insets.EMPTY);
|
||||||
|
pane.setLeft(left);
|
||||||
|
pane.setRight(message);
|
||||||
|
pane.setBottom(null);
|
||||||
|
|
||||||
|
updateLeftIcon(stageNode.status.get());
|
||||||
|
if (statusChangeListener == null)
|
||||||
|
statusChangeListener = new StatusChangeListener(this);
|
||||||
|
stageNode.status.addListener(statusChangeListener);
|
||||||
|
prevStageNodeRef = new WeakReference<>(stageNode);
|
||||||
|
} else { // item == null
|
||||||
|
title.setText("");
|
||||||
|
message.setText("");
|
||||||
|
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||||
|
pane.setPadding(Insets.EMPTY);
|
||||||
|
pane.setLeft(null);
|
||||||
|
pane.setRight(null);
|
||||||
|
pane.setBottom(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StatusChangeListener implements ChangeListener<StageNode.Status>, WeakListener {
|
||||||
|
|
||||||
|
private final WeakReference<Cell> cellRef;
|
||||||
|
|
||||||
|
private StatusChangeListener(Cell cell) {
|
||||||
|
this.cellRef = new WeakReference<>(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wasGarbageCollected() {
|
||||||
|
return cellRef.get() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends StageNode.Status> observable,
|
||||||
|
StageNode.Status oldValue,
|
||||||
|
StageNode.Status newValue) {
|
||||||
|
Cell cell = cellRef.get();
|
||||||
|
if (cell == null) {
|
||||||
|
if (observable != null)
|
||||||
|
observable.removeListener(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cell.updateLeftIcon(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static abstract class Node {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StageNode extends Node {
|
||||||
|
private enum Status {
|
||||||
|
WAITING(SVG.MORE_HORIZ),
|
||||||
|
RUNNING(SVG.ARROW_FORWARD),
|
||||||
|
SUCCESS(SVG.CHECK),
|
||||||
|
FAILED(SVG.CLOSE);
|
||||||
|
|
||||||
|
private final SVG svg;
|
||||||
|
|
||||||
|
Status(SVG svg) {
|
||||||
|
this.svg = svg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.WAITING);
|
||||||
|
private final StringProperty title = new SimpleStringProperty();
|
||||||
private final String message;
|
private final String message;
|
||||||
private int count = 0;
|
private int count = 0;
|
||||||
private int total = 0;
|
private int total = 0;
|
||||||
private boolean started = false;
|
|
||||||
|
|
||||||
public StageNode(String stage) {
|
private StageNode(String stage) {
|
||||||
this.stage = stage;
|
String stageKey;
|
||||||
|
String stageValue;
|
||||||
|
|
||||||
String stageKey = StringUtils.substringBefore(stage, ':');
|
int idx = stage.indexOf(':');
|
||||||
String stageValue = StringUtils.substringAfter(stage, ':');
|
if (idx >= 0) {
|
||||||
|
stageKey = stage.substring(0, idx);
|
||||||
|
stageValue = stage.substring(idx + 1);
|
||||||
|
} else {
|
||||||
|
stageKey = stage;
|
||||||
|
stageValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHECKSTYLE:OFF
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
switch (stageKey) {
|
switch (stageKey) {
|
||||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||||
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
||||||
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
||||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||||
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
|
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
|
||||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||||
default: message = i18n(stageKey); break;
|
default: message = i18n(stageKey); break;
|
||||||
}
|
}
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
// CHECKSTYLE:ON
|
||||||
|
|
||||||
title.setText(message);
|
title.set(message);
|
||||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
|
||||||
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
|
|
||||||
setPadding(new Insets(0, 0, 8, 4));
|
|
||||||
setCenter(title);
|
|
||||||
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void begin() {
|
private void begin() {
|
||||||
if (started) return;
|
if (status.get() == Status.WAITING) {
|
||||||
started = true;
|
status.set(Status.RUNNING);
|
||||||
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fail() {
|
public void fail() {
|
||||||
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
|
status.set(Status.FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void succeed() {
|
public void succeed() {
|
||||||
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
|
status.set(Status.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void count() {
|
public void count() {
|
||||||
@@ -315,46 +464,33 @@ public final class TaskListPane extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateCounter(int count, int total) {
|
public void updateCounter(int count, int total) {
|
||||||
if (total > 0)
|
title.setValue(total > 0
|
||||||
title.setText(String.format("%s - %d/%d", message, count, total));
|
? message + " - " + count + "/" + total
|
||||||
else
|
: message
|
||||||
title.setText(message);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ProgressListNode extends BorderPane {
|
private static final class ProgressListNode extends Node {
|
||||||
private final JFXProgressBar bar = new JFXProgressBar();
|
private final String title;
|
||||||
private final Label title = new Label();
|
private final StringProperty message = new SimpleStringProperty("");
|
||||||
private final Label state = new Label();
|
private final DoubleProperty progress = new SimpleDoubleProperty(0.0);
|
||||||
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
|
|
||||||
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
|
|
||||||
paddingProperty(), widthProperty());
|
|
||||||
|
|
||||||
public ProgressListNode(Task<?> task) {
|
private ProgressListNode(Task<?> task) {
|
||||||
bar.progressProperty().bind(task.progressProperty());
|
this.title = task.getName();
|
||||||
title.setText(task.getName());
|
message.bind(task.messageProperty());
|
||||||
state.textProperty().bind(task.messageProperty());
|
progress.bind(task.progressProperty());
|
||||||
|
|
||||||
setLeft(title);
|
|
||||||
setRight(state);
|
|
||||||
setBottom(bar);
|
|
||||||
|
|
||||||
bar.minWidthProperty().bind(binding);
|
|
||||||
bar.prefWidthProperty().bind(binding);
|
|
||||||
bar.maxWidthProperty().bind(binding);
|
|
||||||
|
|
||||||
paddingProperty().bind(progressNodePadding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unbind() {
|
public void unbind() {
|
||||||
bar.progressProperty().unbind();
|
message.unbind();
|
||||||
state.textProperty().unbind();
|
progress.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThrowable(Throwable throwable) {
|
public void setThrowable(Throwable throwable) {
|
||||||
unbind();
|
unbind();
|
||||||
state.setText(throwable.getLocalizedMessage());
|
message.set(throwable.getLocalizedMessage());
|
||||||
bar.setProgress(0);
|
progress.set(0.);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user