From 094353634ae47fcaa4e0080b01277d1cccaa4649 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:03:43 +0800 Subject: [PATCH 01/16] init --- .../com/jfoenix/controls/JFXTextField.java | 166 ++++----- .../jfoenix/controls/ValidationControl.java | 100 +++++ .../controls/base/IFXLabelFloatControl.java | 49 +++ .../controls/base/IFXStaticControl.java | 31 ++ .../controls/base/IFXValidatableControl.java | 39 ++ .../com/jfoenix/skins/JFXTextFieldSkin.java | 159 ++++++++ .../com/jfoenix/skins/PromptLinesWrapper.java | 342 ++++++++++++++++++ .../com/jfoenix/skins/ValidationPane.java | 242 +++++++++++++ .../java/com/jfoenix/utils/JFXUtilities.java | 81 +++++ 9 files changed, 1128 insertions(+), 81 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/ValidationControl.java create mode 100644 HMCL/src/main/java/com/jfoenix/controls/base/IFXLabelFloatControl.java create mode 100644 HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java create mode 100644 HMCL/src/main/java/com/jfoenix/controls/base/IFXValidatableControl.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java create mode 100644 HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java index cb4e2d6af0..4c9406aafe 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java @@ -19,16 +19,14 @@ package com.jfoenix.controls; +import com.jfoenix.controls.base.IFXLabelFloatControl; import com.jfoenix.skins.JFXTextFieldSkin; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.*; import javafx.css.converter.BooleanConverter; import javafx.css.converter.PaintConverter; -import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.control.TextField; import javafx.scene.paint.Color; @@ -47,14 +45,7 @@ * @version 1.0 * @since 2016-03-09 */ -public class JFXTextField extends TextField { - /** - * Initialize the style class to 'jfx-text-field'. - *

- * This is the selector class from which CSS can be used to style - * this control. - */ - private static final String DEFAULT_STYLE_CLASS = "jfx-text-field"; +public class JFXTextField extends TextField implements IFXLabelFloatControl { /** * {@inheritDoc} @@ -76,15 +67,11 @@ public JFXTextField(String text) { */ @Override protected Skin createDefaultSkin() { - return new JFXTextFieldSkin(this); + return new JFXTextFieldSkin<>(this); } private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); - if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) { - this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextFieldSkinAndroid\";"); - } - useJFXContextMenu(this); } @@ -94,78 +81,72 @@ private void initialize() { * * **************************************************************************/ - /** - * holds the current active validator on the text field in case of validation error - */ - private ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); + /// wrapper for validation properties / methods + protected final ValidationControl validationControl = new ValidationControl(this); + @Override public ValidatorBase getActiveValidator() { - return activeValidator == null ? null : activeValidator.get(); + return validationControl.getActiveValidator(); } + @Override public ReadOnlyObjectProperty activeValidatorProperty() { - return this.activeValidator.getReadOnlyProperty(); + return validationControl.activeValidatorProperty(); } - /** - * list of validators that will validate the text value upon calling - * {{@link #validate()} - */ - private ObservableList validators = FXCollections.observableArrayList(); - + @Override public ObservableList getValidators() { - return validators; + return validationControl.getValidators(); } + @Override public void setValidators(ValidatorBase... validators) { - this.validators.addAll(validators); + validationControl.setValidators(validators); } - /** - * validates the text value using the list of validators provided by the user - * {{@link #setValidators(ValidatorBase...)} - * - * @return true if the value is valid else false - */ + @Override public boolean validate() { - for (ValidatorBase validator : validators) { - if (validator.getSrcControl() == null) { - validator.setSrcControl(this); - } - validator.validate(); - if (validator.getHasErrors()) { - activeValidator.set(validator); - return false; - } - } - activeValidator.set(null); - return true; + return validationControl.validate(); } + @Override public void resetValidation() { - pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false); - activeValidator.set(null); + validationControl.resetValidation(); } /*************************************************************************** * * - * styleable Properties * + * Styleable Properties * * * **************************************************************************/ + /** + * Initialize the style class to 'jfx-text-field'. + *

+ * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "jfx-text-field"; + /** * set true to show a float the prompt text when focusing the field */ - private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextField.this, "lableFloat", false); + private final StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, + JFXTextField.this, + "labelFloat", + false); + @Override public final StyleableBooleanProperty labelFloatProperty() { return this.labelFloat; } + @Override public final boolean isLabelFloat() { return this.labelFloatProperty().get(); } + @Override public final void setLabelFloat(final boolean labelFloat) { this.labelFloatProperty().set(labelFloat); } @@ -173,16 +154,24 @@ public final void setLabelFloat(final boolean labelFloat) { /** * default color used when the field is unfocused */ - private StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextField.this, "unFocusColor", Color.rgb(77, 77, 77)); + private final StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, + JFXTextField.this, + "unFocusColor", + Color.rgb(77, + 77, + 77)); + @Override public Paint getUnFocusColor() { return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get(); } + @Override public StyleableObjectProperty unFocusColorProperty() { return this.unFocusColor; } + @Override public void setUnFocusColor(Paint color) { this.unFocusColor.set(color); } @@ -190,16 +179,22 @@ public void setUnFocusColor(Paint color) { /** * default color used when the field is focused */ - private StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextField.this, "focusColor", Color.valueOf("#4059A9")); + private final StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, + JFXTextField.this, + "focusColor", + Color.valueOf("#4059A9")); + @Override public Paint getFocusColor() { return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get(); } + @Override public StyleableObjectProperty focusColorProperty() { return this.focusColor; } + @Override public void setFocusColor(Paint color) { this.focusColor.set(color); } @@ -207,22 +202,32 @@ public void setFocusColor(Paint color) { /** * disable animation on validation */ - private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextField.this, "disableAnimation", false); + private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, + JFXTextField.this, + "disableAnimation", + false); + @Override public final StyleableBooleanProperty disableAnimationProperty() { return this.disableAnimation; } + @Override public final Boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } + @Override public final void setDisableAnimation(final Boolean disabled) { this.disableAnimationProperty().set(disabled); } - private final static class StyleableProperties { - private static final CssMetaData UNFOCUS_COLOR = new CssMetaData("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) { + + private static class StyleableProperties { + private static final CssMetaData UNFOCUS_COLOR = new CssMetaData( + "-jfx-unfocus-color", + PaintConverter.getInstance(), + Color.valueOf("#A6A6A6")) { @Override public boolean isSettable(JFXTextField control) { return control.unFocusColor == null || !control.unFocusColor.isBound(); @@ -233,7 +238,10 @@ public StyleableProperty getStyleableProperty(JFXTextField control) { return control.unFocusColorProperty(); } }; - private static final CssMetaData FOCUS_COLOR = new CssMetaData("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) { + private static final CssMetaData FOCUS_COLOR = new CssMetaData( + "-jfx-focus-color", + PaintConverter.getInstance(), + Color.valueOf("#3f51b5")) { @Override public boolean isSettable(JFXTextField control) { return control.focusColor == null || !control.focusColor.isBound(); @@ -244,7 +252,10 @@ public StyleableProperty getStyleableProperty(JFXTextField control) { return control.focusColorProperty(); } }; - private static final CssMetaData LABEL_FLOAT = new CssMetaData("-jfx-label-float", BooleanConverter.getInstance(), false) { + private static final CssMetaData LABEL_FLOAT = new CssMetaData( + "-jfx-label-float", + BooleanConverter.getInstance(), + false) { @Override public boolean isSettable(JFXTextField control) { return control.labelFloat == null || !control.labelFloat.isBound(); @@ -256,44 +267,37 @@ public StyleableBooleanProperty getStyleableProperty(JFXTextField control) { } }; - private static final CssMetaData DISABLE_ANIMATION = new CssMetaData("-jfx-disable-animation", BooleanConverter.getInstance(), false) { - @Override - public boolean isSettable(JFXTextField control) { - return control.disableAnimation == null || !control.disableAnimation.isBound(); - } + private static final CssMetaData DISABLE_ANIMATION = + new CssMetaData("-jfx-disable-animation", + BooleanConverter.getInstance(), false) { + @Override + public boolean isSettable(JFXTextField control) { + return control.disableAnimation == null || !control.disableAnimation.isBound(); + } - @Override - public StyleableBooleanProperty getStyleableProperty(JFXTextField control) { - return control.disableAnimationProperty(); - } - }; + @Override + public StyleableBooleanProperty getStyleableProperty(JFXTextField control) { + return control.disableAnimationProperty(); + } + }; private static final List> CHILD_STYLEABLES; static { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); + final List> styleables = new ArrayList<>( + TextField.getClassCssMetaData()); Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION); CHILD_STYLEABLES = Collections.unmodifiableList(styleables); } } - // inherit the styleable properties from parent - private List> STYLEABLES; - @Override public List> getControlCssMetaData() { - if (STYLEABLES == null) { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); - styleables.addAll(getClassCssMetaData()); - styleables.addAll(TextField.getClassCssMetaData()); - STYLEABLES = Collections.unmodifiableList(styleables); - } - return STYLEABLES; + return getClassCssMetaData(); } public static List> getClassCssMetaData() { return StyleableProperties.CHILD_STYLEABLES; } } - diff --git a/HMCL/src/main/java/com/jfoenix/controls/ValidationControl.java b/HMCL/src/main/java/com/jfoenix/controls/ValidationControl.java new file mode 100644 index 0000000000..f63018fc66 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/ValidationControl.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.controls.base.IFXValidatableControl; +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.Control; + +/// this class used as validation model wrapper for all [IFXValidatableControl] +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2018-07-19 +class ValidationControl implements IFXValidatableControl { + + private final ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); + + private final Control control; + + public ValidationControl(Control control) { + this.control = control; + } + + @Override + public ValidatorBase getActiveValidator() { + return activeValidator == null ? null : activeValidator.get(); + } + + @Override + public ReadOnlyObjectProperty activeValidatorProperty() { + return this.activeValidator.getReadOnlyProperty(); + } + + ReadOnlyObjectWrapper activeValidatorWritableProperty() { + return this.activeValidator; + } + + /** + * list of validators that will validate the text value upon calling + * {{@link #validate()} + */ + private final ObservableList validators = FXCollections.observableArrayList(); + + @Override + public ObservableList getValidators() { + return validators; + } + + @Override + public void setValidators(ValidatorBase... validators) { + this.validators.addAll(validators); + } + + /** + * validates the text value using the list of validators provided by the user + * {{@link #setValidators(ValidatorBase...)} + * + * @return true if the value is valid else false + */ + @Override + public boolean validate() { + for (ValidatorBase validator : validators) { + // source control must be set to allow validators re-usability + validator.setSrcControl(control); + validator.validate(); + if (validator.getHasErrors()) { + activeValidator.set(validator); + return false; + } + } + activeValidator.set(null); + return true; + } + + public void resetValidation() { + control.pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false); + activeValidator.set(null); + } +} diff --git a/HMCL/src/main/java/com/jfoenix/controls/base/IFXLabelFloatControl.java b/HMCL/src/main/java/com/jfoenix/controls/base/IFXLabelFloatControl.java new file mode 100644 index 0000000000..e473fecfd9 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/base/IFXLabelFloatControl.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.base; + +import javafx.css.StyleableBooleanProperty; +import javafx.css.StyleableObjectProperty; +import javafx.scene.paint.Paint; + +/// this class is created for internal use only, to remove duplication between text input controls +/// skins +/// +/// Created by sshahine on 7/14/2017. +public interface IFXLabelFloatControl extends IFXValidatableControl, IFXStaticControl { + + StyleableBooleanProperty labelFloatProperty(); + + boolean isLabelFloat(); + + void setLabelFloat(boolean labelFloat); + + Paint getUnFocusColor(); + + StyleableObjectProperty unFocusColorProperty(); + + void setUnFocusColor(Paint color); + + Paint getFocusColor(); + + StyleableObjectProperty focusColorProperty(); + + void setFocusColor(Paint color); +} diff --git a/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java b/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java new file mode 100644 index 0000000000..1ea098807b --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.base; + +import javafx.css.StyleableBooleanProperty; + +public interface IFXStaticControl { + + StyleableBooleanProperty disableAnimationProperty(); + + Boolean isDisableAnimation(); + + void setDisableAnimation(Boolean disabled); +} diff --git a/HMCL/src/main/java/com/jfoenix/controls/base/IFXValidatableControl.java b/HMCL/src/main/java/com/jfoenix/controls/base/IFXValidatableControl.java new file mode 100644 index 0000000000..8f310bbeb5 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/base/IFXValidatableControl.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls.base; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.collections.ObservableList; + +public interface IFXValidatableControl { + + ValidatorBase getActiveValidator(); + + ReadOnlyObjectProperty activeValidatorProperty(); + + ObservableList getValidators(); + + void setValidators(ValidatorBase... validators); + + boolean validate(); + + void resetValidation(); +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java new file mode 100644 index 0000000000..530ca5c12a --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.adapters.ReflectionHelper; +import com.jfoenix.controls.base.IFXLabelFloatControl; +import javafx.beans.property.DoubleProperty; +import javafx.beans.value.ObservableDoubleValue; +import javafx.scene.Node; +import javafx.scene.control.TextField; +import javafx.scene.control.skin.TextFieldSkin; +import javafx.scene.layout.Pane; +import javafx.scene.text.Text; + +import java.lang.reflect.Field; + +/// # Material Design Text input control Skin, used for both JFXTextField/JFXPasswordField +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-01-25 +public class JFXTextFieldSkin extends TextFieldSkin { + + private boolean invalid = true; + + private Text promptText; + private Pane textPane; + private Node textNode; + private ObservableDoubleValue textRight; + private DoubleProperty textTranslateX; + + private ValidationPane errorContainer; + private PromptLinesWrapper linesWrapper; + + public JFXTextFieldSkin(T textField) { + super(textField); + textPane = (Pane) this.getChildren().get(0); + + // get parent fields + textNode = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textNode"); + textTranslateX = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textTranslateX"); + textRight = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textRight"); + + linesWrapper = new PromptLinesWrapper( + textField, + promptTextFillProperty(), + textField.textProperty(), + textField.promptTextProperty(), + () -> promptText); + + linesWrapper.init(() -> createPromptNode(), textPane); + + ReflectionHelper.setFieldContent(TextFieldSkin.class, this, "usePromptText", linesWrapper.usePromptText); + + errorContainer = new ValidationPane<>(textField); + + getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); + + registerChangeListener(textField.disableProperty(), obs -> linesWrapper.updateDisabled()); + registerChangeListener(textField.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); + registerChangeListener(textField.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); + registerChangeListener(textField.disableAnimationProperty(), obs -> errorContainer.updateClip()); + } + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + super.layoutChildren(x, y, w, h); + + final double height = getSkinnable().getHeight(); + linesWrapper.layoutLines(x, y, w, h, height, Math.floor(h)); + errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), w, h); + + if (getSkinnable().getWidth() > 0) { + updateTextPos(); + } + + linesWrapper.updateLabelFloatLayout(); + + if (invalid) { + invalid = false; + // update validation container + errorContainer.invalid(w); + // focus + linesWrapper.invalid(); + } + } + + + private void updateTextPos() { + double textWidth = textNode.getLayoutBounds().getWidth(); + final double promptWidth = promptText == null ? 0 : promptText.getLayoutBounds().getWidth(); + switch (getSkinnable().getAlignment().getHpos()) { + case CENTER: + linesWrapper.promptTextScale.setPivotX(promptWidth / 2); + double midPoint = textRight.get() / 2; + double newX = midPoint - textWidth / 2; + if (newX + textWidth <= textRight.get()) { + textTranslateX.set(newX); + } + break; + case LEFT: + linesWrapper.promptTextScale.setPivotX(0); + break; + case RIGHT: + linesWrapper.promptTextScale.setPivotX(promptWidth); + break; + } + + } + + private void createPromptNode() { + if (promptText != null || !linesWrapper.usePromptText.get()) { + return; + } + promptText = new Text(); + promptText.setManaged(false); + promptText.getStyleClass().add("text"); + promptText.visibleProperty().bind(linesWrapper.usePromptText); + promptText.fontProperty().bind(getSkinnable().fontProperty()); + promptText.textProperty().bind(getSkinnable().promptTextProperty()); + promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); + promptText.setLayoutX(1); + promptText.getTransforms().add(linesWrapper.promptTextScale); + linesWrapper.promptContainer.getChildren().add(promptText); + if (getSkinnable().isFocused() && ((IFXLabelFloatControl) getSkinnable()).isLabelFloat()) { + promptText.setTranslateY(-Math.floor(textPane.getHeight())); + linesWrapper.promptTextScale.setX(0.85); + linesWrapper.promptTextScale.setY(0.85); + } + + try { + Field field = ReflectionHelper.getField(TextFieldSkin.class, "promptNode"); + Object oldValue = field.get(this); + if (oldValue != null) { + textPane.getChildren().remove(oldValue); + } + field.set(this, promptText); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java b/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java new file mode 100644 index 0000000000..ed0cc94c88 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java @@ -0,0 +1,342 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.controls.base.IFXLabelFloatControl; +import com.jfoenix.transitions.JFXAnimationTimer; +import com.jfoenix.transitions.JFXKeyFrame; +import com.jfoenix.transitions.JFXKeyValue; +import javafx.animation.Interpolator; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WritableValue; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Control; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; +import javafx.scene.transform.Scale; +import javafx.util.Duration; + +import java.util.function.Supplier; + +/// this class used to create label-float/focus-lines for all [IFXLabelFloatControl] +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2018-07-19 +public class PromptLinesWrapper { + + private final Supplier promptTextSupplier; + private final T control; + + public StackPane line = new StackPane(); + public StackPane focusedLine = new StackPane(); + public StackPane promptContainer = new StackPane(); + + private JFXAnimationTimer focusTimer; + private JFXAnimationTimer unfocusTimer; + + private final double initScale = 0.05; + public final Scale promptTextScale = new Scale(1, 1, 0, 0); + private final Scale scale = new Scale(initScale, 1); + public final Rectangle clip = new Rectangle(); + + public ObjectProperty animatedPromptTextFill; + public BooleanBinding usePromptText; + private final ObjectProperty promptTextFill; + private final ObservableValue valueProperty; + private final ObservableValue promptTextProperty; + + private boolean animating = false; + private double contentHeight = 0; + + public PromptLinesWrapper(T control, ObjectProperty promptTextFill, + ObservableValue valueProperty, + ObservableValue promptTextProperty, + Supplier promptTextSupplier) { + this.control = control; + this.promptTextSupplier = promptTextSupplier; + this.promptTextFill = promptTextFill; + this.valueProperty = valueProperty; + this.promptTextProperty = promptTextProperty; + } + + public void init(Runnable createPromptNodeRunnable, Node... cachedNodes) { + animatedPromptTextFill = new SimpleObjectProperty<>(promptTextFill.get()); + usePromptText = Bindings.createBooleanBinding(this::usePromptText, + valueProperty, + promptTextProperty, + control.labelFloatProperty(), + promptTextFill); + + // draw lines + line.setManaged(false); + line.getStyleClass().add("input-line"); + line.setBackground(new Background( + new BackgroundFill(control.getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); + + // focused line + focusedLine.setManaged(false); + focusedLine.getStyleClass().add("input-focused-line"); + focusedLine.setBackground(new Background( + new BackgroundFill(control.getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); + focusedLine.setOpacity(0); + focusedLine.getTransforms().add(scale); + + + if (usePromptText.get()) { + createPromptNodeRunnable.run(); + } + usePromptText.addListener(observable -> { + createPromptNodeRunnable.run(); + control.requestLayout(); + }); + + final Supplier> promptTargetSupplier = + () -> promptTextSupplier.get() == null ? null : promptTextSupplier.get().translateYProperty(); + + focusTimer = new JFXAnimationTimer( + new JFXKeyFrame(Duration.millis(1), + JFXKeyValue.builder() + .setTarget(focusedLine.opacityProperty()) + .setEndValue(1) + .setInterpolator(Interpolator.EASE_BOTH) + .setAnimateCondition(control::isFocused).build()), + + new JFXKeyFrame(Duration.millis(160), + JFXKeyValue.builder() + .setTarget(scale.xProperty()) + .setEndValue(1) + .setInterpolator(Interpolator.EASE_BOTH) + .setAnimateCondition(control::isFocused).build(), + JFXKeyValue.builder() + .setTarget(animatedPromptTextFill) + .setEndValueSupplier(control::getFocusColor) + .setInterpolator(Interpolator.EASE_BOTH) + .setAnimateCondition(() -> control.isFocused() && control.isLabelFloat()).build(), + JFXKeyValue.builder() + .setTargetSupplier(promptTargetSupplier) + .setEndValueSupplier(() -> -contentHeight) + .setAnimateCondition(control::isLabelFloat) + .setInterpolator(Interpolator.EASE_BOTH).build(), + JFXKeyValue.builder() + .setTarget(promptTextScale.xProperty()) + .setEndValue(0.85) + .setAnimateCondition(control::isLabelFloat) + .setInterpolator(Interpolator.EASE_BOTH).build(), + JFXKeyValue.builder() + .setTarget(promptTextScale.yProperty()) + .setEndValue(0.85) + .setAnimateCondition(control::isLabelFloat) + .setInterpolator(Interpolator.EASE_BOTH).build()) + ); + + unfocusTimer = new JFXAnimationTimer( + new JFXKeyFrame(Duration.millis(160), + JFXKeyValue.builder() + .setTargetSupplier(promptTargetSupplier) + .setEndValue(0) + .setInterpolator(Interpolator.EASE_BOTH).build(), + JFXKeyValue.builder() + .setTarget(promptTextScale.xProperty()) + .setEndValue(1) + .setInterpolator(Interpolator.EASE_BOTH).build(), + JFXKeyValue.builder() + .setTarget(promptTextScale.yProperty()) + .setEndValue(1) + .setInterpolator(Interpolator.EASE_BOTH).build()) + ); + + promptContainer.getStyleClass().add("prompt-container"); + promptContainer.setManaged(false); + promptContainer.setMouseTransparent(true); + + // clip prompt container + clip.setSmooth(false); + clip.setX(0); + clip.widthProperty().bind(promptContainer.widthProperty()); + promptContainer.setClip(clip); + + focusTimer.setOnFinished(() -> animating = false); + unfocusTimer.setOnFinished(() -> animating = false); + focusTimer.setCacheNodes(cachedNodes); + unfocusTimer.setCacheNodes(cachedNodes); + + // handle animation on focus gained/lost event + control.focusedProperty().addListener(observable -> { + if (control.isFocused()) { + focus(); + } else { + unFocus(); + } + }); + + promptTextFill.addListener(observable -> { + if (!control.isLabelFloat() || !control.isFocused()) { + animatedPromptTextFill.set(promptTextFill.get()); + } + }); + + updateDisabled(); + } + + private Object getControlValue() { + Object text = valueProperty.getValue(); + text = validateComboBox(text); + return text; + } + + private Object validateComboBox(Object text) { + if (control instanceof ComboBox comboBox&& comboBox.isEditable()) { + final String editorText = comboBox.getEditor().getText(); + text = editorText == null || editorText.isEmpty() ? null : text; + } + return text; + } + + private void focus() { + unfocusTimer.stop(); + animating = true; + runTimer(focusTimer, true); + } + + private void unFocus() { + focusTimer.stop(); + scale.setX(initScale); + focusedLine.setOpacity(0); + if (control.isLabelFloat()) { + animatedPromptTextFill.set(promptTextFill.get()); + Object text = getControlValue(); + if (text == null || text.toString().isEmpty()) { + animating = true; + runTimer(unfocusTimer, true); + } + } + } + + public void updateFocusColor() { + Paint paint = control.getFocusColor(); + focusedLine.setBackground(paint == null ? Background.EMPTY + : new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY))); + } + + public void updateUnfocusColor() { + Paint paint = control.getUnFocusColor(); + line.setBackground(paint == null ? Background.EMPTY + : new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY))); + } + + private void updateLabelFloat(boolean animation) { + if (control.isLabelFloat()) { + if (control.isFocused()) { + animateFloatingLabel(true, animation); + } else { + Object text = getControlValue(); + animateFloatingLabel(!(text == null || text.toString().isEmpty()), animation); + } + } + } + + /** + * this method is called when the text property is changed when the + * field is not focused (changed in code) + * + * @param up direction of the prompt label + */ + private void animateFloatingLabel(boolean up, boolean animation) { + if (promptTextSupplier.get() == null) { + return; + } + if (up) { + if (promptTextSupplier.get().getTranslateY() != -contentHeight) { + unfocusTimer.stop(); + runTimer(focusTimer, animation); + } + } else { + if (promptTextSupplier.get().getTranslateY() != 0) { + focusTimer.stop(); + runTimer(unfocusTimer, animation); + } + } + } + + private void runTimer(JFXAnimationTimer timer, boolean animation) { + if (animation) { + if (!timer.isRunning()) { + timer.start(); + } + } else { + timer.applyEndValues(); + } + } + + private boolean usePromptText() { + Object txt = getControlValue(); + String promptTxt = promptTextProperty.getValue(); + boolean isLabelFloat = control.isLabelFloat(); + return isLabelFloat || (promptTxt != null + && (txt == null || txt.toString().isEmpty()) + && !promptTxt.isEmpty() + && !promptTextFill.get().equals(Color.TRANSPARENT)); + } + + public void layoutLines(double x, double y, double w, double h, double controlHeight, double translateY) { + this.contentHeight = translateY; + clip.setY(-contentHeight); + clip.setHeight(controlHeight + contentHeight); + focusedLine.resizeRelocate(x, controlHeight, w, focusedLine.prefHeight(-1)); + line.resizeRelocate(x, controlHeight, w, line.prefHeight(-1)); + promptContainer.resizeRelocate(x, y, w, h); + scale.setPivotX(w / 2); + } + + public void updateLabelFloatLayout() { + if (!animating) { + updateLabelFloat(false); + } else if (unfocusTimer.isRunning()) { + // handle the case when changing the control value when losing focus + unfocusTimer.stop(); + updateLabelFloat(true); + } + } + + public void invalid() { + if (control.isFocused()) { + focus(); + } + } + + public void updateDisabled() { + final boolean disabled = control.isDisable(); + line.setBorder(!disabled ? Border.EMPTY : + new Border(new BorderStroke(control.getUnFocusColor(), + BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1)))); + line.setBackground(new Background( + new BackgroundFill(disabled ? Color.TRANSPARENT : control.getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); + } +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java b/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java new file mode 100644 index 0000000000..b3ae0cbb38 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.controls.base.IFXStaticControl; +import com.jfoenix.controls.base.IFXValidatableControl; +import com.jfoenix.utils.JFXUtilities; +import com.jfoenix.validation.base.ValidatorBase; +import javafx.animation.*; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; +import javafx.scene.transform.Scale; +import javafx.util.Duration; + +/// this class used to create validation ui for all [IFXValidatableControl] +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2018-07-19 +public class ValidationPane extends HBox { + + private final Label errorLabel = new Label(); + private final StackPane errorIcon = new StackPane(); + + private final Rectangle errorContainerClip = new Rectangle(); + private final Scale errorClipScale = new Scale(1, 0, 0, 0); + private final Timeline errorHideTransition = new Timeline(new KeyFrame(Duration.millis(80), + new KeyValue(opacityProperty(), 0, Interpolator.LINEAR))); + private final Timeline errorShowTransition = new Timeline(new KeyFrame(Duration.millis(80), + new KeyValue(opacityProperty(), 1, Interpolator.EASE_OUT))); + private final Timeline scale1 = new Timeline(); + private final Timeline scaleLess1 = new Timeline(); + + private final T control; + + public ValidationPane(T control) { + this.control = control; + setManaged(false); + + errorLabel.getStyleClass().add("error-label"); + + final StackPane labelContainer = new StackPane(errorLabel); + labelContainer.getStyleClass().add("error-label-container"); + labelContainer.setAlignment(Pos.TOP_LEFT); + getChildren().setAll(labelContainer, errorIcon); + HBox.setHgrow(labelContainer, Priority.ALWAYS); + + setSpacing(8); + setAlignment(Pos.CENTER_LEFT); + setPadding(new Insets(4, 0, 0, 0)); + setVisible(false); + setOpacity(0); + + errorContainerClip.getTransforms().add(errorClipScale); + setClip(control.isDisableAnimation() ? null : errorContainerClip); + + + control.activeValidatorProperty().addListener((ObservableValue o, ValidatorBase oldVal, ValidatorBase newVal) -> { + if (!control.isDisableAnimation()) { + if (newVal != null) { + errorHideTransition.setOnFinished(finish -> { + showError(newVal); + final double w = control.getWidth(); + double errorContainerHeight = computeErrorHeight(computeErrorWidth(w)); + if (errorLabel.isWrapText()) { + // animate opacity + scale + if (errorContainerHeight < getHeight()) { + // update animation frames + scaleLess1.getKeyFrames().setAll(createSmallerScaleFrame(errorContainerHeight)); + scaleLess1.setOnFinished(event -> { + updateErrorContainerSize(w, errorContainerHeight); + errorClipScale.setY(1); + }); + SequentialTransition transition = new SequentialTransition(scaleLess1, + errorShowTransition); + transition.play(); + } else { + errorClipScale.setY(oldVal == null ? 0 : getHeight() / errorContainerHeight); + updateErrorContainerSize(w, errorContainerHeight); + // update animation frames + scale1.getKeyFrames().setAll(createScaleToOneFrames()); + // play animation + ParallelTransition parallelTransition = new ParallelTransition(); + parallelTransition.getChildren().addAll(scale1, errorShowTransition); + parallelTransition.play(); + } + } else { + // animate opacity only + errorClipScale.setY(1); + updateErrorContainerSize(w, errorContainerHeight); + ParallelTransition parallelTransition = new ParallelTransition(errorShowTransition); + parallelTransition.play(); + } + }); + errorHideTransition.play(); + } else { + errorHideTransition.setOnFinished(null); + if (errorLabel.isWrapText()) { + // animate scale only + scaleLess1.getKeyFrames().setAll(new KeyFrame(Duration.millis(100), + new KeyValue(errorClipScale.yProperty(), 0, Interpolator.EASE_BOTH))); + scaleLess1.setOnFinished(event -> { + hideError(); + errorClipScale.setY(0); + }); + SequentialTransition transition = new SequentialTransition(scaleLess1); + transition.play(); + } else { + errorClipScale.setY(0); + } + // animate opacity only + errorHideTransition.play(); + } + } else { + if (newVal != null) { + JFXUtilities.runInFXAndWait(() -> showError(newVal)); + } else { + JFXUtilities.runInFXAndWait(this::hideError); + } + } + }); + } + + public void layoutPane(final double x, final double y, final double w, final double h) { + relocate(x, y); + // resize error container if animation is disabled + if (control.isDisableAnimation() || isErrorVisible()) { + resize(w, computeErrorHeight(computeErrorWidth(w))); + errorContainerClip.setWidth(w); + } + } + + public void invalid(double w) { + final ValidatorBase activeValidator = control.getActiveValidator(); + if (activeValidator != null) { + showError(activeValidator); + final double errorContainerWidth = w - errorIcon.prefWidth(-1); + setOpacity(1); + resize(w, computeErrorHeight(errorContainerWidth)); + errorContainerClip.setWidth(w); + errorContainerClip.setHeight(getHeight()); + errorClipScale.setY(1); + } + } + + public void updateClip() { + setClip(control.isDisableAnimation() ? null : errorContainerClip); + } + + private boolean isErrorVisible() { + return isVisible() + && errorShowTransition.getStatus().equals(Animation.Status.STOPPED) + && errorHideTransition.getStatus().equals(Animation.Status.STOPPED); + } + + private double computeErrorWidth(double w) { + return w - errorIcon.prefWidth(-1); + } + + private double computeErrorHeight(double errorContainerWidth) { + return errorLabel.prefHeight(errorContainerWidth) + + snappedBottomInset() + + snappedTopInset(); + } + + /** + * update the size of error container and its clip + */ + private void updateErrorContainerSize(double w, double errorContainerHeight) { + errorContainerClip.setWidth(w); + errorContainerClip.setHeight(errorContainerHeight); + resize(w, errorContainerHeight); + } + + /** + * creates error animation frames when moving from large -> small error container + */ + private KeyFrame createSmallerScaleFrame(double errorContainerHeight) { + return new KeyFrame(Duration.millis(100), + new KeyValue(errorClipScale.yProperty(), + errorContainerHeight / getHeight(), + Interpolator.EASE_BOTH)); + } + + /** + * creates error animation frames when moving from small -> large error container + */ + private KeyFrame createScaleToOneFrames() { + return new KeyFrame(Duration.millis(100), new + KeyValue(errorClipScale.yProperty(), 1, Interpolator.EASE_BOTH)); + } + + + private void showError(ValidatorBase validator) { + // set text in error label + errorLabel.setText(validator.getMessage()); + // show error icon + Node icon = validator.getIcon(); + errorIcon.getChildren().clear(); + if (icon != null) { + errorIcon.getChildren().setAll(icon); + StackPane.setAlignment(icon, Pos.CENTER_RIGHT); + } + setVisible(true); + } + + private void hideError() { + // clear error label text + errorLabel.setText(null); + // clear error icon + errorIcon.getChildren().clear(); + // reset the height of the text field + // hide error container + setVisible(false); + } + +} diff --git a/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java b/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java new file mode 100644 index 0000000000..b6ddc461f5 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.utils; + +import javafx.application.Platform; + +import java.util.concurrent.CountDownLatch; +import java.util.function.Function; + + +/// # JavaFX FX Thread utilities +/// JFXUtilities allow sync mechanism to the FX thread +/// +/// +/// @author pmoufarrej +/// @version 1.0 +/// @since 2016-03-09 +public class JFXUtilities { + + /// This method is used to run a specified Runnable in the FX Application thread, + /// it returns before the task finished execution + /// + /// @param doRun This is the sepcifed task to be excuted by the FX Application thread + public static void runInFX(Runnable doRun) { + if (Platform.isFxApplicationThread()) { + doRun.run(); + return; + } + Platform.runLater(doRun); + } + + /// This method is used to run a specified Runnable in the FX Application thread, + /// it waits for the task to finish before returning to the main thread. + /// + /// @param doRun This is the sepcifed task to be excuted by the FX Application thread + public static void runInFXAndWait(Runnable doRun) { + if (Platform.isFxApplicationThread()) { + doRun.run(); + return; + } + final CountDownLatch doneLatch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + doRun.run(); + } finally { + doneLatch.countDown(); + } + }); + try { + doneLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static T[] concat(T[] a, T[] b, Function supplier) { + final int aLen = a.length; + final int bLen = b.length; + T[] array = supplier.apply(aLen + bLen); + System.arraycopy(a, 0, array, 0, aLen); + System.arraycopy(b, 0, array, aLen, bLen); + return array; + } +} From 46a16676e22c17db9a4af3f02eb88d3936f86064 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:05:04 +0800 Subject: [PATCH 02/16] init --- .../com/jfoenix/skins/JFXTextFieldSkin.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java index 530ca5c12a..afb4e5db6a 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java @@ -41,13 +41,13 @@ public class JFXTextFieldSkin extend private boolean invalid = true; private Text promptText; - private Pane textPane; - private Node textNode; - private ObservableDoubleValue textRight; - private DoubleProperty textTranslateX; + private final Pane textPane; + private final Node textNode; + private final ObservableDoubleValue textRight; + private final DoubleProperty textTranslateX; - private ValidationPane errorContainer; - private PromptLinesWrapper linesWrapper; + private final ValidationPane errorContainer; + private final PromptLinesWrapper linesWrapper; public JFXTextFieldSkin(T textField) { super(textField); @@ -58,14 +58,14 @@ public JFXTextFieldSkin(T textField) { textTranslateX = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textTranslateX"); textRight = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textRight"); - linesWrapper = new PromptLinesWrapper( - textField, - promptTextFillProperty(), - textField.textProperty(), - textField.promptTextProperty(), - () -> promptText); + linesWrapper = new PromptLinesWrapper<>( + textField, + promptTextFillProperty(), + textField.textProperty(), + textField.promptTextProperty(), + () -> promptText); - linesWrapper.init(() -> createPromptNode(), textPane); + linesWrapper.init(this::createPromptNode, textPane); ReflectionHelper.setFieldContent(TextFieldSkin.class, this, "usePromptText", linesWrapper.usePromptText); From f9a6889c66956d194a5e0ef97ec9aaae0c7ecba8 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:07:05 +0800 Subject: [PATCH 03/16] update --- .../com/jfoenix/controls/JFXComboBox.java | 130 +++++++++++++----- 1 file changed, 98 insertions(+), 32 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index 8c9b634cfb..adcb8182f7 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -19,9 +19,12 @@ package com.jfoenix.controls; +import com.jfoenix.controls.base.IFXLabelFloatControl; import com.jfoenix.converters.base.NodeConverter; import com.jfoenix.skins.JFXComboBoxListViewSkin; +import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import javafx.css.*; @@ -30,7 +33,10 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.Skin; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.StackPane; @@ -42,8 +48,6 @@ import java.util.Collections; import java.util.List; -import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu; - /** * JFXComboBox is the material design implementation of a combobox. * @@ -51,7 +55,7 @@ * @version 1.0 * @since 2016-03-09 */ -public class JFXComboBox extends ComboBox { +public class JFXComboBox extends ComboBox implements IFXLabelFloatControl { /** * {@inheritDoc} @@ -98,8 +102,6 @@ protected void updateItem(T item, boolean empty) { } }); - - useJFXContextMenu(editorProperty().get()); } /** @@ -107,7 +109,7 @@ protected void updateItem(T item, boolean empty) { */ @Override protected Skin createDefaultSkin() { - return new JFXComboBoxListViewSkin(this); + return new JFXComboBoxListViewSkin<>(this); } /** @@ -190,9 +192,8 @@ private boolean updateDisplayText(ListCell cell, T item, boolean empty) { cell.setGraphic(null); cell.setText(null); return true; - } else if (item instanceof Node) { + } else if (item instanceof Node newNode) { Node currentNode = cell.getGraphic(); - Node newNode = (Node) item; // create a node from the selected node of the listview // using JFXComboBox {@link #nodeConverterProperty() NodeConverter}) NodeConverter nc = this.getNodeConverter(); @@ -212,6 +213,45 @@ private boolean updateDisplayText(ListCell cell, T item, boolean empty) { } } + /*************************************************************************** + * * + * Properties * + * * + **************************************************************************/ + + /// wrapper for validation properties / methods + private final ValidationControl validationControl = new ValidationControl(this); + + @Override + public ValidatorBase getActiveValidator() { + return validationControl.getActiveValidator(); + } + + @Override + public ReadOnlyObjectProperty activeValidatorProperty() { + return validationControl.activeValidatorProperty(); + } + + @Override + public ObservableList getValidators() { + return validationControl.getValidators(); + } + + @Override + public void setValidators(ValidatorBase... validators) { + validationControl.setValidators(validators); + } + + @Override + public boolean validate() { + return validationControl.validate(); + } + + @Override + public void resetValidation() { + validationControl.resetValidation(); + } + /*************************************************************************** * * * styleable Properties * @@ -221,7 +261,7 @@ private boolean updateDisplayText(ListCell cell, T item, boolean empty) { /** * set true to show a float the prompt text when focusing the field */ - private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, + private final StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXComboBox.this, "lableFloat", false); @@ -241,7 +281,7 @@ public final void setLabelFloat(final boolean labelFloat) { /** * default color used when the field is unfocused */ - private StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, + private final StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXComboBox.this, "unFocusColor", Color.rgb(77, @@ -263,7 +303,7 @@ public void setUnFocusColor(Paint color) { /** * default color used when the field is focused */ - private StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, + private final StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXComboBox.this, "focusColor", Color.valueOf("#4059A9")); @@ -280,8 +320,33 @@ public void setFocusColor(Paint color) { this.focusColor.set(color); } - private final static class StyleableProperties { - private static final CssMetaData, Paint> UNFOCUS_COLOR = new CssMetaData, Paint>( + + /** + * disable animation on validation + */ + private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, + JFXComboBox.this, + "disableAnimation", + false); + + @Override + public final StyleableBooleanProperty disableAnimationProperty() { + return this.disableAnimation; + } + + @Override + public final Boolean isDisableAnimation() { + return disableAnimation != null && this.disableAnimationProperty().get(); + } + + @Override + public final void setDisableAnimation(final Boolean disabled) { + this.disableAnimationProperty().set(disabled); + } + + + private static class StyleableProperties { + private static final CssMetaData, Paint> UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) { @@ -295,7 +360,7 @@ public StyleableProperty getStyleableProperty(JFXComboBox control) { return control.unFocusColorProperty(); } }; - private static final CssMetaData, Paint> FOCUS_COLOR = new CssMetaData, Paint>( + private static final CssMetaData, Paint> FOCUS_COLOR = new CssMetaData<>( "-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) { @@ -309,7 +374,7 @@ public StyleableProperty getStyleableProperty(JFXComboBox control) { return control.focusColorProperty(); } }; - private static final CssMetaData, Boolean> LABEL_FLOAT = new CssMetaData, Boolean>( + private static final CssMetaData, Boolean> LABEL_FLOAT = new CssMetaData<>( "-jfx-label-float", BooleanConverter.getInstance(), false) { @@ -324,34 +389,35 @@ public StyleableBooleanProperty getStyleableProperty(JFXComboBox control) { } }; + private static final CssMetaData, Boolean> DISABLE_ANIMATION = + new CssMetaData<>("-jfx-disable-animation", + BooleanConverter.getInstance(), false) { + @Override + public boolean isSettable(JFXComboBox control) { + return control.disableAnimation == null || !control.disableAnimation.isBound(); + } + + @Override + public StyleableBooleanProperty getStyleableProperty(JFXComboBox control) { + return control.disableAnimationProperty(); + } + }; private static final List> CHILD_STYLEABLES; static { - final List> styleables = new ArrayList<>( - Control.getClassCssMetaData()); - Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT); - CHILD_STYLEABLES = Collections.unmodifiableList(styleables); + final List> styleables = new ArrayList<>(ComboBox.getClassCssMetaData()); + Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION); + CHILD_STYLEABLES = List.copyOf(styleables); } } - // inherit the styleable properties from parent - private List> STYLEABLES; - @Override public List> getControlCssMetaData() { - if (STYLEABLES == null) { - final List> styleables = new ArrayList<>( - Control.getClassCssMetaData()); - styleables.addAll(getClassCssMetaData()); - styleables.addAll(Control.getClassCssMetaData()); - STYLEABLES = Collections.unmodifiableList(styleables); - } - return STYLEABLES; + return getClassCssMetaData(); } public static List> getClassCssMetaData() { return StyleableProperties.CHILD_STYLEABLES; } } - From ec2e7fe00f98ab00b01f4a3086fd4b9a301ca1a0 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:09:36 +0800 Subject: [PATCH 04/16] update --- .../skins/JFXComboBoxListViewSkin.java | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java new file mode 100644 index 0000000000..e3957530c0 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.controls.JFXComboBox; +import javafx.beans.property.ObjectProperty; +import javafx.css.CssMetaData; +import javafx.css.Styleable; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.css.converter.PaintConverter; +import javafx.scene.Node; +import javafx.scene.control.skin.ComboBoxListViewSkin; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.text.Text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// # Material Design ComboBox Skin +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-01-25 +public class JFXComboBoxListViewSkin extends ComboBoxListViewSkin { + + /*************************************************************************** + * * + * Private fields * + * * + **************************************************************************/ + + private boolean invalid = true; + + private Text promptText; + private final ValidationPane> errorContainer; + private final PromptLinesWrapper> linesWrapper; + + protected final ObjectProperty promptTextFill = new StyleableObjectProperty<>(Color.GRAY) { + @Override + public Object getBean() { + return JFXComboBoxListViewSkin.this; + } + + @Override + public String getName() { + return "promptTextFill"; + } + + @Override + @SuppressWarnings("unchecked") + public CssMetaData, Paint> getCssMetaData() { + return (CssMetaData, Paint>) (CssMetaData) StyleableProperties.PROMPT_TEXT_FILL; + } + }; + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + + public JFXComboBoxListViewSkin(final JFXComboBox comboBox) { + super(comboBox); + + + linesWrapper = new PromptLinesWrapper<>( + comboBox, + promptTextFill, + comboBox.valueProperty(), + comboBox.promptTextProperty(), + () -> promptText); + + linesWrapper.init(this::createPromptNode); + Pane arrowButton = null; + for (Node node : getChildren()) { + if (node.getId().equals("arrow-button")) { + arrowButton = (Pane) node; + break; + } + } + if (arrowButton != null) { + linesWrapper.clip.widthProperty().bind(linesWrapper.promptContainer.widthProperty().subtract(arrowButton.widthProperty())); + } + + errorContainer = new ValidationPane<>(comboBox); + + getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); + + if (comboBox.isEditable()) { + comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 0.333333em 0em;"); + comboBox.getEditor().promptTextProperty().unbind(); + comboBox.getEditor().setPromptText(null); + comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> linesWrapper.usePromptText.invalidate()); + } + + registerChangeListener(comboBox.disableProperty(), obs -> linesWrapper.updateDisabled()); + registerChangeListener(comboBox.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); + registerChangeListener(comboBox.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); + registerChangeListener(comboBox.disableAnimationProperty(), obs -> errorContainer.updateClip()); + } + + /*************************************************************************** + * * + * Public API * + * * + **************************************************************************/ + + @Override + protected void layoutChildren(final double x, final double y, + final double w, final double h) { + super.layoutChildren(x, y, w, h); + final double height = getSkinnable().getHeight(); + linesWrapper.layoutLines(x, y, w, h, height, + promptText == null ? 0 : snapPositionX(promptText.getBaselineOffset() + promptText.getLayoutBounds().getHeight() * .36)); + errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), w, h); + + linesWrapper.updateLabelFloatLayout(); + + if (invalid) { + invalid = false; + // update validation container + errorContainer.invalid(w); + // focus + linesWrapper.invalid(); + } + } + + private void createPromptNode() { + if (promptText != null || !linesWrapper.usePromptText.get()) { + return; + } + promptText = new Text(); + // create my custom pane for the prompt node + promptText.textProperty().bind(getSkinnable().promptTextProperty()); + promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); + promptText.getStyleClass().addAll("text"); + promptText.getTransforms().add(linesWrapper.promptTextScale); + promptText.visibleProperty().bind(linesWrapper.usePromptText); + promptText.setTranslateX(1); + linesWrapper.promptContainer.getChildren().add(promptText); + + if (getSkinnable().isFocused() && ((JFXComboBox) getSkinnable()).isLabelFloat()) { + promptText.setTranslateY(-snapPositionY(promptText.getBaselineOffset() + promptText.getLayoutBounds().getHeight() * .36)); + linesWrapper.promptTextScale.setX(0.85); + linesWrapper.promptTextScale.setY(0.85); + } + } + + private static class StyleableProperties { + private static final CssMetaData, Paint> PROMPT_TEXT_FILL = + new CssMetaData<>("-fx-prompt-text-fill", + PaintConverter.getInstance(), Color.GRAY) { + + @Override + public boolean isSettable(JFXComboBox n) { + final JFXComboBoxListViewSkin skin = (JFXComboBoxListViewSkin) n.getSkin(); + return skin.promptTextFill == null || !skin.promptTextFill.isBound(); + } + + @Override + @SuppressWarnings("unchecked") + public StyleableProperty getStyleableProperty(JFXComboBox n) { + final JFXComboBoxListViewSkin skin = (JFXComboBoxListViewSkin) n.getSkin(); + return (StyleableProperty) skin.promptTextFill; + } + }; + + private static final List> STYLEABLES; + + static { + List> styleables = + new ArrayList<>(ComboBoxListViewSkin.getClassCssMetaData()); + styleables.add(PROMPT_TEXT_FILL); + STYLEABLES = Collections.unmodifiableList(styleables); + } + } + + + /// @return The CssMetaData associated with this class, which may include the + /// CssMetaData of its super classes. + public static List> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } + + /// {@inheritDoc} + @Override + public List> getCssMetaData() { + return getClassCssMetaData(); + } +} From 6d4fdec26dfc477e4770ada394a5524ce12447ee Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:13:25 +0800 Subject: [PATCH 05/16] update --- .../com/jfoenix/controls/JFXComboBox.java | 6 +- .../com/jfoenix/controls/JFXTextArea.java | 166 +++++++----------- .../com/jfoenix/skins/JFXTextAreaSkin.java | 135 ++++++++++++++ 3 files changed, 204 insertions(+), 103 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index adcb8182f7..12df5769f9 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -258,12 +258,10 @@ public void resetValidation() { * * **************************************************************************/ - /** - * set true to show a float the prompt text when focusing the field - */ + /// set true to show a float the prompt text when focusing the field private final StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXComboBox.this, - "lableFloat", + "labelFloat", false); public final StyleableBooleanProperty labelFloatProperty() { diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java index df7409941d..732b8d69dc 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java @@ -19,16 +19,14 @@ package com.jfoenix.controls; +import com.jfoenix.controls.base.IFXLabelFloatControl; import com.jfoenix.skins.JFXTextAreaSkin; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.*; import javafx.css.converter.BooleanConverter; import javafx.css.converter.PaintConverter; -import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.control.TextArea; import javafx.scene.paint.Color; @@ -38,8 +36,6 @@ import java.util.Collections; import java.util.List; -import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu; - /** * JFXTextArea is the material design implementation of a text area. * @@ -47,33 +43,17 @@ * @version 1.0 * @since 2016-03-09 */ -public class JFXTextArea extends TextArea { - /** - * Initialize the style class to 'jfx-text-field'. - *

- * This is the selector class from which CSS can be used to style - * this control. - */ - private static final String DEFAULT_STYLE_CLASS = "jfx-text-area"; +public class JFXTextArea extends TextArea implements IFXLabelFloatControl { - /** - * {@inheritDoc} - */ public JFXTextArea() { initialize(); } - /** - * {@inheritDoc} - */ public JFXTextArea(String text) { super(text); initialize(); } - /** - * {@inheritDoc} - */ @Override protected Skin createDefaultSkin() { return new JFXTextAreaSkin(this); @@ -81,11 +61,6 @@ protected Skin createDefaultSkin() { private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); - if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) { - this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextAreaSkinAndroid\";"); - } - - useJFXContextMenu(this); } /*************************************************************************** @@ -95,68 +70,55 @@ private void initialize() { **************************************************************************/ /** - * holds the current active validator on the text area in case of validation error + * wrapper for validation properties / methods */ - private ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); + protected final ValidationControl validationControl = new ValidationControl(this); + @Override public ValidatorBase getActiveValidator() { - return activeValidator == null ? null : activeValidator.get(); + return validationControl.getActiveValidator(); } + @Override public ReadOnlyObjectProperty activeValidatorProperty() { - return this.activeValidator.getReadOnlyProperty(); + return validationControl.activeValidatorProperty(); } - /** - * list of validators that will validate the text value upon calling - * {{@link #validate()} - */ - private ObservableList validators = FXCollections.observableArrayList(); - + @Override public ObservableList getValidators() { - return validators; + return validationControl.getValidators(); } + @Override public void setValidators(ValidatorBase... validators) { - this.validators.addAll(validators); + validationControl.setValidators(validators); } - /** - * validates the text value using the list of validators provided by the user - * {{@link #setValidators(ValidatorBase...)} - * - * @return true if the value is valid else false - */ + @Override public boolean validate() { - for (ValidatorBase validator : validators) { - if (validator.getSrcControl() == null) { - validator.setSrcControl(this); - } - validator.validate(); - if (validator.getHasErrors()) { - activeValidator.set(validator); - return false; - } - } - activeValidator.set(null); - return true; + return validationControl.validate(); } + @Override public void resetValidation() { - pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false); - activeValidator.set(null); + validationControl.resetValidation(); } /*************************************************************************** * * - * styleable Properties * + * Styleable Properties * * * **************************************************************************/ - /** - * set true to show a float the prompt text when focusing the field - */ - private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextArea.this, "lableFloat", false); + /// Initialize the style class to 'jfx-text-field'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-text-area"; + + /// set true to show a float the prompt text when focusing the field + private final StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, + JFXTextArea.this, "labelFloat", false); public final StyleableBooleanProperty labelFloatProperty() { return this.labelFloat; @@ -170,10 +132,9 @@ public final void setLabelFloat(final boolean labelFloat) { this.labelFloatProperty().set(labelFloat); } - /** - * default color used when the text area is unfocused - */ - private StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77)); + /// default color used when the text area is unfocused + private final StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, + JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77)); public Paint getUnFocusColor() { return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get(); @@ -187,10 +148,11 @@ public void setUnFocusColor(Paint color) { this.unFocusColor.set(color); } - /** - * default color used when the text area is focused - */ - private StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextArea.this, "focusColor", Color.valueOf("#4059A9")); + /// default color used when the text area is focused + private final StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, + JFXTextArea.this, + "focusColor", + Color.valueOf("#4059A9")); public Paint getFocusColor() { return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get(); @@ -207,7 +169,10 @@ public void setFocusColor(Paint color) { /** * disable animation on validation */ - private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextArea.this, "disableAnimation", false); + private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, + JFXTextArea.this, + "disableAnimation", + false); public final StyleableBooleanProperty disableAnimationProperty() { return this.disableAnimation; @@ -221,8 +186,11 @@ public final void setDisableAnimation(final Boolean disabled) { this.disableAnimationProperty().set(disabled); } - private final static class StyleableProperties { - private static final CssMetaData UNFOCUS_COLOR = new CssMetaData("-jfx-unfocus-color", PaintConverter.getInstance(), Color.rgb(77, 77, 77)) { + private static class StyleableProperties { + private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( + "-jfx-unfocus-color", + PaintConverter.getInstance(), + Color.rgb(77, 77, 77)) { @Override public boolean isSettable(JFXTextArea control) { return control.unFocusColor == null || !control.unFocusColor.isBound(); @@ -233,7 +201,10 @@ public StyleableProperty getStyleableProperty(JFXTextArea control) { return control.unFocusColorProperty(); } }; - private static final CssMetaData FOCUS_COLOR = new CssMetaData("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#4059A9")) { + private static final CssMetaData FOCUS_COLOR = new CssMetaData<>( + "-jfx-focus-color", + PaintConverter.getInstance(), + Color.valueOf("#4059A9")) { @Override public boolean isSettable(JFXTextArea control) { return control.focusColor == null || !control.focusColor.isBound(); @@ -244,7 +215,10 @@ public StyleableProperty getStyleableProperty(JFXTextArea control) { return control.focusColorProperty(); } }; - private static final CssMetaData LABEL_FLOAT = new CssMetaData("-jfx-label-float", BooleanConverter.getInstance(), false) { + private static final CssMetaData LABEL_FLOAT = new CssMetaData<>( + "-jfx-label-float", + BooleanConverter.getInstance(), + false) { @Override public boolean isSettable(JFXTextArea control) { return control.labelFloat == null || !control.labelFloat.isBound(); @@ -256,39 +230,33 @@ public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) { } }; - private static final CssMetaData DISABLE_ANIMATION = new CssMetaData("-jfx-disable-animation", BooleanConverter.getInstance(), false) { - @Override - public boolean isSettable(JFXTextArea control) { - return control.disableAnimation == null || !control.disableAnimation.isBound(); - } + private static final CssMetaData DISABLE_ANIMATION = + new CssMetaData<>("-jfx-disable-animation", + BooleanConverter.getInstance(), false) { + @Override + public boolean isSettable(JFXTextArea control) { + return control.disableAnimation == null || !control.disableAnimation.isBound(); + } - @Override - public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) { - return control.disableAnimationProperty(); - } - }; + @Override + public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) { + return control.disableAnimationProperty(); + } + }; private static final List> CHILD_STYLEABLES; static { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); + final List> styleables = new ArrayList<>( + TextArea.getClassCssMetaData()); Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION); - CHILD_STYLEABLES = Collections.unmodifiableList(styleables); + CHILD_STYLEABLES = List.copyOf(styleables); } } - // inherit the styleable properties from parent - private List> STYLEABLES; - @Override public List> getControlCssMetaData() { - if (STYLEABLES == null) { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); - styleables.addAll(getClassCssMetaData()); - styleables.addAll(TextArea.getClassCssMetaData()); - STYLEABLES = Collections.unmodifiableList(styleables); - } - return STYLEABLES; + return getClassCssMetaData(); } public static List> getClassCssMetaData() { diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java new file mode 100644 index 0000000000..516b37a01d --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.adapters.ReflectionHelper; +import com.jfoenix.controls.JFXTextArea; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.skin.TextAreaSkin; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +/// # Material Design TextArea Skin +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-01-25 +public class JFXTextAreaSkin extends TextAreaSkin { + + private boolean invalid = true; + + private final ScrollPane scrollPane; + private Text promptText; + + private final ValidationPane errorContainer; + private final PromptLinesWrapper linesWrapper; + + public JFXTextAreaSkin(JFXTextArea textArea) { + super(textArea); + // init text area properties + scrollPane = (ScrollPane) getChildren().get(0); + textArea.setWrapText(true); + + linesWrapper = new PromptLinesWrapper<>( + textArea, + promptTextFillProperty(), + textArea.textProperty(), + textArea.promptTextProperty(), + () -> promptText); + + linesWrapper.init(this::createPromptNode, scrollPane); + errorContainer = new ValidationPane<>(textArea); + getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); + + registerChangeListener(textArea.disableProperty(), obs -> linesWrapper.updateDisabled()); + registerChangeListener(textArea.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); + registerChangeListener(textArea.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); + registerChangeListener(textArea.disableAnimationProperty(), obs -> errorContainer.updateClip()); + + } + + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + super.layoutChildren(x, y, w, h); + + final double height = getSkinnable().getHeight(); + linesWrapper.layoutLines(x, y, w, h, height, promptText == null ? 0 : promptText.getLayoutBounds().getHeight() + 3); + errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), w, h); + linesWrapper.updateLabelFloatLayout(); + + if (invalid) { + invalid = false; + // set the default background of text area viewport to white + Region viewPort = (Region) scrollPane.getChildrenUnmodifiable().get(0); + viewPort.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, + CornerRadii.EMPTY, + Insets.EMPTY))); + // reapply css of scroll pane in case set by the user + viewPort.applyCss(); + errorContainer.invalid(w); + // focus + linesWrapper.invalid(); + } + } + + private void createPromptNode() { + if (promptText != null || !linesWrapper.usePromptText.get()) { + return; + } + promptText = new Text(); + promptText.setManaged(false); + promptText.getStyleClass().add("text"); + promptText.visibleProperty().bind(linesWrapper.usePromptText); + promptText.fontProperty().bind(getSkinnable().fontProperty()); + promptText.textProperty().bind(getSkinnable().promptTextProperty()); + promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); + promptText.setLayoutX(1); + promptText.setTranslateX(1); + promptText.getTransforms().add(linesWrapper.promptTextScale); + linesWrapper.promptContainer.getChildren().add(promptText); + if (getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) { + promptText.setTranslateY(-Math.floor(scrollPane.getHeight())); + linesWrapper.promptTextScale.setX(0.85); + linesWrapper.promptTextScale.setY(0.85); + } + + try { + Field field = ReflectionHelper.getField(TextAreaSkin.class, "promptNode"); + Object oldValue = field.get(this); + if (oldValue != null) { + removeHighlight(List.of((Node) oldValue)); + } + field.set(this, promptText); + } catch (Exception e) { + e.printStackTrace(); + } + } +} From 592fb409748a7192216f5eb0f131c76a139aa57f Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:16:11 +0800 Subject: [PATCH 06/16] update --- .../jfoenix/controls/JFXPasswordField.java | 181 +++++++++--------- .../com/jfoenix/controls/JFXTextField.java | 5 +- 2 files changed, 92 insertions(+), 94 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java index d256ba13f2..966d9a56a0 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java @@ -19,28 +19,23 @@ package com.jfoenix.controls; -import com.jfoenix.skins.JFXPasswordFieldSkin; +import com.jfoenix.controls.base.IFXLabelFloatControl; +import com.jfoenix.skins.JFXTextFieldSkin; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.*; import javafx.css.converter.BooleanConverter; import javafx.css.converter.PaintConverter; -import javafx.scene.control.Control; import javafx.scene.control.PasswordField; import javafx.scene.control.Skin; -import javafx.scene.control.TextField; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import org.jackhuang.hmcl.ui.FXUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; - -import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu; /** * JFXPasswordField is the material design implementation of a password Field. @@ -49,7 +44,7 @@ * @version 1.0 * @since 2016-03-09 */ -public class JFXPasswordField extends PasswordField { +public class JFXPasswordField extends PasswordField implements IFXLabelFloatControl { /** * {@inheritDoc} @@ -63,27 +58,14 @@ public JFXPasswordField() { */ @Override protected Skin createDefaultSkin() { - return new JFXPasswordFieldSkin(this); + return new JFXTextFieldSkin<>(this); } private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); - if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) { - this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";"); - } - - - useJFXContextMenu(this); + FXUtils.useJFXContextMenu(this); } - /** - * Initialize the style class to 'jfx-password-field'. - *

- * This is the selector class from which CSS can be used to style - * this control. - */ - private static final String DEFAULT_STYLE_CLASS = "jfx-password-field"; - /*************************************************************************** * * * Properties * @@ -91,77 +73,73 @@ private void initialize() { **************************************************************************/ /** - * holds the current active validator on the password field in case of validation error + * wrapper for validation properties / methods */ - private ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); + private final ValidationControl validationControl = new ValidationControl(this); + @Override public ValidatorBase getActiveValidator() { - return activeValidator == null ? null : activeValidator.get(); + return validationControl.getActiveValidator(); } + @Override public ReadOnlyObjectProperty activeValidatorProperty() { - return this.activeValidator.getReadOnlyProperty(); + return validationControl.activeValidatorProperty(); } - /** - * list of validators that will validate the password value upon calling - * {{@link #validate()} - */ - private ObservableList validators = FXCollections.observableArrayList(); - + @Override public ObservableList getValidators() { - return validators; + return validationControl.getValidators(); } + @Override public void setValidators(ValidatorBase... validators) { - this.validators.addAll(validators); + validationControl.setValidators(validators); } - /** - * validates the password value using the list of validators provided by the user - * {{@link #setValidators(ValidatorBase...)} - * - * @return true if the value is valid else false - */ + @Override public boolean validate() { - for (ValidatorBase validator : validators) { - if (validator.getSrcControl() == null) { - validator.setSrcControl(this); - } - validator.validate(); - if (validator.getHasErrors()) { - activeValidator.set(validator); - return false; - } - } - activeValidator.set(null); - return true; + return validationControl.validate(); } + @Override public void resetValidation() { - pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false); - activeValidator.set(null); + validationControl.resetValidation(); } /*************************************************************************** * * - * styleable Properties * + * Styleable Properties * * * **************************************************************************/ + /** + * Initialize the style class to 'jfx-password-field'. + *

+ * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "jfx-password-field"; + /** * set true to show a float the prompt text when focusing the field */ - private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXPasswordField.this, "lableFloat", false); + private final StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, + JFXPasswordField.this, + "labelFloat", + false); + @Override public final StyleableBooleanProperty labelFloatProperty() { return this.labelFloat; } + @Override public final boolean isLabelFloat() { return this.labelFloatProperty().get(); } + @Override public final void setLabelFloat(final boolean labelFloat) { this.labelFloatProperty().set(labelFloat); } @@ -169,16 +147,24 @@ public final void setLabelFloat(final boolean labelFloat) { /** * default color used when the field is unfocused */ - private StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXPasswordField.this, "unFocusColor", Color.rgb(77, 77, 77)); + private final StyleableObjectProperty unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, + JFXPasswordField.this, + "unFocusColor", + Color.rgb(77, + 77, + 77)); + @Override public Paint getUnFocusColor() { return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get(); } + @Override public StyleableObjectProperty unFocusColorProperty() { return this.unFocusColor; } + @Override public void setUnFocusColor(Paint color) { this.unFocusColor.set(color); } @@ -186,39 +172,53 @@ public void setUnFocusColor(Paint color) { /** * default color used when the field is focused */ - private StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXPasswordField.this, "focusColor", Color.valueOf("#4059A9")); + private StyleableObjectProperty focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, + JFXPasswordField.this, + "focusColor", + Color.valueOf("#4059A9")); + @Override public Paint getFocusColor() { return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get(); } + @Override public StyleableObjectProperty focusColorProperty() { return this.focusColor; } + @Override public void setFocusColor(Paint color) { this.focusColor.set(color); } - /** - * disable animation on validation - */ - private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXPasswordField.this, "disableAnimation", false); + /// disable animation on validation + private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, + JFXPasswordField.this, + "disableAnimation", + false); + @Override public final StyleableBooleanProperty disableAnimationProperty() { return this.disableAnimation; } + @Override public final Boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } + @Override public final void setDisableAnimation(final Boolean disabled) { this.disableAnimationProperty().set(disabled); } - private final static class StyleableProperties { - private static final CssMetaData UNFOCUS_COLOR = new CssMetaData("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) { + + private static class StyleableProperties { + private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( + "-jfx-unfocus-color", + PaintConverter.getInstance(), + Color.valueOf("#A6A6A6")) { @Override public boolean isSettable(JFXPasswordField control) { return control.unFocusColor == null || !control.unFocusColor.isBound(); @@ -229,7 +229,10 @@ public StyleableProperty getStyleableProperty(JFXPasswordField control) { return control.unFocusColorProperty(); } }; - private static final CssMetaData FOCUS_COLOR = new CssMetaData("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) { + private static final CssMetaData FOCUS_COLOR = new CssMetaData<>( + "-jfx-focus-color", + PaintConverter.getInstance(), + Color.valueOf("#3f51b5")) { @Override public boolean isSettable(JFXPasswordField control) { return control.focusColor == null || !control.focusColor.isBound(); @@ -241,7 +244,10 @@ public StyleableProperty getStyleableProperty(JFXPasswordField control) { } }; - private static final CssMetaData LABEL_FLOAT = new CssMetaData("-jfx-label-float", BooleanConverter.getInstance(), false) { + private static final CssMetaData LABEL_FLOAT = new CssMetaData<>( + "-jfx-label-float", + BooleanConverter.getInstance(), + false) { @Override public boolean isSettable(JFXPasswordField control) { return control.labelFloat == null || !control.labelFloat.isBound(); @@ -253,40 +259,34 @@ public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) { } }; - private static final CssMetaData DISABLE_ANIMATION = new CssMetaData("-fx-disable-animation", BooleanConverter.getInstance(), false) { - @Override - public boolean isSettable(JFXPasswordField control) { - return control.disableAnimation == null || !control.disableAnimation.isBound(); - } + private static final CssMetaData DISABLE_ANIMATION = + new CssMetaData<>("-fx-disable-animation", + BooleanConverter.getInstance(), false) { + @Override + public boolean isSettable(JFXPasswordField control) { + return control.disableAnimation == null || !control.disableAnimation.isBound(); + } - @Override - public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) { - return control.disableAnimationProperty(); - } - }; + @Override + public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) { + return control.disableAnimationProperty(); + } + }; private static final List> CHILD_STYLEABLES; static { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); + final List> styleables = new ArrayList<>( + PasswordField.getClassCssMetaData()); Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION); - CHILD_STYLEABLES = Collections.unmodifiableList(styleables); + CHILD_STYLEABLES = List.copyOf(styleables); } } - // inherit the styleable properties from parent - private List> STYLEABLES; - @Override public List> getControlCssMetaData() { - if (STYLEABLES == null) { - final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); - styleables.addAll(getClassCssMetaData()); - styleables.addAll(TextField.getClassCssMetaData()); - STYLEABLES = Collections.unmodifiableList(styleables); - } - return STYLEABLES; + return getClassCssMetaData(); } public static List> getClassCssMetaData() { @@ -294,4 +294,3 @@ public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) { } } - diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java index 4c9406aafe..9bc0f75f6d 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java @@ -31,13 +31,12 @@ import javafx.scene.control.TextField; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import org.jackhuang.hmcl.ui.FXUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu; - /** * JFXTextField is the material design implementation of a text Field. * @@ -72,7 +71,7 @@ protected Skin createDefaultSkin() { private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); - useJFXContextMenu(this); + FXUtils.useJFXContextMenu(this); } /*************************************************************************** From f7bd18fd9432810e662f91ba3d73113c52bd4f2c Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:18:11 +0800 Subject: [PATCH 07/16] update --- HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java | 3 +++ HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index 12df5769f9..4685989a10 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -43,6 +43,7 @@ import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.util.StringConverter; +import org.jackhuang.hmcl.ui.FXUtils; import java.util.ArrayList; import java.util.Collections; @@ -102,6 +103,8 @@ protected void updateItem(T item, boolean empty) { } }); + + FXUtils.useJFXContextMenu(editorProperty().get()); } /** diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java index 732b8d69dc..901aabc77f 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java @@ -31,6 +31,7 @@ import javafx.scene.control.TextArea; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import org.jackhuang.hmcl.ui.FXUtils; import java.util.ArrayList; import java.util.Collections; @@ -61,6 +62,7 @@ protected Skin createDefaultSkin() { private void initialize() { this.getStyleClass().add(DEFAULT_STYLE_CLASS); + FXUtils.useJFXContextMenu(this); } /*************************************************************************** From ca0ed8c09c8dba8850f94f2adcc59d4e032d345d Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:22:02 +0800 Subject: [PATCH 08/16] update --- .../java/com/jfoenix/controls/JFXTooltip.java | 550 ++++++++++++++++++ .../jfoenix/validation/DoubleValidator.java | 66 +++ .../jfoenix/validation/IntegerValidator.java | 70 +++ .../jfoenix/validation/NumberValidator.java | 114 ++++ .../jfoenix/validation/RegexValidator.java | 86 +++ .../validation/RequiredFieldValidator.java | 73 +++ .../validation/StringLengthValidator.java | 101 ++++ .../jfoenix/validation/ValidationFacade.java | 315 ++++++++++ .../validation/base/ValidatorBase.java | 338 +++++++++++ 9 files changed, 1713 insertions(+) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/DoubleValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/IntegerValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/RegexValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/RequiredFieldValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java create mode 100644 HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java new file mode 100644 index 0000000000..da9cf18ccb --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java @@ -0,0 +1,550 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.transitions.JFXAnimationTimer; +import com.jfoenix.transitions.JFXKeyFrame; +import com.jfoenix.transitions.JFXKeyValue; +import com.sun.javafx.event.EventHandlerManager; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.event.EventDispatchChain; +import javafx.event.EventHandler; +import javafx.event.WeakEventHandler; +import javafx.geometry.Bounds; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Skin; +import javafx.scene.control.Tooltip; +import javafx.scene.control.skin.TooltipSkin; +import javafx.scene.input.MouseEvent; +import javafx.stage.Window; +import javafx.stage.WindowEvent; +import javafx.util.Duration; + +/** + * JFXTooltip is the material design implementation of the tooltip. + * + * @author Shadi Shaheen + * @version 1.0 + * @since 3/29/2019 + */ +public class JFXTooltip extends Tooltip { + + private static final TooltipBehavior BEHAVIOR = new TooltipBehavior( + Duration.millis(650), + Duration.millis(1500), + Duration.millis(200)); + + /** + * updates the hover duration for {@link JFXTooltip} behavior + */ + public static void setHoverDelay(Duration duration) { + BEHAVIOR.setHoverDelay(duration == null ? Duration.millis(650) : duration); + } + + /** + * updates the visible duration for {@link JFXTooltip} behavior + */ + public static void setVisibleDuration(Duration duration) { + BEHAVIOR.setVisibleDuration(duration == null ? Duration.millis(1500) : duration); + } + + /** + * updates the left duration for {@link JFXTooltip} behavior + */ + public static void setLeftDelay(Duration duration) { + BEHAVIOR.setLeftDelay(duration == null ? Duration.millis(200) : duration); + } + + /** + * Associates the given {@link JFXTooltip} tooltip to the given node. + */ + public static void install(Node node, JFXTooltip tooltip) { + BEHAVIOR.install(node, tooltip); + } + + /** + * Associates the given {@link JFXTooltip} tooltip to the given node. + * The tooltip will be shown according to the given {@link Pos} pos + */ + public static void install(Node node, JFXTooltip tooltip, Pos pos) { + tooltip.setPos(pos); + BEHAVIOR.install(node, tooltip); + } + + /** + * Removes {@link JFXTooltip} tooltip from the given node + */ + public static void uninstall(Node node) { + BEHAVIOR.uninstall(node); + } + + private Node root = null; + private boolean hiding = false; + + private final JFXAnimationTimer animation = new JFXAnimationTimer( + JFXKeyFrame.builder().setDuration(Duration.millis(150)) + .setAnimateCondition(() -> !hiding) + .setKeyValues(JFXKeyValue.builder() + .setTargetSupplier(() -> root.opacityProperty()) + .setEndValue(1).build(), + JFXKeyValue.builder() + .setTargetSupplier(() -> root.scaleXProperty()) + .setEndValue(1).build(), + JFXKeyValue.builder() + .setTargetSupplier(() -> root.scaleYProperty()) + .setEndValue(1).build()) + .build(), + JFXKeyFrame.builder().setDuration(Duration.millis(75)) + .setAnimateCondition(() -> hiding) + .setKeyValues(JFXKeyValue.builder() + .setTargetSupplier(() -> root.opacityProperty()) + .setEndValue(0).build()).build() + ); + + /** + * {@inheritDoc} + */ + public JFXTooltip() { + this(null); + } + + /** + * {@inheritDoc} + */ + public JFXTooltip(String text, Pos pos) { + this(text); + setPos(pos); + } + + /** + * {@inheritDoc} + */ + public JFXTooltip(String text) { + super(text); + init(); + } + + private void init() { + getStyleClass().add(DEFAULT_STYLE_CLASS); + eventHandlerManager.addEventHandler(WindowEvent.WINDOW_SHOWING, event -> { + root = getScene().getRoot(); + root.setOpacity(0); + root.setScaleY(0.8); + root.setScaleX(0.8); + animation.setOnFinished(null); + }); + eventHandlerManager.addEventHandler(WindowEvent.WINDOW_SHOWN, event -> { + setAnchorX(getUpdatedAnchorX(getAnchorX())); + setAnchorY(getUpdatedAnchorY(getAnchorY())); + animation.reverseAndContinue(); + }); + } + + /*************************************************************************** + * * + * Properties * + * * + **************************************************************************/ + + private Pos pos = Pos.BOTTOM_CENTER; + private double margin = 8; + + /** + * @return the tooltip position + */ + public Pos getPos() { + return pos; + } + + /** + * sets the tooltip position with respect to its node + * + */ + public void setPos(Pos pos) { + this.pos = pos == null ? Pos.BOTTOM_CENTER : pos; + } + + /** + * @return the gap between tooltip and the associated node + */ + public double getMargin() { + return margin; + } + + /** + * sets the gap between tooltip and the associated node. + * the default value is 8 + * + */ + public void setMargin(double margin) { + this.margin = margin; + } + + private double getUpdatedAnchorY(double anchor) { + switch (pos.getVpos()) { + case CENTER: + return anchor - getHeight() / 2; + case TOP: + case BASELINE: + return anchor - getHeight(); + default: + return anchor; + } + } + + private double getUpdatedAnchorX(double anchor) { + switch (pos.getHpos()) { + case CENTER: + return anchor - getWidth() / 2; + case LEFT: + return anchor - getWidth(); + default: + return anchor; + } + } + + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /** + * Initialize the style class to 'jfx-tooltip'. + *

+ * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "jfx-tooltip"; + + + /** + * {@inheritDoc} + */ + @Override + public void hide() { + hiding = true; + animation.setOnFinished(super::hide); + animation.reverseAndContinue(); + } + + /** + * {@inheritDoc} + * this method ignores anchorX, anchorY. It shows the tooltip according + * to tooltip {@link JFXTooltip#pos} field + *

+ * NOTE: if you want to manually show the tooltip on forced local anchors + * you can use {@link JFXTooltip#showOnAnchors(Node, double, double)} method. + */ + @Override + public void show(Node ownerNode, double anchorX, double anchorY) { + // if tooltip hide animation still running, then hide method is not called yet + // thus only reverse the animation to show the tooltip again + hiding = false; + final Bounds sceneBounds = ownerNode.localToScene(ownerNode.getBoundsInLocal()); + if (isShowing()) { + animation.setOnFinished(null); + animation.reverseAndContinue(); + anchorX = ownerX(ownerNode, sceneBounds) + getHPosForNode(sceneBounds); + anchorY = ownerY(ownerNode, sceneBounds) + getVPosForNode(sceneBounds); + setAnchorY(getUpdatedAnchorY(anchorY)); + setAnchorX(getUpdatedAnchorX(anchorX)); + } else { + // tooltip was not showing compute its anchors and show it + anchorX = ownerX(ownerNode, sceneBounds) + getHPosForNode(sceneBounds); + anchorY = ownerY(ownerNode, sceneBounds) + getVPosForNode(sceneBounds); + super.show(ownerNode, anchorX, anchorY); + } + } + + /** + * @param sceneBounds is the owner node scene Bounds + * @return anchorX that represents the local minX of owner node + */ + private double ownerX(Node ownerNode, Bounds sceneBounds) { + Window parent = ownerNode.getScene().getWindow(); + return parent.getX() + sceneBounds.getMinX() + ownerNode.getScene().getX(); + } + + /** + * @param sceneBounds is the owner node scene Bounds + * @return anchorY that represents the local minY of owner node + */ + private double ownerY(Node ownerNode, Bounds sceneBounds) { + Window parent = ownerNode.getScene().getWindow(); + return parent.getY() + sceneBounds.getMinY() + ownerNode.getScene().getY(); + } + + + public void showOnAnchors(Node ownerNode, double anchorX, double anchorY) { + hiding = false; + final Bounds sceneBounds = ownerNode.localToScene(ownerNode.getBoundsInLocal()); + if (isShowing()) { + animation.setOnFinished(null); + animation.reverseAndContinue(); + anchorX += ownerX(ownerNode, sceneBounds); + anchorY += ownerY(ownerNode, sceneBounds); + setAnchorX(getUpdatedAnchorX(anchorX)); + setAnchorY(getUpdatedAnchorY(anchorY)); + } else { + anchorX += ownerX(ownerNode, sceneBounds); + anchorY += ownerY(ownerNode, sceneBounds); + super.show(ownerNode, anchorX, anchorY); + } + } + + private double getHPosForNode(Bounds sceneBounds) { + double hx = -margin; + switch (pos.getHpos()) { + case CENTER: + hx = (sceneBounds.getWidth() / 2); + break; + case RIGHT: + hx = sceneBounds.getWidth() + margin; + break; + } + return hx; + } + + private double getVPosForNode(Bounds sceneBounds) { + double vy = -margin; + switch (pos.getVpos()) { + case CENTER: + vy = (sceneBounds.getHeight() / 2); + break; + case BOTTOM: + vy = sceneBounds.getHeight() + margin; + break; + } + return vy; + } + + /** + * {@inheritDoc} + */ + @Override + protected Skin createDefaultSkin() { + return new TooltipSkin(this) { + { + Node node = getNode(); + node.setEffect(null); + } + }; + } + + private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this); + + /** + * {@inheritDoc} + */ + @Override + public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { + return super.buildEventDispatchChain(tail).prepend(eventHandlerManager); + } + + private static class TooltipBehavior { + + private static String TOOLTIP_PROP = "jfoenix-tooltip"; + private Timeline hoverTimer = new Timeline(); + private Timeline visibleTimer = new Timeline(); + private Timeline leftTimer = new Timeline(); + /** + * the currently hovered node + */ + private Node hoveredNode; + /** + * the next tooltip to be shown + */ + private JFXTooltip nextTooltip; + /** + * the current showing tooltip + */ + private JFXTooltip currentTooltip; + + private TooltipBehavior(Duration hoverDelay, Duration visibleDuration, Duration leftDelay) { + setHoverDelay(hoverDelay); + hoverTimer.setOnFinished(event -> { + ensureHoveredNodeIsVisible(() -> { + // set tooltip orientation + NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation(); + nextTooltip.getScene().setNodeOrientation(nodeOrientation); + //show tooltip + showTooltip(nextTooltip); + currentTooltip = nextTooltip; + hoveredNode = null; + // start visible timer + visibleTimer.playFromStart(); + }); + // clear next tooltip + nextTooltip = null; + }); + setVisibleDuration(visibleDuration); + visibleTimer.setOnFinished(event -> hideCurrentTooltip()); + setLeftDelay(leftDelay); + leftTimer.setOnFinished(event -> hideCurrentTooltip()); + } + + private void setHoverDelay(Duration duration) { + hoverTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void setVisibleDuration(Duration duration) { + visibleTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void setLeftDelay(Duration duration) { + leftTimer.getKeyFrames().setAll(new KeyFrame(duration)); + } + + private void hideCurrentTooltip() { + currentTooltip.hide(); + currentTooltip = null; + hoveredNode = null; + } + + private void showTooltip(JFXTooltip tooltip) { + // anchors are computed differently for each tooltip + tooltip.show(hoveredNode, -1, -1); + } + + private EventHandler moveHandler = (MouseEvent event) -> { + // if tool tip is already showing, do nothing + if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { + return; + } + hoveredNode = (Node) event.getSource(); + Object property = hoveredNode.getProperties().get(TOOLTIP_PROP); + if (property instanceof JFXTooltip) { + JFXTooltip tooltip = (JFXTooltip) property; + ensureHoveredNodeIsVisible(() -> { + // if a tooltip is already showing then show this tooltip immediately + if (leftTimer.getStatus() == Timeline.Status.RUNNING) { + if (currentTooltip != null) { + currentTooltip.hide(); + } + currentTooltip = tooltip; + // show the tooltip + showTooltip(tooltip); + // stop left timer and start the visible timer to hide the tooltip + // once finished + leftTimer.stop(); + visibleTimer.playFromStart(); + } else { + // else mark the tooltip as the next tooltip to be shown once the hover + // timer is finished (restart the timer) +// t.setActivated(true); + nextTooltip = tooltip; + hoverTimer.stop(); + hoverTimer.playFromStart(); + } + }); + } else { + uninstall(hoveredNode); + } + }; + private WeakEventHandler weakMoveHandler = new WeakEventHandler<>(moveHandler); + + private EventHandler exitHandler = (MouseEvent event) -> { + // stop running hover timer as the mouse exited the node + if (hoverTimer.getStatus() == Timeline.Status.RUNNING) { + hoverTimer.stop(); + } else if (visibleTimer.getStatus() == Timeline.Status.RUNNING) { + // if tool tip was already showing, stop the visible timer + // and start the left timer to hide the current tooltip + visibleTimer.stop(); + leftTimer.playFromStart(); + } + hoveredNode = null; + nextTooltip = null; + }; + private WeakEventHandler weakExitHandler = new WeakEventHandler<>(exitHandler); + + // if mouse is pressed then stop all timers / clear all fields + private EventHandler pressedHandler = (MouseEvent event) -> { + // stop timers + hoverTimer.stop(); + visibleTimer.stop(); + leftTimer.stop(); + // hide current tooltip + if (currentTooltip != null) { + currentTooltip.hide(); + } + // clear fields + hoveredNode = null; + currentTooltip = null; + nextTooltip = null; + }; + private WeakEventHandler weakPressedHandler = new WeakEventHandler<>(pressedHandler); + + private void install(Node node, JFXTooltip tooltip) { + if (node == null) { + return; + } + if (tooltip == null) { + uninstall(node); + return; + } + node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + node.addEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.addEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.addEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + node.getProperties().put(TOOLTIP_PROP, tooltip); + } + + private void uninstall(Node node) { + if (node == null) { + return; + } + node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler); + node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler); + node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler); + Object tooltip = node.getProperties().get(TOOLTIP_PROP); + if (tooltip != null) { + node.getProperties().remove(TOOLTIP_PROP); + if (tooltip.equals(currentTooltip) || tooltip.equals(nextTooltip)) { + weakPressedHandler.handle(null); + } + } + } + + private void ensureHoveredNodeIsVisible(Runnable visibleRunnable) { + final Window owner = getWindow(hoveredNode); + if (owner != null && owner.isShowing()) { + // final boolean treeVisible = NodeHelper.isTreeVisible(hoveredNode); + final boolean treeVisible = hoveredNode.isVisible(); // FIXME + if (treeVisible) { + visibleRunnable.run(); + } + } + } + + private Window getWindow(final Node node) { + final Scene scene = node == null ? null : node.getScene(); + return scene == null ? null : scene.getWindow(); + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/DoubleValidator.java b/HMCL/src/main/java/com/jfoenix/validation/DoubleValidator.java new file mode 100644 index 0000000000..9943f6ee2a --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/DoubleValidator.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.DefaultProperty; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; + +/** + * Double field validation, that is applied on text input controls + * such as {@link TextField} and {@link TextArea} + * + * @author eralpsahin + * @version 1.0 + * @since 2017-01-27 + */ +@DefaultProperty(value = "icon") +public class DoubleValidator extends ValidatorBase { + + public DoubleValidator() { + setMessage("Value must be a rational number"); + } + + public DoubleValidator(String message) { + super(message); + } + + /** + * {@inheritDoc} + */ + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + try { + Double.parseDouble(textField.getText()); + hasErrors.set(false); + } catch (Exception e) { + hasErrors.set(true); + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/IntegerValidator.java b/HMCL/src/main/java/com/jfoenix/validation/IntegerValidator.java new file mode 100644 index 0000000000..35cb5eae89 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/IntegerValidator.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.DefaultProperty; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; + +/** + * An example of Number field validation, that is applied on text input controls + * such as {@link TextField} and {@link TextArea} + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2016-03-09 + */ +@DefaultProperty(value = "icon") +public class IntegerValidator extends ValidatorBase { + + public IntegerValidator() { + setMessage("Value must be a number"); + } + + public IntegerValidator(String message) { + super(message); + } + + /** + * {@inheritDoc} + */ + @Override + + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + String text = textField.getText(); + try { + hasErrors.set(false); + if (!text.isEmpty()) { + Integer.parseInt(text); + } + } catch (Exception e) { + hasErrors.set(true); + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java b/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java new file mode 100644 index 0000000000..4009086d26 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.DefaultProperty; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; +import javafx.util.converter.NumberStringConverter; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; + +/** + * An example of Number field validation, that is applied on text input controls + * such as {@link TextField} and {@link TextArea} + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2016-03-09 + */ +@DefaultProperty(value = "icon") +public class NumberValidator extends ValidatorBase { + + private NumberStringConverter numberStringConverter = new NumberStringConverter(){ + @Override + public Number fromString(String string) { + try { + if (string == null) { + return null; + } + string = string.trim(); + if (string.length() < 1) { + return null; + } + // Create and configure the parser to be used + NumberFormat parser = getNumberFormat(); + ParsePosition parsePosition = new ParsePosition(0); + Number result = parser.parse(string, parsePosition); + final int index = parsePosition.getIndex(); + if (index == 0 || index < string.length()) { + throw new ParseException("Unparseable number: \"" + string + "\"", parsePosition.getErrorIndex()); + } + return result; + } catch (ParseException ex) { + throw new RuntimeException(ex); + } + } + }; + + public NumberValidator() { } + + public NumberValidator(String message) { + super(message); + } + + public NumberValidator(NumberStringConverter numberStringConverter) { + this.numberStringConverter = numberStringConverter; + } + + public NumberValidator(String message, NumberStringConverter numberStringConverter) { + super(message); + this.numberStringConverter = numberStringConverter; + } + + /** + * {@inheritDoc} + */ + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + String text = textField.getText(); + try { + hasErrors.set(false); + if (!text.isEmpty()) + numberStringConverter.fromString(text); + } catch (Exception e) { + hasErrors.set(true); + } + } + + public NumberStringConverter getNumberStringConverter() { + return numberStringConverter; + } + + public void setNumberStringConverter(NumberStringConverter numberStringConverter) { + this.numberStringConverter = numberStringConverter; + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/RegexValidator.java b/HMCL/src/main/java/com/jfoenix/validation/RegexValidator.java new file mode 100644 index 0000000000..1fa78ae380 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/RegexValidator.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.DefaultProperty; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; + +import java.util.regex.Pattern; + + +/** + * Regex validation, that is applied on text input controls + * such as {@link TextField} and {@link TextArea}. + * + * @version 1.0 + * @since 2018-08-06 + */ +@DefaultProperty(value = "icon") +public class RegexValidator extends ValidatorBase { + + private String regexPattern; + + public RegexValidator(String message) { + super(message); + } + + public RegexValidator() { + + } + + + private Pattern regexPatternCompiled; + + /** + * {@inheritDoc} + */ + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + String text = (textField.getText() == null) ? "" : textField.getText(); // Treat null like empty string + + if (regexPatternCompiled.matcher(text).matches()) { + hasErrors.set(false); + } else { + hasErrors.set(true); + } + } + + /* + * GETTER AND SETTER + */ + public void setRegexPattern(String regexPattern) { + this.regexPattern = regexPattern; + this.regexPatternCompiled = Pattern.compile(regexPattern); + } + + public String getRegexPattern() { + return regexPattern; + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/RequiredFieldValidator.java b/HMCL/src/main/java/com/jfoenix/validation/RequiredFieldValidator.java new file mode 100644 index 0000000000..4ca39fa518 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/RequiredFieldValidator.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.beans.DefaultProperty; +import javafx.scene.control.ComboBoxBase; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; + +/** + * An example of required field validation, that is applied on text input + * controls such as {@link TextField} and {@link TextArea} + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2016-03-09 + */ +@DefaultProperty(value = "icon") +public class RequiredFieldValidator extends ValidatorBase { + + public RequiredFieldValidator(String message) { + super(message); + } + + public RequiredFieldValidator() { + } + + /** + * {@inheritDoc} + */ + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + if (srcControl.get() instanceof ComboBoxBase) { + evalComboBoxField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + if (textField.getText() == null || textField.getText().isEmpty()) { + hasErrors.set(true); + } else { + hasErrors.set(false); + } + } + + private void evalComboBoxField() { + ComboBoxBase comboField = (ComboBoxBase) srcControl.get(); + Object value = comboField.getValue(); + hasErrors.set(value == null || value.toString().isEmpty()); + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java b/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java new file mode 100644 index 0000000000..ec07b9bfd7 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.scene.control.TextInputControl; + +/** + * @author Victor Espino + * @version 1.0 + * @since 2019-08-10 + */ +public class StringLengthValidator extends ValidatorBase { + + int StringLength; + + /** + * Basic constructor with Default message this way: + * "Max length is " + StringLength +" character(s) " + * + * @param StringLengh Length of the string in the input field to validate. + */ + public StringLengthValidator(int StringLengh) { + super("Max length is " + StringLengh + " character(s) "); + this.StringLength = StringLengh + 1; + } + + + /** + * The displayed message shown will be concatenated by the message with StringLength + * this way "message" + StringLength. + * + * @param StringLength Length of the string in the input field to validate. + * @param message Message to show. + */ + public StringLengthValidator(int StringLength, String message) { + this.StringLength = StringLength + 1; + setMessage(message + StringLength); + } + + /** + * The displayed message will be personalized, + * but still need to indicate the StringLength to validate. + * + * @param StringLength Length of the string in the input field to validate. + * @param message Message to show. + */ + public StringLengthValidator(String message, int StringLength) { + super(message); + this.StringLength = StringLength + 1; + } + + public void changeStringLength(int newLength) { + this.StringLength = newLength + 1; + } + + public int getStringLength() { + return StringLength - 1; + } + + /** + * {@inheritDoc} + */ + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = (TextInputControl) srcControl.get(); + String text = textField.getText(); + hasErrors.set(false); + + if (!text.isEmpty()) { + if (text.length() > StringLength - 1) { + hasErrors.set(true); + // textField.textProperty().set(text.substring(0, 19)); + + } + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java b/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java new file mode 100644 index 0000000000..4318ec955d --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation; + +import com.jfoenix.utils.JFXUtilities; +import com.jfoenix.validation.base.ValidatorBase; +import javafx.animation.Animation.Status; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +/** + * this class has been deprecated and will be removed in later versions of JFoenix, + * we are moving validations into each control that implements the interface + * {@Link IFXValidatableControl}. Validation will be applied through the control itself + * similar to {@link com.jfoenix.controls.JFXTextField}, it's straight forward and + * simpler than using the ValidationFacade. + */ +@Deprecated +public class ValidationFacade extends VBox { + + /** + * Initialize the style class to 'validation-facade'. + *

+ * This is the selector class from which CSS can be used to style + * this control. + */ + private static final String DEFAULT_STYLE_CLASS = "validation-facade"; + + private Label errorLabel; + private StackPane errorIcon; + private HBox errorContainer; + + private double oldErrorLabelHeight = -1; + private double initYlayout = -1; + private double initHeight = -1; + private boolean errorShown = false; + private double currentFieldHeight = -1; + private double errorLabelInitHeight = 0; + + private boolean heightChanged = false; + private boolean disableAnimation = false; + + private Timeline hideErrorAnimation; + + public ValidationFacade() { + getStyleClass().add(DEFAULT_STYLE_CLASS); + setPadding(new Insets(0, 0, 0, 0)); + setSpacing(0); + + errorLabel = new Label(); + errorLabel.getStyleClass().add("error-label"); + errorLabel.setWrapText(true); + + StackPane errorLabelContainer = new StackPane(); + errorLabelContainer.getChildren().add(errorLabel); + StackPane.setAlignment(errorLabel, Pos.CENTER_LEFT); + + errorIcon = new StackPane(); + errorContainer = new HBox(); + errorContainer.setAlignment(Pos.TOP_LEFT); + errorContainer.getChildren().add(errorLabelContainer); + errorContainer.getChildren().add(errorIcon); + + HBox.setHgrow(errorLabelContainer, Priority.ALWAYS); + errorLabelContainer.setMaxWidth(Double.MAX_VALUE); + + errorIcon.setTranslateY(5); + errorContainer.setSpacing(10); + errorContainer.setVisible(false); + errorContainer.setOpacity(0); + + // add listeners to show error label + errorLabel.heightProperty().addListener((o, oldVal, newVal) -> { + if (errorShown) { + if (oldErrorLabelHeight == -1) { + oldErrorLabelHeight = errorLabelInitHeight = oldVal.doubleValue(); + } + heightChanged = true; + double newHeight = getHeight() - oldErrorLabelHeight + newVal.doubleValue(); + // show the error + Timeline errorAnimation = new Timeline(new KeyFrame(Duration.ZERO, + new KeyValue(minHeightProperty(), + currentFieldHeight, + Interpolator.EASE_BOTH)), + new KeyFrame(Duration.millis(160), + // text pane animation + new KeyValue(translateYProperty(), + (initYlayout + getMaxHeight() / 2) - newHeight / 2, + Interpolator.EASE_BOTH), + // animate the height change effect + new KeyValue(minHeightProperty(), + newHeight, + Interpolator.EASE_BOTH))); + errorAnimation.play(); + // show the error label when finished + errorAnimation.setOnFinished(finish -> new Timeline(new KeyFrame(Duration.millis(160), + new KeyValue(errorContainer.opacityProperty(), + 1, + Interpolator.EASE_BOTH))).play()); + currentFieldHeight = newHeight; + oldErrorLabelHeight = newVal.doubleValue(); + } + }); + errorContainer.visibleProperty().addListener((o, oldVal, newVal) -> { + // show the error label if it's not shown + new Timeline(new KeyFrame(Duration.millis(160), + new KeyValue(errorContainer.opacityProperty(), + 1, + Interpolator.EASE_BOTH))).play(); + }); + + activeValidatorProperty().addListener((o, oldVal, newVal) -> { + if (!isDisableAnimation()) { + if (hideErrorAnimation != null && hideErrorAnimation.getStatus() == Status.RUNNING) { + hideErrorAnimation.stop(); + } + if (newVal != null) { + hideErrorAnimation = new Timeline(new KeyFrame(Duration.millis(160), + new KeyValue(errorContainer.opacityProperty(), + 0, + Interpolator.EASE_BOTH))); + hideErrorAnimation.setOnFinished(finish -> { + JFXUtilities.runInFX(() -> showError(newVal)); + }); + hideErrorAnimation.play(); + } else { + JFXUtilities.runInFX(this::hideError); + } + } else { + if (newVal != null) { + JFXUtilities.runInFXAndWait(() -> showError(newVal)); + } else { + JFXUtilities.runInFXAndWait(this::hideError); + } + } + }); + + } + + /*************************************************************************** + * * Properties * * + **************************************************************************/ + + /** + * holds the current active validator on the text field in case of + * validation error + */ + private ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); + + public ValidatorBase getActiveValidator() { + return activeValidator == null ? null : activeValidator.get(); + } + + public ReadOnlyObjectProperty activeValidatorProperty() { + return this.activeValidator.getReadOnlyProperty(); + } + + /** + * list of validators that will validate the text value upon calling { + * {@link #validate()} + */ + private ObservableList validators = FXCollections.observableArrayList(); + + public ObservableList getValidators() { + return validators; + } + + public void setValidators(ValidatorBase... validators) { + this.validators.addAll(validators); + } + + /** + * validates the text value using the list of validators provided by the + * user {{@link #setValidators(ValidatorBase...)} + * + * @return true if the value is valid else false + */ + public static boolean validate(Control control) { + ValidationFacade facade = (ValidationFacade) control.getParent(); + for (ValidatorBase validator : facade.validators) { + validator.setSrcControl(facade.controlProperty.get()); + validator.validate(); + if (validator.getHasErrors()) { + facade.activeValidator.set(validator); + control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, true); + return false; + } + } + control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, false); + facade.activeValidator.set(null); + return true; + } + + public static void reset(Control control) { + ValidationFacade facade = (ValidationFacade) control.getParent(); + control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, false); + facade.activeValidator.set(null); + } + + private ObjectProperty controlProperty = new SimpleObjectProperty<>(); + + public Control getControl() { + return controlProperty.get(); + } + + public void setControl(Control control) { + maxWidthProperty().bind(control.maxWidthProperty()); + prefWidthProperty().bind(control.prefWidthProperty()); + prefHeightProperty().bind(control.prefHeightProperty()); + + errorContainer.setMaxWidth(control.getMaxWidth() > -1 ? control.getMaxWidth() : control.getPrefWidth()); + errorContainer.prefWidthProperty().bind(control.widthProperty()); + errorContainer.prefHeightProperty().bind(control.heightProperty()); + + getChildren().clear(); + getChildren().add(control); + getChildren().add(errorContainer); + this.controlProperty.set(control); + } + + private void showError(ValidatorBase validator) { + + // set text in error label + errorLabel.setText(validator.getMessage()); + // show error icon + Node awsomeIcon = validator.getIcon(); + errorIcon.getChildren().clear(); + if (awsomeIcon != null) { + errorIcon.getChildren().add(awsomeIcon); + StackPane.setAlignment(awsomeIcon, Pos.TOP_RIGHT); + } + // init only once, to fix the text pane from resizing + if (initYlayout == -1) { + initYlayout = getBoundsInParent().getMinY(); + initHeight = getHeight(); + currentFieldHeight = initHeight; + } + errorContainer.setVisible(true); + + errorShown = true; + + } + + private void hideError() { + if (heightChanged) { + new Timeline(new KeyFrame(Duration.millis(160), + new KeyValue(translateYProperty(), 0, Interpolator.EASE_BOTH))).play(); + // reset the height of text field + new Timeline(new KeyFrame(Duration.millis(160), + new KeyValue(minHeightProperty(), initHeight, Interpolator.EASE_BOTH))).play(); + heightChanged = false; + } + // clear error label text + errorLabel.setText(null); + oldErrorLabelHeight = errorLabelInitHeight; + // clear error icon + errorIcon.getChildren().clear(); + // reset the height of the text field + currentFieldHeight = initHeight; + // hide error container + errorContainer.setVisible(false); + + errorShown = false; + } + + public boolean isDisableAnimation() { + return disableAnimation; + } + + public void setDisableAnimation(boolean disableAnimation) { + this.disableAnimation = disableAnimation; + } + + /** + * this style class will be activated when a validation error occurs + */ + private static final PseudoClass PSEUDO_CLASS_ERROR = PseudoClass.getPseudoClass("error"); + +} diff --git a/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java new file mode 100644 index 0000000000..9ed7ee7e2e --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.validation.base; + +import com.jfoenix.controls.JFXTooltip; +import com.jfoenix.validation.RegexValidator; +import com.jfoenix.validation.RequiredFieldValidator; +import javafx.beans.property.*; +import javafx.css.PseudoClass; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Tooltip; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +/** + * An abstract class that defines the basic validation functionality for a certain control. + * + * @author Shadi Shaheen + * @version 1.0 + * @since 2016-03-09 + */ +public abstract class ValidatorBase { + + /** + * This {@link PseudoClass} will be activated when a validation error occurs. + *

+ * Some components have default styling for this pseudo class. See {@code jfx-text-field.css} + * and {@code jfx-combo-box.css} for examples. + */ + public static final PseudoClass PSEUDO_CLASS_ERROR = PseudoClass.getPseudoClass("error"); + + /** + * When using {@code Tooltip.install(node, tooltip)}, the given tooltip is stored in the Node's properties + * under this key. + * + * @see Tooltip#install(Node, Tooltip) + */ + private static final String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip"; + + /** + * Default error tooltip style class + */ + public static final String ERROR_TOOLTIP_STYLE_CLASS = "error-tooltip"; + + /** + * Key used to stash control tooltip upon validation + */ + private static final String TEMP_TOOLTIP_KEY = "stashed-tootlip"; + + /** + * supported tooltips keys + */ + private static final Set supportedTooltipKeys = new HashSet<>( + Arrays.asList( + "javafx.scene.control.Tooltip", + "jfoenix-tooltip" + ) + ); + + /** + * @param message will be set as the validator's {@link #message}. + * @see #ValidatorBase() + */ + public ValidatorBase(String message) { + this(); + this.setMessage(message); + } + + /** + * When creating a new validator you need to define the validation condition by implementing {@link #eval()}. + *

+ * For examples of how you might implement it, see {@link RequiredFieldValidator} and + * {@link RegexValidator}. + */ + public ValidatorBase() { + + } + + /////////////////////////////////////////////////////////////////////////// + // Methods + /////////////////////////////////////////////////////////////////////////// + + /** + * Will validate the source control. + *

+ * Calls {@link #eval()} and then {@link #onEval()}. + */ + public void validate() { + eval(); + onEval(); + } + + /** + * Should evaluate the validation condition and set {@link #hasErrors} to true or false. It should + * be true when the value is invalid (it has errors) and false when the value is valid (no errors). + *

+ * This method is fired once {@link #validate()} is called. + */ + protected abstract void eval(); + + /** + * This method will update the source control after evaluating the validation condition (see {@link #eval()}). + *

+ * If the validator isn't "passing" the {@link #PSEUDO_CLASS_ERROR :error} pseudoclass is applied to the + * {@link #srcControl}. + *

+ * Applies the {@link #PSEUDO_CLASS_ERROR :error} pseudo class and the errorTooltip to + * the {@link #srcControl}. + */ + protected void onEval() { + Node control = getSrcControl(); + boolean invalid = hasErrors.get(); + control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, invalid); + Tooltip activeTooltip = getActiveTooltip(control); + if (invalid) { + Tooltip errorTooltip = errorTooltipSupplier.get(); + errorTooltip.getStyleClass().add(ERROR_TOOLTIP_STYLE_CLASS); + errorTooltip.setText(getMessage()); + install(control, activeTooltip, errorTooltip); + } else { + Tooltip orgTooltip = (Tooltip) control.getProperties().remove(TEMP_TOOLTIP_KEY); + install(control, activeTooltip, orgTooltip); + } + } + + private final Tooltip getActiveTooltip(Node node) { + Tooltip tooltip = null; + for (String key : supportedTooltipKeys) { + tooltip = (Tooltip) node.getProperties().get(key); + if (tooltip != null) { + break; + } + } + return tooltip; + } + + private void install(Node node, Tooltip oldVal, Tooltip newVal) { + // stash old tooltip if it's not error tooltip + if (oldVal != null && !oldVal.getStyleClass().contains(ERROR_TOOLTIP_STYLE_CLASS)) { + node.getProperties().put(TEMP_TOOLTIP_KEY, oldVal); + } + if (node instanceof Control) { + // uninstall + if (oldVal != null) { + if (oldVal instanceof JFXTooltip) { + JFXTooltip.uninstall(node); + } + if (!(newVal instanceof JFXTooltip)) { + ((Control) node).setTooltip(newVal); + return; + } + if (newVal instanceof JFXTooltip) { + ((Control) node).setTooltip(null); + } + } + // install + if (newVal instanceof JFXTooltip) { + install(node, newVal); + } else { + ((Control) node).setTooltip(newVal); + } + } else { + uninstall(node, oldVal); + install(node, newVal); + } + } + + private void uninstall(Node node, Tooltip tooltip) { + if (tooltip instanceof JFXTooltip) { + JFXTooltip.uninstall(node); + } else { + Tooltip.uninstall(node, tooltip); + } + } + + private void install(Node node, Tooltip tooltip) { + if (tooltip == null) { + return; + } + if (tooltip instanceof JFXTooltip) { + JFXTooltip.install(node, (JFXTooltip) tooltip); + } else { + Tooltip.install(node, tooltip); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Properties + /////////////////////////////////////////////////////////////////////////// + + /** + * The {@link Control}/{@link Node} that the validator is checking the value of. + *

+ * Supports {@link Node}s because not all things that need validating are {@link Control}s. + */ + protected SimpleObjectProperty srcControl = new SimpleObjectProperty<>(); + + /** + * @see #srcControl + */ + public void setSrcControl(Node srcControl) { + this.srcControl.set(srcControl); + } + + /** + * @see #srcControl + */ + public Node getSrcControl() { + return this.srcControl.get(); + } + + /** + * @see #srcControl + */ + public ObjectProperty srcControlProperty() { + return this.srcControl; + } + + /** + * Tells whether the validator is "passing" or not. + *

+ * In a validator's implementation of {@link #eval()}, if the value the validator is checking is invalid, it should + * set this to true. If the value is valid, it should set this to false. + *

+ * When hasErrors is true, the validator will automatically apply the {@link #PSEUDO_CLASS_ERROR :error} + * pseudoclass to the {@link #srcControl}; the {@link #srcControl} will also have a {@link Tooltip} containing the + * {@link #message} applied to it. + */ + protected ReadOnlyBooleanWrapper hasErrors = new ReadOnlyBooleanWrapper(false); + + /** + * @see #hasErrors + */ + public boolean getHasErrors() { + return hasErrors.get(); + } + + /** + * @see #hasErrors + */ + public ReadOnlyBooleanProperty hasErrorsProperty() { + return hasErrors.getReadOnlyProperty(); + } + + private Supplier errorTooltipSupplier = () -> new Tooltip(); + + public Supplier getErrorTooltipSupplier() { + return errorTooltipSupplier; + } + + public void setErrorTooltipSupplier(Supplier errorTooltipSupplier) { + this.errorTooltipSupplier = errorTooltipSupplier; + } + + /** + * The error message to display when the validator is not "passing." + *

+ * When {@link #hasErrors} is true, this message is displayed near the {@link #srcControl} (usually below); + * it's also displayed in a {@link Tooltip} applied to the {@link #srcControl}. + */ + protected SimpleStringProperty message = new SimpleStringProperty(); + + /** + * @see #message + */ + public void setMessage(String msg) { + this.message.set(msg); + } + + /** + * @see #message + */ + public String getMessage() { + return this.message.get(); + } + + /** + * @see #message + */ + public StringProperty messageProperty() { + return this.message; + } + + + /***** Icon *****/ + protected SimpleObjectProperty> iconSupplier = new SimpleObjectProperty>(); + + public void setIconSupplier(Supplier icon) { + this.iconSupplier.set(icon); + } + + public SimpleObjectProperty> iconSupplierProperty() { + return this.iconSupplier; + } + + public Supplier getIconSupplier() { + return iconSupplier.get(); + } + + /** + * allow setting icon in FXML + */ + public void setIcon(Node icon) { + iconSupplier.set(() -> icon); + } + + public Node getIcon() { + if (iconSupplier.get() == null) { + return null; + } + Node icon = iconSupplier.get().get(); + if (icon != null) { + icon.getStyleClass().add("error-icon"); + } + return icon; + } +} From 043ac40e4c293a0e8dc4d9a37ddd775bb1ded3af Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 18 Feb 2026 22:40:38 +0800 Subject: [PATCH 09/16] update --- .../validation/base/ValidatorBase.java | 18 +- HMCL/src/main/resources/assets/css/root.css | 267 +++++++++++++----- 2 files changed, 216 insertions(+), 69 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java index 9ed7ee7e2e..ca30158de5 100644 --- a/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java +++ b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java @@ -23,6 +23,8 @@ import com.jfoenix.validation.RegexValidator; import com.jfoenix.validation.RequiredFieldValidator; import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; import javafx.css.PseudoClass; import javafx.scene.Node; import javafx.scene.control.Control; @@ -72,10 +74,10 @@ public abstract class ValidatorBase { * supported tooltips keys */ private static final Set supportedTooltipKeys = new HashSet<>( - Arrays.asList( - "javafx.scene.control.Tooltip", - "jfoenix-tooltip" - ) + Arrays.asList( + "javafx.scene.control.Tooltip", + "jfoenix-tooltip" + ) ); /** @@ -205,6 +207,14 @@ private void install(Node node, Tooltip tooltip) { } } + private ObservableMap properties; + + public ObservableMap getProperties() { + if (properties == null) properties = FXCollections.observableHashMap(); + return properties; + + } + /////////////////////////////////////////////////////////////////////////// // Properties /////////////////////////////////////////////////////////////////////////// diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 7bcdb98109..f6be851109 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -923,23 +923,49 @@ /******************************************************************************* * * -* JFX Textfield * +* JFX Text Field * * * *******************************************************************************/ -.jfx-text-field, .jfx-password-field, .jfx-text-area { - -fx-background-color: -monet-surface-container-highest; - -fx-highlight-fill: -monet-primary; - -fx-text-fill: -monet-on-surface; - -fx-font-weight: BOLD; - -fx-prompt-text-fill: -monet-on-surface-variant; - -fx-alignment: top-left; - -fx-max-width: 1000000000; +.jfx-text-field { -jfx-focus-color: -monet-primary; - -fx-padding: 8; - -jfx-unfocus-color: transparent; + -jfx-unfocus-color: #4d4d4d; + -fx-padding: 0.333333em 0 0.333333em 0; +} + +.jfx-text-field > .input-line { + -fx-background-color: -jfx-unfocus-color; + -fx-pref-height: 1px; + -fx-translate-y: 1px; +} + +.jfx-text-field > .input-focused-line { + -fx-background-color: -jfx-focus-color; + -fx-pref-height: 2px; +} + +.jfx-text-field, +.jfx-text-field:focused { + -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT; + -fx-prompt-text-fill: -monet-on-surface-variant; +} + +.jfx-text-field:error { + -jfx-focus-color: -monet-error; + -jfx-unfocus-color: -monet-error; } +.jfx-text-field .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-text-field .error-icon { + -fx-fill: -monet-error; +} + +/* ??? */ + .jfx-text-field .context-menu { -fx-background-color: -monet-surface-container; -fx-text-fill: -monet-on-surface; @@ -950,6 +976,133 @@ -fx-text-fill: -monet-on-secondary-container; } +/* -------------- STYLES FOR THE DEFAULT JFXTEXTFIELD-BASED COMBOBOX ------------- */ + +.combo-box-base:editable > .jfx-text-field, +.date-picker > .jfx-text-field, +.jfx-date-picker > .jfx-text-field, +.combo-box-base:editable:focused > .text-field, +.combo-box-base:editable > .text-field:focused, +.date-picker:focused > .text-field, +.date-picker > .text-field:focused { + -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT; +} + +.jfx-date-picker > .jfx-text-field, +.jfx-time-picker > .jfx-text-field{ + -jfx-focus-color: -jfx-default-color; +} + +.combo-box-base:error > .jfx-text-field, +.combo-box-base:error > .jfx-text-field { + -jfx-focus-color: -monet-error; + -jfx-unfocus-color: -monet-error; +} + +/******************************************************************************* +* * +* JFX Password Field * +* * +*******************************************************************************/ + +.jfx-password-field { + -jfx-focus-color: #4059A9; + -jfx-unfocus-color: #4d4d4d; + -fx-padding: 0.333333em 0 0.333333em 0; +} + +.jfx-password-field > .input-line { + -fx-background-color: -jfx-unfocus-color; + -fx-pref-height: 1px; + -fx-translate-y: 1px; +} + +.jfx-password-field > .input-focused-line{ + -fx-background-color: -jfx-focus-color; + -fx-pref-height: 2px; +} + +.jfx-password-field > .input-focused-line{ + -fx-pref-height: 2px; + -fx-background-color: -jfx-focus-color; +} + +.jfx-password-field, +.jfx-password-field:focused { + -fx-prompt-text-fill: -monet-on-surface-variant; + -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT; +} + +.jfx-password-field:error { + -jfx-focus-color: -monet-error; + -jfx-unfocus-color: -monet-error; +} + +.jfx-password-field .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-password-field .error-icon { + -fx-fill: -monet-error; +} + +/******************************************************************************* +* * +* JFX TextArea * +* * +*******************************************************************************/ + +.jfx-text-area { + -jfx-focus-color: -monet-primary; + -jfx-unfocus-color: -monet-surface-container-highest; + -fx-padding: 0.333333em 0 0 0 ; +} + +.jfx-text-area > .input-line { + -fx-background-color: -jfx-unfocus-color; + -fx-pref-height: 1px; + -fx-translate-y: 1px; +} + +.jfx-text-area > .input-focused-line { + -fx-background-color: -jfx-focus-color; + -fx-pref-height: 2px; +} + +.jfx-text-area, +.jfx-text-area:focused { + -fx-prompt-text-fill: -monet-on-surface-variant; +} + +.jfx-text-area, +.jfx-text-area .scroll-pane, +.jfx-text-area:focused, +.jfx-text-area:focused .scroll-pane{ + -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT; +} + +.jfx-text-area .content, +.jfx-text-area:focused .content{ + -fx-background-color:TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT; + -fx-padding: 0 0 0 0; +} + +.jfx-text-area:error { + -jfx-focus-color: -monet-error; + -jfx-unfocus-color: -monet-error; +} + +.jfx-text-area .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-text-area .error-icon { + -fx-fill: -monet-error; +} + + /******************************************************************************* * * * JFX List View * @@ -1528,49 +1681,59 @@ *******************************************************************************/ .jfx-combo-box { - -jfx-focus-color: transparent; - -jfx-unfocus-color: transparent; - -fx-background-color: -monet-surface-container-highest; - -fx-padding: 4; - -fx-max-width: 1000000000; + -jfx-focus-color: -monet-primary; + -jfx-unfocus-color: -monet-surface-container-highest; + -fx-prompt-text-fill: -monet-on-surface-variant; } -.jfx-combo-box .text { - -fx-fill: -monet-on-surface; +.jfx-combo-box > .input-line { + -fx-background-color: -jfx-unfocus-color; + -fx-pref-height: 1px; + -fx-translate-y: 1px; } -.jfx-combo-box .text-field { - -fx-text-fill: -monet-on-surface; +.jfx-combo-box > .input-focused-line { + -fx-background-color: -jfx-focus-color; + -fx-pref-height: 2px; } -.combo-box-popup .list-view { - -fx-background-color: -monet-surface-container; +.jfx-combo-box > .prompt-container { + -fx-alignment: center-left; } -.combo-box-popup .list-view .jfx-list-cell { - -fx-background-color: -monet-surface-container; - -fx-text-fill: -monet-on-surface; - -fx-background-insets: 0.0; +.jfx-combo-box, +.jfx-combo-box:focused, +.jfx-combo-box:editable, +.jfx-combo-box:editable:focused { + -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT; + -fx-background-radius: 3px; + -fx-background-insets: 0px; } -.combo-box-popup .list-view .list-cell:odd:selected > .jfx-rippler > StackPane, -.combo-box-popup .list-view .list-cell:even:selected > .jfx-rippler > StackPane { - -fx-background-color: -monet-secondary-container; - -fx-text-fill: -monet-on-secondary-container; +.jfx-combo-box .combo-box-button-container { + -fx-background-color: TRANSPARENT; +} + +.jfx-combo-box > .arrow-button, +.jfx-combo-box:editable > .arrow-button, +.jfx-combo-box:editable:focused > .arrow-button { + -fx-background-color: TRANSPARENT; } -.jfx-combo-box-warning { - -jfx-focus-color: #D34336; - -jfx-unfocus-color: #D34336; +.jfx-combo-box:error { + -jfx-focus-color: -monet-error; + -jfx-unfocus-color: -monet-error; } -.jfx-combo-box-warning .text { - -fx-fill: #D34336; +.jfx-combo-box .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-combo-box .error-icon { + -fx-fill: -monet-error; } -/*.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {*/ -/* -jfx-rippler-fill: -monet-primary-container;*/ -/*}*/ /******************************************************************************* * * @@ -1837,36 +2000,10 @@ /******************************************************************************* * * -* Error Facade * +* Error Facade (???) * * * *******************************************************************************/ -.error-label { - -fx-text-fill: -monet-error; - -fx-font-size: 0.75em; - -fx-font-weight: bold; -} - -.error-icon { - -fx-fill: -monet-error; - -fx-font-size: 1.0em; -} - -.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error, .jfx-combo-box:error { - -jfx-focus-color: -monet-error; - -jfx-unfocus-color: -monet-error; -} - -.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label { - -fx-text-fill: -monet-error; - -fx-font-size: 0.75em; -} - -.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon { - -fx-fill: -monet-error; - -fx-font-size: 1.0em; -} - .fit-width { -fx-pref-width: 100%; } From 805280fdd8fdb4b2658d37edca10dd0d47580938 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 20:49:03 +0800 Subject: [PATCH 10/16] update --- .../src/main/java/com/jfoenix/controls/JFXTextField.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java index 9bc0f75f6d..d254972d90 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java @@ -223,7 +223,7 @@ public final void setDisableAnimation(final Boolean disabled) { private static class StyleableProperties { - private static final CssMetaData UNFOCUS_COLOR = new CssMetaData( + private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) { @@ -237,7 +237,7 @@ public StyleableProperty getStyleableProperty(JFXTextField control) { return control.unFocusColorProperty(); } }; - private static final CssMetaData FOCUS_COLOR = new CssMetaData( + private static final CssMetaData FOCUS_COLOR = new CssMetaData<>( "-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) { @@ -251,7 +251,7 @@ public StyleableProperty getStyleableProperty(JFXTextField control) { return control.focusColorProperty(); } }; - private static final CssMetaData LABEL_FLOAT = new CssMetaData( + private static final CssMetaData LABEL_FLOAT = new CssMetaData<>( "-jfx-label-float", BooleanConverter.getInstance(), false) { @@ -267,7 +267,7 @@ public StyleableBooleanProperty getStyleableProperty(JFXTextField control) { }; private static final CssMetaData DISABLE_ANIMATION = - new CssMetaData("-jfx-disable-animation", + new CssMetaData<>("-jfx-disable-animation", BooleanConverter.getInstance(), false) { @Override public boolean isSettable(JFXTextField control) { @@ -280,7 +280,6 @@ public StyleableBooleanProperty getStyleableProperty(JFXTextField control) { } }; - private static final List> CHILD_STYLEABLES; static { From 1ae19c5c523d730b7ac18782d607fbd48d4add07 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 20:52:35 +0800 Subject: [PATCH 11/16] update --- HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java | 5 ++--- .../main/java/com/jfoenix/controls/JFXPasswordField.java | 5 ++--- HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java | 7 +++++-- HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java | 7 +++---- .../java/com/jfoenix/controls/base/IFXStaticControl.java | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index 4685989a10..d76d374331 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -336,16 +336,15 @@ public final StyleableBooleanProperty disableAnimationProperty() { } @Override - public final Boolean isDisableAnimation() { + public final boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } @Override - public final void setDisableAnimation(final Boolean disabled) { + public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - private static class StyleableProperties { private static final CssMetaData, Paint> UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java index 966d9a56a0..bf0230c999 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java @@ -204,16 +204,15 @@ public final StyleableBooleanProperty disableAnimationProperty() { } @Override - public final Boolean isDisableAnimation() { + public final boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } @Override - public final void setDisableAnimation(final Boolean disabled) { + public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - private static class StyleableProperties { private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java index 901aabc77f..4c26d71a78 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java @@ -176,15 +176,18 @@ public void setFocusColor(Paint color) { "disableAnimation", false); + @Override public final StyleableBooleanProperty disableAnimationProperty() { return this.disableAnimation; } - public final Boolean isDisableAnimation() { + @Override + public final boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } - public final void setDisableAnimation(final Boolean disabled) { + @Override + public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java index d254972d90..56dd02bf4b 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextField.java @@ -212,17 +212,16 @@ public final StyleableBooleanProperty disableAnimationProperty() { } @Override - public final Boolean isDisableAnimation() { + public final boolean isDisableAnimation() { return disableAnimation != null && this.disableAnimationProperty().get(); } @Override - public final void setDisableAnimation(final Boolean disabled) { + public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - - private static class StyleableProperties { + private static final class StyleableProperties { private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), diff --git a/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java b/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java index 1ea098807b..06cf07af3f 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java +++ b/HMCL/src/main/java/com/jfoenix/controls/base/IFXStaticControl.java @@ -25,7 +25,7 @@ public interface IFXStaticControl { StyleableBooleanProperty disableAnimationProperty(); - Boolean isDisableAnimation(); + boolean isDisableAnimation(); - void setDisableAnimation(Boolean disabled); + void setDisableAnimation(boolean disabled); } From 261d629f3e711808b2810149ca65da05c2795116 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 20:54:58 +0800 Subject: [PATCH 12/16] update --- .../com/jfoenix/controls/JFXComboBox.java | 4 +- .../converters/base/NodeConverter.java | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/converters/base/NodeConverter.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index d76d374331..6fc6435bc7 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -138,7 +138,7 @@ public ObjectProperty> nodeConverterProperty() { } private ObjectProperty> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter", - JFXComboBox.defaultNodeConverter()); + JFXComboBox.defaultNodeConverter()); public final void setNodeConverter(NodeConverter value) { nodeConverterProperty().set(value); @@ -149,7 +149,7 @@ public final NodeConverter getNodeConverter() { } private static NodeConverter defaultNodeConverter() { - return new NodeConverter() { + return new NodeConverter<>() { @Override public Node toNode(T object) { if (object == null) { diff --git a/HMCL/src/main/java/com/jfoenix/converters/base/NodeConverter.java b/HMCL/src/main/java/com/jfoenix/converters/base/NodeConverter.java new file mode 100644 index 0000000000..721d53ee84 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/converters/base/NodeConverter.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.converters.base; + +import javafx.scene.Node; + +/// Converter defines conversion behavior between Nodes and Objects. +/// The type of Objects are defined by the subclasses of Converter. +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public abstract class NodeConverter { + /// Converts the object provided into its node form. + /// Styling of the returned node is defined by the specific converter. + /// + /// @return a node representation of the object passed in. + public abstract Node toNode(T object); + + /// Converts the node provided into an object defined by the specific converter. + /// Format of the node and type of the resulting object is defined by the specific converter. + /// + /// @return an object representation of the node passed in. + public abstract T fromNode(Node node); + + /// Converts the object provided into a String defined by the specific converter. + /// Format of the String is defined by the specific converter. + /// + /// @return a String representation of the node passed in. + public abstract String toString(T object); +} From 4f613b5a7ac4a2f7ee4d987488d941d70eed1cfb Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 20:58:42 +0800 Subject: [PATCH 13/16] update --- .../com/jfoenix/controls/JFXComboBox.java | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index 6fc6435bc7..5b14b091ee 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -49,25 +49,17 @@ import java.util.Collections; import java.util.List; -/** - * JFXComboBox is the material design implementation of a combobox. - * - * @author Shadi Shaheen - * @version 1.0 - * @since 2016-03-09 - */ +/// JFXComboBox is the material design implementation of a combobox. +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 public class JFXComboBox extends ComboBox implements IFXLabelFloatControl { - /** - * {@inheritDoc} - */ public JFXComboBox() { initialize(); } - /** - * {@inheritDoc} - */ public JFXComboBox(ObservableList items) { super(items); initialize(); @@ -128,62 +120,68 @@ protected Skin createDefaultSkin() { * Node Converter Property * * * **************************************************************************/ - /** - * Converts the user-typed input (when the ComboBox is - * {@link #editableProperty() editable}) to an object of type T, such that - * the input may be retrieved via the {@link #valueProperty() value} property. - */ + + private static final class DefaultNodeConverter extends NodeConverter { + private static final DefaultNodeConverter INSTANCE = new DefaultNodeConverter<>(); + + @SuppressWarnings("unchecked") + static DefaultNodeConverter getInstance() { + return (DefaultNodeConverter) INSTANCE; + } + + @Override + public Node toNode(T object) { + if (object == null) { + return null; + } + StackPane selectedValueContainer = new StackPane(); + selectedValueContainer.getStyleClass().add("combo-box-selected-value-container"); + selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null))); + Label selectedValueLabel = object instanceof Label label + ? new Label(label.getText()) + : new Label(object.toString()); + selectedValueLabel.setTextFill(Color.BLACK); + selectedValueContainer.getChildren().add(selectedValueLabel); + StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT); + StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5)); + return selectedValueContainer; + } + + @SuppressWarnings("unchecked") + @Override + public T fromNode(Node node) { + return (T) node; + } + + @Override + public String toString(T object) { + if (object == null) { + return null; + } + if (object instanceof Label label) { + return label.getText(); + } + return object.toString(); + } + } + + private ObjectProperty> nodeConverter; + + /// Converts the user-typed input (when the ComboBox is + /// [editable][#editableProperty()]) to an object of type T, such that + /// the input may be retrieved via the [value][#valueProperty()] property. public ObjectProperty> nodeConverterProperty() { + if (nodeConverter == null) + nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter", DefaultNodeConverter.getInstance()); return nodeConverter; } - private ObjectProperty> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter", - JFXComboBox.defaultNodeConverter()); - public final void setNodeConverter(NodeConverter value) { nodeConverterProperty().set(value); } public final NodeConverter getNodeConverter() { - return nodeConverterProperty().get(); - } - - private static NodeConverter defaultNodeConverter() { - return new NodeConverter<>() { - @Override - public Node toNode(T object) { - if (object == null) { - return null; - } - StackPane selectedValueContainer = new StackPane(); - selectedValueContainer.getStyleClass().add("combo-box-selected-value-container"); - selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null))); - Label selectedValueLabel = object instanceof Label ? new Label(((Label) object).getText()) : new Label( - object.toString()); - selectedValueLabel.setTextFill(Color.BLACK); - selectedValueContainer.getChildren().add(selectedValueLabel); - StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT); - StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5)); - return selectedValueContainer; - } - - @SuppressWarnings("unchecked") - @Override - public T fromNode(Node node) { - return (T) node; - } - - @Override - public String toString(T object) { - if (object == null) { - return null; - } - if (object instanceof Label) { - return ((Label) object).getText(); - } - return object.toString(); - } - }; + return nodeConverter != null ? nodeConverter.get() : DefaultNodeConverter.getInstance(); } private boolean updateDisplayText(ListCell cell, T item, boolean empty) { @@ -321,10 +319,7 @@ public void setFocusColor(Paint color) { this.focusColor.set(color); } - - /** - * disable animation on validation - */ + /// disable animation on validation private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXComboBox.this, "disableAnimation", From aefd9ce9a51d6394f809a2c9d5b32c4cda55b89b Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 21:13:06 +0800 Subject: [PATCH 14/16] update --- HMCL/src/main/resources/assets/css/root.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index acd56d29a9..a831a663c2 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1019,8 +1019,8 @@ *******************************************************************************/ .jfx-password-field { - -jfx-focus-color: #4059A9; - -jfx-unfocus-color: #4d4d4d; + -jfx-focus-color: -monet-primary; + -jfx-unfocus-color: -monet-on-surface; -fx-padding: 0.333333em 0 0.333333em 0; } From 11386866fd9ca63da64e47a37381f1eea3c88213 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 21:22:55 +0800 Subject: [PATCH 15/16] update --- HMCL/src/main/resources/assets/css/root.css | 57 ++++++++++----------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index a831a663c2..fe98abecaf 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -968,15 +968,6 @@ -jfx-unfocus-color: -monet-error; } -.jfx-text-field .error-label { - -fx-text-fill: -monet-error; - -fx-font-size: 0.75em; -} - -.jfx-text-field .error-icon { - -fx-fill: -monet-error; -} - /* ??? */ .jfx-text-field .context-menu { @@ -1051,15 +1042,6 @@ -jfx-unfocus-color: -monet-error; } -.jfx-password-field .error-label { - -fx-text-fill: -monet-error; - -fx-font-size: 0.75em; -} - -.jfx-password-field .error-icon { - -fx-fill: -monet-error; -} - /******************************************************************************* * * * JFX TextArea * @@ -1106,16 +1088,6 @@ -jfx-unfocus-color: -monet-error; } -.jfx-text-area .error-label { - -fx-text-fill: -monet-error; - -fx-font-size: 0.75em; -} - -.jfx-text-area .error-icon { - -fx-fill: -monet-error; -} - - /******************************************************************************* * * * JFX List View * @@ -2011,10 +1983,37 @@ /******************************************************************************* * * -* Error Facade (???) * +* Error Facade * * * *******************************************************************************/ +.jfx-text-field .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-text-field .error-icon { + -fx-fill: -monet-error; +} + +.jfx-password-field .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-password-field .error-icon { + -fx-fill: -monet-error; +} + +.jfx-text-area .error-label { + -fx-text-fill: -monet-error; + -fx-font-size: 0.75em; +} + +.jfx-text-area .error-icon { + -fx-fill: -monet-error; +} + .fit-width { -fx-pref-width: 100%; } From 16765cb95f4c5abdc8441ec3cf390d0bfa7b9429 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 21:31:18 +0800 Subject: [PATCH 16/16] update --- .../com/jfoenix/controls/JFXComboBox.java | 2 +- .../jfoenix/controls/JFXPasswordField.java | 2 +- .../com/jfoenix/controls/JFXTextArea.java | 2 +- .../java/com/jfoenix/controls/JFXTooltip.java | 12 +- .../skins/JFXComboBoxListViewSkin.java | 3 +- .../com/jfoenix/skins/JFXTextAreaSkin.java | 2 - .../com/jfoenix/skins/JFXTextFieldSkin.java | 1 - .../com/jfoenix/skins/PromptLinesWrapper.java | 2 +- .../com/jfoenix/skins/ValidationPane.java | 1 - .../java/com/jfoenix/utils/JFXUtilities.java | 7 +- .../jfoenix/validation/NumberValidator.java | 5 +- .../validation/StringLengthValidator.java | 47 +-- .../jfoenix/validation/ValidationFacade.java | 315 ------------------ .../validation/base/ValidatorBase.java | 2 +- 14 files changed, 35 insertions(+), 368 deletions(-) delete mode 100644 HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java index 5b14b091ee..bc8c0bdebd 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXComboBox.java @@ -340,7 +340,7 @@ public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - private static class StyleableProperties { + private static final class StyleableProperties { private static final CssMetaData, Paint> UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java index bf0230c999..7d9e2c5747 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXPasswordField.java @@ -213,7 +213,7 @@ public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - private static class StyleableProperties { + private static final class StyleableProperties { private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java index 4c26d71a78..3b90a0c817 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTextArea.java @@ -191,7 +191,7 @@ public final void setDisableAnimation(final boolean disabled) { this.disableAnimationProperty().set(disabled); } - private static class StyleableProperties { + private static final class StyleableProperties { private static final CssMetaData UNFOCUS_COLOR = new CssMetaData<>( "-jfx-unfocus-color", PaintConverter.getInstance(), diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java b/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java index da9cf18ccb..189283c51c 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXTooltip.java @@ -297,7 +297,6 @@ private double ownerY(Node ownerNode, Bounds sceneBounds) { return parent.getY() + sceneBounds.getMinY() + ownerNode.getScene().getY(); } - public void showOnAnchors(Node ownerNode, double anchorX, double anchorY) { hiding = false; final Bounds sceneBounds = ownerNode.localToScene(ownerNode.getBoundsInLocal()); @@ -364,12 +363,11 @@ public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { return super.buildEventDispatchChain(tail).prepend(eventHandlerManager); } - private static class TooltipBehavior { - - private static String TOOLTIP_PROP = "jfoenix-tooltip"; - private Timeline hoverTimer = new Timeline(); - private Timeline visibleTimer = new Timeline(); - private Timeline leftTimer = new Timeline(); + private static final class TooltipBehavior { + private static final String TOOLTIP_PROP = "jfoenix-tooltip"; + private final Timeline hoverTimer = new Timeline(); + private final Timeline visibleTimer = new Timeline(); + private final Timeline leftTimer = new Timeline(); /** * the currently hovered node */ diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java index e3957530c0..30b3044932 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXComboBoxListViewSkin.java @@ -167,7 +167,7 @@ private void createPromptNode() { } } - private static class StyleableProperties { + private static final class StyleableProperties { private static final CssMetaData, Paint> PROMPT_TEXT_FILL = new CssMetaData<>("-fx-prompt-text-fill", PaintConverter.getInstance(), Color.GRAY) { @@ -196,7 +196,6 @@ public StyleableProperty getStyleableProperty(JFXComboBox n) { } } - /// @return The CssMetaData associated with this class, which may include the /// CssMetaData of its super classes. public static List> getClassCssMetaData() { diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java index 516b37a01d..d8f18dd300 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXTextAreaSkin.java @@ -33,7 +33,6 @@ import javafx.scene.text.Text; import java.lang.reflect.Field; -import java.util.Arrays; import java.util.List; /// # Material Design TextArea Skin @@ -75,7 +74,6 @@ public JFXTextAreaSkin(JFXTextArea textArea) { } - @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { super.layoutChildren(x, y, w, h); diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java index afb4e5db6a..e685ad88b1 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXTextFieldSkin.java @@ -102,7 +102,6 @@ protected void layoutChildren(final double x, final double y, final double w, fi } } - private void updateTextPos() { double textWidth = textNode.getLayoutBounds().getWidth(); final double promptWidth = promptText == null ? 0 : promptText.getLayoutBounds().getWidth(); diff --git a/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java b/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java index ed0cc94c88..e0d7555071 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java +++ b/HMCL/src/main/java/com/jfoenix/skins/PromptLinesWrapper.java @@ -212,7 +212,7 @@ private Object getControlValue() { } private Object validateComboBox(Object text) { - if (control instanceof ComboBox comboBox&& comboBox.isEditable()) { + if (control instanceof ComboBox comboBox && comboBox.isEditable()) { final String editorText = comboBox.getEditor().getText(); text = editorText == null || editorText.isEmpty() ? null : text; } diff --git a/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java b/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java index b3ae0cbb38..6891593cab 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java +++ b/HMCL/src/main/java/com/jfoenix/skins/ValidationPane.java @@ -215,7 +215,6 @@ private KeyFrame createScaleToOneFrames() { KeyValue(errorClipScale.yProperty(), 1, Interpolator.EASE_BOTH)); } - private void showError(ValidatorBase validator) { // set text in error label errorLabel.setText(validator.getMessage()); diff --git a/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java b/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java index b6ddc461f5..da89198b4d 100644 --- a/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java +++ b/HMCL/src/main/java/com/jfoenix/utils/JFXUtilities.java @@ -24,15 +24,13 @@ import java.util.concurrent.CountDownLatch; import java.util.function.Function; - /// # JavaFX FX Thread utilities /// JFXUtilities allow sync mechanism to the FX thread /// -/// /// @author pmoufarrej /// @version 1.0 /// @since 2016-03-09 -public class JFXUtilities { +public final class JFXUtilities { /// This method is used to run a specified Runnable in the FX Application thread, /// it returns before the task finished execution @@ -78,4 +76,7 @@ public static T[] concat(T[] a, T[] b, Function supplier) { System.arraycopy(b, 0, array, aLen, bLen); return array; } + + private JFXUtilities() { + } } diff --git a/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java b/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java index 4009086d26..3be7fd67c8 100644 --- a/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java +++ b/HMCL/src/main/java/com/jfoenix/validation/NumberValidator.java @@ -41,7 +41,7 @@ @DefaultProperty(value = "icon") public class NumberValidator extends ValidatorBase { - private NumberStringConverter numberStringConverter = new NumberStringConverter(){ + private NumberStringConverter numberStringConverter = new NumberStringConverter() { @Override public Number fromString(String string) { try { @@ -67,7 +67,8 @@ public Number fromString(String string) { } }; - public NumberValidator() { } + public NumberValidator() { + } public NumberValidator(String message) { super(message); diff --git a/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java b/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java index ec07b9bfd7..0c42a51cbc 100644 --- a/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java +++ b/HMCL/src/main/java/com/jfoenix/validation/StringLengthValidator.java @@ -22,46 +22,37 @@ import com.jfoenix.validation.base.ValidatorBase; import javafx.scene.control.TextInputControl; -/** - * @author Victor Espino - * @version 1.0 - * @since 2019-08-10 - */ +/// @author Victor Espino +/// @version 1.0 +/// @since 2019-08-10 public class StringLengthValidator extends ValidatorBase { int StringLength; - /** - * Basic constructor with Default message this way: - * "Max length is " + StringLength +" character(s) " - * - * @param StringLengh Length of the string in the input field to validate. - */ + /// Basic constructor with Default message this way: + /// "Max length is " + StringLength +" character(s) " + /// + /// @param StringLengh Length of the string in the input field to validate. public StringLengthValidator(int StringLengh) { super("Max length is " + StringLengh + " character(s) "); this.StringLength = StringLengh + 1; } - - /** - * The displayed message shown will be concatenated by the message with StringLength - * this way "message" + StringLength. - * - * @param StringLength Length of the string in the input field to validate. - * @param message Message to show. - */ + /// The displayed message shown will be concatenated by the message with StringLength + /// this way "message" + StringLength. + /// + /// @param StringLength Length of the string in the input field to validate. + /// @param message Message to show. public StringLengthValidator(int StringLength, String message) { this.StringLength = StringLength + 1; setMessage(message + StringLength); } - /** - * The displayed message will be personalized, - * but still need to indicate the StringLength to validate. - * - * @param StringLength Length of the string in the input field to validate. - * @param message Message to show. - */ + /// The displayed message will be personalized, + /// but still need to indicate the StringLength to validate. + /// + /// @param StringLength Length of the string in the input field to validate. + /// @param message Message to show. public StringLengthValidator(String message, int StringLength) { super(message); this.StringLength = StringLength + 1; @@ -75,9 +66,6 @@ public int getStringLength() { return StringLength - 1; } - /** - * {@inheritDoc} - */ @Override protected void eval() { if (srcControl.get() instanceof TextInputControl) { @@ -94,7 +82,6 @@ private void evalTextInputField() { if (text.length() > StringLength - 1) { hasErrors.set(true); // textField.textProperty().set(text.substring(0, 19)); - } } } diff --git a/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java b/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java deleted file mode 100644 index 4318ec955d..0000000000 --- a/HMCL/src/main/java/com/jfoenix/validation/ValidationFacade.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.jfoenix.validation; - -import com.jfoenix.utils.JFXUtilities; -import com.jfoenix.validation.base.ValidatorBase; -import javafx.animation.Animation.Status; -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.css.PseudoClass; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.util.Duration; - -/** - * this class has been deprecated and will be removed in later versions of JFoenix, - * we are moving validations into each control that implements the interface - * {@Link IFXValidatableControl}. Validation will be applied through the control itself - * similar to {@link com.jfoenix.controls.JFXTextField}, it's straight forward and - * simpler than using the ValidationFacade. - */ -@Deprecated -public class ValidationFacade extends VBox { - - /** - * Initialize the style class to 'validation-facade'. - *

- * This is the selector class from which CSS can be used to style - * this control. - */ - private static final String DEFAULT_STYLE_CLASS = "validation-facade"; - - private Label errorLabel; - private StackPane errorIcon; - private HBox errorContainer; - - private double oldErrorLabelHeight = -1; - private double initYlayout = -1; - private double initHeight = -1; - private boolean errorShown = false; - private double currentFieldHeight = -1; - private double errorLabelInitHeight = 0; - - private boolean heightChanged = false; - private boolean disableAnimation = false; - - private Timeline hideErrorAnimation; - - public ValidationFacade() { - getStyleClass().add(DEFAULT_STYLE_CLASS); - setPadding(new Insets(0, 0, 0, 0)); - setSpacing(0); - - errorLabel = new Label(); - errorLabel.getStyleClass().add("error-label"); - errorLabel.setWrapText(true); - - StackPane errorLabelContainer = new StackPane(); - errorLabelContainer.getChildren().add(errorLabel); - StackPane.setAlignment(errorLabel, Pos.CENTER_LEFT); - - errorIcon = new StackPane(); - errorContainer = new HBox(); - errorContainer.setAlignment(Pos.TOP_LEFT); - errorContainer.getChildren().add(errorLabelContainer); - errorContainer.getChildren().add(errorIcon); - - HBox.setHgrow(errorLabelContainer, Priority.ALWAYS); - errorLabelContainer.setMaxWidth(Double.MAX_VALUE); - - errorIcon.setTranslateY(5); - errorContainer.setSpacing(10); - errorContainer.setVisible(false); - errorContainer.setOpacity(0); - - // add listeners to show error label - errorLabel.heightProperty().addListener((o, oldVal, newVal) -> { - if (errorShown) { - if (oldErrorLabelHeight == -1) { - oldErrorLabelHeight = errorLabelInitHeight = oldVal.doubleValue(); - } - heightChanged = true; - double newHeight = getHeight() - oldErrorLabelHeight + newVal.doubleValue(); - // show the error - Timeline errorAnimation = new Timeline(new KeyFrame(Duration.ZERO, - new KeyValue(minHeightProperty(), - currentFieldHeight, - Interpolator.EASE_BOTH)), - new KeyFrame(Duration.millis(160), - // text pane animation - new KeyValue(translateYProperty(), - (initYlayout + getMaxHeight() / 2) - newHeight / 2, - Interpolator.EASE_BOTH), - // animate the height change effect - new KeyValue(minHeightProperty(), - newHeight, - Interpolator.EASE_BOTH))); - errorAnimation.play(); - // show the error label when finished - errorAnimation.setOnFinished(finish -> new Timeline(new KeyFrame(Duration.millis(160), - new KeyValue(errorContainer.opacityProperty(), - 1, - Interpolator.EASE_BOTH))).play()); - currentFieldHeight = newHeight; - oldErrorLabelHeight = newVal.doubleValue(); - } - }); - errorContainer.visibleProperty().addListener((o, oldVal, newVal) -> { - // show the error label if it's not shown - new Timeline(new KeyFrame(Duration.millis(160), - new KeyValue(errorContainer.opacityProperty(), - 1, - Interpolator.EASE_BOTH))).play(); - }); - - activeValidatorProperty().addListener((o, oldVal, newVal) -> { - if (!isDisableAnimation()) { - if (hideErrorAnimation != null && hideErrorAnimation.getStatus() == Status.RUNNING) { - hideErrorAnimation.stop(); - } - if (newVal != null) { - hideErrorAnimation = new Timeline(new KeyFrame(Duration.millis(160), - new KeyValue(errorContainer.opacityProperty(), - 0, - Interpolator.EASE_BOTH))); - hideErrorAnimation.setOnFinished(finish -> { - JFXUtilities.runInFX(() -> showError(newVal)); - }); - hideErrorAnimation.play(); - } else { - JFXUtilities.runInFX(this::hideError); - } - } else { - if (newVal != null) { - JFXUtilities.runInFXAndWait(() -> showError(newVal)); - } else { - JFXUtilities.runInFXAndWait(this::hideError); - } - } - }); - - } - - /*************************************************************************** - * * Properties * * - **************************************************************************/ - - /** - * holds the current active validator on the text field in case of - * validation error - */ - private ReadOnlyObjectWrapper activeValidator = new ReadOnlyObjectWrapper<>(); - - public ValidatorBase getActiveValidator() { - return activeValidator == null ? null : activeValidator.get(); - } - - public ReadOnlyObjectProperty activeValidatorProperty() { - return this.activeValidator.getReadOnlyProperty(); - } - - /** - * list of validators that will validate the text value upon calling { - * {@link #validate()} - */ - private ObservableList validators = FXCollections.observableArrayList(); - - public ObservableList getValidators() { - return validators; - } - - public void setValidators(ValidatorBase... validators) { - this.validators.addAll(validators); - } - - /** - * validates the text value using the list of validators provided by the - * user {{@link #setValidators(ValidatorBase...)} - * - * @return true if the value is valid else false - */ - public static boolean validate(Control control) { - ValidationFacade facade = (ValidationFacade) control.getParent(); - for (ValidatorBase validator : facade.validators) { - validator.setSrcControl(facade.controlProperty.get()); - validator.validate(); - if (validator.getHasErrors()) { - facade.activeValidator.set(validator); - control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, true); - return false; - } - } - control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, false); - facade.activeValidator.set(null); - return true; - } - - public static void reset(Control control) { - ValidationFacade facade = (ValidationFacade) control.getParent(); - control.pseudoClassStateChanged(PSEUDO_CLASS_ERROR, false); - facade.activeValidator.set(null); - } - - private ObjectProperty controlProperty = new SimpleObjectProperty<>(); - - public Control getControl() { - return controlProperty.get(); - } - - public void setControl(Control control) { - maxWidthProperty().bind(control.maxWidthProperty()); - prefWidthProperty().bind(control.prefWidthProperty()); - prefHeightProperty().bind(control.prefHeightProperty()); - - errorContainer.setMaxWidth(control.getMaxWidth() > -1 ? control.getMaxWidth() : control.getPrefWidth()); - errorContainer.prefWidthProperty().bind(control.widthProperty()); - errorContainer.prefHeightProperty().bind(control.heightProperty()); - - getChildren().clear(); - getChildren().add(control); - getChildren().add(errorContainer); - this.controlProperty.set(control); - } - - private void showError(ValidatorBase validator) { - - // set text in error label - errorLabel.setText(validator.getMessage()); - // show error icon - Node awsomeIcon = validator.getIcon(); - errorIcon.getChildren().clear(); - if (awsomeIcon != null) { - errorIcon.getChildren().add(awsomeIcon); - StackPane.setAlignment(awsomeIcon, Pos.TOP_RIGHT); - } - // init only once, to fix the text pane from resizing - if (initYlayout == -1) { - initYlayout = getBoundsInParent().getMinY(); - initHeight = getHeight(); - currentFieldHeight = initHeight; - } - errorContainer.setVisible(true); - - errorShown = true; - - } - - private void hideError() { - if (heightChanged) { - new Timeline(new KeyFrame(Duration.millis(160), - new KeyValue(translateYProperty(), 0, Interpolator.EASE_BOTH))).play(); - // reset the height of text field - new Timeline(new KeyFrame(Duration.millis(160), - new KeyValue(minHeightProperty(), initHeight, Interpolator.EASE_BOTH))).play(); - heightChanged = false; - } - // clear error label text - errorLabel.setText(null); - oldErrorLabelHeight = errorLabelInitHeight; - // clear error icon - errorIcon.getChildren().clear(); - // reset the height of the text field - currentFieldHeight = initHeight; - // hide error container - errorContainer.setVisible(false); - - errorShown = false; - } - - public boolean isDisableAnimation() { - return disableAnimation; - } - - public void setDisableAnimation(boolean disableAnimation) { - this.disableAnimation = disableAnimation; - } - - /** - * this style class will be activated when a validation error occurs - */ - private static final PseudoClass PSEUDO_CLASS_ERROR = PseudoClass.getPseudoClass("error"); - -} diff --git a/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java index ca30158de5..62f05cddbf 100644 --- a/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java +++ b/HMCL/src/main/java/com/jfoenix/validation/base/ValidatorBase.java @@ -146,7 +146,7 @@ protected void onEval() { } } - private final Tooltip getActiveTooltip(Node node) { + private Tooltip getActiveTooltip(Node node) { Tooltip tooltip = null; for (String key : supportedTooltipKeys) { tooltip = (Tooltip) node.getProperties().get(key);