mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2026-10-16 21:21:33 +08:00
Move modpack file controls into reusable tree cells (#6840)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Displays a [CheckBoxTreeItem] with a material-design checkbox.
|
||||
///
|
||||
/// The checkbox's selected and indeterminate states are bidirectionally bound to
|
||||
/// the current tree item. Reusing or clearing the cell removes the old bindings.
|
||||
/// Installing a checkbox skin or switching tree items applies the selection state
|
||||
/// immediately, without playing a selection animation.
|
||||
/// Ordinary tree items retain the display provided by [JFXTreeCell].
|
||||
/// Empty cells and `null` values display neither content nor a checkbox.
|
||||
///
|
||||
/// @param <T> the type of value stored in each tree item
|
||||
@NotNullByDefault
|
||||
public class JFXCheckTreeCell<T> extends JFXTreeCell<T> {
|
||||
|
||||
/// The checkbox bound to the currently displayed checkable tree item.
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
|
||||
/// Places the checkbox before the graphic supplied by the superclass.
|
||||
private final HBox graphic = new HBox();
|
||||
|
||||
/// The tree item whose selection properties are currently bound, if any.
|
||||
private @Nullable CheckBoxTreeItem<?> boundTreeItem;
|
||||
|
||||
/// Creates an empty cell whose checkbox toggles the current tree item's selection.
|
||||
public JFXCheckTreeCell() {
|
||||
checkBox.setAllowIndeterminate(false);
|
||||
checkBox.setFocusTraversable(false);
|
||||
graphic.setAlignment(Pos.CENTER_LEFT);
|
||||
graphic.setPickOnBounds(false);
|
||||
checkBox.skinProperty().addListener(observable -> finishSelectionAnimation());
|
||||
treeItemProperty().addListener(observable -> updateDisplay(getItem(), isEmpty()));
|
||||
}
|
||||
|
||||
/// Finishes animations from the previous item and immediately displays the current state.
|
||||
private void finishSelectionAnimation() {
|
||||
if (checkBox.getSkin() instanceof JFXCheckBoxSkin skin) {
|
||||
skin.finishSelectionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the checkbox to the inherited display and updates its selection bindings.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
// Release graphics before the superclass reuses or reparents them.
|
||||
graphic.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
@Nullable CheckBoxTreeItem<?> treeItem = !empty && item != null
|
||||
&& getTreeItem() instanceof CheckBoxTreeItem<?> checkTreeItem ? checkTreeItem : null;
|
||||
if (boundTreeItem != treeItem) {
|
||||
if (boundTreeItem != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(boundTreeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().unbindBidirectional(boundTreeItem.indeterminateProperty());
|
||||
}
|
||||
boundTreeItem = treeItem;
|
||||
if (treeItem != null) {
|
||||
checkBox.selectedProperty().bindBidirectional(treeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(treeItem.indeterminateProperty());
|
||||
} else {
|
||||
checkBox.setSelected(false);
|
||||
checkBox.setIndeterminate(false);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
}
|
||||
|
||||
if (treeItem != null) {
|
||||
@Nullable Node itemGraphic = getGraphic();
|
||||
graphic.getChildren().add(checkBox);
|
||||
if (itemGraphic != null) {
|
||||
graphic.getChildren().add(itemGraphic);
|
||||
}
|
||||
setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -83,8 +84,7 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
treeItemRef = new WeakReference<>(newTreeItem);
|
||||
}
|
||||
};
|
||||
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
|
||||
treeItemProperty().addListener(weakTreeItemListener);
|
||||
treeItemProperty().addListener(treeItemInvalidationListener);
|
||||
if (getTreeItem() != null) {
|
||||
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
|
||||
}
|
||||
@@ -104,13 +104,20 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
selectedPane.setVisible(isSelected());
|
||||
}
|
||||
|
||||
private void updateDisplay(T item, boolean empty) {
|
||||
/// Updates the text and graphic for the current value and tree item.
|
||||
///
|
||||
/// @implSpec Called when the cell value or the current tree item's graphic changes.
|
||||
/// Subclasses may override this method to decorate the default display.
|
||||
///
|
||||
/// @param item the cell value, or `null` to clear the display
|
||||
/// @param empty whether the cell has no item
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
if (item == null || empty) {
|
||||
hbox = null;
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
TreeItem<T> treeItem = getTreeItem();
|
||||
@Nullable TreeItem<T> treeItem = getTreeItem();
|
||||
if (treeItem != null && treeItem.getGraphic() != null) {
|
||||
if (item instanceof Node) {
|
||||
setText(null);
|
||||
|
||||
@@ -169,11 +169,8 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
};
|
||||
}
|
||||
|
||||
private void playSelectAnimation(Boolean selection) {
|
||||
if (selection == null) {
|
||||
selection = false;
|
||||
}
|
||||
|
||||
/// Animates a selection change, or applies it immediately when animations are disabled.
|
||||
private void playSelectAnimation(boolean selection) {
|
||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||
|
||||
this.box.setBorder(new Border(new BorderStroke(
|
||||
@@ -183,17 +180,7 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
new BorderWidths(this.lineThick))));
|
||||
|
||||
if (!AnimationUtils.isAnimationEnabled()) {
|
||||
if (selection) {
|
||||
this.mark.setVisible(true);
|
||||
this.mark.setScaleX(1.0);
|
||||
this.mark.setScaleY(1.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, control.getCheckedColor());
|
||||
} else {
|
||||
this.mark.setVisible(false);
|
||||
this.mark.setScaleX(0.0);
|
||||
this.mark.setScaleY(0.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, Color.TRANSPARENT);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,6 +190,22 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
this.select.play();
|
||||
}
|
||||
|
||||
/// Stops selection animations and immediately displays the checkbox's current selected state.
|
||||
///
|
||||
/// The check mark, fill, and border are updated even if no animation is running.
|
||||
/// This also suppresses the initial selection animation on the first layout.
|
||||
/// The skin must not have been disposed.
|
||||
public void finishSelectionAnimation() {
|
||||
transition.stop();
|
||||
select.stop();
|
||||
invalid = false;
|
||||
boolean selected = getSkinnable().isSelected();
|
||||
mark.setVisible(selected);
|
||||
mark.setScaleX(selected ? 1.0 : 0.0);
|
||||
mark.setScaleY(selected ? 1.0 : 0.0);
|
||||
updateColors();
|
||||
}
|
||||
|
||||
private void createFillTransition() {
|
||||
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXCheckTreeCell;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -66,13 +67,14 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
private final ModAdviser adviser;
|
||||
private @Nullable ModpackFileTreeItem rootNode;
|
||||
|
||||
/// Creates a file-selection page and starts loading the instance's exportable files.
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameInstance gameInstance, ModAdviser adviser) {
|
||||
this.controller = controller;
|
||||
this.gameInstance = gameInstance;
|
||||
this.adviser = adviser;
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
treeView.setCellFactory(__ -> new ModpackFileTreeCell());
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
onEscPressed(treeView, () -> controller.onPrev(true));
|
||||
|
||||
@@ -196,14 +198,16 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return node;
|
||||
}
|
||||
|
||||
private void getFilesNeeded(ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
/// Appends selected paths relative to `minecraft/`, traversing partially selected directories.
|
||||
/// A `null` node contributes no paths.
|
||||
private void getFilesNeeded(@Nullable ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
if (node == null) return;
|
||||
if (node.isSelected() || node.isIndeterminate()) {
|
||||
if (basePath.length() > "minecraft/".length())
|
||||
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
|
||||
for (TreeItem<String> child : node.getChildren()) {
|
||||
if (child instanceof ModpackFileTreeItem mChild) {
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getFileName(), list);
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getValue(), list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,39 +248,69 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
pair("minecraft/scripts", i18n("modpack.files.scripts"))
|
||||
);
|
||||
|
||||
/// Stores a file name, optional explanation, and hierarchical selection state.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeItem extends CheckBoxTreeItem<String> {
|
||||
|
||||
private final String fileName;
|
||||
/// The localized explanation for a recognized path, or `null` if none exists.
|
||||
private final @Nullable String comment;
|
||||
|
||||
/// Creates an item whose value is the file name; only the `minecraft` root starts expanded.
|
||||
public ModpackFileTreeItem(String fileName, String basePath) {
|
||||
this.fileName = fileName;
|
||||
|
||||
HBox graphic = new HBox();
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
checkBox.selectedProperty().bindBidirectional(this.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(this.indeterminateProperty());
|
||||
graphic.getChildren().add(checkBox);
|
||||
|
||||
{
|
||||
Label text = new Label(fileName);
|
||||
text.setMouseTransparent(true);
|
||||
graphic.getChildren().add(text);
|
||||
}
|
||||
if (TRANSLATION.containsKey(basePath)) {
|
||||
Label comment = new Label(TRANSLATION.get(basePath));
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
graphic.getChildren().add(comment);
|
||||
}
|
||||
graphic.setPickOnBounds(false);
|
||||
super(fileName);
|
||||
this.comment = TRANSLATION.get(basePath);
|
||||
this.setExpanded("minecraft".equals(basePath));
|
||||
this.setValue(""); // To disable the default display of text
|
||||
this.setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders file names and localized explanations alongside the bound checkbox.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeCell extends JFXCheckTreeCell<String> {
|
||||
|
||||
/// Contains the inherited checkbox graphic followed by the file labels.
|
||||
private final HBox content = new HBox();
|
||||
|
||||
/// Displays the current file name.
|
||||
private final Label fileName = new Label();
|
||||
|
||||
/// Displays the current path's optional explanation.
|
||||
private final Label comment = new Label();
|
||||
|
||||
/// Creates reusable labels whose mouse events pass through to the cell.
|
||||
private ModpackFileTreeCell() {
|
||||
fileName.setMouseTransparent(true);
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
content.setPickOnBounds(false);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
/// Refreshes the file labels and removes content when the cell is cleared.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable String item, boolean empty) {
|
||||
content.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
fileName.setText(null);
|
||||
comment.setText(null);
|
||||
if (empty || item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Updating the text makes the skin reattach the current graphic to the cell.
|
||||
// Clear it before moving the checkbox graphic into the content container.
|
||||
setText(null);
|
||||
@Nullable Node graphic = getGraphic();
|
||||
if (graphic != null) {
|
||||
content.getChildren().add(graphic);
|
||||
}
|
||||
fileName.setText(item);
|
||||
content.getChildren().add(fileName);
|
||||
if (getTreeItem() instanceof ModpackFileTreeItem treeItem && treeItem.comment != null) {
|
||||
comment.setText(treeItem.comment);
|
||||
content.getChildren().add(comment);
|
||||
}
|
||||
setGraphic(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user