package com.twasyl.slideshowfx.ui.controls; import com.twasyl.slideshowfx.ui.controls.validators.IValidator; import javafx.animation.Animation; import javafx.animation.FadeTransition; import javafx.beans.property.*; import javafx.css.PseudoClass; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.Duration; import java.util.Set; /** * A control displaying the label above a text field and under certain circumstances. <br /> * If the text field is empty, only the prompt text is displayed. <br /> * If the text field is focused, the label is displayed above the text field and has the {@code focused} pseudo class * state. </br /> * If the text field is filled, the label is still displayed above the text field, without the pseudo class state * {@code focused}. * <p> * The control also allows to set {@link IValidator} on it in order to validate the value of the text field. If the * value of the text field is invalid, the text field will have the {@code error} pseudo class state. * <p> * The control allows to set the text field as mandatory. If the text field looses the focus and is empty, the * {@code error} pseudo class state is set on the text field. * * @author Thierry Wasylczenko * @version 1.0 * @since SlideshowFX 1.3 */ public class ExtendedTextField extends VBox { private static final PseudoClass FOCUSED = PseudoClass.getPseudoClass("focused"); private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); private Label uiLabel = new Label(); private TextField textField = new TextField(); private FadeTransition uiLabelAnimation = new FadeTransition(Duration.millis(100), this.uiLabel); private StringProperty label = new SimpleStringProperty(); private BooleanProperty mandatory = new SimpleBooleanProperty(false); private IValidator<String> validator; public ExtendedTextField() { super(0); initializeUILabel(); initializeUITextField(); this.getStyleClass().add("extended-text-field"); this.initializeFocusEvents(); this.getChildren().addAll(this.uiLabel, this.textField); } public ExtendedTextField(final String label) { this(label, false); } public ExtendedTextField(final String label, final boolean mandatory) { this(); this.label.set(label); this.setMandatory(mandatory); } public ExtendedTextField(final String label, final boolean mandatory, final int prefColumnCount) { this(label, mandatory); this.setPrefColumnCount(prefColumnCount); } private void initializeUILabel() { this.uiLabel.textProperty().bind(this.label); this.uiLabel.setOpacity(0); } protected void initializeUITextField() { this.textField.promptTextProperty().bind(this.label); this.textField.textProperty().addListener((textValue, oldText, newText) -> { if (newText != null && !newText.isEmpty()) { this.uiLabel.setOpacity(1); } }); } private void initializeFocusEvents() { this.textField.focusedProperty().addListener((focusedValue, oldFocus, newFocus) -> { if (newFocus || (!newFocus && this.textField.getText().isEmpty())) { this.animateLabel(newFocus); if (!newFocus) { this.textField.pseudoClassStateChanged(ERROR, isMandatory()); this.showPromptText(); } else { this.textField.pseudoClassStateChanged(ERROR, false); this.hidePromptText(); } } else if (!newFocus && !this.textField.getText().isEmpty()) { if (this.isMandatory() && this.validator != null && !isValid()) { this.textField.pseudoClassStateChanged(ERROR, true); } this.uiLabel.pseudoClassStateChanged(FOCUSED, newFocus); } }); } /** * Animate the label above the text field. The label may be displayed or hidden. * * @param show {@code true} to display the label, {@code false} to hide it. */ private void animateLabel(boolean show) { if (this.uiLabelAnimation.getStatus() != Animation.Status.STOPPED) { this.uiLabelAnimation.pause(); } if (show) { this.uiLabel.pseudoClassStateChanged(FOCUSED, show); this.uiLabelAnimation.setToValue(1); } else { this.uiLabelAnimation.setToValue(0); } this.uiLabelAnimation.setOnFinished(event -> { this.uiLabel.pseudoClassStateChanged(FOCUSED, show); }); this.uiLabelAnimation.playFromStart(); } /** * Hide the prompt text of the underlying text field. */ private void hidePromptText() { final Node promptText = getPromptText(); if (promptText != null) { promptText.setVisible(false); } } /** * Show the prompt text of the underlying text field. */ private void showPromptText() { final Node promptText = getPromptText(); if (promptText != null) { promptText.setVisible(true); } } /** * Try to get the prompt text {@link Node} of the underlying text field. If it is found, then the * {@link TextField#visibleProperty()} is unbind in order to modify it. * * @return The prompt text {@link Node} if it is found. */ private Node getPromptText() { final Set<Node> nodes = this.textField.lookupAll(".text"); final Node promptText = nodes.stream() .filter(node -> node instanceof Text && this.label.get().equals(((Text) node).getText())) .findFirst() .orElse(null); if (promptText != null && promptText.visibleProperty().isBound()) { promptText.visibleProperty().unbind(); } return promptText; } public IntegerProperty prefColumnCountProperty() { return textField.prefColumnCountProperty(); } public int getPrefColumnCount() { return textField.getPrefColumnCount(); } public void setPrefColumnCount(int value) { textField.setPrefColumnCount(value); } public String getText() { return textField.getText(); } public void setText(String value) { textField.setText(value); } public StringProperty textProperty() { return textField.textProperty(); } public StringProperty labelProperty() { return label; } public String getLabel() { return label.get(); } public void setLabel(String label) { this.label.set(label); } public boolean isMandatory() { return mandatory.get(); } public BooleanProperty mandatoryProperty() { return mandatory; } public void setMandatory(boolean mandatory) { this.mandatory.set(mandatory); } public IValidator<String> getValidator() { return validator; } public void setValidator(IValidator<String> validator) { this.validator = validator; } public boolean isValid() { if (this.getValidator() == null) { throw new IllegalArgumentException("No validator defined for the control"); } final boolean valid = this.validator.isValid(this.textField.getText()); if (!valid) { this.textField.pseudoClassStateChanged(ERROR, true); } return valid; } }