优化 ComponentList (#5385)
This commit is contained in:
@@ -17,13 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.construct;
|
package org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
import javafx.beans.DefaultProperty;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
|
||||||
import javafx.beans.property.IntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
@@ -33,88 +28,21 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.Control;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||||
|
|
||||||
import java.util.List;
|
public class ComponentList extends Control implements NoPaddingComponent {
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
@DefaultProperty("content")
|
|
||||||
public class ComponentList extends Control {
|
|
||||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
|
||||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle", "");
|
|
||||||
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
|
|
||||||
private boolean hasSubtitle = false;
|
|
||||||
public final ObservableList<Node> content = FXCollections.observableArrayList();
|
|
||||||
private Supplier<List<? extends Node>> lazyInitializer;
|
|
||||||
|
|
||||||
public ComponentList() {
|
public ComponentList() {
|
||||||
getStyleClass().add("options-list");
|
getStyleClass().add("options-list");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
|
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||||
this();
|
|
||||||
this.lazyInitializer = lazyInitializer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty titleProperty() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title.set(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSubtitle() {
|
|
||||||
return subtitle.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty subtitleProperty() {
|
|
||||||
return subtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubtitle(String subtitle) {
|
|
||||||
this.subtitle.set(subtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDepth() {
|
|
||||||
return depth.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IntegerProperty depthProperty() {
|
|
||||||
return depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDepth(int depth) {
|
|
||||||
this.depth.set(depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHasSubtitle() {
|
|
||||||
return hasSubtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHasSubtitle(boolean hasSubtitle) {
|
|
||||||
this.hasSubtitle = hasSubtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableList<Node> getContent() {
|
public ObservableList<Node> getContent() {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
void doLazyInit() {
|
|
||||||
if (lazyInitializer != null) {
|
|
||||||
this.getContent().setAll(lazyInitializer.get());
|
|
||||||
setNeedsLayout(true);
|
|
||||||
lazyInitializer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Orientation getContentBias() {
|
public Orientation getContentBias() {
|
||||||
return Orientation.HORIZONTAL;
|
return Orientation.HORIZONTAL;
|
||||||
@@ -130,49 +58,72 @@ public class ComponentList extends Control {
|
|||||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||||
|
|
||||||
private final ObservableList<Node> list;
|
private final ObservableList<Node> list;
|
||||||
private final ObjectBinding<Node> firstItem;
|
|
||||||
private final ObjectBinding<Node> lastItem;
|
|
||||||
|
|
||||||
Skin(ComponentList control) {
|
Skin(ComponentList control) {
|
||||||
super(control);
|
super(control);
|
||||||
|
|
||||||
list = MappedObservableList.create(control.getContent(), node -> {
|
list = MappedObservableList.create(control.getContent(), node -> {
|
||||||
ComponentListCell cell = new ComponentListCell(node);
|
Pane wrapper;
|
||||||
cell.getStyleClass().add("options-list-item");
|
if (node instanceof ComponentSublist sublist) {
|
||||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
sublist.getStyleClass().remove("options-list");
|
||||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
sublist.getStyleClass().add("options-sublist");
|
||||||
|
wrapper = new ComponentSublistWrapper(sublist);
|
||||||
|
} else {
|
||||||
|
wrapper = new StackPane(node);
|
||||||
}
|
}
|
||||||
if (node instanceof LineButtonBase || node instanceof LinePane || node.getProperties().containsKey("ComponentList.noPadding")) {
|
|
||||||
cell.getStyleClass().add("no-padding");
|
wrapper.getStyleClass().add("options-list-item");
|
||||||
|
|
||||||
|
if (node.getProperties().get("ComponentList.vgrow") instanceof Priority priority) {
|
||||||
|
VBox.setVgrow(wrapper, priority);
|
||||||
}
|
}
|
||||||
return cell;
|
|
||||||
|
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||||
|
wrapper.getStyleClass().add("no-padding");
|
||||||
|
}
|
||||||
|
return wrapper;
|
||||||
});
|
});
|
||||||
|
|
||||||
firstItem = Bindings.valueAt(list, 0);
|
updateStyle();
|
||||||
firstItem.addListener((observable, oldValue, newValue) -> {
|
list.addListener((InvalidationListener) o -> updateStyle());
|
||||||
if (newValue != null)
|
|
||||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
|
||||||
if (oldValue != null)
|
|
||||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
|
||||||
});
|
|
||||||
if (!list.isEmpty())
|
|
||||||
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
|
||||||
|
|
||||||
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
|
|
||||||
lastItem.addListener((observable, oldValue, newValue) -> {
|
|
||||||
if (newValue != null)
|
|
||||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
|
||||||
if (oldValue != null)
|
|
||||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
|
||||||
});
|
|
||||||
if (!list.isEmpty())
|
|
||||||
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
|
||||||
|
|
||||||
VBox vbox = new VBox();
|
VBox vbox = new VBox();
|
||||||
vbox.setFillWidth(true);
|
vbox.setFillWidth(true);
|
||||||
Bindings.bindContent(vbox.getChildren(), list);
|
Bindings.bindContent(vbox.getChildren(), list);
|
||||||
node = vbox;
|
node = vbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Node prevFirstItem;
|
||||||
|
private Node prevLastItem;
|
||||||
|
|
||||||
|
private void updateStyle() {
|
||||||
|
Node firstItem;
|
||||||
|
Node lastItem;
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
firstItem = null;
|
||||||
|
lastItem = null;
|
||||||
|
} else {
|
||||||
|
firstItem = list.get(0);
|
||||||
|
lastItem = list.get(list.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstItem != prevFirstItem) {
|
||||||
|
if (prevFirstItem != null)
|
||||||
|
prevFirstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
||||||
|
if (firstItem != null)
|
||||||
|
firstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||||
|
prevFirstItem = firstItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastItem != prevLastItem) {
|
||||||
|
if (prevLastItem != null)
|
||||||
|
prevLastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||||
|
if (lastItem != null)
|
||||||
|
lastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||||
|
prevLastItem = lastItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Node createComponentListTitle(String title) {
|
public static Node createComponentListTitle(String title) {
|
||||||
@@ -189,4 +140,8 @@ public class ComponentList extends Control {
|
|||||||
public static void setVgrow(Node node, Priority priority) {
|
public static void setVgrow(Node node, Priority priority) {
|
||||||
node.getProperties().put("ComponentList.vgrow", priority);
|
node.getProperties().put("ComponentList.vgrow", priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setNoPadding(Node node) {
|
||||||
|
node.getProperties().put("ComponentList.noPadding", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
/*
|
|
||||||
* Hello Minecraft! Launcher
|
|
||||||
* Copyright (C) 2020 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 org.jackhuang.hmcl.ui.construct;
|
|
||||||
|
|
||||||
import javafx.animation.*;
|
|
||||||
import javafx.application.Platform;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.input.MouseButton;
|
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.scene.layout.*;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
import org.jackhuang.hmcl.theme.Themes;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author huangyuhui
|
|
||||||
*/
|
|
||||||
final class ComponentListCell extends StackPane {
|
|
||||||
private final Node content;
|
|
||||||
private Animation expandAnimation;
|
|
||||||
private boolean expanded = false;
|
|
||||||
|
|
||||||
ComponentListCell(Node content) {
|
|
||||||
this.content = content;
|
|
||||||
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLayout() {
|
|
||||||
if (content instanceof ComponentList list) {
|
|
||||||
content.getStyleClass().remove("options-list");
|
|
||||||
content.getStyleClass().add("options-sublist");
|
|
||||||
|
|
||||||
getStyleClass().add("no-padding");
|
|
||||||
|
|
||||||
VBox groupNode = new VBox();
|
|
||||||
|
|
||||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
|
||||||
expandIcon.setMouseTransparent(true);
|
|
||||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
|
||||||
|
|
||||||
VBox labelVBox = new VBox();
|
|
||||||
labelVBox.setMouseTransparent(true);
|
|
||||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
boolean overrideHeaderLeft = false;
|
|
||||||
if (list instanceof ComponentSublist) {
|
|
||||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
|
||||||
if (leftNode != null) {
|
|
||||||
labelVBox.getChildren().setAll(leftNode);
|
|
||||||
overrideHeaderLeft = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overrideHeaderLeft) {
|
|
||||||
Label label = new Label();
|
|
||||||
label.textProperty().bind(list.titleProperty());
|
|
||||||
label.getStyleClass().add("title-label");
|
|
||||||
labelVBox.getChildren().add(label);
|
|
||||||
|
|
||||||
if (list.isHasSubtitle()) {
|
|
||||||
Label subtitleLabel = new Label();
|
|
||||||
subtitleLabel.textProperty().bind(list.subtitleProperty());
|
|
||||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
|
||||||
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
|
||||||
labelVBox.getChildren().add(subtitleLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HBox header = new HBox();
|
|
||||||
header.setSpacing(16);
|
|
||||||
header.getChildren().add(labelVBox);
|
|
||||||
header.setPadding(new Insets(10, 16, 10, 16));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
|
||||||
if (list instanceof ComponentSublist) {
|
|
||||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
|
||||||
if (rightNode != null)
|
|
||||||
header.getChildren().add(rightNode);
|
|
||||||
}
|
|
||||||
header.getChildren().add(expandIcon);
|
|
||||||
|
|
||||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
|
||||||
groupNode.getChildren().add(headerRippler);
|
|
||||||
|
|
||||||
VBox container = new VBox();
|
|
||||||
boolean hasPadding = !(list instanceof ComponentSublist subList) || subList.hasComponentPadding();
|
|
||||||
if (hasPadding) {
|
|
||||||
container.setPadding(new Insets(8, 16, 10, 16));
|
|
||||||
}
|
|
||||||
FXUtils.setLimitHeight(container, 0);
|
|
||||||
FXUtils.setOverflowHidden(container);
|
|
||||||
container.getChildren().setAll(content);
|
|
||||||
groupNode.getChildren().add(container);
|
|
||||||
|
|
||||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
|
||||||
if (event.getButton() != MouseButton.PRIMARY)
|
|
||||||
return;
|
|
||||||
|
|
||||||
event.consume();
|
|
||||||
|
|
||||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
|
||||||
expandAnimation.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean expanded = !this.expanded;
|
|
||||||
this.expanded = expanded;
|
|
||||||
if (expanded) {
|
|
||||||
list.doLazyInit();
|
|
||||||
list.layout();
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
|
||||||
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 4)) * (expanded ? 1 : -1);
|
|
||||||
double contentHeight = expanded ? newAnimatedHeight : 0;
|
|
||||||
double targetRotate = expanded ? -180 : 0;
|
|
||||||
|
|
||||||
if (AnimationUtils.isAnimationEnabled()) {
|
|
||||||
double currentRotate = expandIcon.getRotate();
|
|
||||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
|
||||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
|
||||||
|
|
||||||
expandAnimation = new Timeline(
|
|
||||||
new KeyFrame(duration,
|
|
||||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
|
||||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
|
||||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
|
||||||
);
|
|
||||||
|
|
||||||
expandAnimation.play();
|
|
||||||
} else {
|
|
||||||
container.setMinHeight(contentHeight);
|
|
||||||
container.setMaxHeight(contentHeight);
|
|
||||||
expandIcon.setRotate(targetRotate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
getChildren().setAll(groupNode);
|
|
||||||
} else {
|
|
||||||
getStyleClass().remove("no-padding");
|
|
||||||
getChildren().setAll(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,57 +17,100 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.construct;
|
package org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
import javafx.beans.DefaultProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.BooleanProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
|
||||||
@DefaultProperty("content")
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class ComponentSublist extends ComponentList {
|
public class ComponentSublist extends ComponentList {
|
||||||
|
|
||||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
Supplier<List<? extends Node>> lazyInitializer;
|
||||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
|
||||||
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
|
|
||||||
|
|
||||||
public ComponentSublist() {
|
public ComponentSublist() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node getHeaderLeft() {
|
public ComponentSublist(Supplier<List<? extends Node>> lazyInitializer) {
|
||||||
return headerLeft.get();
|
this.lazyInitializer = lazyInitializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<Node> headerLeftProperty() {
|
void doLazyInit() {
|
||||||
|
if (lazyInitializer != null) {
|
||||||
|
this.getContent().setAll(lazyInitializer.get());
|
||||||
|
setNeedsLayout(true);
|
||||||
|
lazyInitializer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||||
|
|
||||||
|
public StringProperty titleProperty() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return titleProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
titleProperty().set(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringProperty subtitle;
|
||||||
|
|
||||||
|
public StringProperty subtitleProperty() {
|
||||||
|
if (subtitle == null)
|
||||||
|
subtitle = new SimpleStringProperty(this, "subtitle", "");
|
||||||
|
|
||||||
|
return subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubtitle() {
|
||||||
|
return subtitleProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtitle(String subtitle) {
|
||||||
|
subtitleProperty().set(subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasSubtitle = false;
|
||||||
|
|
||||||
|
public boolean isHasSubtitle() {
|
||||||
|
return hasSubtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasSubtitle(boolean hasSubtitle) {
|
||||||
|
this.hasSubtitle = hasSubtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node headerLeft;
|
||||||
|
|
||||||
|
public Node getHeaderLeft() {
|
||||||
return headerLeft;
|
return headerLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeaderLeft(Node headerLeft) {
|
public void setHeaderLeft(Node headerLeft) {
|
||||||
this.headerLeft.set(headerLeft);
|
this.headerLeft = headerLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Node headerRight;
|
||||||
|
|
||||||
public Node getHeaderRight() {
|
public Node getHeaderRight() {
|
||||||
return headerRight.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Node> headerRightProperty() {
|
|
||||||
return headerRight;
|
return headerRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeaderRight(Node headerRight) {
|
public void setHeaderRight(Node headerRight) {
|
||||||
this.headerRight.set(headerRight);
|
this.headerRight = headerRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean componentPadding = true;
|
||||||
|
|
||||||
public boolean hasComponentPadding() {
|
public boolean hasComponentPadding() {
|
||||||
return componentPadding.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BooleanProperty componentPaddingProperty() {
|
|
||||||
return componentPadding;
|
return componentPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setComponentPadding(boolean componentPadding) {
|
public void setComponentPadding(boolean componentPadding) {
|
||||||
this.componentPadding.set(componentPadding);
|
this.componentPadding = componentPadding;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
|
import javafx.animation.*;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.jackhuang.hmcl.theme.Themes;
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
|
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||||
|
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||||
|
private VBox container;
|
||||||
|
|
||||||
|
private Animation expandAnimation;
|
||||||
|
private boolean expanded = false;
|
||||||
|
|
||||||
|
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||||
|
boolean noPadding = !sublist.hasComponentPadding();
|
||||||
|
|
||||||
|
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||||
|
expandIcon.setMouseTransparent(true);
|
||||||
|
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||||
|
|
||||||
|
VBox labelVBox = new VBox();
|
||||||
|
labelVBox.setMouseTransparent(true);
|
||||||
|
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
Node leftNode = sublist.getHeaderLeft();
|
||||||
|
if (leftNode == null) {
|
||||||
|
Label label = new Label();
|
||||||
|
label.textProperty().bind(sublist.titleProperty());
|
||||||
|
label.getStyleClass().add("title-label");
|
||||||
|
labelVBox.getChildren().add(label);
|
||||||
|
|
||||||
|
if (sublist.isHasSubtitle()) {
|
||||||
|
Label subtitleLabel = new Label();
|
||||||
|
subtitleLabel.textProperty().bind(sublist.subtitleProperty());
|
||||||
|
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||||
|
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
||||||
|
labelVBox.getChildren().add(subtitleLabel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelVBox.getChildren().setAll(leftNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
HBox header = new HBox();
|
||||||
|
header.setSpacing(16);
|
||||||
|
header.getChildren().add(labelVBox);
|
||||||
|
header.setPadding(new Insets(10, 16, 10, 16));
|
||||||
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||||
|
Node rightNode = sublist.getHeaderRight();
|
||||||
|
if (rightNode != null)
|
||||||
|
header.getChildren().add(rightNode);
|
||||||
|
header.getChildren().add(expandIcon);
|
||||||
|
|
||||||
|
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||||
|
this.getChildren().add(headerRippler);
|
||||||
|
|
||||||
|
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||||
|
if (event.getButton() != MouseButton.PRIMARY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
event.consume();
|
||||||
|
|
||||||
|
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||||
|
expandAnimation.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean expanded = !this.expanded;
|
||||||
|
this.expanded = expanded;
|
||||||
|
if (expanded) {
|
||||||
|
sublist.doLazyInit();
|
||||||
|
|
||||||
|
if (container == null) {
|
||||||
|
this.container = new VBox();
|
||||||
|
|
||||||
|
if (!noPadding) {
|
||||||
|
container.setPadding(new Insets(8, 16, 10, 16));
|
||||||
|
}
|
||||||
|
FXUtils.setLimitHeight(container, 0);
|
||||||
|
FXUtils.setOverflowHidden(container);
|
||||||
|
container.getChildren().setAll(sublist);
|
||||||
|
ComponentSublistWrapper.this.getChildren().add(container);
|
||||||
|
|
||||||
|
this.applyCss();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||||
|
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
|
||||||
|
double targetRotate = expanded ? -180 : 0;
|
||||||
|
|
||||||
|
if (AnimationUtils.isAnimationEnabled()) {
|
||||||
|
double currentRotate = expandIcon.getRotate();
|
||||||
|
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||||
|
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||||
|
|
||||||
|
expandAnimation = new Timeline(
|
||||||
|
new KeyFrame(duration,
|
||||||
|
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||||
|
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||||
|
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||||
|
);
|
||||||
|
|
||||||
|
expandAnimation.play();
|
||||||
|
} else {
|
||||||
|
container.setMinHeight(contentHeight);
|
||||||
|
container.setMaxHeight(contentHeight);
|
||||||
|
expandIcon.setRotate(targetRotate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
/// @author Glavo
|
/// @author Glavo
|
||||||
public abstract class LineButtonBase extends StackPane {
|
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent {
|
||||||
|
|
||||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import javafx.scene.layout.Region;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
/// @author Glavo
|
/// @author Glavo
|
||||||
public class LinePane extends BorderPane {
|
public class LinePane extends BorderPane implements NoPaddingComponent {
|
||||||
|
|
||||||
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
private static final Insets PADDING = new Insets(8, 8, 8, 16);
|
||||||
|
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
* Hello Minecraft! Launcher
|
|
||||||
* Copyright (C) 2020 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 org.jackhuang.hmcl.ui.construct;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXColorPicker;
|
|
||||||
import com.jfoenix.controls.JFXRadioButton;
|
|
||||||
import javafx.beans.NamedArg;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.Toggle;
|
|
||||||
import javafx.scene.control.ToggleGroup;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
public class MultiColorItem extends ComponentList {
|
|
||||||
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
|
|
||||||
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
|
|
||||||
|
|
||||||
private final ToggleGroup group = new ToggleGroup();
|
|
||||||
private final JFXColorPicker colorPicker = new JFXColorPicker();
|
|
||||||
private final JFXRadioButton radioCustom = new JFXRadioButton();
|
|
||||||
private final BorderPane custom = new BorderPane();
|
|
||||||
private final VBox pane = new VBox();
|
|
||||||
private final boolean hasCustom;
|
|
||||||
|
|
||||||
private Consumer<Toggle> toggleSelectedListener;
|
|
||||||
private Consumer<Color> colorConsumer;
|
|
||||||
|
|
||||||
public MultiColorItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
|
|
||||||
this.hasCustom = hasCustom;
|
|
||||||
|
|
||||||
radioCustom.textProperty().bind(customTitleProperty());
|
|
||||||
radioCustom.setToggleGroup(group);
|
|
||||||
colorPicker.disableProperty().bind(radioCustom.selectedProperty().not());
|
|
||||||
|
|
||||||
colorPicker.setOnAction(e -> Optional.ofNullable(colorConsumer).ifPresent(c -> c.accept(colorPicker.getValue())));
|
|
||||||
|
|
||||||
custom.setLeft(radioCustom);
|
|
||||||
custom.setStyle("-fx-padding: 3;");
|
|
||||||
HBox right = new HBox();
|
|
||||||
right.setSpacing(3);
|
|
||||||
right.getChildren().addAll(colorPicker);
|
|
||||||
custom.setRight(right);
|
|
||||||
FXUtils.setLimitHeight(custom, 40);
|
|
||||||
|
|
||||||
pane.setStyle("-fx-padding: 0 0 10 0;");
|
|
||||||
pane.setSpacing(8);
|
|
||||||
|
|
||||||
if (hasCustom)
|
|
||||||
pane.getChildren().add(custom);
|
|
||||||
getContent().add(pane);
|
|
||||||
|
|
||||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
|
||||||
if (toggleSelectedListener != null)
|
|
||||||
toggleSelectedListener.accept(newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Node createChildren(String title) {
|
|
||||||
return createChildren(title, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Node createChildren(String title, Object userData) {
|
|
||||||
return createChildren(title, "", userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Node createChildren(String title, String subtitle, Object userData) {
|
|
||||||
BorderPane pane = new BorderPane();
|
|
||||||
pane.setStyle("-fx-padding: 3;");
|
|
||||||
|
|
||||||
JFXRadioButton left = new JFXRadioButton(title);
|
|
||||||
left.setToggleGroup(group);
|
|
||||||
left.setUserData(userData);
|
|
||||||
pane.setLeft(left);
|
|
||||||
|
|
||||||
Label right = new Label(subtitle);
|
|
||||||
right.getStyleClass().add("subtitle-label");
|
|
||||||
right.setStyle("-fx-font-size: 10;");
|
|
||||||
pane.setRight(right);
|
|
||||||
|
|
||||||
return pane;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadChildren(Collection<Node> list) {
|
|
||||||
pane.getChildren().setAll(list);
|
|
||||||
|
|
||||||
if (hasCustom)
|
|
||||||
pane.getChildren().add(custom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToggleGroup getGroup() {
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCustomTitle() {
|
|
||||||
return customTitle.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty customTitleProperty() {
|
|
||||||
return customTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomTitle(String customTitle) {
|
|
||||||
this.customTitle.set(customTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChooserTitle() {
|
|
||||||
return chooserTitle.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringProperty chooserTitleProperty() {
|
|
||||||
return chooserTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChooserTitle(String chooserTitle) {
|
|
||||||
this.chooserTitle.set(chooserTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomUserData(Object userData) {
|
|
||||||
radioCustom.setUserData(userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCustomToggle(Toggle toggle) {
|
|
||||||
return radioCustom == toggle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
|
|
||||||
toggleSelectedListener = consumer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnColorPickerChanged(Consumer<Color> consumer) {
|
|
||||||
colorConsumer = consumer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(Color color) {
|
|
||||||
colorPicker.setValue(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
|
/// Marker interface for no padding in [ComponentList].
|
||||||
|
///
|
||||||
|
/// @author Glavo
|
||||||
|
interface NoPaddingComponent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.css.CssMetaData;
|
||||||
|
import javafx.css.Styleable;
|
||||||
|
import javafx.css.StyleableObjectProperty;
|
||||||
|
import javafx.css.StyleableProperty;
|
||||||
|
import javafx.css.converter.InsetsConverter;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
// TODO: We plan to replace ComponentList with this class, but we need to address some issues first
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
public final class OptionsList extends Control {
|
||||||
|
public OptionsList() {
|
||||||
|
this.getStyleClass().add("options-list");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
return new OptionsListSkin(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ObservableList<Element> elements = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
public ObservableList<Element> getElements() {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTitle(String title) {
|
||||||
|
elements.add(new Title(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNode(Node node) {
|
||||||
|
elements.add(new NodeElement(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListElement(@NotNull Node node) {
|
||||||
|
elements.add(new ListElement(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListElements(@NotNull Node... nodes) {
|
||||||
|
for (Node node : nodes) {
|
||||||
|
elements.add(new ListElement(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final StyleableObjectProperty<Insets> contentPadding = new StyleableObjectProperty<>() {
|
||||||
|
@Override
|
||||||
|
public Object getBean() {
|
||||||
|
return OptionsList.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "contentPadding";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public javafx.css.CssMetaData<OptionsList, Insets> getCssMetaData() {
|
||||||
|
return StyleableProperties.CONTENT_PADDING;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public StyleableObjectProperty<Insets> contentPaddingProperty() {
|
||||||
|
return contentPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Insets getContentPadding() {
|
||||||
|
return contentPaddingProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPadding(Insets padding) {
|
||||||
|
contentPaddingProperty().set(padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StyleableProperties {
|
||||||
|
private static final CssMetaData<OptionsList, Insets> CONTENT_PADDING = new CssMetaData<>("-jfx-content-padding", InsetsConverter.getInstance()) {
|
||||||
|
@Override
|
||||||
|
public boolean isSettable(OptionsList styleable) {
|
||||||
|
return styleable.contentPadding == null || !styleable.contentPadding.isBound();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StyleableProperty<Insets> getStyleableProperty(OptionsList styleable) {
|
||||||
|
return styleable.contentPaddingProperty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||||
|
Collections.addAll(styleables, CONTENT_PADDING);
|
||||||
|
STYLEABLES = List.copyOf(styleables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||||
|
return StyleableProperties.STYLEABLES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||||
|
return getClassCssMetaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Element {
|
||||||
|
protected Node node;
|
||||||
|
|
||||||
|
Node getNode() {
|
||||||
|
if (node == null)
|
||||||
|
node = createNode();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Node createNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Title extends Element {
|
||||||
|
private final @NotNull String title;
|
||||||
|
|
||||||
|
public Title(@NotNull String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Node createNode() {
|
||||||
|
Label label = new Label(title);
|
||||||
|
label.setPadding(new Insets(8, 0, 8, 0));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return this == obj || obj instanceof Title that && Objects.equals(this.title, that.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return title.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Title[%s]".formatted(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class NodeElement extends Element {
|
||||||
|
public NodeElement(@NotNull Node node) {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Node createNode() {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return this == obj || obj instanceof NodeElement that && this.node.equals(that.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return node.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "NodeElement[node=%s]".formatted(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ListElement extends Element {
|
||||||
|
private final Node original;
|
||||||
|
|
||||||
|
public ListElement(@NotNull Node node) {
|
||||||
|
this.original = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Node createNode() {
|
||||||
|
if (original instanceof ComponentSublist sublist) {
|
||||||
|
return new ComponentSublistWrapper(sublist);
|
||||||
|
} else {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return this == obj || obj instanceof ListElement that && this.original.equals(that.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return original.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ListElement[node=%s]".formatted(original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXListView;
|
||||||
|
import javafx.beans.InvalidationListener;
|
||||||
|
import javafx.beans.WeakInvalidationListener;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.SkinBase;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
public final class OptionsListSkin extends SkinBase<OptionsList> {
|
||||||
|
|
||||||
|
private final JFXListView<OptionsList.Element> listView;
|
||||||
|
private final ObjectBinding<ContentPaddings> contentPaddings;
|
||||||
|
|
||||||
|
OptionsListSkin(OptionsList control) {
|
||||||
|
super(control);
|
||||||
|
|
||||||
|
this.listView = new JFXListView<>();
|
||||||
|
listView.setItems(control.getElements());
|
||||||
|
listView.setCellFactory(listView1 -> new Cell());
|
||||||
|
|
||||||
|
this.contentPaddings = Bindings.createObjectBinding(() -> {
|
||||||
|
Insets padding = control.getContentPadding();
|
||||||
|
return padding == null ? ContentPaddings.EMPTY : new ContentPaddings(
|
||||||
|
new Insets(padding.getTop(), padding.getRight(), 0, padding.getLeft()),
|
||||||
|
new Insets(0, padding.getRight(), padding.getBottom(), padding.getLeft()),
|
||||||
|
new Insets(0, padding.getRight(), 0, padding.getLeft())
|
||||||
|
);
|
||||||
|
}, control.contentPaddingProperty());
|
||||||
|
|
||||||
|
this.getChildren().setAll(listView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ContentPaddings(Insets first, Insets last, Insets middle) {
|
||||||
|
static final ContentPaddings EMPTY = new ContentPaddings(Insets.EMPTY, Insets.EMPTY, Insets.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Cell extends ListCell<OptionsList.Element> {
|
||||||
|
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
|
||||||
|
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final InvalidationListener updateStyleListener = o -> updateStyle();
|
||||||
|
|
||||||
|
private StackPane wrapper;
|
||||||
|
|
||||||
|
public Cell() {
|
||||||
|
FXUtils.limitCellWidth(listView, this);
|
||||||
|
|
||||||
|
WeakInvalidationListener weakListener = new WeakInvalidationListener(updateStyleListener);
|
||||||
|
listView.itemsProperty().addListener((o, oldValue, newValue) -> {
|
||||||
|
if (oldValue != null)
|
||||||
|
oldValue.removeListener(weakListener);
|
||||||
|
if (newValue != null)
|
||||||
|
newValue.addListener(weakListener);
|
||||||
|
|
||||||
|
weakListener.invalidated(o);
|
||||||
|
});
|
||||||
|
itemProperty().addListener(weakListener);
|
||||||
|
contentPaddings.addListener(weakListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(OptionsList.Element item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (empty || item == null) {
|
||||||
|
setGraphic(null);
|
||||||
|
} else if (item instanceof OptionsList.ListElement element) {
|
||||||
|
if (wrapper == null)
|
||||||
|
wrapper = createWrapper();
|
||||||
|
else
|
||||||
|
wrapper.getStyleClass().remove("no-padding");
|
||||||
|
|
||||||
|
Node node = element.getNode();
|
||||||
|
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding"))
|
||||||
|
wrapper.getStyleClass().add("no-padding");
|
||||||
|
|
||||||
|
wrapper.getChildren().setAll(node);
|
||||||
|
|
||||||
|
setGraphic(wrapper);
|
||||||
|
} else {
|
||||||
|
setGraphic(item.getNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPane createWrapper() {
|
||||||
|
var wrapper = new StackPane();
|
||||||
|
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
wrapper.getStyleClass().add("options-list-item");
|
||||||
|
updateStyle();
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStyle() {
|
||||||
|
OptionsList.Element item = getItem();
|
||||||
|
int index = getIndex();
|
||||||
|
ObservableList<OptionsList.Element> items = getListView().getItems();
|
||||||
|
|
||||||
|
if (item == null || index < 0 || index >= items.size()) {
|
||||||
|
this.setPadding(Insets.EMPTY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFirst = index == 0;
|
||||||
|
boolean isLast = index == items.size() - 1;
|
||||||
|
|
||||||
|
ContentPaddings paddings = contentPaddings.get();
|
||||||
|
if (isFirst) {
|
||||||
|
this.setPadding(paddings.first);
|
||||||
|
} else if (isLast) {
|
||||||
|
this.setPadding(paddings.last);
|
||||||
|
} else {
|
||||||
|
this.setPadding(paddings.middle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item instanceof OptionsList.ListElement && wrapper != null) {
|
||||||
|
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, isFirst || !(items.get(index - 1) instanceof OptionsList.ListElement));
|
||||||
|
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_LAST, isLast || !(items.get(index + 1) instanceof OptionsList.ListElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -197,8 +197,7 @@ public class PersonalizationPage extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ComponentList logPane = new ComponentSublist();
|
ComponentList logPane = new ComponentList();
|
||||||
logPane.setTitle(i18n("settings.launcher.log"));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
VBox fontPane = new VBox();
|
VBox fontPane = new VBox();
|
||||||
@@ -254,8 +253,7 @@ public class PersonalizationPage extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ComponentSublist fontPane = new ComponentSublist();
|
ComponentList fontPane = new ComponentList();
|
||||||
fontPane.setTitle(i18n("settings.launcher.font"));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
VBox vbox = new VBox();
|
VBox vbox = new VBox();
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
|||||||
rootPane.setStyle("-fx-padding: 20;");
|
rootPane.setStyle("-fx-padding: 20;");
|
||||||
{
|
{
|
||||||
ComponentList componentList = new ComponentList();
|
ComponentList componentList = new ComponentList();
|
||||||
componentList.setDepth(1);
|
|
||||||
{
|
{
|
||||||
BorderPane profileNamePane = new BorderPane();
|
BorderPane profileNamePane = new BorderPane();
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ public class TerracottaControllerPage extends StackPane {
|
|||||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||||
exportLog.setContent(exportLogInner);
|
exportLog.setContent(exportLogInner);
|
||||||
exportLog.getProperties().put("ComponentList.noPadding", true);
|
ComponentList.setNoPadding(exportLog);
|
||||||
// FIXME: SpinnerPane loses its content width in loading state.
|
// FIXME: SpinnerPane loses its content width in loading state.
|
||||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@ public class TerracottaControllerPage extends StackPane {
|
|||||||
getChildren().setAll(scrollPane);
|
getChildren().setAll(scrollPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComponentList getThirdPartyDownloadNodes() {
|
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||||
ComponentSublist locals = new ComponentSublist();
|
ComponentSublist locals = new ComponentSublist();
|
||||||
locals.setComponentPadding(false);
|
locals.setComponentPadding(false);
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ public class TerracottaControllerPage extends StackPane {
|
|||||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||||
() -> FXUtils.openLink(url)
|
() -> FXUtils.openLink(url)
|
||||||
));
|
));
|
||||||
container.getProperties().put("ComponentList.noPadding", true);
|
ComponentList.setNoPadding(container);
|
||||||
locals.getContent().add(container);
|
locals.getContent().add(container);
|
||||||
}
|
}
|
||||||
return locals;
|
return locals;
|
||||||
@@ -666,7 +666,7 @@ public class TerracottaControllerPage extends StackPane {
|
|||||||
|
|
||||||
container.getChildren().setAll(nodes);
|
container.getChildren().setAll(nodes);
|
||||||
}, button.title, button.subTitle, button.left, button.right));
|
}, button.title, button.subTitle, button.left, button.right));
|
||||||
button.getProperties().put("ComponentList.noPadding", true);
|
ComponentList.setNoPadding(button);
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,13 +300,13 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
|
|
||||||
for (String gameVersion : control.versions.keys().stream()
|
for (String gameVersion : control.versions.keys().stream()
|
||||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||||
.collect(Collectors.toList())) {
|
.toList()) {
|
||||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||||
if (versions == null || versions.isEmpty()) {
|
if (versions == null || versions.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ComponentList sublist = new ComponentList(() -> {
|
var sublist = new ComponentSublist(() -> {
|
||||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||||
for (RemoteMod.Version v : versions) {
|
for (RemoteMod.Version v : versions) {
|
||||||
items.add(new ModItem(control.addon, v, control));
|
items.add(new ModItem(control.addon, v, control));
|
||||||
@@ -465,13 +465,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
box.getChildren().setAll(modItem);
|
box.getChildren().setAll(modItem);
|
||||||
SpinnerPane spinnerPane = new SpinnerPane();
|
SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
|
ComponentList dependenciesList = new ComponentList();
|
||||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||||
|
|
||||||
scrollPane.setContent(dependenciesList);
|
scrollPane.setContent(dependenciesList);
|
||||||
scrollPane.setFitToWidth(true);
|
scrollPane.setFitToWidth(true);
|
||||||
scrollPane.setFitToHeight(true);
|
scrollPane.setFitToHeight(true);
|
||||||
|
FXUtils.smoothScrolling(scrollPane);
|
||||||
spinnerPane.setContent(scrollPane);
|
spinnerPane.setContent(scrollPane);
|
||||||
box.getChildren().add(spinnerPane);
|
box.getChildren().add(spinnerPane);
|
||||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
|
|
||||||
{
|
{
|
||||||
componentList = new ComponentList();
|
componentList = new ComponentList();
|
||||||
componentList.setDepth(1);
|
|
||||||
|
|
||||||
if (!globalSetting) {
|
if (!globalSetting) {
|
||||||
BorderPane copyGlobalPane = new BorderPane();
|
BorderPane copyGlobalPane = new BorderPane();
|
||||||
|
|||||||
Reference in New Issue
Block a user