diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java deleted file mode 100644 index 5cd9e74ea..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FileItem.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -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); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java new file mode 100644 index 000000000..3e134a217 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/LineFileChooserButton.java @@ -0,0 +1,240 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +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; + + public ObjectProperty 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 initialFileName; + + public final ObjectProperty 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 extensionFilters; + + public ObservableList 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 + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index 41f302b9f..064f580d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -107,7 +107,7 @@ public final class LocalModpackPage extends ModpackPage { .whenComplete(Schedulers.javafx(), (manifest, exception) -> { if (exception instanceof ManuallyCreatedModpackException) { hideSpinner(); - lblName.setText(FileUtils.getName(selectedFile)); + nameProperty.set(FileUtils.getName(selectedFile)); installAsVersion.set(false); if (name == null) { @@ -127,9 +127,9 @@ public final class LocalModpackPage extends ModpackPage { } else { hideSpinner(); controller.getSettings().put(MODPACK_MANIFEST, manifest); - lblName.setText(manifest.getName()); - lblVersion.setText(manifest.getVersion()); - lblAuthor.setText(manifest.getAuthor()); + nameProperty.set(manifest.getName()); + versionProperty.set(manifest.getVersion()); + authorProperty.set(manifest.getAuthor()); if (name == null) { // trim: https://github.com/HMCL-dev/HMCL/issues/962 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java index 9e6602266..fd5de2072 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java @@ -2,14 +2,15 @@ package org.jackhuang.hmcl.ui.download; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; -import javafx.geometry.Insets; +import javafx.beans.property.StringProperty; import javafx.geometry.Pos; -import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.ui.FXUtils; 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.wizard.WizardController; 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 Label lblName; - protected final Label lblVersion; - protected final Label lblAuthor; + protected final StringProperty nameProperty; + protected final StringProperty versionProperty; + protected final StringProperty authorProperty; protected final JFXTextField txtModpackName; protected final JFXButton btnInstall; protected final JFXButton btnDescription; @@ -39,46 +40,37 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage { ComponentList componentList = new ComponentList(); { - BorderPane archiveNamePane = new BorderPane(); + var archiveNamePane = new LinePane(); { - Label label = new Label(i18n("archive.file.name")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - archiveNamePane.setLeft(label); + archiveNamePane.setTitle(i18n("archive.file.name")); 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); - archiveNamePane.setCenter(txtModpackName); + archiveNamePane.setRight(txtModpackName); } - BorderPane modpackNamePane = new BorderPane(); + var modpackNamePane = new LineTextPane(); { - modpackNamePane.setLeft(new Label(i18n("modpack.name"))); - - lblName = new Label(); - BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT); - modpackNamePane.setCenter(lblName); + modpackNamePane.setTitle(i18n("modpack.name")); + nameProperty = modpackNamePane.textProperty(); } - BorderPane versionPane = new BorderPane(); + var versionPane = new LineTextPane(); { - versionPane.setLeft(new Label(i18n("archive.version"))); - - lblVersion = new Label(); - BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT); - versionPane.setCenter(lblVersion); + versionPane.setTitle(i18n("archive.version")); + versionProperty = versionPane.textProperty(); } - BorderPane authorPane = new BorderPane(); + var authorPane = new LineTextPane(); { - authorPane.setLeft(new Label(i18n("archive.author"))); - - lblAuthor = new Label(); - BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT); - authorPane.setCenter(lblAuthor); + authorPane.setTitle(i18n("archive.author")); + authorProperty = authorPane.textProperty(); } - BorderPane descriptionPane = new BorderPane(); + var descriptionPane = new BorderPane(); { btnDescription = FXUtils.newBorderButton(i18n("modpack.description")); btnDescription.setOnAction(e -> onDescribe()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java index d1d078ec2..2ace1017f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java @@ -53,9 +53,9 @@ public final class RemoteModpackPage extends ModpackPage { return; } - lblName.setText(manifest.getName()); - lblVersion.setText(manifest.getVersion()); - lblAuthor.setText(manifest.getAuthor()); + nameProperty.set(manifest.getName()); + versionProperty.set(manifest.getVersion()); + authorProperty.set(manifest.getAuthor()); Profile profile = controller.getSettings().get(ModpackPage.PROFILE); String name = controller.getSettings().get(MODPACK_NAME); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java index 222f6bd2f..a1e796d21 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java @@ -17,7 +17,11 @@ */ 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.property.*; import javafx.collections.ObservableList; @@ -53,8 +57,6 @@ import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; 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_MODRINTH; 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.setAuthor(author.get()); exportInfo.setDescription(description.get()); - exportInfo.setPackWithLauncher(packWithLauncher.get()); exportInfo.setUrl(url.get()); exportInfo.setForceUpdate(forceUpdate.get()); exportInfo.setPackWithLauncher(packWithLauncher.get()); @@ -171,10 +172,11 @@ public final class ModpackInfoPage extends Control implements WizardPage { public static class ModpackInfoPageSkin extends SkinBase { private ObservableList originList; + private final List validatingFields = new ArrayList<>(); + public ModpackInfoPageSkin(ModpackInfoPage skinnable) { super(skinnable); - Insets insets = new Insets(5, 0, 12, 0); Insets componentListMargin = new Insets(16, 0, 16, 0); ScrollPane scroll = new ScrollPane(); @@ -182,7 +184,6 @@ public final class ModpackInfoPage extends Control implements WizardPage { scroll.setFitToHeight(true); getChildren().setAll(scroll); - List validatingFields = new ArrayList<>(); { BorderPane borderPane = new BorderPane(); @@ -209,83 +210,52 @@ public final class ModpackInfoPage extends Control implements WizardPage { BorderPane.setMargin(list, componentListMargin); borderPane.setCenter(list); + var instanceNamePane = new LineTextPane(); { - BorderPane borderPane1 = new BorderPane(); - borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version"))); + instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version")); + instanceNamePane.setText(skinnable.versionName); - Label versionNameLabel = new Label(); - versionNameLabel.setText(skinnable.versionName); - borderPane1.setRight(versionNameLabel); - list.getContent().add(borderPane1); + list.getContent().add(instanceNamePane); } - { - 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(); - txtModpackName.textProperty().bindBidirectional(skinnable.name); - txtModpackName.getValidators().add(new RequiredValidator()); - validatingFields.add(txtModpackName); - pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName); + if (skinnable.options.isRequireFileApi()) { + list.getContent().add(createTextFieldLinePane( + i18n("modpack.file_api"), skinnable.fileApi, + skinnable.options.isValidateFileApi() ? new RequiredValidator() : null, + new URLValidator(true) + )); + } - JFXTextField txtModpackAuthor = new JFXTextField(); - txtModpackAuthor.textProperty().bindBidirectional(skinnable.author); - txtModpackAuthor.getValidators().add(new RequiredValidator()); - validatingFields.add(txtModpackAuthor); - pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor); + if (skinnable.options.isRequireLaunchArguments()) { + list.getContent().add(createTextFieldLinePane( + i18n("settings.advanced.minecraft_arguments"), skinnable.launchArguments + )); + } - JFXTextField txtModpackVersion = new JFXTextField(); - txtModpackVersion.textProperty().bindBidirectional(skinnable.version); - txtModpackVersion.getValidators().add(new RequiredValidator()); - validatingFields.add(txtModpackVersion); - pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion); + if (skinnable.options.isRequireJavaArguments()) { + list.getContent().add(createTextFieldLinePane( + i18n("settings.advanced.jvm_args"), skinnable.javaArguments + )); + } - if (skinnable.options.isRequireFileApi()) { - JFXTextField txtModpackFileApi = new JFXTextField(); - txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi); - 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.isRequireUrl()) { + list.getContent().add(createTextFieldLinePane( + i18n("modpack.origin.url"), skinnable.url + )); + } + if (skinnable.options.isRequireOrigins()) { + list.getContent().add(createTextFieldLinePane( + i18n("modpack.origin.mcbbs"), skinnable.mcbbsThreadId, + new NumberValidator(i18n("input.number"), true) + )); } if (skinnable.options.isRequireMinMemory()) { @@ -339,78 +309,51 @@ public final class ModpackInfoPage extends Control implements WizardPage { } if (skinnable.options.isRequireAuthlibInjectorServer()) { - JFXComboBox cboServers = new JFXComboBox<>(); - cboServers.setMaxWidth(Double.MAX_VALUE); - cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl()))); - cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName)); - Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers()); + var serversSelectButton = new LineSelectButton(); + serversSelectButton.setTitle(i18n("account.injector.server")); + serversSelectButton.setConverter(AuthlibInjectorServer::getName); + serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl); + serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers()); - skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> - Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()) - .map(AuthlibInjectorServer::getUrl) - .orElse(null))); + skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> { + AuthlibInjectorServer selected = serversSelectButton.getValue(); + return selected != null ? selected.getUrl() : null; + }, serversSelectButton.valueProperty())); - BorderPane pane = new BorderPane(); - - Label left = new Label(i18n("account.injector.server")); - BorderPane.setAlignment(left, Pos.CENTER_LEFT); - pane.setLeft(left); - pane.setRight(cboServers); - - list.getContent().add(pane); + list.getContent().add(serversSelectButton); } if (skinnable.options.isRequireForceUpdate()) { - BorderPane pane = new BorderPane(); - pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update"))); - list.getContent().add(pane); + var requireForceUpdateButton = new LineToggleButton(); + requireForceUpdateButton.setTitle(i18n("modpack.wizard.step.initialization.force_update")); + requireForceUpdateButton.selectedProperty().bindBidirectional(skinnable.forceUpdate); - JFXToggleButton button = new JFXToggleButton(); - button.selectedProperty().bindBidirectional(skinnable.forceUpdate); - button.setSize(8); - button.setMinHeight(16); - button.setMaxHeight(16); - pane.setRight(button); + list.getContent().add(requireForceUpdateButton); } { - BorderPane pane = new BorderPane(); - pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher"))); - list.getContent().add(pane); + var canIncludeLauncherButton = new LineToggleButton(); + canIncludeLauncherButton.setTitle(i18n("modpack.wizard.step.initialization.include_launcher")); + canIncludeLauncherButton.setDisable(!skinnable.canIncludeLauncher); + canIncludeLauncherButton.selectedProperty().bindBidirectional(skinnable.packWithLauncher); - JFXToggleButton button = new JFXToggleButton(); - button.setDisable(!skinnable.canIncludeLauncher); - button.selectedProperty().bindBidirectional(skinnable.packWithLauncher); - button.setSize(8); - button.setMinHeight(16); - button.setMaxHeight(16); - pane.setRight(button); + list.getContent().add(canIncludeLauncherButton); } if (skinnable.options.isRequireNoCreateRemoteFiles()) { - BorderPane noCreateRemoteFiles = new BorderPane(); - noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files"))); - list.getContent().add(noCreateRemoteFiles); + var requireNoCreateRemoteFilesButton = new LineToggleButton(); + requireNoCreateRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.no_create_remote_files")); + requireNoCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles); - JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton(); - noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles); - noCreateRemoteFilesButton.setSize(8); - noCreateRemoteFilesButton.setMinHeight(16); - noCreateRemoteFilesButton.setMaxHeight(16); - noCreateRemoteFiles.setRight(noCreateRemoteFilesButton); + list.getContent().add(requireNoCreateRemoteFilesButton); } if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) { - BorderPane skipCurseForgeRemoteFiles = new BorderPane(); - skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"))); - list.getContent().add(skipCurseForgeRemoteFiles); - - JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton(); + var skipCurseForgeRemoteFilesButton = new LineToggleButton(); + skipCurseForgeRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")); skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles); - skipCurseForgeRemoteFilesButton.setSize(8); - skipCurseForgeRemoteFilesButton.setMinHeight(16); - skipCurseForgeRemoteFilesButton.setMaxHeight(16); - skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton); + + list.getContent().add(skipCurseForgeRemoteFilesButton); } } @@ -436,5 +379,31 @@ public final class ModpackInfoPage extends Control implements WizardPage { 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; + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java index 881b93b91..e972bbab8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java @@ -35,10 +35,7 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.ComponentList; -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.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.StringUtils; 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 Profile profile; private final JFXTextField txtProfileName; - private final FileItem gameDir; + private final LineFileChooserButton gameDir; private final LineToggleButton toggleUseRelativePath; /** @@ -107,10 +104,11 @@ public final class ProfilePage extends BorderPane implements DecoratorPage { }); } - gameDir = new FileItem(); - gameDir.setName(i18n("profile.instance_directory")); - gameDir.setTitle(i18n("profile.instance_directory.choose")); - gameDir.pathProperty().bindBidirectional(location); + gameDir = new LineFileChooserButton(); + gameDir.setTitle(i18n("profile.instance_directory")); + gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose")); + gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY); + gameDir.locationProperty().bindBidirectional(location); toggleUseRelativePath = new LineToggleButton(); toggleUseRelativePath.setTitle(i18n("profile.use_relative_path")); @@ -188,7 +186,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage { } } else { if (StringUtils.isBlank(getLocation())) { - gameDir.onExplore(); + gameDir.fire(); } Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation())); newProfile.setUseRelativePath(toggleUseRelativePath.isSelected()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 6bcf9659c..f0424eba7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -175,27 +175,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag componentList = new ComponentList(); if (!globalSetting) { - BorderPane copyGlobalPane = new BorderPane(); - { - Label label = new Label(i18n("settings.game.copy_global")); - copyGlobalPane.setLeft(label); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); + var copyGlobalButton = LineButton.createNavigationButton(); + copyGlobalButton.setTitle(i18n("settings.game.copy_global")); + copyGlobalButton.setOnAction(event -> + Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> { + Set ignored = new HashSet<>(Arrays.asList( + "usesGlobal", + "versionIcon" + )); - JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all")); - copyAll.disableProperty().bind(modpack); - copyGlobalPane.setRight(copyAll); - copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> { - Set ignored = new HashSet<>(Arrays.asList( - "usesGlobal", - "versionIcon" - )); + PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name)); + }, null)); - PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name)); - }, null)); - BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT); - } - - componentList.getContent().add(copyGlobalPane); + componentList.getContent().add(copyGlobalButton); } javaItem = new MultiFileItem<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java index 98226cbb2..1430b4b41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java @@ -20,17 +20,16 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; import javafx.beans.binding.Bindings; -import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Label; import javafx.scene.control.SkinBase; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import org.jackhuang.hmcl.ui.FXUtils; 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.Paths; @@ -42,47 +41,40 @@ public class WorldExportPageSkin extends SkinBase { public WorldExportPageSkin(WorldExportPage skinnable) { super(skinnable); - Insets insets = new Insets(0, 0, 12, 0); VBox container = new VBox(); container.setSpacing(16); container.setAlignment(Pos.CENTER); 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(); - FileItem fileItem = new FileItem(); - fileItem.setName(i18n("world.export.location")); - fileItem.pathProperty().bindBidirectional(skinnable.pathProperty()); - list.getContent().add(fileItem); + var chooseFileButton = new LineFileChooserButton(); + chooseFileButton.setTitle(i18n("world.export.location")); + chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE); + 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(); txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty()); - txtWorldName.setLabelFloat(true); - txtWorldName.setPromptText(i18n("world.name")); - StackPane.setMargin(txtWorldName, insets); - list.getContent().add(txtWorldName); + txtWorldName.setPrefWidth(300); + worldNamePane.setRight(txtWorldName); - Label lblGameVersionTitle = new Label(i18n("world.game_version")); - Label lblGameVersion = new Label(); - lblGameVersion.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); + LineTextPane gameVersionPane = new LineTextPane(); + gameVersionPane.setTitle(i18n("world.game_version")); + gameVersionPane.textProperty().bind(skinnable.gameVersionProperty()); - 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")); - btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())), - txtWorldName.textProperty().isEmpty(), fileItem.pathProperty())); + btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())), + txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty())); btnExport.setOnAction(e -> skinnable.export()); HBox bottom = new HBox(); bottom.setAlignment(Pos.CENTER_RIGHT); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 2fd9176db..89010afd5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -72,7 +72,7 @@ public final class WorldManageUIUtils { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("world.export.title")); 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())); if (file == null) { return;