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 6634ca4ca86..ee50c210e8f 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 @@ -51,6 +51,7 @@ import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemInfo; +import org.jetbrains.annotations.Nullable; import java.nio.file.Path; import java.util.*; @@ -172,7 +173,8 @@ protected Skin createDefaultSkin() { public static class ModpackInfoPageSkin extends SkinBase { private ObservableList originList; - private final List validatingFields = new ArrayList<>(); + /// Tracks validation errors that disable the next button. + private final List validationErrors = new ArrayList<>(); public ModpackInfoPageSkin(ModpackInfoPage skinnable) { super(skinnable); @@ -267,7 +269,12 @@ public ModpackInfoPageSkin(ModpackInfoPage skinnable) { VBox pane = new VBox(); Label title = new Label(i18n("settings.memory")); - VBox.setMargin(title, new Insets(0, 0, 8, 0)); + Label errorLabel = new Label(); + errorLabel.getStyleClass().add("error-label"); + errorLabel.setStyle("-fx-text-fill: -monet-error;"); + HBox titlePane = new HBox(4, title, errorLabel); + titlePane.setAlignment(Pos.CENTER_LEFT); + VBox.setMargin(titlePane, new Insets(0, 0, 8, 0)); HBox lowerBoundPane = new HBox(8); lowerBoundPane.setAlignment(Pos.CENTER); @@ -293,14 +300,13 @@ public ModpackInfoPageSkin(ModpackInfoPage skinnable) { JFXTextField txtMinMemory = new JFXTextField(); FXUtils.bindInt(txtMinMemory, skinnable.minMemory); - txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false)); + registerValidators(txtMinMemory, errorLabel, new NumberValidator(i18n("input.number"), false)); FXUtils.setLimitWidth(txtMinMemory, 60); - validatingFields.add(txtMinMemory); lowerBoundPane.getChildren().setAll(label, slider, txtMinMemory, new Label("MiB")); } - pane.getChildren().setAll(title, lowerBoundPane); + pane.getChildren().setAll(titlePane, lowerBoundPane); list.getContent().add(pane); } @@ -372,12 +378,9 @@ public ModpackInfoPageSkin(ModpackInfoPage skinnable) { nextButton.setPrefWidth(100); nextButton.setPrefHeight(40); nextButton.disableProperty().bind( - // Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill - // our requirement. - Bindings.createBooleanBinding(() -> validatingFields.stream() - .map(field -> !field.validate()) - .reduce(false, (left, right) -> left || right), - validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new))); + Bindings.createBooleanBinding( + () -> validationErrors.stream().anyMatch(BooleanProperty::get), + validationErrors.toArray(BooleanProperty[]::new))); hbox.getChildren().add(nextButton); } } @@ -385,30 +388,54 @@ public ModpackInfoPageSkin(ModpackInfoPage skinnable) { FXUtils.smoothScrolling(scroll); } - private LinePane createTextFieldLinePane(String title, StringProperty property, ValidatorBase... validators) { + /// Creates a compact text input row with its validation message beside the title. + private LinePane createTextFieldLinePane(String title, StringProperty property, @Nullable ValidatorBase... validators) { LinePane linePane = new LinePane(); JFXTextField textField = new JFXTextField(); + Label errorLabel = new Label(); + errorLabel.getStyleClass().add("error-label"); + errorLabel.setStyle("-fx-text-fill: -monet-error;"); textField.setMinWidth(500); linePane.setTitle(title); + linePane.setTitleTrailing(errorLabel); 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); - } + registerValidators(textField, errorLabel, validators); + + return linePane; + } + + /// Runs validators without activating the text field skin's message below the input. + private void registerValidators(JFXTextField textField, Label errorLabel, @Nullable ValidatorBase... validators) { + List validatorList = new ArrayList<>(); + for (@Nullable ValidatorBase validator : validators) { + if (validator != null) { + validator.setSrcControl(textField); + validatorList.add(validator); } } - if (needValidation) { - FXUtils.setValidateWhileTextChanged(textField, true); - validatingFields.add(textField); - } + if (validatorList.isEmpty()) + return; + + BooleanProperty hasError = new SimpleBooleanProperty(); + Runnable validate = () -> { + for (ValidatorBase validator : validatorList) { + validator.validate(); + if (validator.getHasErrors()) { + errorLabel.setText(validator.getMessage()); + hasError.set(true); + return; + } + } - return linePane; + errorLabel.setText(null); + hasError.set(false); + }; + textField.textProperty().addListener(observable -> validate.run()); + validate.run(); + validationErrors.add(hasError); } } }