优化 ComponentList (#5385)
This commit is contained in:
@@ -17,13 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
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.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -33,88 +28,21 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.util.List;
|
||||
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 class ComponentList extends Control implements NoPaddingComponent {
|
||||
|
||||
public ComponentList() {
|
||||
getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
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;
|
||||
}
|
||||
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<Node> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Orientation getContentBias() {
|
||||
return Orientation.HORIZONTAL;
|
||||
@@ -130,49 +58,72 @@ public class ComponentList extends Control {
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final ObservableList<Node> list;
|
||||
private final ObjectBinding<Node> firstItem;
|
||||
private final ObjectBinding<Node> lastItem;
|
||||
|
||||
Skin(ComponentList control) {
|
||||
super(control);
|
||||
|
||||
list = MappedObservableList.create(control.getContent(), node -> {
|
||||
ComponentListCell cell = new ComponentListCell(node);
|
||||
cell.getStyleClass().add("options-list-item");
|
||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
||||
Pane wrapper;
|
||||
if (node instanceof ComponentSublist sublist) {
|
||||
sublist.getStyleClass().remove("options-list");
|
||||
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);
|
||||
firstItem.addListener((observable, oldValue, newValue) -> {
|
||||
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);
|
||||
updateStyle();
|
||||
list.addListener((InvalidationListener) o -> updateStyle());
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.setFillWidth(true);
|
||||
Bindings.bindContent(vbox.getChildren(), list);
|
||||
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) {
|
||||
@@ -189,4 +140,8 @@ public class ComponentList extends Control {
|
||||
public static void setVgrow(Node node, Priority 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;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.Node;
|
||||
|
||||
@DefaultProperty("content")
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ComponentSublist extends ComponentList {
|
||||
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
|
||||
Supplier<List<? extends Node>> lazyInitializer;
|
||||
|
||||
public ComponentSublist() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Node getHeaderLeft() {
|
||||
return headerLeft.get();
|
||||
public ComponentSublist(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
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;
|
||||
}
|
||||
|
||||
public void setHeaderLeft(Node headerLeft) {
|
||||
this.headerLeft.set(headerLeft);
|
||||
this.headerLeft = headerLeft;
|
||||
}
|
||||
|
||||
private Node headerRight;
|
||||
|
||||
public Node getHeaderRight() {
|
||||
return headerRight.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerRightProperty() {
|
||||
return headerRight;
|
||||
}
|
||||
|
||||
public void setHeaderRight(Node headerRight) {
|
||||
this.headerRight.set(headerRight);
|
||||
this.headerRight = headerRight;
|
||||
}
|
||||
|
||||
private boolean componentPadding = true;
|
||||
|
||||
public boolean hasComponentPadding() {
|
||||
return componentPadding.get();
|
||||
}
|
||||
|
||||
public BooleanProperty componentPaddingProperty() {
|
||||
return 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;
|
||||
|
||||
/// @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);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/// @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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
logPane.setTitle(i18n("settings.launcher.log"));
|
||||
ComponentList logPane = new ComponentList();
|
||||
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
@@ -254,8 +253,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
ComponentSublist fontPane = new ComponentSublist();
|
||||
fontPane.setTitle(i18n("settings.launcher.font"));
|
||||
ComponentList fontPane = new ComponentList();
|
||||
|
||||
{
|
||||
VBox vbox = new VBox();
|
||||
|
||||
@@ -79,7 +79,6 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
rootPane.setStyle("-fx-padding: 20;");
|
||||
{
|
||||
ComponentList componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
{
|
||||
BorderPane profileNamePane = new BorderPane();
|
||||
{
|
||||
|
||||
@@ -455,7 +455,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||
exportLog.setContent(exportLogInner);
|
||||
exportLog.getProperties().put("ComponentList.noPadding", true);
|
||||
ComponentList.setNoPadding(exportLog);
|
||||
// FIXME: SpinnerPane loses its content width in loading state.
|
||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||
|
||||
@@ -559,7 +559,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private ComponentList getThirdPartyDownloadNodes() {
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
locals.setComponentPadding(false);
|
||||
|
||||
@@ -587,7 +587,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||
() -> FXUtils.openLink(url)
|
||||
));
|
||||
container.getProperties().put("ComponentList.noPadding", true);
|
||||
ComponentList.setNoPadding(container);
|
||||
locals.getContent().add(container);
|
||||
}
|
||||
return locals;
|
||||
@@ -666,7 +666,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
button.getProperties().put("ComponentList.noPadding", true);
|
||||
ComponentList.setNoPadding(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -300,13 +300,13 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.collect(Collectors.toList())) {
|
||||
.toList()) {
|
||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComponentList sublist = new ComponentList(() -> {
|
||||
var sublist = new ComponentSublist(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
@@ -465,13 +465,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
box.getChildren().setAll(modItem);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
|
||||
ComponentList dependenciesList = new ComponentList();
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
|
||||
@@ -173,7 +173,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
{
|
||||
componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
|
||||
if (!globalSetting) {
|
||||
BorderPane copyGlobalPane = new BorderPane();
|
||||
|
||||
Reference in New Issue
Block a user