创建通用的 LineButton 控件并简化 TerracottaControllerPage (#5395)

This commit is contained in:
Glavo
2026-02-03 21:09:59 +08:00
committed by GitHub
parent 2a66880a58
commit 19079ac123
8 changed files with 441 additions and 396 deletions

View File

@@ -0,0 +1,179 @@
/*
* 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.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public final class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setRightIcon(SVG.ARROW_FORWARD);
return button;
}
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
FXUtils.onClicked(container, this::fire);
}
public void fire() {
fireEvent(new ActionEvent());
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
if (onAction == null) {
onAction = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
@Override
public Object getBean() {
return LineButton.this;
}
@Override
public String getName() {
return "onAction";
}
};
}
return onAction;
}
public EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
public void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
private StringProperty message;
public StringProperty messageProperty() {
if (message == null) {
message = new StringPropertyBase() {
@Override
public Object getBean() {
return LineButton.this;
}
@Override
public String getName() {
return "message";
}
@Override
protected void invalidated() {
updateRight();
}
};
}
return message;
}
public String getMessage() {
return message == null ? "" : message.get();
}
public void setMessage(String message) {
messageProperty().set(message);
}
private SVG rightIcon;
private double rightIconSize;
public void setRightIcon(SVG rightIcon) {
setRightIcon(rightIcon, SVG.DEFAULT_SIZE);
}
public void setRightIcon(SVG rightIcon, double size) {
this.rightIcon = rightIcon;
this.rightIconSize = size;
updateRight();
}
//region Right
private Label messageLabel;
private Node rightIconNode;
private SVG currentRightIcon;
private double currentRightIconSize;
private void updateRight() {
HBox right;
if (root.getRight() instanceof HBox box) {
right = box;
} else {
right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
root.setRight(right);
}
right.getChildren().clear();
String message = getMessage();
if (message != null && !message.isEmpty()) {
if (messageLabel == null) {
messageLabel = new Label();
messageLabel.getStyleClass().add("subtitle");
}
messageLabel.setText(message);
right.getChildren().add(messageLabel);
} else if (messageLabel != null) {
messageLabel.setText("");
}
if (rightIcon != currentRightIcon || rightIconSize != currentRightIconSize) {
if (rightIcon != null) {
rightIconNode = rightIcon.createIcon(rightIconSize);
HBox.setMargin(rightIconNode, new Insets(0, 8, 0, 8));
} else {
rightIconNode = null;
}
currentRightIcon = rightIcon;
currentRightIconSize = rightIconSize;
}
if (rightIconNode != null)
right.getChildren().add(rightIconNode);
}
//endregion
}

View File

@@ -19,19 +19,15 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
/// @author Glavo /// @author Glavo
public abstract class LineButtonBase extends StackPane implements NoPaddingComponent { public abstract class LineButtonBase extends StackPane implements LineComponent {
private static final Insets PADDING = new Insets(8, 8, 8, 16); private static final String DEFAULT_STYLE_CLASS = "line-button-base";
protected final BorderPane root; protected final BorderPane root;
protected final RipplerContainer container; protected final RipplerContainer container;
@@ -39,9 +35,11 @@ public abstract class LineButtonBase extends StackPane implements NoPaddingCompo
private final Label titleLabel; private final Label titleLabel;
public LineButtonBase() { public LineButtonBase() {
this.getStyleClass().addAll(LineComponent.DEFAULT_STYLE_CLASS, LineButtonBase.DEFAULT_STYLE_CLASS);
this.root = new BorderPane(); this.root = new BorderPane();
root.setPadding(PADDING); root.setPadding(LineComponent.PADDING);
root.setMinHeight(48); root.setMinHeight(LineComponent.MIN_HEIGHT);
this.container = new RipplerContainer(root); this.container = new RipplerContainer(root);
this.getChildren().setAll(container); this.getChildren().setAll(container);
@@ -53,59 +51,32 @@ public abstract class LineButtonBase extends StackPane implements NoPaddingCompo
titleLabel.getStyleClass().add("title"); titleLabel.getStyleClass().add("title");
} }
@Override
public BorderPane getRoot() {
return root;
}
private final StringProperty title = new SimpleStringProperty(this, "title"); private final StringProperty title = new SimpleStringProperty(this, "title");
@Override
public StringProperty titleProperty() { public StringProperty titleProperty() {
return title; return title;
} }
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle; private StringProperty subtitle;
@Override
public StringProperty subtitleProperty() { public StringProperty subtitleProperty() {
if (subtitle == null) { if (subtitle == null) {
subtitle = new StringPropertyBase() { subtitle = new LineComponent.SubtitleProperty() {
private VBox left;
private Label subtitleLabel;
@Override @Override
public String getName() { public LineButtonBase getBean() {
return "subtitle";
}
@Override
public Object getBean() {
return LineButtonBase.this; return LineButtonBase.this;
} }
@Override @Override
protected void invalidated() { public Label getTitleLabel() {
String subtitle = get(); return titleLabel;
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
root.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
root.setCenter(titleLabel);
}
} }
}; };
} }
@@ -113,11 +84,4 @@ public abstract class LineButtonBase extends StackPane implements NoPaddingCompo
return subtitle; return subtitle;
} }
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
} }

View File

@@ -0,0 +1,143 @@
/*
* 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.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public interface LineComponent extends NoPaddingComponent {
String DEFAULT_STYLE_CLASS = "line-component";
PseudoClass PSEUDO_LARGER_TITLE = PseudoClass.getPseudoClass("large-title");
Insets PADDING = new Insets(8, 8, 8, 16);
Insets ICON_MARGIN = new Insets(0, 16, 0, 0);
double MIN_HEIGHT = 48.0;
private Node self() {
return (Node) this;
}
BorderPane getRoot();
StringProperty titleProperty();
default String getTitle() {
return titleProperty().get();
}
default void setTitle(String title) {
titleProperty().set(title);
}
abstract class SubtitleProperty extends StringPropertyBase {
private VBox left;
private Label subtitleLabel;
@Override
public String getName() {
return "subtitle";
}
@Override
public abstract LineComponent getBean();
public abstract Label getTitleLabel();
@Override
protected void invalidated() {
String subtitle = get();
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(getTitleLabel(), subtitleLabel);
getBean().getRoot().setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
getBean().getRoot().setCenter(getTitleLabel());
}
}
}
StringProperty subtitleProperty();
default String getSubtitle() {
return subtitleProperty().get();
}
default void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
default void setLeftIcon(Image icon) {
setLeftIcon(icon, -1.0);
}
default void setLeftIcon(Image icon, double size) {
ImageView imageView = new ImageView(icon);
imageView.getStyleClass().add("left-icon");
if (size > 0) {
imageView.setFitWidth(size);
imageView.setFitHeight(size);
imageView.setPreserveRatio(true);
imageView.setSmooth(true);
}
imageView.setMouseTransparent(true);
BorderPane.setAlignment(imageView, Pos.CENTER);
BorderPane.setMargin(imageView, ICON_MARGIN);
getRoot().setLeft(imageView);
}
default void setLeftIcon(SVG svg) {
setLeftIcon(svg, SVG.DEFAULT_SIZE);
}
default void setLeftIcon(SVG svg, double size) {
Node node = svg.createIcon(size);
node.getStyleClass().add("left-icon");
node.setMouseTransparent(true);
BorderPane.setAlignment(node, Pos.CENTER);
BorderPane.setMargin(node, ICON_MARGIN);
getRoot().setLeft(node);
}
default void setLargeTitle(boolean largeTitle) {
self().pseudoClassStateChanged(PSEUDO_LARGER_TITLE, largeTitle);
}
}

View File

@@ -1,112 +0,0 @@
/*
* 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.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo
public final class LineNavigationButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-navigation-button";
public LineNavigationButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
root.setMouseTransparent(true);
HBox right = new HBox();
root.setRight(right);
{
right.setAlignment(Pos.CENTER_RIGHT);
Label valueLabel = new Label();
valueLabel.getStyleClass().add("subtitle");
valueLabel.textProperty().bind(messageProperty());
Node arrowIcon = SVG.ARROW_FORWARD.createIcon(24);
HBox.setMargin(arrowIcon, new Insets(0, 8, 0, 8));
disabledProperty().addListener((observable, oldValue, newValue) ->
arrowIcon.setOpacity(newValue ? 0.4 : 1.0));
right.getChildren().setAll(valueLabel, arrowIcon);
}
FXUtils.onClicked(container, this::fire);
}
public void fire() {
fireEvent(new ActionEvent());
}
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
@Override
public Object getBean() {
return LineNavigationButton.this;
}
@Override
public String getName() {
return "onAction";
}
};
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
public EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
public void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
private final StringProperty message = new SimpleStringProperty(this, "message", "");
public StringProperty messageProperty() {
return message;
}
public String getMessage() {
return messageProperty().get();
}
public void setMessage(String message) {
messageProperty().set(message);
}
}

View File

@@ -19,24 +19,21 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
/// @author Glavo /// @author Glavo
public class LinePane extends BorderPane implements NoPaddingComponent { public class LinePane extends BorderPane implements LineComponent {
private static final String DEFAULT_STYLE_CLASS = "line-pane";
private static final Insets PADDING = new Insets(8, 8, 8, 16);
private final Label titleLabel; private final Label titleLabel;
public LinePane() { public LinePane() {
this.setPadding(PADDING); this.getStyleClass().addAll(LineComponent.DEFAULT_STYLE_CLASS, LinePane.DEFAULT_STYLE_CLASS);
this.setMinHeight(48);
this.setPadding(LineComponent.PADDING);
this.setMinHeight(LineComponent.MIN_HEIGHT);
this.titleLabel = new Label(); this.titleLabel = new Label();
this.setCenter(titleLabel); this.setCenter(titleLabel);
@@ -45,71 +42,36 @@ public class LinePane extends BorderPane implements NoPaddingComponent {
titleLabel.getStyleClass().add("title"); titleLabel.getStyleClass().add("title");
} }
@Override
public BorderPane getRoot() {
return this;
}
private final StringProperty title = new SimpleStringProperty(this, "title"); private final StringProperty title = new SimpleStringProperty(this, "title");
@Override
public StringProperty titleProperty() { public StringProperty titleProperty() {
return title; return title;
} }
public String getTitle() {
return titleProperty().get();
}
public void setTitle(String title) {
this.titleProperty().set(title);
}
private StringProperty subtitle; private StringProperty subtitle;
@Override
public StringProperty subtitleProperty() { public StringProperty subtitleProperty() {
if (subtitle == null) { if (subtitle == null) {
subtitle = new StringPropertyBase() { subtitle = new LineComponent.SubtitleProperty() {
private VBox left;
private Label subtitleLabel;
@Override @Override
public String getName() { public LinePane getBean() {
return "subtitle";
}
@Override
public Object getBean() {
return LinePane.this; return LinePane.this;
} }
@Override @Override
protected void invalidated() { public Label getTitleLabel() {
String subtitle = get(); return titleLabel;
if (subtitle != null && !subtitle.isEmpty()) {
if (left == null) {
left = new VBox();
left.setMouseTransparent(true);
left.setAlignment(Pos.CENTER_LEFT);
subtitleLabel = new Label();
subtitleLabel.setWrapText(true);
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
subtitleLabel.getStyleClass().add("subtitle");
}
subtitleLabel.setText(subtitle);
left.getChildren().setAll(titleLabel, subtitleLabel);
LinePane.this.setCenter(left);
} else if (left != null) {
subtitleLabel.setText(null);
LinePane.this.setCenter(titleLabel);
}
} }
}; };
} }
return subtitle; return subtitle;
} }
public String getSubtitle() {
return subtitleProperty().get();
}
public void setSubtitle(String subtitle) {
subtitleProperty().set(subtitle);
}
} }

View File

@@ -22,8 +22,6 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener; import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@@ -34,13 +32,8 @@ import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
@@ -57,13 +50,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils; import org.jackhuang.hmcl.util.i18n.LocaleUtils;
@@ -163,12 +150,12 @@ public class TerracottaControllerPage extends StackPane {
body.getStyleClass().add("terracotta-hint"); body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4); body.setLineSpacing(4);
LineButton download = LineButton.of(); var download = createLargeTitleLineButton();
download.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png")); download.setLeftIcon(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork))); download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork)));
download.setSubtitle(i18n("terracotta.status.uninitialized.desc")); download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
download.setRightIcon(SVG.ARROW_FORWARD); download.setRightIcon(SVG.ARROW_FORWARD, ICON_SIZE);
FXUtils.onClicked(download, () -> { download.setOnAction(event -> {
TerracottaState.Preparing s = TerracottaManager.download(); TerracottaState.Preparing s = TerracottaManager.download();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -207,12 +194,12 @@ public class TerracottaControllerPage extends StackPane {
flow.getStyleClass().add("terracotta-hint"); flow.getStyleClass().add("terracotta-hint");
flow.setLineSpacing(4); flow.setLineSpacing(4);
LineButton host = LineButton.of(); var host = createLargeTitleLineButton();
host.setLeftIcon(SVG.HOST); host.setLeftIcon(SVG.HOST, ICON_SIZE);
host.setTitle(i18n("terracotta.status.waiting.host.title")); host.setTitle(i18n("terracotta.status.waiting.host.title"));
host.setSubtitle(i18n("terracotta.status.waiting.host.desc")); host.setSubtitle(i18n("terracotta.status.waiting.host.desc"));
host.setRightIcon(SVG.ARROW_FORWARD); host.setRightIcon(SVG.ARROW_FORWARD, ICON_SIZE);
FXUtils.onClicked(host, () -> { host.setOnAction(event -> {
if (LauncherHelper.countMangedProcesses() >= 1) { if (LauncherHelper.countMangedProcesses() >= 1) {
TerracottaState.HostScanning s1 = TerracottaManager.setScanning(); TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
if (s1 != null) { if (s1 != null) {
@@ -239,12 +226,12 @@ public class TerracottaControllerPage extends StackPane {
} }
}); });
LineButton guest = LineButton.of(); var guest = createLargeTitleLineButton();
guest.setLeftIcon(SVG.ADD_CIRCLE); guest.setLeftIcon(SVG.ADD_CIRCLE, ICON_SIZE);
guest.setTitle(i18n("terracotta.status.waiting.guest.title")); guest.setTitle(i18n("terracotta.status.waiting.guest.title"));
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc")); guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
guest.setRightIcon(SVG.ARROW_FORWARD); guest.setRightIcon(SVG.ARROW_FORWARD, ICON_SIZE);
FXUtils.onClicked(guest, () -> { guest.setOnAction(event -> {
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> { Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code); Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
if (task != null) { if (task != null) {
@@ -263,11 +250,11 @@ public class TerracottaControllerPage extends StackPane {
}); });
if (ThreadLocalRandom.current().nextDouble() < 0.02D) { if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
LineButton feedback = LineButton.of(); var feedback = createLargeTitleLineButton();
feedback.setLeftIcon(SVG.FEEDBACK); feedback.setLeftIcon(SVG.FEEDBACK, ICON_SIZE);
feedback.setTitle(i18n("terracotta.feedback.title")); feedback.setTitle(i18n("terracotta.feedback.title"));
feedback.setSubtitle(i18n("terracotta.feedback.desc")); feedback.setSubtitle(i18n("terracotta.feedback.desc"));
feedback.setRightIcon(SVG.OPEN_IN_NEW); feedback.setRightIcon(SVG.OPEN_IN_NEW, ICON_SIZE);
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK)); FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
nodesProperty.setAll(flow, host, guest, feedback); nodesProperty.setAll(flow, host, guest, feedback);
@@ -282,11 +269,11 @@ public class TerracottaControllerPage extends StackPane {
body.getStyleClass().add("terracotta-hint"); body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4); body.setLineSpacing(4);
LineButton room = LineButton.of(); var room = createLargeTitleLineButton();
room.setLeftIcon(SVG.ARROW_BACK); room.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back")); room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.scanning.back")); room.setSubtitle(i18n("terracotta.status.scanning.back"));
FXUtils.onClicked(room, () -> { room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -298,11 +285,11 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.host_starting")); statusProperty.set(i18n("terracotta.status.host_starting"));
progressProperty.set(-1); progressProperty.set(-1);
LineButton room = LineButton.of(); var room = createLargeTitleLineButton();
room.setLeftIcon(SVG.ARROW_BACK); room.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back")); room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.host_starting.back")); room.setSubtitle(i18n("terracotta.status.host_starting.back"));
FXUtils.onClicked(room, () -> { room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -342,17 +329,17 @@ public class TerracottaControllerPage extends StackPane {
code.setCursor(Cursor.HAND); code.setCursor(Cursor.HAND);
FXUtils.onClicked(code, () -> copyCode(cs)); FXUtils.onClicked(code, () -> copyCode(cs));
LineButton copy = LineButton.of(); var copy = createLargeTitleLineButton();
copy.setLeftIcon(SVG.CONTENT_COPY); copy.setLeftIcon(SVG.CONTENT_COPY, ICON_SIZE);
copy.setTitle(i18n("terracotta.status.host_ok.code.copy")); copy.setTitle(i18n("terracotta.status.host_ok.code.copy"));
copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc")); copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc"));
FXUtils.onClicked(copy, () -> copyCode(cs)); FXUtils.onClicked(copy, () -> copyCode(cs));
LineButton back = LineButton.of(); var back = createLargeTitleLineButton();
back.setLeftIcon(SVG.ARROW_BACK); back.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back")); back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.host_ok.back")); back.setSubtitle(i18n("terracotta.status.host_ok.back"));
FXUtils.onClicked(back, () -> { back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -369,11 +356,11 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.guest_starting")); statusProperty.set(i18n("terracotta.status.guest_starting"));
progressProperty.set(-1); progressProperty.set(-1);
LineButton room = LineButton.of(); var room = createLargeTitleLineButton();
room.setLeftIcon(SVG.ARROW_BACK); room.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
room.setTitle(i18n("terracotta.back")); room.setTitle(i18n("terracotta.back"));
room.setSubtitle(i18n("terracotta.status.guest_starting.back")); room.setSubtitle(i18n("terracotta.status.guest_starting.back"));
FXUtils.onClicked(room, () -> { room.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -384,12 +371,12 @@ public class TerracottaControllerPage extends StackPane {
if (state instanceof TerracottaState.GuestStarting) { if (state instanceof TerracottaState.GuestStarting) {
TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty(); TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty();
if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) { if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) {
LineButton info = LineButton.of(); var info = createLargeTitleLineButton();
info.setLeftIcon(switch (difficulty) { info.setLeftIcon(switch (difficulty) {
case UNKNOWN -> throw new AssertionError(); case UNKNOWN -> throw new AssertionError();
case EASIEST, SIMPLE -> SVG.INFO; case EASIEST, SIMPLE -> SVG.INFO;
case MEDIUM, TOUGH -> SVG.WARNING; case MEDIUM, TOUGH -> SVG.WARNING;
}); }, ICON_SIZE);
String difficultyID = difficulty.name().toLowerCase(Locale.ROOT); String difficultyID = difficulty.name().toLowerCase(Locale.ROOT);
info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID))); info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID)));
@@ -412,15 +399,15 @@ public class TerracottaControllerPage extends StackPane {
statusProperty.set(i18n("terracotta.status.guest_ok")); statusProperty.set(i18n("terracotta.status.guest_ok"));
progressProperty.set(1); progressProperty.set(1);
LineButton tutorial = LineButton.of(); var tutorial = createLargeTitleLineButton();
tutorial.setTitle(i18n("terracotta.status.guest_ok.title")); tutorial.setTitle(i18n("terracotta.status.guest_ok.title"));
tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl())); tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl()));
LineButton back = LineButton.of(); var back = createLargeTitleLineButton();
back.setLeftIcon(SVG.ARROW_BACK); back.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back")); back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.guest_ok.back")); back.setSubtitle(i18n("terracotta.status.guest_ok.back"));
FXUtils.onClicked(back, () -> { back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -438,11 +425,11 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(1); progressProperty.set(1);
nodesProperty.setAll(); nodesProperty.setAll();
LineButton back = LineButton.of(); var back = createLargeTitleLineButton();
back.setLeftIcon(SVG.ARROW_BACK); back.setLeftIcon(SVG.ARROW_BACK, ICON_SIZE);
back.setTitle(i18n("terracotta.back")); back.setTitle(i18n("terracotta.back"));
back.setSubtitle(i18n("terracotta.status.exception.back")); back.setSubtitle(i18n("terracotta.status.exception.back"));
FXUtils.onClicked(back, () -> { back.setOnAction(event -> {
TerracottaState.Waiting s = TerracottaManager.setWaiting(); TerracottaState.Waiting s = TerracottaManager.setWaiting();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -450,8 +437,8 @@ public class TerracottaControllerPage extends StackPane {
}); });
SpinnerPane exportLog = new SpinnerPane(); SpinnerPane exportLog = new SpinnerPane();
LineButton exportLogInner = LineButton.of(); var exportLogInner = createLargeTitleLineButton();
exportLogInner.setLeftIcon(SVG.OUTPUT); exportLogInner.setLeftIcon(SVG.OUTPUT, ICON_SIZE);
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);
@@ -459,7 +446,7 @@ public class TerracottaControllerPage extends StackPane {
// 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());
FXUtils.onClicked(exportLogInner, () -> { exportLogInner.setOnAction(event -> {
exportLog.setLoading(true); exportLog.setLoading(true);
TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> { TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> {
@@ -493,11 +480,11 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(1); progressProperty.set(1);
if (fatal.isRecoverable()) { if (fatal.isRecoverable()) {
LineButton retry = LineButton.of(); var retry = createLargeTitleLineButton();
retry.setLeftIcon(SVG.RESTORE); retry.setLeftIcon(SVG.RESTORE, ICON_SIZE);
retry.setTitle(i18n("terracotta.status.fatal.retry")); retry.setTitle(i18n("terracotta.status.fatal.retry"));
retry.setSubtitle(message); retry.setSubtitle(message);
FXUtils.onClicked(retry, () -> { retry.setOnAction(event -> {
TerracottaState s = TerracottaManager.recover(); TerracottaState s = TerracottaManager.recover();
if (s != null) { if (s != null) {
UI_STATE.set(s); UI_STATE.set(s);
@@ -534,7 +521,8 @@ public class TerracottaControllerPage extends StackPane {
children.add(statusPane); children.add(statusPane);
children.addAll(nodesProperty); children.addAll(nodesProperty);
} }
// Prevent the shadow of components from being clipped
StackPane.setMargin(components, new Insets(0, 0, 5, 0));
transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN); transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN);
}; };
listener.changed(UI_STATE, null, UI_STATE.get()); listener.changed(UI_STATE, null, UI_STATE.get());
@@ -561,34 +549,27 @@ public class TerracottaControllerPage extends StackPane {
private ComponentSublist getThirdPartyDownloadNodes() { private ComponentSublist getThirdPartyDownloadNodes() {
ComponentSublist locals = new ComponentSublist(); ComponentSublist locals = new ComponentSublist();
locals.setComponentPadding(false);
LineButton header = LineButton.of(false); var header = new LinePane();
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png")); header.setLargeTitle(true);
header.setPadding(Insets.EMPTY);
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
header.setMouseTransparent(true);
header.setLeftIcon(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
header.setTitle(i18n("terracotta.from_local.title")); header.setTitle(i18n("terracotta.from_local.title"));
header.setSubtitle(i18n("terracotta.from_local.desc")); header.setSubtitle(i18n("terracotta.from_local.desc"));
locals.setHeaderLeft(header); locals.setHeaderLeft(header);
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) { for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
HBox node = new HBox(); LineButton item = new LineButton();
node.setAlignment(Pos.CENTER_LEFT); item.setRightIcon(SVG.OPEN_IN_NEW);
node.setPadding(new Insets(10, 16, 10, 16)); item.setTitle(link.description().getText(I18n.getLocale().getCandidateLocales()));
item.setOnAction(event -> Controllers.dialog(
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
HBox placeholder = new HBox();
HBox.setHgrow(placeholder, Priority.ALWAYS);
Node icon = SVG.OPEN_IN_NEW.createIcon(16);
node.getChildren().setAll(description, placeholder, icon);
String url = link.link();
RipplerContainer container = new RipplerContainer(node);
container.setOnMouseClicked(ev -> Controllers.dialog(
i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME), i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME),
i18n("message.info"), MessageDialogPane.MessageType.INFO, i18n("message.info"), MessageDialogPane.MessageType.INFO,
() -> FXUtils.openLink(url) () -> FXUtils.openLink(link.link())
)); ));
ComponentList.setNoPadding(container); locals.getContent().add(item);
locals.getContent().add(container);
} }
return locals; return locals;
} }
@@ -597,103 +578,12 @@ public class TerracottaControllerPage extends StackPane {
FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast")); FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast"));
} }
private static final class LineButton extends RipplerContainer { private static final double ICON_SIZE = 28;
private final WeakListenerHolder holder = new WeakListenerHolder();
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left"); private static LineButton createLargeTitleLineButton() {
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right"); var lineButton = new LineButton();
private final StringProperty title = new SimpleStringProperty(this, "title", ""); lineButton.setLargeTitle(true);
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", ""); return lineButton;
public static LineButton of() {
return of(true);
}
public static LineButton of(boolean padding) {
HBox container = new HBox();
if (padding) {
container.setPadding(new Insets(10, 16, 10, 16));
}
container.setAlignment(Pos.CENTER_LEFT);
container.setCursor(Cursor.HAND);
container.setSpacing(16);
LineButton button = new LineButton(container);
VBox spacing = new VBox();
HBox.setHgrow(spacing, Priority.ALWAYS);
button.holder.add(FXUtils.observeWeak(() -> {
List<Node> nodes = new ArrayList<>(4);
Node left = button.left.get();
if (left != null) {
nodes.add(left);
}
{
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
VBox middle = new VBox();
middle.getStyleClass().add("two-line-list-item");
middle.setMouseTransparent(true);
{
HBox firstLine = new HBox();
firstLine.getStyleClass().add("first-line");
{
Label lblTitle = new Label(button.title.get());
lblTitle.getStyleClass().add("title");
firstLine.getChildren().setAll(lblTitle);
}
HBox secondLine = new HBox();
secondLine.getStyleClass().add("second-line");
{
Text text = new Text(button.subTitle.get());
TextFlow lblSubtitle = new TextFlow(text);
lblSubtitle.getStyleClass().add("subtitle");
secondLine.getChildren().setAll(lblSubtitle);
}
middle.getChildren().setAll(firstLine, secondLine);
}
nodes.add(middle);
}
nodes.add(spacing);
Node right = button.right.get();
if (right != null) {
nodes.add(right);
}
container.getChildren().setAll(nodes);
}, button.title, button.subTitle, button.left, button.right));
ComponentList.setNoPadding(button);
return button;
}
private LineButton(Node container) {
super(container);
}
public void setTitle(String title) {
this.title.set(title);
}
public void setSubtitle(String subtitle) {
this.subTitle.set(subtitle);
}
public void setLeftImage(Image left) {
this.left.set(new ImageView(left));
}
public void setLeftIcon(SVG left) {
this.left.set(left.createIcon(28));
}
public void setRightIcon(SVG right) {
this.right.set(right.createIcon(28));
}
} }
private static final class PlayerProfileUI extends VBox { private static final class PlayerProfileUI extends VBox {

View File

@@ -424,7 +424,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP); serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
} }
LineNavigationButton showAdvancedSettingPane = new LineNavigationButton(); var showAdvancedSettingPane = LineButton.createNavigationButton();
showAdvancedSettingPane.setTitle(i18n("settings.advanced")); showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
showAdvancedSettingPane.setOnAction(event -> { showAdvancedSettingPane.setOnAction(event -> {
if (lastVersionSetting != null) { if (lastVersionSetting != null) {

View File

@@ -1860,10 +1860,29 @@
/******************************************************************************* /*******************************************************************************
* * * *
* Line Button * * Line Component *
* * * *
******************************************************************************/ ******************************************************************************/
.line-component .title {
-fx-text-fill: -monet-on-surface;
}
.line-component:large-title .title {
-fx-font-size: 15px;
}
.line-component .subtitle {
-fx-text-fill: -monet-on-surface-variant;
}
.line-button .svg {
-fx-opacity: 1;
}
.line-button .svg:disabled {
-fx-opacity: 0.4;
}
.line-select-button .svg { .line-select-button .svg {
-fx-opacity: 1; -fx-opacity: 1;