使用 LineComponent 简化代码 (#5408)

This commit is contained in:
Glavo
2026-02-05 21:37:51 +08:00
committed by GitHub
parent 9f097149b2
commit 982c062a01
10 changed files with 409 additions and 388 deletions

View File

@@ -1,162 +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.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class FileItem extends BorderPane {
private final Label lblPath = new Label();
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
public FileItem() {
VBox left = new VBox();
Label name = new Label();
name.textProperty().bind(nameProperty());
lblPath.getStyleClass().addAll("subtitle-label");
lblPath.textProperty().bind(path);
left.getChildren().addAll(name, lblPath);
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.EDIT.createIcon(16));
right.getStyleClass().add("toggle-icon4");
right.setOnAction(e -> onExplore());
FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right);
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
}
/**
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given;
try {
given = Path.of(path).toAbsolutePath().normalize();
} catch (IllegalArgumentException e) {
return path;
}
if (isConvertToRelativePath()) {
try {
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return given.normalize().toString();
}
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (path.get() != null) {
Path file;
try {
file = Path.of(path.get());
if (Files.exists(file)) {
if (Files.isRegularFile(file))
file = file.toAbsolutePath().normalize().getParent();
else if (Files.isDirectory(file))
file = file.toAbsolutePath().normalize();
chooser.setInitialDirectory(file.toFile());
}
} catch (InvalidPathException e) {
LOG.warning("Failed to resolve path: " + path.get());
}
}
chooser.titleProperty().bind(titleProperty());
var selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
chooser.titleProperty().unbind();
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getPath() {
return path.get();
}
public StringProperty pathProperty() {
return path;
}
public void setPath(String path) {
this.path.set(path);
}
public boolean isConvertToRelativePath() {
return convertToRelativePath.get();
}
public BooleanProperty convertToRelativePathProperty() {
return convertToRelativePath;
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
this.convertToRelativePath.set(convertToRelativePath);
}
}

View File

@@ -0,0 +1,240 @@
/*
* 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.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class LineFileChooserButton extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-file-select-button";
public LineFileChooserButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setTrailingIcon(SVG.EDIT);
}
/// Converts the given path to absolute/relative(if possible) path according to [#convertToRelativePathProperty()].
private String processPath(Path path) {
if (isConvertToRelativePath() && path.isAbsolute()) {
try {
return Metadata.CURRENT_DIRECTORY.relativize(path).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return path.normalize().toString();
}
@Override
public void fire() {
super.fire();
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
String windowTitle = getFileChooserTitle();
Path initialDirectory = null;
if (getLocation() != null) {
Path file;
try {
file = FileUtils.toAbsolute(Path.of(getLocation()));
if (Files.exists(file)) {
if (Files.isRegularFile(file))
initialDirectory = file.getParent();
else if (Files.isDirectory(file))
initialDirectory = file;
}
} catch (IllegalArgumentException e) {
LOG.warning("Failed to resolve path: " + getLocation());
}
}
Path path;
Type type = getType();
if (type == Type.OPEN_DIRECTORY) {
var directoryChooser = new DirectoryChooser();
if (windowTitle != null)
directoryChooser.setTitle(windowTitle);
if (initialDirectory != null)
directoryChooser.setInitialDirectory(initialDirectory.toFile());
path = FileUtils.toPath(directoryChooser.showDialog(owner));
} else {
var fileChooser = new FileChooser();
if (windowTitle != null)
fileChooser.setTitle(windowTitle);
if (initialDirectory != null)
fileChooser.setInitialDirectory(initialDirectory.toFile());
if (extensionFilters != null)
fileChooser.getExtensionFilters().setAll(extensionFilters);
fileChooser.setInitialFileName(getInitialFileName());
path = FileUtils.toPath(switch (type) {
case OPEN_FILE -> fileChooser.showOpenDialog(owner);
case SAVE_FILE -> fileChooser.showSaveDialog(owner);
default -> throw new AssertionError("Unknown Type: " + type);
});
}
if (path != null) {
setLocation(processPath(path));
}
}
private final StringProperty location = new StringPropertyBase() {
@Override
public Object getBean() {
return LineFileChooserButton.this;
}
@Override
public String getName() {
return "location";
}
@Override
protected void invalidated() {
setTrailingText(get());
}
};
public StringProperty locationProperty() {
return location;
}
public String getLocation() {
return locationProperty().get();
}
public void setLocation(String location) {
locationProperty().set(location);
}
private final StringProperty fileChooserTitle = new SimpleStringProperty(this, "fileChooserTitle");
public StringProperty fileChooserTitleProperty() {
return fileChooserTitle;
}
public String getFileChooserTitle() {
return fileChooserTitleProperty().get();
}
public void setFileChooserTitle(String fileChooserTitle) {
fileChooserTitleProperty().set(fileChooserTitle);
}
private ObjectProperty<Type> type;
public ObjectProperty<Type> typeProperty() {
if (type == null) {
type = new SimpleObjectProperty<>(this, "type", Type.OPEN_FILE);
}
return type;
}
public Type getType() {
return type != null ? type.get() : Type.OPEN_FILE;
}
public void setType(Type type) {
typeProperty().set(type);
}
private ObjectProperty<String> initialFileName;
public final ObjectProperty<String> initialFileNameProperty() {
if (initialFileName == null)
initialFileName = new SimpleObjectProperty<>(this, "initialFileName");
return initialFileName;
}
public final String getInitialFileName() {
return initialFileName != null ? initialFileName.get() : null;
}
public final void setInitialFileName(String value) {
initialFileNameProperty().set(value);
}
private ObservableList<FileChooser.ExtensionFilter> extensionFilters;
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
if (extensionFilters == null)
extensionFilters = FXCollections.observableArrayList();
return extensionFilters;
}
private BooleanProperty convertToRelativePath;
public BooleanProperty convertToRelativePathProperty() {
if (convertToRelativePath == null)
convertToRelativePath = new BooleanPropertyBase(false) {
@Override
public Object getBean() {
return LineFileChooserButton.this;
}
@Override
public String getName() {
return "convertToRelativePath";
}
@Override
protected void invalidated() {
String location = getLocation();
if (location == null)
return;
try {
setLocation(processPath(FileUtils.toAbsolute(Path.of(getLocation()))));
} catch (IllegalArgumentException ignored) {
}
}
};
return convertToRelativePath;
}
public boolean isConvertToRelativePath() {
return convertToRelativePath != null && convertToRelativePath.get();
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
convertToRelativePathProperty().set(convertToRelativePath);
}
public enum Type {
OPEN_FILE,
OPEN_DIRECTORY,
SAVE_FILE
}
}

View File

@@ -107,7 +107,7 @@ public final class LocalModpackPage extends ModpackPage {
.whenComplete(Schedulers.javafx(), (manifest, exception) -> { .whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) { if (exception instanceof ManuallyCreatedModpackException) {
hideSpinner(); hideSpinner();
lblName.setText(FileUtils.getName(selectedFile)); nameProperty.set(FileUtils.getName(selectedFile));
installAsVersion.set(false); installAsVersion.set(false);
if (name == null) { if (name == null) {
@@ -127,9 +127,9 @@ public final class LocalModpackPage extends ModpackPage {
} else { } else {
hideSpinner(); hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest); controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName()); nameProperty.set(manifest.getName());
lblVersion.setText(manifest.getVersion()); versionProperty.set(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor()); authorProperty.set(manifest.getAuthor());
if (name == null) { if (name == null) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962 // trim: https://github.com/HMCL-dev/HMCL/issues/962

View File

@@ -2,14 +2,15 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import javafx.geometry.Insets; import javafx.beans.property.StringProperty;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LinePane;
import org.jackhuang.hmcl.ui.construct.LineTextPane;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -23,9 +24,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final WizardController controller; protected final WizardController controller;
protected final Label lblName; protected final StringProperty nameProperty;
protected final Label lblVersion; protected final StringProperty versionProperty;
protected final Label lblAuthor; protected final StringProperty authorProperty;
protected final JFXTextField txtModpackName; protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall; protected final JFXButton btnInstall;
protected final JFXButton btnDescription; protected final JFXButton btnDescription;
@@ -39,46 +40,37 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
ComponentList componentList = new ComponentList(); ComponentList componentList = new ComponentList();
{ {
BorderPane archiveNamePane = new BorderPane(); var archiveNamePane = new LinePane();
{ {
Label label = new Label(i18n("archive.file.name")); archiveNamePane.setTitle(i18n("archive.file.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
archiveNamePane.setLeft(label);
txtModpackName = new JFXTextField(); txtModpackName = new JFXTextField();
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32)); txtModpackName.setPrefWidth(300);
// FIXME: Validator are not shown properly
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT); BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setCenter(txtModpackName); archiveNamePane.setRight(txtModpackName);
} }
BorderPane modpackNamePane = new BorderPane(); var modpackNamePane = new LineTextPane();
{ {
modpackNamePane.setLeft(new Label(i18n("modpack.name"))); modpackNamePane.setTitle(i18n("modpack.name"));
nameProperty = modpackNamePane.textProperty();
lblName = new Label();
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
modpackNamePane.setCenter(lblName);
} }
BorderPane versionPane = new BorderPane(); var versionPane = new LineTextPane();
{ {
versionPane.setLeft(new Label(i18n("archive.version"))); versionPane.setTitle(i18n("archive.version"));
versionProperty = versionPane.textProperty();
lblVersion = new Label();
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
versionPane.setCenter(lblVersion);
} }
BorderPane authorPane = new BorderPane(); var authorPane = new LineTextPane();
{ {
authorPane.setLeft(new Label(i18n("archive.author"))); authorPane.setTitle(i18n("archive.author"));
authorProperty = authorPane.textProperty();
lblAuthor = new Label();
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
authorPane.setCenter(lblAuthor);
} }
BorderPane descriptionPane = new BorderPane(); var descriptionPane = new BorderPane();
{ {
btnDescription = FXUtils.newBorderButton(i18n("modpack.description")); btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
btnDescription.setOnAction(e -> onDescribe()); btnDescription.setOnAction(e -> onDescribe());

View File

@@ -53,9 +53,9 @@ public final class RemoteModpackPage extends ModpackPage {
return; return;
} }
lblName.setText(manifest.getName()); nameProperty.set(manifest.getName());
lblVersion.setText(manifest.getVersion()); versionProperty.set(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor()); authorProperty.set(manifest.getAuthor());
Profile profile = controller.getSettings().get(ModpackPage.PROFILE); Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
String name = controller.getSettings().get(MODPACK_NAME); String name = controller.getSettings().get(MODPACK_NAME);

View File

@@ -17,7 +17,11 @@
*/ */
package org.jackhuang.hmcl.ui.export; package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.*; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -53,8 +57,6 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE; import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH; import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER; import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
@@ -127,7 +129,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
exportInfo.setVersion(version.get()); exportInfo.setVersion(version.get());
exportInfo.setAuthor(author.get()); exportInfo.setAuthor(author.get());
exportInfo.setDescription(description.get()); exportInfo.setDescription(description.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setUrl(url.get()); exportInfo.setUrl(url.get());
exportInfo.setForceUpdate(forceUpdate.get()); exportInfo.setForceUpdate(forceUpdate.get());
exportInfo.setPackWithLauncher(packWithLauncher.get()); exportInfo.setPackWithLauncher(packWithLauncher.get());
@@ -171,10 +172,11 @@ public final class ModpackInfoPage extends Control implements WizardPage {
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> { public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private ObservableList<Node> originList; private ObservableList<Node> originList;
private final List<JFXTextField> validatingFields = new ArrayList<>();
public ModpackInfoPageSkin(ModpackInfoPage skinnable) { public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable); super(skinnable);
Insets insets = new Insets(5, 0, 12, 0);
Insets componentListMargin = new Insets(16, 0, 16, 0); Insets componentListMargin = new Insets(16, 0, 16, 0);
ScrollPane scroll = new ScrollPane(); ScrollPane scroll = new ScrollPane();
@@ -182,7 +184,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
scroll.setFitToHeight(true); scroll.setFitToHeight(true);
getChildren().setAll(scroll); getChildren().setAll(scroll);
List<JFXTextField> validatingFields = new ArrayList<>();
{ {
BorderPane borderPane = new BorderPane(); BorderPane borderPane = new BorderPane();
@@ -209,83 +210,52 @@ public final class ModpackInfoPage extends Control implements WizardPage {
BorderPane.setMargin(list, componentListMargin); BorderPane.setMargin(list, componentListMargin);
borderPane.setCenter(list); borderPane.setCenter(list);
var instanceNamePane = new LineTextPane();
{ {
BorderPane borderPane1 = new BorderPane(); instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version"))); instanceNamePane.setText(skinnable.versionName);
Label versionNameLabel = new Label(); list.getContent().add(instanceNamePane);
versionNameLabel.setText(skinnable.versionName);
borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1);
} }
{
GridPane pane = new GridPane();
list.getContent().add(pane);
pane.setHgap(16);
pane.setVgap(8);
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
int rowIndex = 0; list.getContent().addAll(
createTextFieldLinePane(i18n("modpack.name"), skinnable.name, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator()),
createTextFieldLinePane(i18n("archive.version"), skinnable.version, new RequiredValidator())
);
JFXTextField txtModpackName = new JFXTextField(); if (skinnable.options.isRequireFileApi()) {
txtModpackName.textProperty().bindBidirectional(skinnable.name); list.getContent().add(createTextFieldLinePane(
txtModpackName.getValidators().add(new RequiredValidator()); i18n("modpack.file_api"), skinnable.fileApi,
validatingFields.add(txtModpackName); skinnable.options.isValidateFileApi() ? new RequiredValidator() : null,
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName); new URLValidator(true)
));
}
JFXTextField txtModpackAuthor = new JFXTextField(); if (skinnable.options.isRequireLaunchArguments()) {
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author); list.getContent().add(createTextFieldLinePane(
txtModpackAuthor.getValidators().add(new RequiredValidator()); i18n("settings.advanced.minecraft_arguments"), skinnable.launchArguments
validatingFields.add(txtModpackAuthor); ));
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor); }
JFXTextField txtModpackVersion = new JFXTextField(); if (skinnable.options.isRequireJavaArguments()) {
txtModpackVersion.textProperty().bindBidirectional(skinnable.version); list.getContent().add(createTextFieldLinePane(
txtModpackVersion.getValidators().add(new RequiredValidator()); i18n("settings.advanced.jvm_args"), skinnable.javaArguments
validatingFields.add(txtModpackVersion); ));
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion); }
if (skinnable.options.isRequireFileApi()) { if (skinnable.options.isRequireUrl()) {
JFXTextField txtModpackFileApi = new JFXTextField(); list.getContent().add(createTextFieldLinePane(
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi); i18n("modpack.origin.url"), skinnable.url
validatingFields.add(txtModpackFileApi); ));
}
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new URLValidator(true));
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
}
if (skinnable.options.isRequireUrl()) {
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
}
if (skinnable.options.isRequireOrigins()) {
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
validatingFields.add(txtMcbbs);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
}
if (skinnable.options.isRequireOrigins()) {
list.getContent().add(createTextFieldLinePane(
i18n("modpack.origin.mcbbs"), skinnable.mcbbsThreadId,
new NumberValidator(i18n("input.number"), true)
));
} }
if (skinnable.options.isRequireMinMemory()) { if (skinnable.options.isRequireMinMemory()) {
@@ -339,78 +309,51 @@ public final class ModpackInfoPage extends Control implements WizardPage {
} }
if (skinnable.options.isRequireAuthlibInjectorServer()) { if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>(); var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
cboServers.setMaxWidth(Double.MAX_VALUE); serversSelectButton.setTitle(i18n("account.injector.server"));
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl()))); serversSelectButton.setConverter(AuthlibInjectorServer::getName);
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName)); serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers()); serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> {
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()) AuthlibInjectorServer selected = serversSelectButton.getValue();
.map(AuthlibInjectorServer::getUrl) return selected != null ? selected.getUrl() : null;
.orElse(null))); }, serversSelectButton.valueProperty()));
BorderPane pane = new BorderPane(); list.getContent().add(serversSelectButton);
Label left = new Label(i18n("account.injector.server"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
pane.setLeft(left);
pane.setRight(cboServers);
list.getContent().add(pane);
} }
if (skinnable.options.isRequireForceUpdate()) { if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane(); var requireForceUpdateButton = new LineToggleButton();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update"))); requireForceUpdateButton.setTitle(i18n("modpack.wizard.step.initialization.force_update"));
list.getContent().add(pane); requireForceUpdateButton.selectedProperty().bindBidirectional(skinnable.forceUpdate);
JFXToggleButton button = new JFXToggleButton(); list.getContent().add(requireForceUpdateButton);
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
} }
{ {
BorderPane pane = new BorderPane(); var canIncludeLauncherButton = new LineToggleButton();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher"))); canIncludeLauncherButton.setTitle(i18n("modpack.wizard.step.initialization.include_launcher"));
list.getContent().add(pane); canIncludeLauncherButton.setDisable(!skinnable.canIncludeLauncher);
canIncludeLauncherButton.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
JFXToggleButton button = new JFXToggleButton(); list.getContent().add(canIncludeLauncherButton);
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
} }
if (skinnable.options.isRequireNoCreateRemoteFiles()) { if (skinnable.options.isRequireNoCreateRemoteFiles()) {
BorderPane noCreateRemoteFiles = new BorderPane(); var requireNoCreateRemoteFilesButton = new LineToggleButton();
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files"))); requireNoCreateRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.no_create_remote_files"));
list.getContent().add(noCreateRemoteFiles); requireNoCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton(); list.getContent().add(requireNoCreateRemoteFilesButton);
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
noCreateRemoteFilesButton.setSize(8);
noCreateRemoteFilesButton.setMinHeight(16);
noCreateRemoteFilesButton.setMaxHeight(16);
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
} }
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) { if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
BorderPane skipCurseForgeRemoteFiles = new BorderPane(); var skipCurseForgeRemoteFilesButton = new LineToggleButton();
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"))); skipCurseForgeRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"));
list.getContent().add(skipCurseForgeRemoteFiles);
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles); skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
skipCurseForgeRemoteFilesButton.setSize(8);
skipCurseForgeRemoteFilesButton.setMinHeight(16); list.getContent().add(skipCurseForgeRemoteFilesButton);
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
} }
} }
@@ -436,5 +379,31 @@ public final class ModpackInfoPage extends Control implements WizardPage {
FXUtils.smoothScrolling(scroll); FXUtils.smoothScrolling(scroll);
} }
private LinePane createTextFieldLinePane(String title, StringProperty property, ValidatorBase... validators) {
LinePane linePane = new LinePane();
JFXTextField textField = new JFXTextField();
textField.setMinWidth(500);
linePane.setTitle(title);
linePane.setRight(textField);
textField.textProperty().bindBidirectional(property);
boolean needValidation = false;
if (validators != null) {
for (ValidatorBase validator : validators) {
if (validator != null) {
needValidation = true;
textField.getValidators().add(validator);
}
}
}
if (needValidation) {
FXUtils.setValidateWhileTextChanged(textField, true);
validatingFields.add(textField);
}
return linePane;
}
} }
} }

View File

@@ -35,10 +35,7 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
@@ -54,7 +51,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
private final StringProperty location; private final StringProperty location;
private final Profile profile; private final Profile profile;
private final JFXTextField txtProfileName; private final JFXTextField txtProfileName;
private final FileItem gameDir; private final LineFileChooserButton gameDir;
private final LineToggleButton toggleUseRelativePath; private final LineToggleButton toggleUseRelativePath;
/** /**
@@ -107,10 +104,11 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
}); });
} }
gameDir = new FileItem(); gameDir = new LineFileChooserButton();
gameDir.setName(i18n("profile.instance_directory")); gameDir.setTitle(i18n("profile.instance_directory"));
gameDir.setTitle(i18n("profile.instance_directory.choose")); gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose"));
gameDir.pathProperty().bindBidirectional(location); gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY);
gameDir.locationProperty().bindBidirectional(location);
toggleUseRelativePath = new LineToggleButton(); toggleUseRelativePath = new LineToggleButton();
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path")); toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
@@ -188,7 +186,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
} }
} else { } else {
if (StringUtils.isBlank(getLocation())) { if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore(); gameDir.fire();
} }
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation())); Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected()); newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());

View File

@@ -175,27 +175,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList = new ComponentList(); componentList = new ComponentList();
if (!globalSetting) { if (!globalSetting) {
BorderPane copyGlobalPane = new BorderPane(); var copyGlobalButton = LineButton.createNavigationButton();
{ copyGlobalButton.setTitle(i18n("settings.game.copy_global"));
Label label = new Label(i18n("settings.game.copy_global")); copyGlobalButton.setOnAction(event ->
copyGlobalPane.setLeft(label); Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
BorderPane.setAlignment(label, Pos.CENTER_LEFT); Set<String> ignored = new HashSet<>(Arrays.asList(
"usesGlobal",
"versionIcon"
));
JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all")); PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
copyAll.disableProperty().bind(modpack); }, null));
copyGlobalPane.setRight(copyAll);
copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
Set<String> ignored = new HashSet<>(Arrays.asList(
"usesGlobal",
"versionIcon"
));
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name)); componentList.getContent().add(copyGlobalButton);
}, null));
BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT);
}
componentList.getContent().add(copyGlobalPane);
} }
javaItem = new MultiFileItem<>(); javaItem = new MultiFileItem<>();

View File

@@ -20,17 +20,16 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem; import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
import org.jackhuang.hmcl.ui.construct.LinePane;
import org.jackhuang.hmcl.ui.construct.LineTextPane;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -42,47 +41,40 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
public WorldExportPageSkin(WorldExportPage skinnable) { public WorldExportPageSkin(WorldExportPage skinnable) {
super(skinnable); super(skinnable);
Insets insets = new Insets(0, 0, 12, 0);
VBox container = new VBox(); VBox container = new VBox();
container.setSpacing(16); container.setSpacing(16);
container.setAlignment(Pos.CENTER); container.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(container, 500); FXUtils.setLimitWidth(container, 500);
{
HBox labelContainer = new HBox();
labelContainer.setPadding(new Insets(0, 0, 0, 5));
Label label = new Label(i18n("world.export"));
labelContainer.getChildren().setAll(label);
container.getChildren().add(labelContainer);
}
ComponentList list = new ComponentList(); ComponentList list = new ComponentList();
FileItem fileItem = new FileItem(); var chooseFileButton = new LineFileChooserButton();
fileItem.setName(i18n("world.export.location")); chooseFileButton.setTitle(i18n("world.export.location"));
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty()); chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE);
list.getContent().add(fileItem); chooseFileButton.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
chooseFileButton.locationProperty().bindBidirectional(skinnable.pathProperty());
var worldNamePane = new LinePane();
worldNamePane.setTitle(i18n("world.name"));
JFXTextField txtWorldName = new JFXTextField(); JFXTextField txtWorldName = new JFXTextField();
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty()); txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
txtWorldName.setLabelFloat(true); txtWorldName.setPrefWidth(300);
txtWorldName.setPromptText(i18n("world.name")); worldNamePane.setRight(txtWorldName);
StackPane.setMargin(txtWorldName, insets);
list.getContent().add(txtWorldName);
Label lblGameVersionTitle = new Label(i18n("world.game_version")); LineTextPane gameVersionPane = new LineTextPane();
Label lblGameVersion = new Label(); gameVersionPane.setTitle(i18n("world.game_version"));
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty()); gameVersionPane.textProperty().bind(skinnable.gameVersionProperty());
BorderPane gameVersionPane = new BorderPane();
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
gameVersionPane.setLeft(lblGameVersionTitle);
gameVersionPane.setRight(lblGameVersion);
list.getContent().add(gameVersionPane);
container.getChildren().add(list); list.getContent().setAll(chooseFileButton, worldNamePane, gameVersionPane);
container.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("world.export")),
list
);
JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export")); JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export"));
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())), btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())),
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty())); txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty()));
btnExport.setOnAction(e -> skinnable.export()); btnExport.setOnAction(e -> skinnable.export());
HBox bottom = new HBox(); HBox bottom = new HBox();
bottom.setAlignment(Pos.CENTER_RIGHT); bottom.setAlignment(Pos.CENTER_RIGHT);

View File

@@ -72,7 +72,7 @@ public final class WorldManageUIUtils {
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("world.export.title")); fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName()); fileChooser.setInitialFileName(world.getWorldName() + ".zip");
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage())); Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
if (file == null) { if (file == null) {
return; return;