Browse Source

:boom: Huge update [Part 1]
:sparkles: New Controls: MFXStepper, MFXStepperToggle, MFXExceptionDialog

Gradle:
:arrow_up: Updated JavaFX plugin to version 0.0.10
:arrow_up: Updated JavaFX to version 16
:fire: Workaround for modularity should not be necessary anymore

Demo:
DemoController:
:recycle: Make the demo pane request focus on mouse pressed

TextfieldsDemoController:
:recycle: Refactor validation properties for the validated field to use the new BindingsUtils class

MaterialFX:
:boom: Huge review of the Validation API

IMFXValidator:
:recycle: Renamed listeners methods

AbstractMFXValidator:
:boom: Implemented the possibility to make a validator depend on other validators. Each validator has its conditions which are evaluated by a BooleanListBinding property
:boom: Added a new read only boolean property that represents the state of validator, valid/invalid(true/false). The value of this property is computed by evaluating the conditions of the validator and the conditions of all other validators(dependencies). The value is computed automatically when the validator's or its dependencies' conditions change, but you can also force the it by calling the update() method
:recycle: Added a new boolean flag to allow controls to be validated as soon as they are laid out in the scene

MFXPriorityValidator:
:sparkles: New validator class
The priority validator is a validator which changes its message automatically depending on its state/conditions. When its state changes, gets the first invalid condition in its messageMap and sets its message accordingly, more info in the documentation

MFXDialogValidator:
:recycle: Refactored to extend MFXPriorityValidator rather than AbstractMFXValidator
:recycle: Added listener to the title property to change the dialog's title as well

Validated:
:sparkles: New interface of the Validation API
All controls that need validation should implement this interface

MFXTextField:
:recycle: Implement the Validated interface
:recycle: Refactored the setupValidator() method, the dialog type is set to ERROR rather than WARNING, added code to update the "invalid" pseudo class state
:recycle: lines' stroke width set to 2.0
:sparkles: Added a PseudoClass, "invalid", for the validator state and customize the control accordingly
:sparkles: Added a new styleable property to specify the lines' stroke cap

MFXTextFieldSkin:
:recycle: Added validator init flag check to constructor
:recycle: Force validator update when focus changes
:recycle: Refactored the lines' code, use bindings rather than listeners. Compute their position using the relocate() method rather than using the translateY property
:recycle: Switched to MFXLabel, compute its position and location using the resizeRelocate() method. The position strategy has changed too, if the label is bigger than the text field then it is positioned to be centered
:recycle: Using the showModal() method rather than show()
:bug: When the cursor is on the validate label its shape should be the DEFAULT
:bug: When the validate label's text changes it's required to recompute the layout

mfx-textfield.css:
:recycle: Switched to MFXLabel, set the text fill to "red" by default
:sparkles: Added new section that specifies the control look when "invalid"

MFXLegacyComboBox:
:recycle: Implement the Validated interface
:recycle: Refactored the setupValidator() method, the dialog type is set to ERROR rather than WARNING, added code to update the "invalid" pseudo class state, removed the selected item condition now it's up to the user's decision
:recycle: lines' stroke width set to 2.0
:sparkles: Added a PseudoClass, "invalid", for the validator state and customize the control accordingly
:sparkles: Added a new styleable property to specify the lines' stroke cap
:bug: Fixed line color mismatch between styleable property and css meta data

MFXLegacyComboBoxSkin:
:recycle: Added validator init flag check to constructor
:recycle: Force validator update when focus changes
:recycle: Refactored the lines' code, use bindings rather than listeners. Compute their position using the relocate() method rather than using the translateY property, changed their name accordingly to other similar controls
:recycle: Switched to MFXLabel, compute its position and location using the resizeRelocate() method. The position strategy has changed too, if the label is bigger than the text field then it is positioned to be centered
:dizzy: Minor changes to the lines animation
:bug: When the validate label's text changes it's required to recompute the layout

mfx-combobox.css:
:recycle: Switched to MFXLabel, set the text fill to "red" by default
:sparkles: Added new section that specifies the control look when "invalid"

MFXDatePicker:
:recycle: Reviewed the layout strategy
:sparkles: Added styleable properties for the line's stroke width and stroke cap
:bug: Line and calendar icon colors should not change if the control is disabled
:bug: Fixed calendar icon not being gray if the control was disabled

:sparkles: New Utils:
BindingUtils: utils class to convert JavaFX expressions/bindings to properties
DialogUtils: ported from one of my personal projects. Utils class to quickly get any sort of stage dialog, including more specific dialogs.
ExceptionUtils: ported from one of my personal project. Utils class to convert a throwable/exception to a String using a StringWriter and a PrintWriter.

:memo: Added and updated documentation

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>

palexdev 4 years ago
parent
commit
4fc8e19c3c
59 changed files with 3315 additions and 408 deletions
  1. 2 5
      build.gradle
  2. 2 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java
  3. 23 7
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextfieldsDemoController.java
  4. 14 25
      demo/src/main/resources/io/github/palexdev/materialfx/demo/combo_boxes_demo.fxml
  5. 6 11
      demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml
  6. 6 1
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/binding/BooleanListBinding.java
  7. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckTreeItem.java
  8. 110 38
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDatePicker.java
  9. 96 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXExceptionDialog.java
  10. 770 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepper.java
  11. 481 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepperToggle.java
  12. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java
  13. 139 47
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java
  14. 5 5
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleButton.java
  15. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java
  16. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeItem.java
  17. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXDialog.java
  18. 1 5
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListView.java
  19. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessCheckListCell.java
  20. 31 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/StepperToggleState.java
  21. 24 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/TextPosition.java
  22. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/RippleClipTypeFactory.java
  23. 131 43
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java
  24. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/MFXScrimEffect.java
  25. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleClipType.java
  26. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXButtonSkin.java
  27. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCheckboxSkin.java
  28. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  29. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDateCellSkin.java
  30. 6 5
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java
  31. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java
  32. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFlowlessListViewSkin.java
  33. 20 3
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXLabelSkin.java
  34. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java
  35. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressBarSkin.java
  36. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressSpinnerSkin.java
  37. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRadioButtonSkin.java
  38. 358 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperSkin.java
  39. 164 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java
  40. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnCellSkin.java
  41. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java
  42. 49 40
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java
  43. 4 5
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXToggleButtonSkin.java
  44. 63 58
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/legacy/MFXLegacyComboBoxSkin.java
  45. 105 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/BindingUtils.java
  46. 97 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/DialogUtils.java
  47. 41 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/ExceptionUtils.java
  48. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoaderUtils.java
  49. 2 3
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java
  50. 30 51
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java
  51. 128 0
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXPriorityValidator.java
  52. 120 18
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java
  53. 5 7
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/IMFXValidator.java
  54. 56 0
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/Validated.java
  55. 28 2
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/legacy/mfx-combobox.css
  56. 22 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-colors.css
  57. 55 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-stepper.css
  58. 79 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-steppertoggle.css
  59. 15 2
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-textfield.css

+ 2 - 5
build.gradle

@@ -1,6 +1,6 @@
 plugins {
     id 'java-library'
-    id 'org.openjfx.javafxplugin' version '0.0.9' apply false
+    id 'org.openjfx.javafxplugin' version '0.0.10' apply false
 }
 
 group 'io.github.palexdev'
@@ -15,12 +15,9 @@ subprojects {
     apply plugin: 'org.openjfx.javafxplugin'
 
     javafx {
-        version = "15.0.1"
+        version = "16"
         modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.web' ]
     }
-
-    // Workaround for newer Gradle versions (see https://github.com/java9-modularity/gradle-modules-plugin/issues/165)
-    modularity.disableEffectiveArgumentsAdjustment()
 }
 
 

+ 2 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java

@@ -162,6 +162,8 @@ public class DemoController implements Initializable {
         });
         navBar.setVisible(false);
         initAnimations();
+
+        demoPane.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> demoPane.requestFocus());
     }
 
     private void initAnimations() {

+ 23 - 7
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextfieldsDemoController.java

@@ -3,8 +3,9 @@ package io.github.palexdev.materialfx.demo.controllers;
 import io.github.palexdev.materialfx.controls.MFXCheckbox;
 import io.github.palexdev.materialfx.controls.MFXDatePicker;
 import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.utils.BindingUtils;
+import javafx.beans.binding.Bindings;
 import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.scene.control.Label;
@@ -30,14 +31,29 @@ public class TextfieldsDemoController implements Initializable {
 
     @Override
     public void initialize(URL location, ResourceBundle resources) {
-        BooleanProperty checkboxValidation = new SimpleBooleanProperty(false);
-        BooleanProperty datePickerValidation = new SimpleBooleanProperty(false);
-        checkboxValidation.bind(checkbox.selectedProperty());
-        datePickerValidation.bind(picker.getDatePicker().valueProperty().isEqualTo(LocalDate.of(1911, Month.OCTOBER, 3)));
+        BooleanProperty checkboxValidation = BindingUtils.toProperty(
+                Bindings.createBooleanBinding(
+                        () -> checkbox.isSelected(),
+                        checkbox.selectedProperty()
+                )
+        );
+        BooleanProperty datePickerValidation = BindingUtils.toProperty(
+                Bindings.createBooleanBinding(
+                        () -> {
+                            LocalDate value = picker.getDatePicker().getValue();
+                            if (value != null) {
+                                return value.equals(LocalDate.of(1911, Month.OCTOBER, 3));
+                            } else {
+                                return false;
+                            }
+                        },
+                        picker.getDatePicker().valueProperty()
+                )
+        );
         validated.getValidator().add(checkboxValidation, "Checkbox must be selected");
         validated.getValidator().add(datePickerValidation, "Selected date must be 03/10/1911");
-        validated.setIsValidated(true);
+        validated.setValidated(true);
 
-        label.visibleProperty().bind(validated.getValidator().validationProperty());
+        label.visibleProperty().bind(validated.getValidator().validProperty());
     }
 }

+ 14 - 25
demo/src/main/resources/io/github/palexdev/materialfx/demo/combo_boxes_demo.fxml

@@ -5,7 +5,7 @@
 <?import javafx.geometry.*?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="450.0" prefWidth="600.0" stylesheets="@css/combo_boxes_demo.css" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.ComboBoxesDemoController">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="450.0" prefWidth="850.0" stylesheets="@css/combo_boxes_demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.ComboBoxesDemoController">
    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Legacy Combo Boxes" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets top="20.0" />
@@ -31,34 +31,18 @@
          <Insets top="125.0" />
       </StackPane.margin>
    </Label>
-   <MFXLegacyComboBox fx:id="editable" prefHeight="23.0" prefWidth="90.0" promptText="Editable" StackPane.alignment="TOP_CENTER">
+   <HBox maxHeight="-Infinity" maxWidth="-Infinity" spacing="100.0">
       <StackPane.margin>
-         <Insets right="120.0" top="170.0" />
+         <Insets bottom="80.0"/>
       </StackPane.margin>
-   </MFXLegacyComboBox>
-   <MFXLegacyComboBox fx:id="labels" prefHeight="23.0" prefWidth="90.0" promptText="Labels" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets right="350.0" top="170.0" />
-      </StackPane.margin>
-   </MFXLegacyComboBox>
-   <MFXLegacyComboBox fx:id="validated" lineColor="#ffdc00" prefHeight="23.0" prefWidth="90.0" promptText="Validated" validated="true" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets left="120.0" top="170.0" />
-      </StackPane.margin>
-   </MFXLegacyComboBox>
-   <MFXLegacyComboBox id="custom" fx:id="customized" prefHeight="23.0" prefWidth="90.0" promptText="CSS" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets left="350.0" top="170.0" />
-      </StackPane.margin>
-   </MFXLegacyComboBox>
-   <MFXCheckbox fx:id="checkbox" text="Validation!" StackPane.alignment="BOTTOM_RIGHT">
-      <StackPane.margin>
-         <Insets bottom="200.0" left="10.0" right="10.0" top="10.0" />
-      </StackPane.margin>
-   </MFXCheckbox>
+      <MFXLegacyComboBox fx:id="labels" prefHeight="23.0" prefWidth="90.0" promptText="Labels"/>
+      <MFXLegacyComboBox fx:id="editable" prefHeight="23.0" prefWidth="90.0" promptText="Editable"/>
+      <MFXLegacyComboBox fx:id="validated" prefHeight="23.0" prefWidth="90.0" promptText="Validated" validated="true"/>
+      <MFXLegacyComboBox id="custom" fx:id="customized" prefHeight="23.0" prefWidth="90.0" promptText="CSS"/>
+   </HBox>
    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="New Combo Boxes">
       <StackPane.margin>
-         <Insets top="105.0" />
+         <Insets top="70.0" />
       </StackPane.margin>
    </Label>
    <MFXComboBox fx:id="style1">
@@ -91,4 +75,9 @@
          <Insets bottom="25.0" left="150.0" />
       </StackPane.margin>
    </MFXFilterComboBox>
+   <MFXCheckbox fx:id="checkbox" text="Validation!" StackPane.alignment="BOTTOM_RIGHT">
+      <StackPane.margin>
+         <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
+      </StackPane.margin>
+   </MFXCheckbox>
 </StackPane>

+ 6 - 11
demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml

@@ -4,7 +4,7 @@
 <?import javafx.geometry.*?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/textfields_demo.css" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TextfieldsDemoController">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/textfields_demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TextfieldsDemoController">
     <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="TextFields" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
           <Insets top="20.0" />
@@ -29,16 +29,11 @@
          <Insets top="130.0" />
       </StackPane.margin>
    </Label>
-   <MFXTextField id="colors" alignment="CENTER" lineColor="#52db32" maxWidth="-Infinity" prefWidth="120.0" promptText="Colors" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets right="150.0" top="180.0" />
-      </StackPane.margin>
-   </MFXTextField>
-   <MFXTextField fx:id="validated" alignment="CENTER" maxWidth="-Infinity" prefWidth="120.0" text="Validation" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets left="150.0" top="180.0" />
-      </StackPane.margin>
-   </MFXTextField>
+   <HBox maxHeight="-Infinity" maxWidth="-Infinity" spacing="100.0">
+      <MFXTextField id="colors" alignment="CENTER" lineColor="#52db32" maxWidth="-Infinity" prefWidth="120.0"
+                    promptText="Colors"/>
+      <MFXTextField fx:id="validated" alignment="CENTER" maxWidth="-Infinity" prefWidth="120.0" text="Validation"/>
+   </HBox>
    <MFXCheckbox fx:id="checkbox" checkedColor="#00e240" markSize="9.0" markType="mfx-variant9-mark" text="CheckBox Validation" StackPane.alignment="BOTTOM_LEFT">
       <StackPane.margin>
          <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />

+ 6 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/beans/binding/BooleanListBinding.java

@@ -23,6 +23,12 @@ import javafx.beans.property.BooleanProperty;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
 
+/**
+ * A {@code BooleanListBinding} is a particular binding that takes a list of
+ * {@link BooleanProperty} and observes each one of them updating it's value
+ * when they change. The value is true only when all properties are true, it's false
+ * when even only one of them is false.
+ */
 public class BooleanListBinding extends BooleanBinding {
     private final ObservableList<BooleanProperty> boundList;
     private final ListChangeListener<BooleanProperty> changeListener;
@@ -42,7 +48,6 @@ public class BooleanListBinding extends BooleanBinding {
                 return false;
             }
         }
-
         return true;
     }
 

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckTreeItem.java

@@ -145,7 +145,7 @@ public class MFXCheckTreeItem<T> extends MFXTreeItem<T> {
     }
 
     //================================================================================
-    // Events Class
+    // Events
     //================================================================================
 
     /**

+ 110 - 38
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDatePicker.java

@@ -21,15 +21,11 @@ package io.github.palexdev.materialfx.controls;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXDatePickerContent;
-import io.github.palexdev.materialfx.utils.LoggingUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.css.*;
-import javafx.geometry.HPos;
-import javafx.geometry.Point2D;
-import javafx.geometry.Pos;
-import javafx.geometry.VPos;
+import javafx.geometry.*;
 import javafx.scene.control.DatePicker;
 import javafx.scene.control.Label;
 import javafx.scene.control.PopupControl;
@@ -39,6 +35,7 @@ import javafx.scene.layout.VBox;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Line;
+import javafx.scene.shape.StrokeLineCap;
 
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
@@ -72,6 +69,7 @@ public class MFXDatePicker extends VBox {
     private final DatePicker datePicker;
     private final ObjectProperty<DateTimeFormatter> dateFormatter = new SimpleObjectProperty<>(DateTimeFormatter.ofPattern("dd/M/yyyy"));
 
+    private StackPane stackPane;
     private Label value;
     private MFXFontIcon calendar;
     private Line line;
@@ -107,26 +105,26 @@ public class MFXDatePicker extends VBox {
         calendar.getStyleClass().add("calendar-icon");
         calendar.setColor(getPickerColor());
         calendar.setSize(20);
-        StackPane pane = new StackPane(value, calendar);
-        pane.setAlignment(Pos.BOTTOM_LEFT);
+        stackPane = new StackPane(value, calendar);
+        stackPane.setPadding(new Insets(5, -2.5, 5, 5));
+        stackPane.setAlignment(Pos.BOTTOM_LEFT);
         StackPane.setAlignment(calendar, Pos.BOTTOM_RIGHT);
 
         line = new Line();
         line.getStyleClass().add("line");
         line.setManaged(false);
         line.setSmooth(true);
-        line.setStrokeWidth(2);
+        line.strokeWidthProperty().bind(lineStrokeWidth);
+        line.strokeLineCapProperty().bind(lineStrokeCap);
         line.setStroke(getLineColor());
-        line.setStartX(-3);
-        line.endXProperty().bind(pane.widthProperty().add(6));
-        line.translateYProperty().bind(heightProperty().add(5));
+        line.endXProperty().bind(widthProperty().add(10));
 
         popup = new PopupControl();
         datePickerContent = new MFXDatePickerContent(datePicker.getValue(), getDateFormatter());
         popup.getScene().setRoot(datePickerContent);
         popup.setAutoHide(true);
 
-        getChildren().addAll(pane, line);
+        getChildren().addAll(stackPane, line);
         addListeners();
 
         if (datePicker.getValue() != null) {
@@ -138,7 +136,7 @@ public class MFXDatePicker extends VBox {
 
     /**
      * Adds listeners to date picker content currentDateProperty, to {@link #dateFormatter}, to {@link #pickerColor},
-     * to {@link #lineColor}, to {@link #colorText} and disabled property.
+     * to {@link #lineColor}, to calendar icon's {@link MFXFontIcon#colorProperty()}, to {@link #colorText} and disabled property.
      * <p>
      * Adds event handler to calendar icon.
      * <p>
@@ -194,7 +192,18 @@ public class MFXDatePicker extends VBox {
                 value.setTextFill(Color.BLACK);
             }
         });
-        lineColor.addListener((observable, oldValue, newValue) -> line.setStroke(newValue));
+        lineColor.addListener((observable, oldValue, newValue) -> {
+            if (!isDisabled()) {
+                line.setStroke(newValue);
+            }
+        });
+        calendar.colorProperty().addListener((observable, oldValue, newValue) -> {
+            if (!isDisabled()) {
+                calendar.setColor(newValue);
+            } else {
+                calendar.setColor(Color.LIGHTGRAY);
+            }
+        });
         colorText.addListener((observable, oldValue, newValue) -> {
             if (newValue) {
                 value.setTextFill(getPickerColor());
@@ -235,10 +244,6 @@ public class MFXDatePicker extends VBox {
     //================================================================================
     // Styleable Properties
     //================================================================================
-
-    /**
-     * Specifies the main color of the date picker and its content.
-     */
     private final StyleableObjectProperty<Paint> pickerColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.PICKER_COLOR,
             this,
@@ -246,9 +251,6 @@ public class MFXDatePicker extends VBox {
             Color.rgb(98, 0, 238)
     );
 
-    /**
-     * Specifies the line color of the date picker.
-     */
     private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.LINE_COLOR,
             this,
@@ -256,9 +258,20 @@ public class MFXDatePicker extends VBox {
             Color.rgb(98, 0, 238, 0.7)
     );
 
-    /**
-     * Specifies if the date picker text should be colored too.
-     */
+    private final StyleableDoubleProperty lineStrokeWidth = new SimpleStyleableDoubleProperty(
+            StyleableProperties.LINE_STROKE_WIDTH,
+            this,
+            "lineStrokeWidth",
+            2.0
+    );
+
+    private final StyleableObjectProperty<StrokeLineCap> lineStrokeCap = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_STROKE_CAP,
+            this,
+            "lineStrokeCap",
+            StrokeLineCap.ROUND
+    );
+
     private final StyleableBooleanProperty colorText = new SimpleStyleableBooleanProperty(
             StyleableProperties.COLOR_TEXT,
             this,
@@ -266,9 +279,6 @@ public class MFXDatePicker extends VBox {
             false
     );
 
-    /**
-     * Specifies if the date picker popup should close on day selected.
-     */
     private final StyleableBooleanProperty closeOnDaySelected = new SimpleStyleableBooleanProperty(
             StyleableProperties.CLOSE_ON_DAY_SELECTED,
             this,
@@ -276,9 +286,6 @@ public class MFXDatePicker extends VBox {
             true
     );
 
-    /**
-     * Specifies if the month change should be animated.
-     */
     private final StyleableBooleanProperty animateCalendar = new SimpleStyleableBooleanProperty(
             StyleableProperties.ANIMATE_CALENDAR,
             this,
@@ -290,17 +297,14 @@ public class MFXDatePicker extends VBox {
         return pickerColor.get();
     }
 
+    /**
+     * Specifies the main color of the date picker and its content.
+     */
     public StyleableObjectProperty<Paint> pickerColorProperty() {
         return pickerColor;
     }
 
     public void setPickerColor(Paint pickerColor) {
-        try {
-            Color.class.cast(pickerColor);
-        } catch (ClassCastException ex) {
-            LoggingUtils.logException("Picker color must be of type Color", ex);
-        }
-
         this.pickerColor.set(pickerColor);
     }
 
@@ -308,6 +312,9 @@ public class MFXDatePicker extends VBox {
         return lineColor.get();
     }
 
+    /**
+     * Specifies the line color of the date picker.
+     */
     public StyleableObjectProperty<Paint> lineColorProperty() {
         return lineColor;
     }
@@ -316,10 +323,43 @@ public class MFXDatePicker extends VBox {
         this.lineColor.set(lineColor);
     }
 
+    public double getLineStrokeWidth() {
+        return lineStrokeWidth.get();
+    }
+
+    /**
+     * Specifies the line's stroke width.
+     */
+    public StyleableDoubleProperty lineStrokeWidthProperty() {
+        return lineStrokeWidth;
+    }
+
+    public void setLineStrokeWidth(double lineStrokeWidth) {
+        this.lineStrokeWidth.set(lineStrokeWidth);
+    }
+
+    public StrokeLineCap getLineStrokeCap() {
+        return lineStrokeCap.get();
+    }
+
+    /**
+     * Specifies the line's stroke cap.
+     */
+    public StyleableObjectProperty<StrokeLineCap> lineStrokeCapProperty() {
+        return lineStrokeCap;
+    }
+
+    public void setLineStrokeCap(StrokeLineCap lineStrokeCap) {
+        this.lineStrokeCap.set(lineStrokeCap);
+    }
+
     public boolean isColorText() {
         return colorText.get();
     }
 
+    /**
+     * Specifies if the date picker text should be colored too.
+     */
     public StyleableBooleanProperty colorTextProperty() {
         return colorText;
     }
@@ -332,6 +372,9 @@ public class MFXDatePicker extends VBox {
         return closeOnDaySelected.get();
     }
 
+    /**
+     * Specifies if the date picker popup should close on day selected.
+     */
     public StyleableBooleanProperty closeOnDaySelectedProperty() {
         return closeOnDaySelected;
     }
@@ -344,6 +387,9 @@ public class MFXDatePicker extends VBox {
         return animateCalendar.get();
     }
 
+    /**
+     * Specifies if the month change should be animated.
+     */
     public StyleableBooleanProperty animateCalendarProperty() {
         return animateCalendar;
     }
@@ -372,6 +418,21 @@ public class MFXDatePicker extends VBox {
                         Color.rgb(90, 0, 238, 0.7)
                 );
 
+        private static final CssMetaData<MFXDatePicker, Number> LINE_STROKE_WIDTH =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-line-stroke-width",
+                        MFXDatePicker::lineStrokeWidthProperty,
+                        2.0
+                );
+
+        private static final CssMetaData<MFXDatePicker, StrokeLineCap> LINE_STROKE_CAP =
+                FACTORY.createEnumCssMetaData(
+                        StrokeLineCap.class,
+                        "-mfx-line-stroke-cap",
+                        MFXDatePicker::lineStrokeCapProperty,
+                        StrokeLineCap.ROUND
+                );
+
         private static final CssMetaData<MFXDatePicker, Boolean> COLOR_TEXT =
                 FACTORY.createBooleanCssMetaData(
                         "-mfx-color-text",
@@ -394,7 +455,10 @@ public class MFXDatePicker extends VBox {
                 );
 
         static {
-            cssMetaDataList = List.of(PICKER_COLOR, LINE_COLOR, COLOR_TEXT, CLOSE_ON_DAY_SELECTED, ANIMATE_CALENDAR);
+            cssMetaDataList = List.of(
+                    PICKER_COLOR, COLOR_TEXT, CLOSE_ON_DAY_SELECTED, ANIMATE_CALENDAR,
+                    LINE_COLOR, LINE_STROKE_WIDTH, LINE_STROKE_CAP
+            );
         }
 
     }
@@ -416,8 +480,16 @@ public class MFXDatePicker extends VBox {
         return MFXDatePicker.getControlCssMetaDataList();
     }
 
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+
+        double ly = snapPositionY(stackPane.getBoundsInParent().getMaxY() + (line.getStrokeWidth() / 2.5));
+        line.relocate(-3, ly);
+    }
+
     //================================================================================
-    // Wrapper Methods
+    // Delegate Methods
     //================================================================================
     public DatePicker getDatePicker() {
         return datePicker;

+ 96 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXExceptionDialog.java

@@ -0,0 +1,96 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.controls.enums.ButtonType;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.ExceptionUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.util.Duration;
+
+/**
+ * Specific dialog to show an exception's stack trace in a text area using {@link ExceptionUtils}
+ * <p></p>
+ * Extends {@link MFXDialog}
+ */
+public class MFXExceptionDialog extends MFXDialog {
+    private final TextArea exceptionArea;
+
+    public MFXExceptionDialog() {
+        setPrefSize(500, 350);
+        setPadding(new Insets(3));
+
+        StackPane headerNode = new StackPane();
+        headerNode.setPrefSize(getPrefWidth(), getPrefHeight() * 0.45);
+        headerNode.getStyleClass().add("header-node");
+        headerNode.setStyle("-fx-background-color: #EF6E6B;\n" + "-fx-background-insets: -3 -3 0 -3");
+
+        MFXFontIcon exceptionIcon = new MFXFontIcon("mfx-x-circle-light");
+        exceptionIcon.setColor(Color.WHITE);
+        exceptionIcon.setSize(96);
+        MFXFontIcon closeIcon = new MFXFontIcon("mfx-x", Color.WHITE);
+
+        MFXButton closeButton = new MFXButton("");
+        closeButton.setPrefSize(20, 20);
+        closeButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        closeButton.setGraphic(closeIcon);
+        closeButton.setRippleRadius(15);
+        closeButton.setRippleColor(Color.rgb(255, 0, 0, 0.1));
+        closeButton.setRippleInDuration(Duration.millis(500));
+        closeButton.setButtonType(ButtonType.FLAT);
+
+        NodeUtils.makeRegionCircular(closeButton);
+
+        StackPane.setAlignment(closeButton, Pos.TOP_RIGHT);
+        StackPane.setMargin(closeButton, new Insets(7, 7, 0, 0));
+        headerNode.getChildren().addAll(exceptionIcon, closeButton);
+
+        exceptionArea = new TextArea();
+        exceptionArea.setEditable(false);
+        exceptionArea.setWrapText(true);
+        exceptionArea.setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE);
+
+        StackPane scrollContent = new StackPane();
+        scrollContent.setPadding(new Insets(-5));
+        scrollContent.setMinSize(USE_PREF_SIZE, USE_PREF_SIZE);
+        scrollContent.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
+        scrollContent.setPrefSize(590, 240);
+        scrollContent.getChildren().setAll(exceptionArea);
+        MFXScrollPane scrollPane = new MFXScrollPane(scrollContent);
+        scrollPane.setPadding(new Insets(10, 10, 0, 10));
+        scrollPane.setFitToWidth(true);
+
+        setTop(headerNode);
+        setCenter(scrollPane);
+        setCloseButtons(closeButton);
+    }
+
+    /**
+     * Sets the textarea text to the specified exception's stack trace.
+     */
+    public void setException(Throwable th) {
+        exceptionArea.setText(ExceptionUtils.getStackTraceString(th));
+    }
+}

+ 770 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepper.java

@@ -0,0 +1,770 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.MFXStepperToggle.MFXStepperToggleEvent;
+import io.github.palexdev.materialfx.controls.enums.StepperToggleState;
+import io.github.palexdev.materialfx.skins.MFXStepperSkin;
+import javafx.beans.property.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.css.*;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is the implementation of a stepper/wizard following material design guidelines in JavaFX.
+ * <p></p>
+ * Steppers display progress through a sequence of logical and numbered steps.
+ * They may also be used for navigation.
+ * <p></p>
+ * Every stepper has a number of {@link MFXStepperToggle}s that should be added to the list
+ * after instantiating the stepper. If the list is changed after the stepper has already been laid out
+ * then a {@link #reset()} attempt is made.
+ * <p></p>
+ * The stepper has two properties to represent the current step index and the progress. The progress
+ * is computed as the number of COMPLETED toggles divided by the number of toggles. The progress is
+ * updated automatically, can be also forced by calling {@link #updateProgress()}.
+ * <p></p>
+ * <b>NOTE:</b> the stepper allows you to change the toggles even after it is already shown, it has been
+ * tested and it seems to work well. However, the stepper is intended to be a "static" control, that means
+ * you should plan ahead of time what toggles to place and their content.
+ */
+public class MFXStepper extends Control {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXStepper> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-stepper";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-stepper.css");
+
+    private final ObservableList<MFXStepperToggle> stepperToggles = FXCollections.observableArrayList();
+    private final DoubleProperty animationDuration = new SimpleDoubleProperty(700.0);
+    private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper();
+    private final ReadOnlyIntegerWrapper currentIndex = new ReadOnlyIntegerWrapper(-1);
+    private final ReadOnlyObjectWrapper<Node> currentContent = new ReadOnlyObjectWrapper<>();
+    private final ReadOnlyBooleanWrapper lastToggle = new ReadOnlyBooleanWrapper(false);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXStepper() {
+        this(new ArrayList<>());
+    }
+
+    public MFXStepper(List<MFXStepperToggle> stepperToggles) {
+        setStepperToggles(stepperToggles);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().setAll(STYLE_CLASS);
+        addListeners();
+    }
+
+    /**
+     * Adds an event handler that listens for {@link MFXStepperToggleEvent#STATE_CHANGED} events to
+     * update the progress property.
+     */
+    private void addListeners() {
+        addEventHandler(MFXStepperToggleEvent.STATE_CHANGED, event -> updateProgress());
+    }
+
+
+    /**
+     * Goes to the next toggle if the validator's state is valid and
+     * updates the {@link #currentIndexProperty()} accordingly.
+     * <p></p>
+     * Special case: if the last toggle is already selected then the progress bar
+     * reaches the 100%.
+     * <p></p>
+     * This method is also responsible for updating the toggles' state
+     * and firing the following events: {@link MFXStepperEvent#NEXT_EVENT}, {@link MFXStepperEvent#LAST_NEXT_EVENT},
+     * {@link MFXStepperEvent#VALIDATION_FAILED_EVENT}.
+     */
+    public void next() {
+        if (stepperToggles.isEmpty()) {
+            return;
+        }
+
+        int currentIndex = getCurrentIndex();
+        if (currentIndex == -1) {
+            MFXStepperToggle first = stepperToggles.get(0);
+            first.setState(StepperToggleState.SELECTED);
+            this.currentIndex.set(0);
+            setCurrentContent(first.getContent());
+            return;
+        }
+
+        MFXStepperToggle current = stepperToggles.get(currentIndex);
+        if (!current.isValid()) {
+            if (current.getState() != StepperToggleState.ERROR) {
+                current.setState(StepperToggleState.ERROR);
+            }
+            fireEvent(MFXStepperEvent.VALIDATION_FAILED_EVENT);
+            return;
+        }
+
+        if (currentIndex < stepperToggles.size() - 1) {
+            MFXStepperToggle next = stepperToggles.get(currentIndex + 1);
+            current.setState(StepperToggleState.COMPLETED);
+            next.setState(StepperToggleState.SELECTED);
+            this.currentIndex.set(currentIndex + 1);
+            setCurrentContent(next.getContent());
+            fireEvent(MFXStepperEvent.NEXT_EVENT);
+        } else {
+            setLastToggle(true);
+            current.setState(StepperToggleState.COMPLETED);
+            fireEvent(MFXStepperEvent.LAST_NEXT_EVENT);
+        }
+    }
+
+    /**
+     * Goes to the previous toggle and updates the
+     * {@link #currentIndexProperty()} accordingly.
+     * <p></p>
+     * This method is also responsible for updating the toggles' state
+     * and firing the following events: {@link MFXStepperEvent#PREVIOUS_EVENT}.
+     */
+    public void previous() {
+        if (stepperToggles.isEmpty()) {
+            return;
+        }
+
+        int currentIndex = getCurrentIndex();
+        if (isLastToggle()) {
+            setLastToggle(false);
+            MFXStepperToggle last = getCurrentStepperNode();
+            last.setState(StepperToggleState.SELECTED);
+            return;
+        }
+
+        if (currentIndex == -1) {
+            MFXStepperToggle stepperNode = stepperToggles.get(0);
+            stepperNode.setState(StepperToggleState.SELECTED);
+            this.currentIndex.set(0);
+            setCurrentContent(stepperNode.getContent());
+            return;
+        }
+        if (currentIndex > 0) {
+            MFXStepperToggle current = stepperToggles.get(currentIndex);
+            MFXStepperToggle previous = stepperToggles.get(currentIndex - 1);
+            current.setState(StepperToggleState.NONE);
+            previous.setState(StepperToggleState.SELECTED);
+            this.currentIndex.set(currentIndex - 1);
+            setCurrentContent(previous.getContent());
+            fireEvent(MFXStepperEvent.PREVIOUS_EVENT);
+        }
+    }
+
+    /**
+     * Resets the stepper and all its toggles to the initial state.
+     */
+    public void reset() {
+        setLastToggle(false);
+        currentIndex.set(-1);
+        stepperToggles.forEach(stepperToggle -> stepperToggle.setState(StepperToggleState.NONE));
+    }
+
+    /**
+     * @return the current selected toggle by using the {@link #currentIndexProperty()},
+     * or null if an exception is captured.
+     */
+    public MFXStepperToggle getCurrentStepperNode() {
+        try {
+            return stepperToggles.get(getCurrentIndex());
+        } catch (IndexOutOfBoundsException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Updates the {@link #progressProperty()} by counting the number of
+     * COMPLETED toggles and dividing it by the total number of controls, so
+     * the progress values go from 0.0 to 1.0.
+     */
+    public void updateProgress() {
+        double completed = Math.toIntExact(stepperToggles.stream()
+                .filter(s -> s.getState() == StepperToggleState.COMPLETED)
+                .count());
+        this.progress.set(completed / stepperToggles.size());
+    }
+
+    /**
+     * @return the stepper's toggles list
+     */
+    public ObservableList<MFXStepperToggle> getStepperToggles() {
+        return stepperToggles;
+    }
+
+    /**
+     * Replaces the stepper's toggles with the specified ones.
+     */
+    public void setStepperToggles(List<MFXStepperToggle> stepperToggles) {
+        this.stepperToggles.setAll(stepperToggles);
+    }
+
+    public double getAnimationDuration() {
+        return animationDuration.get();
+    }
+
+    /**
+     * Specifies, in milliseconds, the duration of the progress bar animation.
+     */
+    public DoubleProperty animationDurationProperty() {
+        return animationDuration;
+    }
+
+    public void setAnimationDuration(double animationDuration) {
+        this.animationDuration.set(animationDuration);
+    }
+
+    public double getProgress() {
+        return progress.get();
+    }
+
+    /**
+     * Specifies the stepper's progress, the number of COMPLETED toggles
+     * divided by the total number of toggles. The values go from 0.0 to 1.0.
+     */
+    public ReadOnlyDoubleProperty progressProperty() {
+        return progress.getReadOnlyProperty();
+    }
+
+    protected void setProgress(double progress) {
+        this.progress.set(progress);
+    }
+
+    public int getCurrentIndex() {
+        return currentIndex.get();
+    }
+
+    /**
+     * Specifies the selected toggle position in the toggles list.
+     * The index is updated by {@link #next()} and {@link #previous()} methods.
+     */
+    public ReadOnlyIntegerProperty currentIndexProperty() {
+        return currentIndex.getReadOnlyProperty();
+    }
+
+    protected void setCurrentIndex(int currentIndex) {
+        this.currentIndex.set(currentIndex);
+    }
+
+    public Node getCurrentContent() {
+        return currentContent.get();
+    }
+
+    /**
+     * Convenience property that holds the selected toggle content node.
+     * <p>
+     * In case one of the toggles has a {@code null} content the content pane's
+     * children list is cleared.
+     */
+    public ReadOnlyObjectProperty<Node> currentContentProperty() {
+        return currentContent.getReadOnlyProperty();
+    }
+
+    protected void setCurrentContent(Node content) {
+        currentContent.set(content);
+    }
+
+    public boolean isLastToggle() {
+        return lastToggle.get();
+    }
+
+    /**
+     * Convenience property that specifies if the last toggle is selected.
+     */
+    public ReadOnlyBooleanProperty lastToggleProperty() {
+        return lastToggle.getReadOnlyProperty();
+    }
+
+    protected void setLastToggle(boolean lastToggle) {
+        this.lastToggle.set(lastToggle);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableDoubleProperty spacing = new SimpleStyleableDoubleProperty(
+            StyleableProperties.SPACING,
+            this,
+            "spacing",
+            128.0
+    );
+
+    private final StyleableDoubleProperty extraSpacing = new SimpleStyleableDoubleProperty(
+            StyleableProperties.EXTRA_SPACING,
+            this,
+            "extraSpacing",
+            64.0
+    );
+
+    private final StyleableObjectProperty<Pos> alignment = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.ALIGNMENT,
+            this,
+            "alignment",
+            Pos.CENTER
+    );
+
+    private final StyleableObjectProperty<Paint> baseColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.BASE_COLOR,
+            this,
+            "baseColor",
+            Color.web("#7F0FFF")
+    );
+
+    private final StyleableObjectProperty<Paint> altColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.ALT_COLOR,
+            this,
+            "altColor",
+            Color.web("BEBEBE")
+    );
+
+    private final StyleableDoubleProperty progressBarBorderRadius = new SimpleStyleableDoubleProperty(
+            StyleableProperties.BORDER_RADIUS,
+            this,
+            "progressBarBorderRadius",
+            7.0
+    );
+
+    private final StyleableObjectProperty<Paint> progressBarBackground = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.PROGRESS_BAR_BACKGROUND,
+            this,
+            "progressBarBackground",
+            Color.web("#F8F8FF")
+    );
+
+    private final StyleableObjectProperty<Paint> progressColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.PROGRESS_COLOR,
+            this,
+            "progressColor",
+            Color.web("#7F0FFF")
+    );
+
+    private final StyleableBooleanProperty animated = new SimpleStyleableBooleanProperty(
+            StyleableProperties.PROGRESS_BAR_ANIMATED,
+            this,
+            "animated",
+            true
+    );
+
+    public double getSpacing() {
+        return spacing.get();
+    }
+
+    /**
+     * Specifies the spacing between toggles.
+     */
+    public StyleableDoubleProperty spacingProperty() {
+        return spacing;
+    }
+
+    public void setSpacing(double spacing) {
+        this.spacing.set(spacing);
+    }
+
+    public double getExtraSpacing() {
+        return extraSpacing.get();
+    }
+
+    /**
+     * Specifies the extra length (at the start and at the end) of the progress bar.
+     */
+    public StyleableDoubleProperty extraSpacingProperty() {
+        return extraSpacing;
+    }
+
+    public void setExtraSpacing(double extraSpacing) {
+        this.extraSpacing.set(extraSpacing);
+    }
+
+    public Pos getAlignment() {
+        return alignment.get();
+    }
+
+    /**
+     * Specifies the alignment of the toggles. Steppers are usually centered though.
+     */
+    public StyleableObjectProperty<Pos> alignmentProperty() {
+        return alignment;
+    }
+
+    public void setAlignment(Pos alignment) {
+        this.alignment.set(alignment);
+    }
+
+    public Paint getBaseColor() {
+        return baseColor.get();
+    }
+
+    /**
+     * Specifies the base color of the stepper.
+     * <p>
+     * In the default CSS file this property affects: the progress color,
+     * the buttons background and borders when focused, also used for the buttons' ripple generators.
+     * <p>
+     * In the {@link MFXStepperToggle} CSS file this property affects:
+     * <p> - State NONE: the icon color
+     * <p> - State SELECTED: the background and borders, the label's text fill
+     * <p> - State COMPLETED: the borders
+     * <p></p>
+     * Default is: #7F0FFF (purple)
+     */
+    public StyleableObjectProperty<Paint> baseColorProperty() {
+        return baseColor;
+    }
+
+    public void setBaseColor(Paint baseColor) {
+        this.baseColor.set(baseColor);
+    }
+
+    public Paint getAltColor() {
+        return altColor.get();
+    }
+
+    /**
+     * Specifies the secondary color of the stepper.
+     * <p>
+     * In the {@link MFXStepperToggle} CSS file this property affects:
+     * <p> - State NONE: the label's text fill
+     * <p> - State COMPLETED: the icon's color, the label's text fill
+     * <p></p>
+     * Default is: #BEBEBE (a light gray)
+     */
+    public StyleableObjectProperty<Paint> altColorProperty() {
+        return altColor;
+    }
+
+    public void setAltColor(Paint altColor) {
+        this.altColor.set(altColor);
+    }
+
+    public double getProgressBarBorderRadius() {
+        return progressBarBorderRadius.get();
+    }
+
+    /**
+     * Specifies the borders radius of the progress bar.
+     */
+    public StyleableDoubleProperty progressBarBorderRadiusProperty() {
+        return progressBarBorderRadius;
+    }
+
+    public void setProgressBarBorderRadius(double progressBarBorderRadius) {
+        this.progressBarBorderRadius.set(progressBarBorderRadius);
+    }
+
+    public Paint getProgressBarBackground() {
+        return progressBarBackground.get();
+    }
+
+    /**
+     * Specifies the progress bar background color (NOT THE PROGRESS COLOR).
+     *
+     * <p></p>
+     * In the {@link MFXStepperToggle} CSS file this property affects:
+     * <p> - State NONE: the toggle's background and borders. This is because
+     * if the toggle's background is transparent you will see the progress bar underneath them
+     * <p></p>
+     * Default is: #F8F8FF (almost white)
+     */
+    public StyleableObjectProperty<Paint> progressBarBackgroundProperty() {
+        return progressBarBackground;
+    }
+
+    public void setProgressBarBackground(Paint progressBarBackground) {
+        this.progressBarBackground.set(progressBarBackground);
+    }
+
+    public Paint getProgressColor() {
+        return progressColor.get();
+    }
+
+    /**
+     * Specifies the progress color.
+     * <p></p>
+     * Default is: #7F0FFF (it's set to be the same as the base color)
+     */
+    public StyleableObjectProperty<Paint> progressColorProperty() {
+        return progressColor;
+    }
+
+    public void setProgressColor(Paint progressColor) {
+        this.progressColor.set(progressColor);
+    }
+
+    public boolean isAnimated() {
+        return animated.get();
+    }
+
+    /**
+     * Specifies if the progress bar should be animated or not.
+     */
+    public StyleableBooleanProperty animatedProperty() {
+        return animated;
+    }
+
+    public void setAnimated(boolean animated) {
+        this.animated.set(animated);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXStepper, Number> SPACING =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-spacing",
+                        MFXStepper::spacingProperty,
+                        128.0
+                );
+
+        private static final CssMetaData<MFXStepper, Number> EXTRA_SPACING =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-extra-spacing",
+                        MFXStepper::extraSpacingProperty,
+                        64.0
+                );
+
+        private static final CssMetaData<MFXStepper, Pos> ALIGNMENT =
+                FACTORY.createEnumCssMetaData(
+                        Pos.class,
+                        "-mfx-alignment",
+                        MFXStepper::alignmentProperty,
+                        Pos.CENTER
+                );
+
+        private static final CssMetaData<MFXStepper, Paint> BASE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-base-color",
+                        MFXStepper::baseColorProperty,
+                        Color.web("7F0FFF")
+                );
+
+        private static final CssMetaData<MFXStepper, Paint> ALT_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-alt-color",
+                        MFXStepper::altColorProperty,
+                        Color.web("BEBEBE")
+                );
+
+        private static final CssMetaData<MFXStepper, Number> BORDER_RADIUS =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-bar-borders-radius",
+                        MFXStepper::progressBarBorderRadiusProperty,
+                        7.0
+                );
+
+        private static final CssMetaData<MFXStepper, Paint> PROGRESS_BAR_BACKGROUND =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-bar-background",
+                        MFXStepper::progressBarBackgroundProperty,
+                        Color.web("#F8F8FF")
+                );
+
+        private static final CssMetaData<MFXStepper, Paint> PROGRESS_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-progress-color",
+                        MFXStepper::progressColorProperty,
+                        Color.web("#7F0FFF")
+                );
+
+        private static final CssMetaData<MFXStepper, Boolean> PROGRESS_BAR_ANIMATED =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-bar-animated",
+                        MFXStepper::animatedProperty,
+                        true
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    SPACING, EXTRA_SPACING, ALIGNMENT, BASE_COLOR, ALT_COLOR,
+                    BORDER_RADIUS, PROGRESS_BAR_BACKGROUND, PROGRESS_COLOR, PROGRESS_BAR_ANIMATED
+            );
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXStepperSkin(this);
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXStepper.getControlCssMetaDataList();
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    //================================================================================
+    // Events
+    //================================================================================
+
+    /**
+     * Events class for MFXSteppers.
+     * <p>
+     * Defines four new EventTypes:
+     * <p>
+     * - NEXT_EVENT: when the {@link MFXStepper#next()} method is called and the index property is updated. <p></p>
+     * - PREVIOUS_EVENT: when the {@link MFXStepper#previous()} method is called and the index property is updated. <p></p>
+     * - LAST_NEXT_EVENT: when the {@link MFXStepper#next()} method is called and the last toggle is selected/already reached. <p></p>
+     * - VALIDATION_FAILED_EVENT: when the {@link MFXStepper#next()} method is called and the validator's state is invalid. <p></p>
+     * <p>
+     *  These events are automatically fired by the control so they should not be fired by users.
+     */
+    public static class MFXStepperEvent extends Event {
+
+        public static final EventType<MFXStepperEvent> NEXT_EVENT = new EventType<>(ANY, "NEXT_EVENT");
+        public static final EventType<MFXStepperEvent> PREVIOUS_EVENT = new EventType<>(ANY, "PREVIOUS_EVENT");
+        public static final EventType<MFXStepperEvent> LAST_NEXT_EVENT = new EventType<>(ANY, "LAST_NEXT_EVENT");
+        public static final EventType<MFXStepperEvent> VALIDATION_FAILED_EVENT = new EventType<>(ANY, "VALIDATION_FAILED_EVENT");
+
+        public MFXStepperEvent(EventType<? extends Event> eventType) {
+            super(eventType);
+        }
+    }
+
+    private final ObjectProperty<EventHandler<MFXStepperEvent>> onNext = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            setEventHandler(MFXStepperEvent.NEXT_EVENT, get());
+        }
+    };
+
+    private final ObjectProperty<EventHandler<MFXStepperEvent>> onPrevious = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            setEventHandler(MFXStepperEvent.PREVIOUS_EVENT, get());
+        }
+    };
+
+    private final ObjectProperty<EventHandler<MFXStepperEvent>> onLastNext = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            setEventHandler(MFXStepperEvent.LAST_NEXT_EVENT, get());
+        }
+    };
+
+    private final ObjectProperty<EventHandler<MFXStepperEvent>> onValidationFailed = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            setEventHandler(MFXStepperEvent.VALIDATION_FAILED_EVENT, get());
+        }
+    };
+
+    public EventHandler<MFXStepperEvent> getOnNext() {
+        return onNext.get();
+    }
+
+    /**
+     * Specifies the action to perform when a {@link MFXStepperEvent#NEXT_EVENT} is fired.
+     *
+     * @see MFXStepperEvent
+     */
+    public ObjectProperty<EventHandler<MFXStepperEvent>> onNextProperty() {
+        return onNext;
+    }
+
+    public void setOnNext(EventHandler<MFXStepperEvent> onNext) {
+        this.onNext.set(onNext);
+    }
+
+    public EventHandler<MFXStepperEvent> getOnPrevious() {
+        return onPrevious.get();
+    }
+
+    /**
+     * Specifies the action to perform when a {@link MFXStepperEvent#PREVIOUS_EVENT} is fired.
+     *
+     * @see MFXStepperEvent
+     */
+    public ObjectProperty<EventHandler<MFXStepperEvent>> onPreviousProperty() {
+        return onPrevious;
+    }
+
+    public void setOnPrevious(EventHandler<MFXStepperEvent> onPrevious) {
+        this.onPrevious.set(onPrevious);
+    }
+
+    public EventHandler<MFXStepperEvent> getOnLastNext() {
+        return onLastNext.get();
+    }
+
+    /**
+     * Specifies the action to perform when a {@link MFXStepperEvent#LAST_NEXT_EVENT} is fired.
+     *
+     * @see MFXStepperEvent
+     */
+    public ObjectProperty<EventHandler<MFXStepperEvent>> onLastNextProperty() {
+        return onLastNext;
+    }
+
+    public void setOnLastNext(EventHandler<MFXStepperEvent> onLastNext) {
+        this.onLastNext.set(onLastNext);
+    }
+
+    public EventHandler<MFXStepperEvent> getOnValidationFailed() {
+        return onValidationFailed.get();
+    }
+
+    /**
+     * Specifies the action to perform when a {@link MFXStepperEvent#VALIDATION_FAILED_EVENT} is fired.
+     *
+     * @see MFXStepperEvent
+     */
+    public ObjectProperty<EventHandler<MFXStepperEvent>> onValidationFailedProperty() {
+        return onValidationFailed;
+    }
+
+    public void setOnValidationFailed(EventHandler<MFXStepperEvent> onValidationFailed) {
+        this.onValidationFailed.set(onValidationFailed);
+    }
+
+    /**
+     * Convenience method to fire {@link MFXStepperEvent} events.
+     */
+    public void fireEvent(EventType<MFXStepperEvent> eventType) {
+        fireEvent(new MFXStepperEvent(eventType));
+    }
+}

+ 481 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepperToggle.java

@@ -0,0 +1,481 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.enums.StepperToggleState;
+import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.skins.MFXStepperSkin;
+import io.github.palexdev.materialfx.skins.MFXStepperToggleSkin;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import io.github.palexdev.materialfx.validation.base.Validated;
+import javafx.beans.property.*;
+import javafx.css.*;
+import javafx.event.Event;
+import javafx.event.EventType;
+import javafx.geometry.Bounds;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A {@code MFXStepperToggle} is a special toggle that has 4 possible states.
+ * <p> In a {@link MFXStepper} these states are used as follows:
+ * <p> - NONE (when initialized)
+ * <p> - SELECTED (self explanatory)
+ * <p> - ERROR (when the validator's state is invalid)
+ * <p> - COMPLETED (when the validator's state in valid and the stepper goes to the next toggle)
+ * <p></p>
+ * Every {@code MFXStepperToggle} has an icon, and a text which will be displayed in a label above or below the toggle
+ * depending on the value of {@link #textPositionProperty()}.
+ * <p>
+ * They also specify the content to be shown in the {@link MFXStepper} when the toggle is selected, the content
+ * can be any {@code Node}.
+ * <p></p>
+ * This control specifies three new PseudoClasses: ":selected", ":completed", ":error" to specify a different style in css
+ * for each state.
+ * <p></p>
+ * This is a {@link Validated} control, that means you can specify certain conditions or dependencies
+ * that must be met in order for the state to be COMPLETED and for the {@link MFXStepper} to go to the next toggle.
+ * <p>
+ * A little note on the usage of the Validation API:
+ * <p>
+ * Pay attention to the difference between "condition" and "dependency" which is further explained by {@link AbstractMFXValidator}.
+ * <p>
+ * If the content needs validation or has some controls that need validation and it is done by a {@link AbstractMFXValidator}, or any subclass, then
+ * the content/controls' validators should be added to the toggle as dependencies using {@link AbstractMFXValidator#addDependencies(AbstractMFXValidator...)}.
+ * <p>
+ * If the validation is not done by a {@link AbstractMFXValidator} then the needed validation is a "condition" and should be added to the toggle using
+ * {@link MFXDialogValidator#add(BooleanProperty, String)}.
+ *
+ */
+public class MFXStepperToggle extends Control implements Validated<MFXDialogValidator> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXStepperToggle> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-stepper-toggle";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-steppertoggle.css");
+
+    private MFXDialogValidator validator;
+    private final BooleanProperty showErrorIcon = new SimpleBooleanProperty(true);
+
+    private Node content;
+    private final StringProperty text = new SimpleStringProperty();
+    private final ObjectProperty<Node> icon = new SimpleObjectProperty<>();
+    private final ObjectProperty<StepperToggleState> state = new SimpleObjectProperty<>(StepperToggleState.NONE);
+
+    protected static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
+    protected static final PseudoClass COMPLETED_PSEUDO_CLASS = PseudoClass.getPseudoClass("completed");
+    protected static final PseudoClass ERROR_PSEUDO_CLASS = PseudoClass.getPseudoClass("error");
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXStepperToggle() {
+        this("", null);
+    }
+
+    public MFXStepperToggle(String text) {
+        this(text, null);
+    }
+
+    public MFXStepperToggle(String text, Node icon) {
+        this(text, icon, null);
+    }
+
+    public MFXStepperToggle(String text, Node icon, Node content) {
+        setText(text);
+        setIcon(icon);
+        this.content = content;
+        initialize();
+    }
+
+    //================================================================================
+    // Validation
+    //================================================================================
+    public MFXStepperToggle installValidator(Supplier<MFXDialogValidator> validatorSupplier) {
+        if (validatorSupplier == null) {
+            throw new IllegalArgumentException("The supplier cannot be null!");
+        }
+        this.validator = validatorSupplier.get();
+        return this;
+    }
+
+    /**
+     * Configures the validator.
+     * <p>
+     * By default the {@link AbstractMFXValidator#isInitControlValidation()} flag is false
+     * so the toggle state is set to ERROR (if the validator's state is invalid of course) when the
+     * {@link MFXStepper} attempts to go to the next toggle.
+     */
+    protected void setupValidator() {
+        validator = new MFXDialogValidator("You can't proceed because of the following errors:");
+    }
+
+    @Override
+    public MFXDialogValidator getValidator() {
+        return validator;
+    }
+
+    /**
+     * Delegate method to get the validator's title.
+     */
+    public String getValidatorTitle() {
+        return validator.getTitle();
+    }
+
+    /**
+     * Delegate method to get the validator's title property.
+     */
+    public StringProperty validatorTitleProperty() {
+        return validator.titleProperty();
+    }
+
+    /**
+     * Delegate method to set the validator's title.
+     */
+    public void setValidatorTitle(String title) {
+        validator.setTitle(title);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().setAll(STYLE_CLASS);
+        setupValidator();
+        addListeners();
+    }
+
+    /**
+     * Adds the following listeners:
+     * <p> - state property to fire a STATE_CHANGED event when invalidated.
+     * <p> - state property to update the PseudoClasses when it changes.
+     */
+    private void addListeners() {
+        state.addListener(invalidated -> fireEvent(new MFXStepperToggleEvent(MFXStepperToggleEvent.STATE_CHANGED, state.get())));
+        state.addListener((observable, oldValue, newValue) -> {
+            resetPseudoClass();
+            switch (newValue) {
+                case SELECTED: {
+                    pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, true);
+                    break;
+                }
+                case COMPLETED: {
+                    pseudoClassStateChanged(COMPLETED_PSEUDO_CLASS, true);
+                    break;
+                }
+                case ERROR: {
+                    pseudoClassStateChanged(ERROR_PSEUDO_CLASS, true);
+                    break;
+                }
+                default: {
+                    resetPseudoClass();
+                }
+            }
+        });
+    }
+
+    /**
+     * Resets all state PseudoClasses to false.
+     */
+    private void resetPseudoClass() {
+        pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, false);
+        pseudoClassStateChanged(COMPLETED_PSEUDO_CLASS, false);
+        pseudoClassStateChanged(ERROR_PSEUDO_CLASS, false);
+    }
+
+    /**
+     * This method is necessary to get the bounds of the toggle's circle, which is
+     * used in the {@link MFXStepperSkin} to resize the progress bar properly.
+     */
+    public Bounds getGraphicBounds() {
+        Node node = lookup("#circle");
+        return node != null ? node.getBoundsInParent() : null;
+    }
+
+    /**
+     * @return the content to be shown in the stepper when selected
+     */
+    public Node getContent() {
+        return content;
+    }
+
+    /**
+     * Sets the content to be shown in the stepper when selected.
+     */
+    public void setContent(Node content) {
+        this.content = content;
+    }
+
+    public String getText() {
+        return text.get();
+    }
+
+    /**
+     * Specifies the text to be shown above or below the toggle.
+     */
+    public StringProperty textProperty() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text.set(text);
+    }
+
+    public Node getIcon() {
+        return icon.get();
+    }
+
+    /**
+     * Specifies the icon shown in the circle of the toggle.
+     */
+    public ObjectProperty<Node> iconProperty() {
+        return icon;
+    }
+
+    public void setIcon(Node icon) {
+        this.icon.set(icon);
+    }
+
+    public StepperToggleState getState() {
+        return state.get();
+    }
+
+    /**
+     * Specifies the state of the toggle.
+     */
+    public ObjectProperty<StepperToggleState> stateProperty() {
+        return state;
+    }
+
+    public void setState(StepperToggleState state) {
+        this.state.set(state);
+    }
+
+    public boolean isShowErrorIcon() {
+        return showErrorIcon.get();
+    }
+
+    /**
+     * Specifies if a little error icon should be shown when the state
+     * is ERROR in the upper right corner of the toggle (default position defined in the skin).
+     */
+    public BooleanProperty showErrorIconProperty() {
+        return showErrorIcon;
+    }
+
+    public void setShowErrorIcon(boolean showErrorIcon) {
+        this.showErrorIcon.set(showErrorIcon);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableDoubleProperty textGap = new SimpleStyleableDoubleProperty(
+            StyleableProperties.TEXT_GAP,
+            this,
+            "textGap",
+            10.0
+    );
+
+    private final StyleableObjectProperty<TextPosition> textPosition = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.TEXT_POSITION,
+            this,
+            "textPosition",
+            TextPosition.BOTTOM
+    );
+
+    private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
+            StyleableProperties.SIZE,
+            this,
+            "size",
+            22.0
+    );
+
+    private final StyleableDoubleProperty strokeWidth = new SimpleStyleableDoubleProperty(
+            StyleableProperties.STROKE_WIDTH,
+            this,
+            "strokeWidth",
+            2.5
+    );
+
+    public double getTextGap() {
+        return textGap.get();
+    }
+
+    /**
+     * Specifies the gap between the toggle's circle and the label.
+     */
+    public StyleableDoubleProperty textGapProperty() {
+        return textGap;
+    }
+
+    public void setTextGap(double textGap) {
+        this.textGap.set(textGap);
+    }
+
+    public TextPosition getTextPosition() {
+        return textPosition.get();
+    }
+
+
+    /**
+     * Specifies the position of the label.
+     */
+    public StyleableObjectProperty<TextPosition> textPositionProperty() {
+        return textPosition;
+    }
+
+    public void setTextPosition(TextPosition textPosition) {
+        this.textPosition.set(textPosition);
+    }
+
+    public double getSize() {
+        return size.get();
+    }
+
+    /**
+     * Specifies the radius of the toggle's circle.
+     */
+    public StyleableDoubleProperty sizeProperty() {
+        return size;
+    }
+
+    public void setSize(double size) {
+        this.size.set(size);
+    }
+
+    public double getStrokeWidth() {
+        return strokeWidth.get();
+    }
+
+    /**
+     * Specifies the stroke width of the toggle's circle.
+     */
+    public StyleableDoubleProperty strokeWidthProperty() {
+        return strokeWidth;
+    }
+
+    public void setStrokeWidth(double strokeWidth) {
+        this.strokeWidth.set(strokeWidth);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXStepperToggle, Number> TEXT_GAP =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-text-gap",
+                        MFXStepperToggle::textGapProperty,
+                        10.0
+                );
+
+        private static final CssMetaData<MFXStepperToggle, TextPosition> TEXT_POSITION =
+                FACTORY.createEnumCssMetaData(
+                        TextPosition.class,
+                        "-mfx-text-position",
+                        MFXStepperToggle::textPositionProperty,
+                        TextPosition.BOTTOM
+                );
+
+        private static final CssMetaData<MFXStepperToggle, Number> SIZE =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-size",
+                        MFXStepperToggle::sizeProperty,
+                        22.0
+                );
+
+        private static final CssMetaData<MFXStepperToggle, Number> STROKE_WIDTH =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-stroke-width",
+                        MFXStepperToggle::strokeWidthProperty,
+                        2.5
+                );
+
+        static {
+            cssMetaDataList = List.of(TEXT_GAP, TEXT_POSITION, SIZE, STROKE_WIDTH);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXStepperToggleSkin(this);
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXStepperToggle.getControlCssMetaDataList();
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    //================================================================================
+    // Events
+    //================================================================================
+
+    /**
+     * Events class for MFXStepperToggles.
+     * <p>
+     * Defines a new EventType:
+     * <p>
+     * - STATE_CHANGED: when the {@link MFXStepperToggle#stateProperty()} ()} changes. <p></p>
+     * <p>
+     * One of the constructors requires to specify the new toggle's state, the class has also a getter for the
+     * state.
+     * <p></p>
+     *  These events are automatically fired by the control so they should not be fired by users.
+     */
+    public static class MFXStepperToggleEvent extends Event {
+        private StepperToggleState state;
+
+        public static final EventType<MFXStepperToggleEvent> STATE_CHANGED = new EventType<>(ANY, "STATE_CHANGED");
+
+        public MFXStepperToggleEvent(EventType<? extends Event> eventType) {
+            super(eventType);
+        }
+
+        public MFXStepperToggleEvent(EventType<? extends Event> eventType, StepperToggleState state) {
+            super(eventType);
+            this.state = state;
+        }
+
+        public StepperToggleState getState() {
+            return state;
+        }
+    }
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java

@@ -166,7 +166,7 @@ public class MFXTableView<T> extends Control {
     }
 
     //================================================================================
-    // Events Class
+    // Events
     //================================================================================
 
     /**

+ 139 - 47
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java

@@ -19,25 +19,31 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.skins.MFXTextFieldSkin;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import io.github.palexdev.materialfx.validation.base.Validated;
+import javafx.beans.property.StringProperty;
 import javafx.css.*;
 import javafx.scene.control.Skin;
 import javafx.scene.control.TextField;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
+import javafx.scene.shape.StrokeLineCap;
 
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * This is the implementation of a TextField restyled to comply with modern standards.
- * <p>
+ * <p></p>
  * Extends {@code TextField}, redefines the style class to "mfx-text-field" for usage in CSS and
  * includes a {@code MFXDialogValidator} for input validation.
- * <p>
- * <b>Note: validator conditions are empty by default</b>
+ * <p></p>
+ * Defines a new PseudoClass: ":invalid" to specify the control's look when the validator's state is invalid.
  */
-public class MFXTextField extends TextField {
+public class MFXTextField extends TextField implements Validated<MFXDialogValidator> {
     //================================================================================
     // Properties
     //================================================================================
@@ -46,6 +52,7 @@ public class MFXTextField extends TextField {
     private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-textfield.css");
 
     private MFXDialogValidator validator;
+    protected static final PseudoClass INVALID_PSEUDO_CLASS = PseudoClass.getPseudoClass("invalid");
 
     //================================================================================
     // Constructors
@@ -59,6 +66,72 @@ public class MFXTextField extends TextField {
         initialize();
     }
 
+    //================================================================================
+    // Validation
+    //================================================================================
+
+    /**
+     * Configures the validator. The first time the error label can appear in two cases:
+     * <p></p>
+     * 1) The validator {@link AbstractMFXValidator#isInitControlValidation()} flag is true,
+     * in this case as soon as the control is laid out in the scene the label visible property is
+     * set accordingly to the validator state. (by default is false) <p>
+     * 2) When the control lose the focus and the the validator's state is invalid.
+     * <p></p>
+     * Then the label visible property is automatically updated when the validator state changes.
+     * <p></p>
+     * The validator is also responsible for updating the ":invalid" pseudo class.
+     */
+    private void setupValidator() {
+        validator = new MFXDialogValidator("Error");
+        validator.setDialogType(DialogType.ERROR);
+        validator.validProperty().addListener(invalidated -> pseudoClassStateChanged(INVALID_PSEUDO_CLASS, !isValid()));
+
+        sceneProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null)
+                if (getValidator().isInitControlValidation()) {
+                    pseudoClassStateChanged(INVALID_PSEUDO_CLASS, !isValid());
+                } else {
+                    pseudoClassStateChanged(INVALID_PSEUDO_CLASS, false);
+                }
+        });
+    }
+
+    @Override
+    public MFXTextField installValidator(Supplier<MFXDialogValidator> validatorSupplier) {
+        this.validator = validatorSupplier.get();
+        return this;
+    }
+
+    /**
+     * Returns the validator instance of this control.
+     */
+    @Override
+    public MFXDialogValidator getValidator() {
+        return validator;
+    }
+
+    /**
+     * Delegate method to get the validator's title.
+     */
+    public String getValidatorTitle() {
+        return validator.getTitle();
+    }
+
+    /**
+     * Delegate method to get the validator's title property.
+     */
+    public StringProperty validatorTitleProperty() {
+        return validator.titleProperty();
+    }
+
+    /**
+     * Delegate method to set the validator's title.
+     */
+    public void setValidatorTitle(String title) {
+        validator.setTitle(title);
+    }
+
     //================================================================================
     // Methods
     //================================================================================
@@ -79,28 +152,9 @@ public class MFXTextField extends TextField {
         });
     }
 
-    /**
-     * Configures the validator. If {@link #isValidated()} is true, by default shows a warning
-     * if no item is selected. The warning is showed as soon as the control is out of focus.
-     */
-    private void setupValidator() {
-        validator = new MFXDialogValidator("Warning");
-    }
-
-    /**
-     * Returns the validator instance of this control.
-     */
-    public MFXDialogValidator getValidator() {
-        return validator;
-    }
-
     //================================================================================
     // Styleable Properties
     //================================================================================
-
-    /**
-     * Specifies the maximum text length.
-     */
     private final StyleableIntegerProperty textLimit = new SimpleStyleableIntegerProperty(
             StyleableProperties.TEXT_LIMIT,
             this,
@@ -108,9 +162,6 @@ public class MFXTextField extends TextField {
             -1
     );
 
-    /**
-     * Specifies the line's color when the control is focused.
-     */
     private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.LINE_COLOR,
             this,
@@ -118,9 +169,6 @@ public class MFXTextField extends TextField {
             Color.rgb(50, 120, 220)
     );
 
-    /**
-     * Specifies the line's color when the control is not focused.
-     */
     private final StyleableObjectProperty<Paint> unfocusedLineColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.UNFOCUSED_LINE_COLOR,
             this,
@@ -128,19 +176,20 @@ public class MFXTextField extends TextField {
             Color.rgb(77, 77, 77)
     );
 
-    /**
-     * Specifies the lines' stroke width.
-     */
     private final StyleableDoubleProperty lineStrokeWidth = new SimpleStyleableDoubleProperty(
             StyleableProperties.LINE_STROKE_WIDTH,
             this,
             "lineStrokeWidth",
-            1.0
+            2.0
+    );
+
+    private final StyleableObjectProperty<StrokeLineCap> lineStrokeCap = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_STROKE_CAP,
+            this,
+            "lineStrokeCap",
+            StrokeLineCap.ROUND
     );
 
-    /**
-     * Specifies if the lines switch between focus/un-focus should be animated.
-     */
     private final StyleableBooleanProperty animateLines = new SimpleStyleableBooleanProperty(
             StyleableProperties.ANIMATE_LINES,
             this,
@@ -148,9 +197,6 @@ public class MFXTextField extends TextField {
             true
     );
 
-    /**
-     * Specifies if validation is required for the control.
-     */
     private final StyleableBooleanProperty validated = new SimpleStyleableBooleanProperty(
             StyleableProperties.IS_VALIDATED,
             this,
@@ -162,6 +208,9 @@ public class MFXTextField extends TextField {
         return textLimit.get();
     }
 
+    /**
+     * Specifies the maximum text length.
+     */
     public StyleableIntegerProperty textLimitProperty() {
         return textLimit;
     }
@@ -174,6 +223,9 @@ public class MFXTextField extends TextField {
         return lineColor.get();
     }
 
+    /**
+     * Specifies the line's color when the control is focused.
+     */
     public StyleableObjectProperty<Paint> lineColorProperty() {
         return lineColor;
     }
@@ -186,6 +238,9 @@ public class MFXTextField extends TextField {
         return unfocusedLineColor.get();
     }
 
+    /**
+     * Specifies the line's color when the control is not focused.
+     */
     public StyleableObjectProperty<Paint> unfocusedLineColorProperty() {
         return unfocusedLineColor;
     }
@@ -198,6 +253,9 @@ public class MFXTextField extends TextField {
         return lineStrokeWidth.get();
     }
 
+    /**
+     * Specifies the lines' stroke width.
+     */
     public StyleableDoubleProperty lineStrokeWidthProperty() {
         return lineStrokeWidth;
     }
@@ -206,10 +264,28 @@ public class MFXTextField extends TextField {
         this.lineStrokeWidth.set(lineStrokeWidth);
     }
 
+    public StrokeLineCap getLineStrokeCap() {
+        return lineStrokeCap.get();
+    }
+
+    /**
+     * Specifies the lines' stroke cap.
+     */
+    public StyleableObjectProperty<StrokeLineCap> lineStrokeCapProperty() {
+        return lineStrokeCap;
+    }
+
+    public void setLineStrokeCap(StrokeLineCap lineStrokeCap) {
+        this.lineStrokeCap.set(lineStrokeCap);
+    }
+
     public boolean isAnimateLines() {
         return animateLines.get();
     }
 
+    /**
+     * Specifies if the lines switch between focus/un-focus should be animated.
+     */
     public StyleableBooleanProperty animateLinesProperty() {
         return animateLines;
     }
@@ -222,12 +298,15 @@ public class MFXTextField extends TextField {
         return validated.get();
     }
 
+    /**
+     * Specifies if validation is required for the control.
+     */
     public StyleableBooleanProperty isValidatedProperty() {
         return validated;
     }
 
-    public void setIsValidated(boolean isValidated) {
-        this.validated.set(isValidated);
+    public void setValidated(boolean validated) {
+        this.validated.set(validated);
     }
 
     //================================================================================
@@ -261,7 +340,15 @@ public class MFXTextField extends TextField {
                 FACTORY.createSizeCssMetaData(
                         "-mfx-line-stroke-width",
                         MFXTextField::lineStrokeWidthProperty,
-                        1.0
+                        2.0
+                );
+
+        private static final CssMetaData<MFXTextField, StrokeLineCap> LINE_STROKE_CAP =
+                FACTORY.createEnumCssMetaData(
+                        StrokeLineCap.class,
+                        "-mfx-line-stroke-cap",
+                        MFXTextField::lineStrokeCapProperty,
+                        StrokeLineCap.ROUND
                 );
 
         private static final CssMetaData<MFXTextField, Boolean> ANIMATE_LINES =
@@ -279,7 +366,12 @@ public class MFXTextField extends TextField {
                 );
 
         static {
-            cssMetaDataList = List.of(TEXT_LIMIT, LINE_COLOR, UNFOCUSED_LINE_COLOR, LINE_STROKE_WIDTH, IS_VALIDATED);
+            cssMetaDataList = List.of(
+                    TEXT_LIMIT,
+                    LINE_COLOR, UNFOCUSED_LINE_COLOR,
+                    LINE_STROKE_WIDTH, LINE_STROKE_CAP,
+                    IS_VALIDATED
+            );
         }
 
     }
@@ -297,12 +389,12 @@ public class MFXTextField extends TextField {
     }
 
     @Override
-    public String getUserAgentStylesheet() {
-        return STYLESHEET;
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXTextField.getControlCssMetaDataList();
     }
 
     @Override
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return MFXTextField.getControlCssMetaDataList();
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
     }
 }

+ 5 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleButton.java

@@ -35,7 +35,7 @@ import java.util.List;
  * This is the implementation of a toggle button following Google's material design guidelines in JavaFX.
  * <p>
  * Extends {@code ToggleButton}, redefines the style class to "mfx-toggle-button" for usage in CSS and
- * includes a {@code RippleGenerator}(in the Skin) to generate ripple effect when toggled/untoggled.
+ * includes a {@code RippleGenerator}(in the Skin) to generate ripple effect when toggled/un-toggled.
  */
 public class MFXToggleButton extends ToggleButton {
     //================================================================================
@@ -94,7 +94,7 @@ public class MFXToggleButton extends ToggleButton {
     );
 
     /**
-     * Specifies the color of the "circle" when untoggled.
+     * Specifies the color of the "circle" when un-toggled.
      *
      * @see Color
      */
@@ -118,7 +118,7 @@ public class MFXToggleButton extends ToggleButton {
     );
 
     /**
-     * Specifies the color of the line when untoggled.
+     * Specifies the color of the line when un-toggled.
      *
      * @see Color
      */
@@ -142,9 +142,9 @@ public class MFXToggleButton extends ToggleButton {
     );
 
     /**
-     * When this is set to true and toggle color is changed the untoggle color is automatically adjusted.
+     * When this is set to true and toggle color is changed the un-toggle color is automatically adjusted.
      * <p>
-     * NOTE: This works only if changing toggle color, if untoggle color is changed the toggle color won't be automatically adjusted.
+     * NOTE: This works only if changing toggle color, if un-toggle color is changed the toggle color won't be automatically adjusted.
      * You can see this behavior in the demo.
      */
     private final BooleanProperty automaticColorAdjustment = new SimpleBooleanProperty(false);

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java

@@ -51,7 +51,7 @@ public class MFXToggleNode extends ToggleButton {
     // Properties
     //================================================================================
     private static final StyleablePropertyFactory<MFXToggleNode> FACTORY = new StyleablePropertyFactory<>(ToggleButton.getClassCssMetaData());
-    private final String STYLECLASS = "mfx-toggle-node";
+    private final String STYLE_CLASS = "mfx-toggle-node";
     private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-togglenode.css");
     protected final RippleGenerator rippleGenerator = new RippleGenerator(this);
 
@@ -82,7 +82,7 @@ public class MFXToggleNode extends ToggleButton {
     // Methods
     //================================================================================
     private void initialize() {
-        getStyleClass().add(STYLECLASS);
+        getStyleClass().add(STYLE_CLASS);
         setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
 

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeItem.java

@@ -327,7 +327,7 @@ public class MFXTreeItem<T> extends AbstractMFXTreeItem<T> {
     }
 
     //================================================================================
-    // Events Class
+    // Events
     //================================================================================
 
     /**

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXDialog.java

@@ -51,7 +51,7 @@ public abstract class AbstractMFXDialog extends BorderPane {
 
     protected final StringProperty title = new SimpleStringProperty("");
     protected final StringProperty content = new SimpleStringProperty("");
-    protected BooleanProperty centerBeforeShow = new SimpleBooleanProperty(true);
+    protected final BooleanProperty centerBeforeShow = new SimpleBooleanProperty(true);
     protected final List<Node> closeButtons = new ArrayList<>();
 
     protected final MFXScrimEffect scrimEffect = new MFXScrimEffect();

+ 1 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListView.java

@@ -149,14 +149,10 @@ public abstract class AbstractMFXFlowlessListView<T, C extends AbstractMFXFlowle
         final double[] derivatives = new double[frictions.length];
 
         Timeline timeline = new Timeline();
-        final EventHandler<MouseEvent> dragHandler = event -> {
-            System.out.println("STOP!");
-            timeline.stop();
-        };
+        final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
 
         final EventHandler<ScrollEvent> scrollHandler = event -> {
             if (event.getEventType() == ScrollEvent.SCROLL) {
-                System.out.println("Smooth Scrolling");
                 int direction = event.getDeltaY() > 0 ? -1 : 1;
                 for (int i = 0; i < pushes.length; i++) {
                     derivatives[i] += direction * pushes[i];

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessCheckListCell.java

@@ -44,7 +44,7 @@ public class MFXFlowlessCheckListCell<T> extends AbstractMFXFlowlessListCell<T>
     // Properties
     //================================================================================
     private final String STYLE_CLASS = "mfx-check-list-cell";
-    private final String STYLESHHET = MFXResourcesLoader.load("css/mfx-flowless-check-listcell.css");
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-flowless-check-listcell.css");
     protected final RippleGenerator rippleGenerator = new RippleGenerator(this);
 
     private final MFXFlowlessCheckListView<T> listView;
@@ -240,7 +240,7 @@ public class MFXFlowlessCheckListCell<T> extends AbstractMFXFlowlessListCell<T>
 
     @Override
     public String getUserAgentStylesheet() {
-        return STYLESHHET;
+        return STYLESHEET;
     }
 
     @Override

+ 31 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/StepperToggleState.java

@@ -0,0 +1,31 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.enums;
+
+import io.github.palexdev.materialfx.controls.MFXStepperToggle;
+
+/**
+ * Enumerator to represent the state of a {@link MFXStepperToggle}
+ */
+public enum StepperToggleState {
+    SELECTED,
+    COMPLETED,
+    ERROR,
+    NONE
+}

+ 24 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/TextPosition.java

@@ -0,0 +1,24 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.enums;
+
+public enum TextPosition {
+    TOP,
+    BOTTOM
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/RippleClipTypeFactory.java

@@ -28,7 +28,7 @@ import javafx.scene.shape.Shape;
  * Convenience class for building Ripple clip shapes.
  */
 public class RippleClipTypeFactory {
-    private RippleClipType rippleClipType = RippleClipType.NOCLIP;
+    private RippleClipType rippleClipType = RippleClipType.NO_CLIP;
     private double arcW = 0;
     private double arcH = 0;
 

+ 131 - 43
materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java

@@ -20,11 +20,14 @@ package io.github.palexdev.materialfx.controls.legacy;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
+import io.github.palexdev.materialfx.controls.MFXComboBox;
 import io.github.palexdev.materialfx.controls.cell.MFXListCell;
+import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.skins.legacy.MFXLegacyComboBoxSkin;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import io.github.palexdev.materialfx.validation.base.Validated;
+import javafx.beans.property.StringProperty;
 import javafx.collections.ObservableList;
 import javafx.css.*;
 import javafx.scene.SnapshotParameters;
@@ -35,16 +38,19 @@ import javafx.scene.control.Skin;
 import javafx.scene.image.WritableImage;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
+import javafx.scene.shape.StrokeLineCap;
 
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * This is a restyle of the JavaFX's combo box.
  * <p>
- * For a combo box which more closely follows the guidelines of material design see {@link io.github.palexdev.materialfx.controls.MFXComboBox}.
+ * For a combo box which more closely follows the guidelines of material design see {@link MFXComboBox}.
  * <p>
  * Extends {@code ComboBox}, redefines the style class to "mfx-legacy-combo-box" for usage in CSS and
- * includes a {@link MFXDialogValidator}.
+ * includes a {@link MFXDialogValidator}. Also, introduces a new PseudoClass ":invalid" to specify
+ * the control's look when the validation fails.
  * <p></p>
  * A few notes on features and usage:
  * <p>
@@ -61,7 +67,7 @@ import java.util.List;
  *
  * @see MFXSnapshotWrapper
  */
-public class MFXLegacyComboBox<T> extends ComboBox<T> {
+public class MFXLegacyComboBox<T> extends ComboBox<T> implements Validated<MFXDialogValidator> {
     //================================================================================
     // Properties
     //================================================================================
@@ -70,6 +76,7 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
     private final String STYLESHEET = MFXResourcesLoader.load("css/legacy/mfx-combobox.css");
 
     private MFXDialogValidator validator;
+    protected static final PseudoClass INVALID_PSEUDO_CLASS = PseudoClass.getPseudoClass("invalid");
 
     //================================================================================
     // Constructors
@@ -83,6 +90,72 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         initialize();
     }
 
+    //================================================================================
+    // Validation
+    //================================================================================
+
+    /**
+     * Configures the validator. The first time the error label can appear in two cases:
+     * <p></p>
+     * 1) The validator {@link AbstractMFXValidator#isInitControlValidation()} flag is true,
+     * in this case as soon as the control is laid out in the scene the label visible property is
+     * set accordingly to the validator state. (by default is false) <p>
+     * 2) When the control lose the focus and the the validator's state is invalid.
+     * <p></p>
+     * Then the label visible property is automatically updated when the validator state changes.
+     * <p></p>
+     * The validator is also responsible for updating the ":invalid" pseudo class.
+     */
+    private void setupValidator() {
+        validator = new MFXDialogValidator("Error");
+        validator.setDialogType(DialogType.ERROR);
+        validator.validProperty().addListener(invalidated -> pseudoClassStateChanged(INVALID_PSEUDO_CLASS, !isValid()));
+
+        sceneProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null)
+                if (getValidator().isInitControlValidation()) {
+                    pseudoClassStateChanged(INVALID_PSEUDO_CLASS, !isValid());
+                } else {
+                    pseudoClassStateChanged(INVALID_PSEUDO_CLASS, false);
+                }
+        });
+    }
+
+    @Override
+    public MFXLegacyComboBox<T> installValidator(Supplier<MFXDialogValidator> validatorSupplier) {
+        if (validatorSupplier == null) {
+            throw new IllegalArgumentException("The supplier cannot be null!");
+        }
+        this.validator = validatorSupplier.get();
+        return this;
+    }
+
+    @Override
+    public MFXDialogValidator getValidator() {
+        return validator;
+    }
+
+    /**
+     * Delegate method to get the validator's title.
+     */
+    public String getValidatorTitle() {
+        return validator.getTitle();
+    }
+
+    /**
+     * Delegate method to get the validator's title property.
+     */
+    public StringProperty validatorTitleProperty() {
+        return validator.titleProperty();
+    }
+
+    /**
+     * Delegate method to set the validator's title.
+     */
+    public void setValidatorTitle(String title) {
+        validator.setTitle(title);
+    }
+
     //================================================================================
     // Methods
     //================================================================================
@@ -142,31 +215,9 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         }
     }
 
-    /**
-     * Configures the validator. If {@link #isValidated()} is true, by default shows a warning
-     * if no item is selected. The warning is showed as soon as the control is out of focus.
-     */
-    private void setupValidator() {
-        BooleanProperty validIndex = new SimpleBooleanProperty(false);
-        validIndex.bind(getSelectionModel().selectedIndexProperty().isNotEqualTo(-1));
-        validator = new MFXDialogValidator("Warning");
-        validator.add(validIndex, "Selected index is not valid");
-    }
-
-    /**
-     * Returns the validator instance of this control.
-     */
-    public MFXDialogValidator getValidator() {
-        return validator;
-    }
-
     //================================================================================
     // Styleable Properties
     //================================================================================
-
-    /**
-     * Specifies the line's color when the control is focused.
-     */
     private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.LINE_COLOR,
             this,
@@ -174,9 +225,6 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
             Color.rgb(50, 120, 220)
     );
 
-    /**
-     * Specifies the line's color when the control is not focused.
-     */
     private final StyleableObjectProperty<Paint> unfocusedLineColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.UNFOCUSED_LINE_COLOR,
             this,
@@ -184,19 +232,20 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
             Color.rgb(77, 77, 77)
     );
 
-    /**
-     * Specifies the lines' width.
-     */
     private final StyleableDoubleProperty lineStrokeWidth = new SimpleStyleableDoubleProperty(
             StyleableProperties.LINE_STROKE_WIDTH,
             this,
             "lineStrokeWidth",
-            1.5
+            2.0
+    );
+
+    private final StyleableObjectProperty<StrokeLineCap> lineStrokeCap = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_STROKE_CAP,
+            this,
+            "lineStrokeCap",
+            StrokeLineCap.ROUND
     );
 
-    /**
-     * Specifies if the lines switch between focus/un-focus should be animated.
-     */
     private final StyleableBooleanProperty animateLines = new SimpleStyleableBooleanProperty(
             StyleableProperties.ANIMATE_LINES,
             this,
@@ -204,9 +253,6 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
             true
     );
 
-    /**
-     * Specifies if validation is required for the control.
-     */
     private final StyleableBooleanProperty isValidated = new SimpleStyleableBooleanProperty(
             StyleableProperties.IS_VALIDATED,
             this,
@@ -218,6 +264,9 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         return lineColor.get();
     }
 
+    /**
+     * Specifies the line's color when the control is focused.
+     */
     public StyleableObjectProperty<Paint> lineColorProperty() {
         return lineColor;
     }
@@ -230,6 +279,9 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         return unfocusedLineColor.get();
     }
 
+    /**
+     * Specifies the line's color when the control is not focused.
+     */
     public StyleableObjectProperty<Paint> unfocusedLineColorProperty() {
         return unfocusedLineColor;
     }
@@ -242,6 +294,9 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         return lineStrokeWidth.get();
     }
 
+    /**
+     * Specifies the lines' stroke width.
+     */
     public StyleableDoubleProperty lineStrokeWidthProperty() {
         return lineStrokeWidth;
     }
@@ -250,10 +305,28 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         this.lineStrokeWidth.set(lineStrokeWidth);
     }
 
+    public StrokeLineCap getLineStrokeCap() {
+        return lineStrokeCap.get();
+    }
+
+    /**
+     * Specifies the lines' stroke cap.
+     */
+    public StyleableObjectProperty<StrokeLineCap> lineStrokeCapProperty() {
+        return lineStrokeCap;
+    }
+
+    public void setLineStrokeCap(StrokeLineCap lineStrokeCap) {
+        this.lineStrokeCap.set(lineStrokeCap);
+    }
+
     public boolean isAnimateLines() {
         return animateLines.get();
     }
 
+    /**
+     * Specifies if the lines switch between focus/un-focus should be animated.
+     */
     public StyleableBooleanProperty animateLinesProperty() {
         return animateLines;
     }
@@ -266,6 +339,9 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
         return isValidated.get();
     }
 
+    /**
+     * Specifies if validation is required for the control.
+     */
     public StyleableBooleanProperty isValidatedProperty() {
         return isValidated;
     }
@@ -284,7 +360,7 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
                 FACTORY.createPaintCssMetaData(
                         "-mfx-line-color",
                         MFXLegacyComboBox::lineColorProperty,
-                        Color.rgb(50, 150, 205)
+                        Color.rgb(50, 120, 220)
                 );
 
         private static final CssMetaData<MFXLegacyComboBox<?>, Paint> UNFOCUSED_LINE_COLOR =
@@ -298,7 +374,15 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
                 FACTORY.createSizeCssMetaData(
                         "-mfx-line-stroke-width",
                         MFXLegacyComboBox::lineStrokeWidthProperty,
-                        1.5
+                        2.0
+                );
+
+        private static final CssMetaData<MFXLegacyComboBox<?>, StrokeLineCap> LINE_STROKE_CAP =
+                FACTORY.createEnumCssMetaData(
+                        StrokeLineCap.class,
+                        "-mfx-line-stroke-cap",
+                        MFXLegacyComboBox::lineStrokeCapProperty,
+                        StrokeLineCap.ROUND
                 );
 
         private static final CssMetaData<MFXLegacyComboBox<?>, Boolean> ANIMATE_LINES =
@@ -316,7 +400,11 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
                 );
 
         static {
-            cssMetaDataList = List.of(LINE_COLOR, UNFOCUSED_LINE_COLOR, LINE_STROKE_WIDTH, IS_VALIDATED);
+            cssMetaDataList = List.of(
+                    LINE_COLOR, UNFOCUSED_LINE_COLOR,
+                    LINE_STROKE_WIDTH, LINE_STROKE_CAP,
+                    IS_VALIDATED
+            );
         }
 
     }

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/effects/MFXScrimEffect.java

@@ -67,10 +67,10 @@ public class MFXScrimEffect {
     /**
      * Adds a scrim effect to the specified pane with specified opacity.
      * It also simulates the modal behavior of {@code Stage}s, leaving only the specified
-     * {@code Node} interactable.
+     * {@code Node} interactive.
      *
      * @param parent  The pane to which add the effect
-     * @param child   The node to leave interactable
+     * @param child   The node to leave interactive
      * @param opacity The effect opacity/strength
      */
     public void modalScrim(Pane parent, Node child, double opacity) {

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleClipType.java

@@ -22,5 +22,5 @@ public enum RippleClipType {
     CIRCLE,
     RECTANGLE,
     ROUNDED_RECTANGLE,
-    NOCLIP,
+    NO_CLIP,
 }

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXButtonSkin.java

@@ -27,7 +27,7 @@ import javafx.scene.control.skin.ButtonSkin;
 import javafx.scene.input.MouseEvent;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXButton}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXButton}.
  */
 public class MFXButtonSkin extends ButtonSkin {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCheckboxSkin.java

@@ -35,7 +35,7 @@ import javafx.scene.text.Font;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXCheckbox}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXCheckbox}.
  */
 public class MFXCheckboxSkin extends SkinBase<MFXCheckbox> {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java

@@ -48,7 +48,7 @@ import javafx.scene.shape.Line;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the Skin associated with every {@code MFXComboBox}.
+ * This is the implementation of the Skin associated with every {@link MFXComboBox}.
  */
 public class MFXComboBoxSkin<T> extends SkinBase<MFXComboBox<T>> {
     //================================================================================

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDateCellSkin.java

@@ -27,9 +27,9 @@ import javafx.scene.input.MouseEvent;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXDateCell}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXDateCell}.
  * <p>
- * This is necessary to make the {@code RippleGenerator work properly}.
+ * This is necessary to make the {@link RippleGenerator work properly}.
  */
 public class MFXDateCellSkin extends DateCellSkin {
     //================================================================================

+ 6 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
+import io.github.palexdev.materialfx.controls.MFXDatePicker;
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
 import io.github.palexdev.materialfx.controls.MFXScrollPane;
 import io.github.palexdev.materialfx.controls.MFXTextField;
@@ -39,6 +40,7 @@ import javafx.geometry.HPos;
 import javafx.geometry.Insets;
 import javafx.geometry.Orientation;
 import javafx.geometry.Pos;
+import javafx.scene.control.DatePicker;
 import javafx.scene.control.Label;
 import javafx.scene.control.Separator;
 import javafx.scene.control.Tooltip;
@@ -67,11 +69,11 @@ import static java.time.temporal.ChronoUnit.DAYS;
 import static java.time.temporal.ChronoUnit.MONTHS;
 
 /**
- * This class is the beating heart of every {@code MFXDatePicker}.
+ * This class is the beating heart of every {@link MFXDatePicker}.
  * <p>
- * Extends {@code VBox}, the style class is set to "mfx-datepicker-content" for usage in CSS.
+ * Extends {@link VBox}, the style class is set to "mfx-datepicker-content" for usage in CSS.
  * <p></p>
- * In JavaFX every {@code DatePicker} has a content like this but the code is a huge mess.
+ * In JavaFX every {@link DatePicker} has a content like this but the code is a huge mess.
  * <p>
  * To make things even worse the class is part of the com.sun.javafx package which means that
  * jvm arguments are needed to make it accessible... this is BAD.
@@ -682,9 +684,8 @@ public class MFXDatePickerContent extends VBox {
             }
         });
 
-        inputField.getValidator().setValidatorMessage("Invalid Date");
         inputField.getValidator().add(validInput, "Invalid Date");
-        inputField.setIsValidated(true);
+        inputField.setValidated(true);
     }
 
     /**

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java

@@ -36,7 +36,7 @@ import java.util.function.Predicate;
 // TODO implement StringConverter (low priority)
 
 /**
- * This is the implementation of the Skin associated with every MFXFilterComboBox.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXFilterComboBox}.
  */
 public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFlowlessListViewSkin.java

@@ -35,7 +35,7 @@ import javafx.scene.control.SkinBase;
 import javafx.util.Duration;
 
 /**
- * Implementation of the skin used by all list views based on Flowless.
+ * Implementation of the {@code Skin} used by all list views based on Flowless.
  */
 public class MFXFlowlessListViewSkin<T> extends SkinBase<AbstractMFXFlowlessListView<T, ?, ?>> {
     //================================================================================

+ 20 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXLabelSkin.java

@@ -38,6 +38,23 @@ import javafx.scene.paint.Color;
 import javafx.scene.shape.Line;
 import javafx.util.Duration;
 
+/**
+ * This is the implementation of the {@code Skin} associated with every {@link MFXLabelSkin}.
+ * <p>
+ * This skin simply wrappers a normal JavaFX {@link Label} in an {@link HBox}.
+ * Why? Because designing a new label entirely from scratch would be too much work, plus I'm not
+ * entirely sure it could be done because lots of apis for the JavaFX label are part of the com.sun.javafx package,
+ * so they are private.
+ * <p>
+ * This leads to the loss of some base features of the JavaFX {@link Label}, you can get the wrapper label
+ * by using the {@link MFXLabel#getTextNode()} method, but I don't guarantee that all options are working.
+ * <p>
+ * That said it's important to remember that {@link MFXLabel} also introduces new features and fixes. For example
+ * you can have two icons, one leading and one trailing. Also, the alignment of the icons with the text should be way better
+ * and you can also control it by setting the margin of the icons using {@link HBox#setMargin(Node, Insets)} since the icons
+ * are added to the {@link HBox}. The label can also be edited like a text field by setting {@link MFXLabel#editableProperty()} to true
+ * and double clicking it.
+ */
 public class MFXLabelSkin extends SkinBase<MFXLabel> {
     //================================================================================
     // Properties
@@ -262,9 +279,9 @@ public class MFXLabelSkin extends SkinBase<MFXLabel> {
     /**
      * Responsible for showing the editor correctly, handles its size and location.
      * <p>
-     * Note that when the editor with is computed we set that same width as the textNode's prefWidth as well,
-     * this is done so the trailing icon position is automatically managed by the container. When the editor is removed
-     * the textNode's prefWidth is set to USE_COMPUTED_SIZE.
+     * Note that when the editor width is computed we set that same width as the textNode's prefWidth as well,
+     * by doing so the trailing icon position will be automatically managed by the container. When the editor is removed
+     * the textNode's prefWidth is set back to USE_COMPUTED_SIZE.
      */
     private void computeEditorPosition(MFXTextField textField) {
         MFXLabel label = getSkinnable();

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java

@@ -40,7 +40,7 @@ import javafx.util.Duration;
 import java.util.Set;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXListView}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXListView}.
  * <p>
  * The most important thing this skin does is replacing the default scrollbars with new ones,
  * this makes styling them a lot more easy.

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressBarSkin.java

@@ -26,7 +26,7 @@ import javafx.scene.shape.Rectangle;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the Skin associated with every {@code MFXProgressBar}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXProgressBar}.
  */
 public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressSpinnerSkin.java

@@ -41,7 +41,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXProgressSpinner}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXProgressSpinner}.
  */
 public class MFXProgressSpinnerSkin extends SkinBase<MFXProgressSpinner> {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRadioButtonSkin.java

@@ -37,7 +37,7 @@ import javafx.scene.text.Text;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXRadioButton}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXRadioButton}.
  */
 public class MFXRadioButtonSkin extends RadioButtonSkin {
     //================================================================================

+ 358 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperSkin.java

@@ -0,0 +1,358 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXButton;
+import io.github.palexdev.materialfx.controls.MFXStepper;
+import io.github.palexdev.materialfx.controls.MFXStepperToggle;
+import io.github.palexdev.materialfx.controls.MFXStepperToggle.MFXStepperToggleEvent;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.effects.RippleClipType;
+import javafx.animation.*;
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.geometry.Bounds;
+import javafx.geometry.Pos;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+/**
+ * This is the implementation of the {@code Skin} associated with every {@link MFXStepper}.
+ * <p>
+ * It is basically a {@link BorderPane} with three sections: top, center, bottom.
+ * <p>
+ * At the top there is the {@link HBox} that contains the {@link MFXStepperToggle}s and the progress bar
+ * which is realized by using a group and two rectangles. One rectangle is for the background and the other is for the progress.
+ * The first one is manually adjusted both for x property and width property.
+ * <p>
+ * At the center there is a {@link StackPane} with a minimum size of {@code 400x400}, it is the content pane namely the node that
+ * will contain the content specifies by each stepper toggle. The style class is set to "content-pane".
+ * <p>
+ * At the bottom there is the {@link HBox} that contains the previous and next buttons. The style class is set to "buttons-box".
+ * <p></p>
+ * The stepper skin is rather delicate because the progress bar is quite hard to manage since every layout change can
+ * potentially break. In fact the skin keeps track with two separate listeners of the stepper's parent size and the scene size.
+ * When they change the progress must be computed again with {@link #computeProgress()}.
+ * A workaround is also needed in case the progress bar is animated and the layout changes. Without the workaround the
+ * progress bar layout is re-computed by using the animation so the reposition process is not instantaneous.
+ * To fix this annoying UI issue a boolean flag (buttonWasPressed) is set to true only when buttons are pressed and then set to false when the animation finishes,
+ * so every layout change is treated without playing the animation.
+ */
+public class MFXStepperSkin extends SkinBase<MFXStepper> {
+    private final StackPane contentPane;
+    private final HBox stepperBar;
+    private final HBox buttonsBox;
+    private final MFXButton nextButton;
+    private final MFXButton previousButton;
+    private ChangeListener<Boolean> parentSizeListener;
+    private ChangeListener<Window> windowListener;
+
+    // Progressbar
+    private final Group progressBar;
+    private final double height = 7;
+    private final Rectangle progressRect;
+    private final Rectangle backgroundRect;
+    private ParallelTransition progressAnimation;
+    private boolean buttonWasPressed = false;
+
+    public MFXStepperSkin(MFXStepper stepper) {
+        super(stepper);
+
+        progressRect = new Rectangle(0, 0, 0, height);
+        progressRect.fillProperty().bind(stepper.progressColorProperty());
+        progressRect.strokeProperty().bind(stepper.progressBarBackgroundProperty());
+        progressRect.widthProperty().bind(stepper.widthProperty());
+        progressRect.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
+        progressRect.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
+        progressRect.getStyleClass().add("bar-progress");
+
+        backgroundRect = new Rectangle(0, height);
+        backgroundRect.fillProperty().bind(stepper.progressBarBackgroundProperty());
+        backgroundRect.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
+        backgroundRect.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
+        backgroundRect.getStyleClass().add("bar-background");
+
+        progressAnimation = new ParallelTransition();
+        progressAnimation.setInterpolator(MFXAnimationFactory.getInterpolatorV1());
+        progressAnimation.setOnFinished(event -> buttonWasPressed = false);
+
+        progressBar = new Group(progressRect, backgroundRect);
+        progressBar.setManaged(false);
+
+        stepperBar = new HBox(progressBar);
+        stepperBar.spacingProperty().bind(stepper.spacingProperty());
+        stepperBar.alignmentProperty().bind(stepper.alignmentProperty());
+        stepperBar.getChildren().addAll(stepper.getStepperToggles());
+        stepperBar.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
+
+        nextButton = new MFXButton("Next");
+        nextButton.setManaged(false);
+        nextButton.getRippleGenerator().setRippleClipTypeFactory(new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE, 34, 34));
+
+        previousButton = new MFXButton("Previous");
+        previousButton.setManaged(false);
+        previousButton.getRippleGenerator().setRippleClipTypeFactory(new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE, 34, 34));
+
+        buttonsBox = new HBox(64, previousButton, nextButton);
+        buttonsBox.getStyleClass().setAll("buttons-box");
+        buttonsBox.setAlignment(Pos.CENTER);
+
+        contentPane = new StackPane();
+        contentPane.setMinSize(400, 400);
+        contentPane.getStyleClass().setAll("content-pane");
+
+        BorderPane container = new BorderPane();
+        container.getStylesheets().setAll(stepper.getUserAgentStylesheet());
+        container.setTop(stepperBar);
+        container.setCenter(contentPane);
+        container.setBottom(buttonsBox);
+        getChildren().add(container);
+
+        parentSizeListener = (observable, oldValue, newValue) -> {
+            if (!newValue) {
+                computeProgress();
+            }
+        };
+        windowListener = (observable, oldValue, newValue) -> computeProgress();
+
+        setListeners();
+    }
+
+    /**
+     * Adds the following listeners and handlers/filters.
+     * <p>
+     * <p> - Adds a filter for MOUSE_PRESSED events to acquire the focus.
+     * <p> - Adds a filter for STATE_CHANGES events to re-compute the progress, {@link #computeProgress()}.
+     * <p> - Adds a listener to the stepper's toggles list, when it is invalidated the stepper state is reset with
+     * {@link MFXStepper#reset()} and the new toggles are placed. For some reason the progress bar width may be
+     * miscalculated so a workaround is needed. A {@link PauseTransition} is played, after 250ms it is requested to
+     * re-compute the layout.
+     * <p> - Adds a listener to the {@link MFXStepper#currentContentProperty()} to update the content pane children.
+     * <p> - Adds a listener to the {@link MFXStepper#lastToggleProperty()} to call {@link #computeProgress()}.
+     * <p> - Specifies the buttons behavior to set the buttonWasPressed to true and call the {@link MFXStepper#next()} and
+     * {@link MFXStepper#previous()} methods.
+     * <p></p>
+     * Calls {@link #manageScene()}.
+     */
+    private void setListeners() {
+        MFXStepper stepper = getSkinnable();
+
+        stepper.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> stepper.requestFocus());
+        stepper.addEventFilter(MFXStepperToggleEvent.STATE_CHANGED, event -> computeProgress());
+
+        stepper.getStepperToggles().addListener((InvalidationListener) invalidated -> {
+            stepper.reset();
+            stepperBar.getChildren().setAll(stepper.getStepperToggles());
+            stepperBar.getChildren().add(0, progressBar);
+            stepper.next();
+
+            PauseTransition pauseTransition = new PauseTransition(Duration.millis(250));
+            pauseTransition.setOnFinished(event -> stepper.requestLayout());
+            pauseTransition.play();
+        });
+        stepper.currentContentProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null) {
+                contentPane.getChildren().setAll(newValue);
+            } else {
+                contentPane.getChildren().clear();
+            }
+        });
+        stepper.lastToggleProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                computeProgress();
+            }
+        });
+
+        nextButton.setOnAction(event -> {
+            buttonWasPressed = true;
+            stepper.next();
+        });
+        previousButton.setOnAction(event -> {
+            buttonWasPressed = true;
+            stepper.previous();
+        });
+
+        manageScene();
+    }
+
+    /**
+     * Responsible for managing the stepper's parent and size. Adds listeners to the parent
+     * needsLayoutProperty to re-compute the layout of the stepper, to the scene to initialize the stepper
+     * by calling {@link MFXStepper#next()} the first time thus selecting the first toggle, to the scene's windowProperty
+     * to re-compute the progress.
+     */
+    private void manageScene() {
+        MFXStepper stepper = getSkinnable();
+
+        Scene scene = stepper.getScene();
+        Parent parent = stepper.getParent();
+        if (parent != null) {
+            parent.needsLayoutProperty().addListener(parentSizeListener);
+        }
+        if (scene != null) {
+            stepper.next();
+            Window window = scene.getWindow();
+            if (window != null) {
+                window.setOnShown(event -> computeProgress());
+            }
+            scene.windowProperty().addListener((observable, oldValue, newValue) -> computeProgress());
+        }
+
+        stepper.parentProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                oldValue.needsLayoutProperty().removeListener(parentSizeListener);
+            }
+            if (newValue != null) {
+                newValue.needsLayoutProperty().addListener(parentSizeListener);
+            }
+        });
+        stepper.sceneProperty().addListener((observableScene, oldScene, newScene) -> {
+            if (newScene != null && stepper.getCurrentIndex() == -1) {
+                stepper.next();
+            }
+            if (newScene != null) {
+                Window window = newScene.getWindow();
+                if (window != null) {
+                    window.setOnShown(event -> computeProgress());
+                }
+                newScene.windowProperty().addListener(windowListener);
+            }
+        });
+    }
+
+    /**
+     * Responsible for computing the position and size of the rectangle used to show the progress.
+     * <p>
+     * Keep in mind that the rectangle which is moved is the background rectangle not the progress one.
+     * Think about it as the background rectangle covers the progress one and when some progress is made you want to
+     * uncover the progress one by moving the background one.
+     * <p></p>
+     * Three cases are evaluated:
+     * <p> - The stepper {@link MFXStepper#lastToggleProperty()} is true, so the background rectangle width will be 0.
+     * <p> - The current stepper toggle is not null, so the background rectangle width will be computed as follows.
+     * The toggle's circle bounds are retrieved using {@link MFXStepperToggle#getGraphicBounds()}. The X is computed
+     * as the minX of those Bounds converted from local to parent using {@link Node#localToParent(Bounds)}. The width
+     * is computed as the stepper's width minus the previously calculated X.
+     * <p> - The current stepper toggle is null so the X is 0 and the width is equal to the stepper's width.
+     * <p></p>
+     * The computed values are used by {@link #updateProgressBar(double, double)}
+     * <p></p>
+     * It can be tricky to understand but with the given information it should be understandable, maybe draw it, it will
+     * be easier.
+     *
+     */
+    private void computeProgress() {
+        MFXStepper stepper = getSkinnable();
+
+        if (stepper.isLastToggle()) {
+            updateProgressBar(stepper.getWidth(), 0);
+            return;
+        }
+
+        MFXStepperToggle stepperToggle = stepper.getCurrentStepperNode();
+        if (stepperToggle != null) {
+            Bounds bounds = stepperToggle.getGraphicBounds();
+            if (bounds != null) {
+                double minX = snapSizeX(stepperToggle.localToParent(bounds).getMinX());
+                double width = snapSizeX(stepper.getWidth() - minX);
+                updateProgressBar(minX, width);
+            }
+        } else {
+            updateProgressBar(0, stepper.getWidth());
+        }
+    }
+
+    /**
+     * Sets the background rectangle x and width properties to the given values.
+     * If the {@link MFXStepper#animatedProperty()} or the buttonWasPressed flag are false
+     * then the properties are updated immediately. Otherwise they are updated by two separate timelines
+     * played at the same time using a {@link ParallelTransition}.
+     */
+    private void updateProgressBar(double x, double width) {
+        MFXStepper stepper = getSkinnable();
+        if (!stepper.isAnimated() || !buttonWasPressed) {
+            backgroundRect.setX(x);
+            backgroundRect.setWidth(width);
+            buttonWasPressed = false;
+            return;
+        }
+
+        KeyFrame keyFrame1 = new KeyFrame(Duration.millis(stepper.getAnimationDuration()), new KeyValue(backgroundRect.xProperty(), x));
+        KeyFrame keyFrame2 = new KeyFrame(Duration.millis(stepper.getAnimationDuration()), new KeyValue(backgroundRect.widthProperty(), width));
+        Timeline timeline1 = new Timeline(keyFrame1);
+        Timeline timeline2 = new Timeline(keyFrame2);
+        progressAnimation.getChildren().setAll(timeline1, timeline2);
+        progressAnimation.playFromStart();
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefWidth(height, topInset, leftInset, bottomInset, rightInset) + (getSkinnable().getExtraSpacing() * 2);
+    }
+
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        if (progressAnimation.getStatus() != Animation.Status.STOPPED) {
+            progressAnimation.stop();
+        }
+        progressAnimation = null;
+        parentSizeListener = null;
+        windowListener = null;
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        double barY = snapPositionY((stepperBar.getHeight() / 2.0) - (height / 2.0));
+        progressBar.resizeRelocate(0.0, barY, w, height);
+
+        double bw = 125;
+        double bh = 34;
+        double pbx = snapPositionX(15);
+        double nbx = snapPositionX(w - bw - 15);
+        double by = snapPositionY((bh / 2.0) - (buttonsBox.getHeight() / 2.0));
+
+        previousButton.resizeRelocate(pbx, by, bw, bh);
+        nextButton.resizeRelocate(nbx, by, bw, bh);
+    }
+}

+ 164 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java

@@ -0,0 +1,164 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.MFXStageDialog;
+import io.github.palexdev.materialfx.controls.MFXStepperToggle;
+import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.controls.enums.StepperToggleState;
+import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.DialogUtils;
+import io.github.palexdev.materialfx.utils.LabelUtils;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import io.github.palexdev.materialfx.validation.MFXPriorityValidator;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.StrokeType;
+
+/**
+ * This is the implementation of the {@code Skin} associated with every {@link MFXStepperToggle}.
+ * <p>
+ * It consists of a {@link Circle} with the css id set to "circle", a {@link MFXLabel}
+ * and a little optional error icon shown in the upper right corner of the toggle.
+ */
+public class MFXStepperToggleSkin extends SkinBase<MFXStepperToggle> {
+    private final StackPane container;
+    private final Circle circle;
+    private final MFXLabel label;
+    private final MFXIconWrapper errorIcon;
+
+    public MFXStepperToggleSkin(MFXStepperToggle stepperToggle) {
+        super(stepperToggle);
+
+        circle = new Circle(0, Color.LIGHTGRAY);
+        circle.setId("circle");
+        circle.radiusProperty().bind(stepperToggle.sizeProperty());
+        circle.strokeWidthProperty().bind(stepperToggle.strokeWidthProperty());
+        circle.setStrokeType(StrokeType.CENTERED);
+
+        container = new StackPane(circle, stepperToggle.getIcon());
+        container.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
+        container.getStylesheets().setAll(stepperToggle.getUserAgentStylesheet());
+
+        label = new MFXLabel();
+        label.getStylesheets().addAll(stepperToggle.getUserAgentStylesheet());
+        label.setText(stepperToggle.getText());
+        label.setLineColor(Color.TRANSPARENT);
+        label.setUnfocusedLineColor(Color.TRANSPARENT);
+        label.setManaged(false);
+
+        errorIcon = new MFXIconWrapper(
+                new MFXFontIcon("mfx-exclamation-triangle", Color.web("#EF6E6B")), 16
+        );
+        errorIcon.setId("errorIcon");
+        errorIcon.setVisible(false);
+        errorIcon.setManaged(false);
+
+        getChildren().addAll(container, label, errorIcon);
+        setListeners();
+    }
+
+    /**
+     * Adds the following listeners, handlers/filters.
+     * <p>
+     * <p> - Adds a listener to the toggle's state property to show the error icon when
+     * the state is ERROR and if the {@link MFXStepperToggle#showErrorIconProperty()} is true.
+     * <p> - Adds a listener to the {@link MFXStepperToggle#showErrorIconProperty()} to make the error icon
+     * work properly, if the property value changes and the state is ERROR or not.
+     * <p> - Adds a listener to the {@link MFXStepperToggle#textPositionProperty()} to re-compute the layout when it changes.
+     * <p> - Adds a listener to the validator's {@link AbstractMFXValidator#validProperty()} to properly update the
+     * toggle's state.
+     * <p> - Adds an handler for MOUSE_PRESSED events to the error icon to call {@link #showErrorsDialog()}.
+     */
+    private void setListeners() {
+        MFXStepperToggle stepperToggle = getSkinnable();
+        MFXDialogValidator validator = stepperToggle.getValidator();
+
+        stepperToggle.stateProperty().addListener((observable, oldValue, newValue) ->
+                errorIcon.setVisible(newValue == StepperToggleState.ERROR && stepperToggle.isShowErrorIcon()));
+
+        stepperToggle.showErrorIconProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                errorIcon.setVisible(stepperToggle.getState() == StepperToggleState.ERROR);
+            } else {
+                errorIcon.setVisible(false);
+            }
+        });
+        stepperToggle.textPositionProperty().addListener(invalidated -> stepperToggle.requestLayout());
+
+        validator.validProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue && stepperToggle.getState() == StepperToggleState.ERROR) {
+                stepperToggle.setState(StepperToggleState.SELECTED);
+            }
+        });
+
+        errorIcon.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> showErrorsDialog());
+    }
+
+    /**
+     * Shows an error dialog that contains the all the validator's unmet conditions, including
+     * the dependency ones too.
+     * <p>
+     * Uses {@link DialogUtils} to build and show the dialog.
+     */
+    protected void showErrorsDialog() {
+        MFXStepperToggle stepperToggle = getSkinnable();
+        MFXDialogValidator validator = stepperToggle.getValidator();
+        StringBuilder sb = new StringBuilder(validator.getUnmetMessages());
+        validator.getDependencies().stream()
+                .filter(v -> v instanceof MFXPriorityValidator)
+                .map(v -> (MFXPriorityValidator) v)
+                .forEach(v -> sb.append(v.getUnmetMessages()));
+        MFXStageDialog dialog = DialogUtils.getStageDialog(stepperToggle.getScene().getWindow(), DialogType.ERROR, validator.getTitle(), sb.toString());
+        dialog.setScrimBackground(true);
+        dialog.show();
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+        MFXStepperToggle stepperToggle = getSkinnable();
+
+        double lw = snapSizeX(LabelUtils.computeTextWidth(label.getFont(), label.getText())) + 20;
+        double lh = snapSizeY(LabelUtils.computeTextHeight(label.getFont(), label.getText()));
+        double lx = snapPositionX(circle.getBoundsInParent().getCenterX() - (lw / 2.0));
+        double ly = 0;
+
+        if (stepperToggle.getTextPosition() == TextPosition.BOTTOM) {
+            label.setTranslateY(0);
+            ly = snapPositionY(circle.getBoundsInParent().getMaxY() + stepperToggle.getTextGap());
+            label.resizeRelocate(lx, ly, lw, lh);
+        } else {
+            label.resizeRelocate(lx, ly, lw, lh);
+            label.setTranslateY(-stepperToggle.getTextGap() - lh);
+        }
+
+        double ix = snapPositionX(circle.getBoundsInParent().getMaxX());
+        double iy = snapPositionY(circle.getBoundsInParent().getMinY() - 6);
+        errorIcon.resizeRelocate(ix, iy, 16, 16);
+    }
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnCellSkin.java

@@ -31,7 +31,7 @@ import javafx.scene.control.skin.LabelSkin;
 import javafx.scene.layout.Region;
 
 /**
- * This is the implementation of the Skin associated with every {@code MFXTableColumnCell}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXTableColumnCell}.
  */
 public class MFXTableColumnCellSkin<T> extends LabelSkin {
     //================================================================================

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java

@@ -71,7 +71,7 @@ import static io.github.palexdev.materialfx.controls.MFXTableView.TableViewEvent
 import static javafx.scene.layout.Region.USE_PREF_SIZE;
 
 /**
- * This is the implementation of the Skin associated with every {@code MFXTableView}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXTableView}.
  * <p>
  * It's the main class since it handles the graphic of the control and its functionalities.
  * <p>

+ 49 - 40
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java

@@ -19,12 +19,14 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXLabel;
 import io.github.palexdev.materialfx.controls.MFXTextField;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.LabelUtils;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
 import javafx.animation.ScaleTransition;
-import javafx.scene.control.Label;
+import javafx.scene.Cursor;
 import javafx.scene.control.skin.TextFieldSkin;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
@@ -32,7 +34,7 @@ import javafx.scene.shape.Line;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXTextField}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXTextField}.
  */
 public class MFXTextFieldSkin extends TextFieldSkin {
     //================================================================================
@@ -42,7 +44,7 @@ public class MFXTextFieldSkin extends TextFieldSkin {
 
     private final Line unfocusedLine;
     private final Line focusedLine;
-    private final Label validate;
+    private final MFXLabel validate;
 
     //================================================================================
     // Constructors
@@ -52,30 +54,40 @@ public class MFXTextFieldSkin extends TextFieldSkin {
 
         unfocusedLine = new Line();
         unfocusedLine.getStyleClass().add("unfocused-line");
-        unfocusedLine.setStroke(textField.getUnfocusedLineColor());
-        unfocusedLine.setStrokeWidth(textField.getLineStrokeWidth());
+        unfocusedLine.setManaged(false);
+        unfocusedLine.strokeWidthProperty().bind(textField.lineStrokeWidthProperty());
+        unfocusedLine.strokeLineCapProperty().bind(textField.lineStrokeCapProperty());
+        unfocusedLine.strokeProperty().bind(textField.unfocusedLineColorProperty());
+        unfocusedLine.endXProperty().bind(textField.widthProperty());
         unfocusedLine.setSmooth(true);
+        unfocusedLine.setManaged(false);
 
         focusedLine = new Line();
         focusedLine.getStyleClass().add("focused-line");
-        focusedLine.setStroke(textField.getLineColor());
-        focusedLine.setStrokeWidth(textField.getLineStrokeWidth());
+        focusedLine.setManaged(false);
+        focusedLine.strokeWidthProperty().bind(textField.lineStrokeWidthProperty());
+        focusedLine.strokeLineCapProperty().bind(textField.lineStrokeCapProperty());
+        focusedLine.strokeProperty().bind(textField.lineColorProperty());
         focusedLine.setSmooth(true);
-        focusedLine.setScaleX(0.0);
-
-        unfocusedLine.endXProperty().bind(textField.widthProperty());
         focusedLine.endXProperty().bind(textField.widthProperty());
-        unfocusedLine.setManaged(false);
+        focusedLine.setScaleX(0.0);
         focusedLine.setManaged(false);
 
         MFXFontIcon warnIcon = new MFXFontIcon("mfx-exclamation-triangle", Color.RED);
         MFXIconWrapper warnWrapper = new MFXIconWrapper(warnIcon, 10);
 
-        validate = new Label("", warnWrapper);
+        validate = new MFXLabel();
+        validate.setLeadingIcon(warnWrapper);
         validate.getStyleClass().add("validate-label");
+        validate.getStylesheets().setAll(textField.getUserAgentStylesheet());
         validate.textProperty().bind(textField.getValidator().validatorMessageProperty());
         validate.setGraphicTextGap(padding);
         validate.setVisible(false);
+        validate.setManaged(false);
+
+        if (textField.isValidated() && textField.getValidator().isInitControlValidation()) {
+            validate.setVisible(!textField.isValid());
+        }
 
         getChildren().addAll(unfocusedLine, focusedLine, validate);
 
@@ -100,28 +112,10 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         MFXTextField textField = (MFXTextField) getSkinnable();
         MFXDialogValidator validator = textField.getValidator();
 
-        textField.lineColorProperty().addListener((observable, oldValue, newValue) -> {
-            if (!newValue.equals(oldValue)) {
-                focusedLine.setStroke(newValue);
-            }
-        });
-
-        textField.unfocusedLineColorProperty().addListener((observable, oldValue, newValue) -> {
-            if (!newValue.equals(oldValue)) {
-                unfocusedLine.setStroke(newValue);
-            }
-        });
-
-        textField.lineStrokeWidthProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue.doubleValue() != oldValue.doubleValue()) {
-                unfocusedLine.setStrokeWidth(newValue.doubleValue());
-                focusedLine.setStrokeWidth(newValue.doubleValue() * 1.3);
-            }
-        });
-
         textField.focusedProperty().addListener((observable, oldValue, newValue) -> {
             if (!newValue && textField.isValidated()) {
-                validate.setVisible(!validator.isValid());
+                textField.getValidator().update();
+                validate.setVisible(!textField.isValid());
             }
 
             if (textField.isAnimateLines()) {
@@ -153,17 +147,19 @@ public class MFXTextFieldSkin extends TextFieldSkin {
 
         textField.disabledProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue) {
-                validate.setVisible(false);
+                validate.setVisible(!textField.isValid());
             }
         });
 
-        validator.addChangeListener((observable, oldValue, newValue) -> {
+        validator.addListener((observable, oldValue, newValue) -> {
             if (textField.isValidated()) {
                 validate.setVisible(!newValue);
             }
         });
 
-        validate.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> validator.show());
+        validate.textProperty().addListener(invalidated -> textField.requestLayout());
+        validate.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> validate.setCursor(Cursor.DEFAULT));
+        validate.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> validator.showModal(textField.getScene().getWindow()));
     }
 
     /**
@@ -189,11 +185,24 @@ public class MFXTextFieldSkin extends TextFieldSkin {
     protected void layoutChildren(double x, double y, double w, double h) {
         super.layoutChildren(x, y, w, h);
 
-        final double size = padding / 2.5;
+        double lw = snapSizeX(
+                LabelUtils.computeLabelWidth(validate.getFont(), validate.getText()) +
+                        (validate.getLeadingIcon() != null ? validate.getLeadingIcon().getBoundsInParent().getWidth() : 0.0) +
+                        (validate.getTrailingIcon() != null ? validate.getTrailingIcon().getBoundsInParent().getWidth() : 0.0) +
+                        (validate.getGraphicTextGap() * 2) +
+                        20.0
+        );
+        double lh = snapSizeY(LabelUtils.computeTextHeight(validate.getFont(), validate.getText()));
+        double lx = w - lw;
+        double ly = h + lh;
+
+        if (lw > w) {
+            lx = -((lw - w) / 2.0);
+        }
+
+        validate.resizeRelocate(lx, ly, lw, lh);
+        focusedLine.relocate(0, h + padding * 0.7);
+        unfocusedLine.relocate(0, h + padding * 0.7);
 
-        focusedLine.setTranslateY(h + padding * 0.7);
-        unfocusedLine.setTranslateY(h + padding * 0.7);
-        validate.resize(w * 1.5, h - size);
-        validate.setTranslateY(focusedLine.getTranslateY() + size);
     }
 }

+ 4 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXToggleButtonSkin.java

@@ -31,14 +31,13 @@ import javafx.scene.Cursor;
 import javafx.scene.control.skin.ToggleButtonSkin;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.StackPane;
-import javafx.scene.paint.Color;
 import javafx.scene.shape.Circle;
 import javafx.scene.shape.Line;
 import javafx.scene.shape.StrokeLineCap;
 import javafx.util.Duration;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXToggleButton}.
+ * This is the implementation of the {@code Skin} associated with every {@link MFXToggleButton}.
  */
 public class MFXToggleButtonSkin extends ToggleButtonSkin {
     //================================================================================
@@ -84,7 +83,7 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
 
         rippleGenerator = new RippleGenerator(container, new RippleClipTypeFactory());
         rippleGenerator.setAnimateBackground(false);
-        rippleGenerator.setRippleColor((Color) (toggleButton.isSelected() ? toggleButton.getUnToggleLineColor() : toggleButton.getToggleLineColor()));
+        rippleGenerator.setRippleColor(toggleButton.isSelected() ? toggleButton.getUnToggleLineColor() : toggleButton.getToggleLineColor());
         rippleGenerator.setRippleRadius(circleRadius * 1.2);
         rippleGenerator.setInDuration(Duration.millis(400));
         rippleGenerator.setTranslateX(-circleRadius);
@@ -108,11 +107,11 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
         toggleButton.selectedProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue) {
                 line.setStroke(toggleButton.getToggleLineColor());
-                rippleGenerator.setRippleColor((Color) toggleButton.getToggleLineColor());
+                rippleGenerator.setRippleColor(toggleButton.getToggleLineColor());
                 circle.setFill(toggleButton.getToggleColor());
             } else {
                 line.setStroke(toggleButton.getUnToggleLineColor());
-                rippleGenerator.setRippleColor((Color) toggleButton.getUnToggleLineColor());
+                rippleGenerator.setRippleColor(toggleButton.getUnToggleLineColor());
                 circle.setFill(toggleButton.getUnToggleColor());
             }
         });

+ 63 - 58
materialfx/src/main/java/io/github/palexdev/materialfx/skins/legacy/MFXLegacyComboBoxSkin.java

@@ -19,17 +19,17 @@
 package io.github.palexdev.materialfx.skins.legacy;
 
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXLabel;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.controls.legacy.MFXLegacyComboBox;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.LabelUtils;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
 import javafx.animation.ScaleTransition;
-import javafx.scene.control.Label;
 import javafx.scene.control.skin.ComboBoxListViewSkin;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
 import javafx.scene.shape.Line;
-import javafx.scene.text.Font;
 import javafx.util.Duration;
 
 /**
@@ -41,9 +41,9 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
     //================================================================================
     private final double padding = 11;
 
-    private final Line line;
-    private final Line focusLine;
-    private final Label validate;
+    private final Line unfocusedLine;
+    private final Line focusedLine;
+    private final MFXLabel validate;
 
     //================================================================================
     // Constructors
@@ -51,33 +51,44 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
     public MFXLegacyComboBoxSkin(MFXLegacyComboBox<T> comboBox) {
         super(comboBox);
 
-        line = new Line();
-        line.getStyleClass().add("unfocused-line");
-        line.setStroke(comboBox.getUnfocusedLineColor());
-        line.setStrokeWidth(comboBox.getLineStrokeWidth());
-        line.setSmooth(true);
-
-        focusLine = new Line();
-        focusLine.getStyleClass().add("focused-line");
-        focusLine.setStroke(comboBox.getLineColor());
-        focusLine.setStrokeWidth(comboBox.getLineStrokeWidth());
-        focusLine.setSmooth(true);
-        focusLine.setScaleX(0.0);
-
-        line.endXProperty().bind(comboBox.widthProperty());
-        focusLine.endXProperty().bind(comboBox.widthProperty());
+        unfocusedLine = new Line();
+        unfocusedLine.getStyleClass().add("unfocused-line");
+        unfocusedLine.setManaged(false);
+        unfocusedLine.strokeWidthProperty().bind(comboBox.lineStrokeWidthProperty());
+        unfocusedLine.strokeLineCapProperty().bind(comboBox.lineStrokeCapProperty());
+        unfocusedLine.strokeProperty().bind(comboBox.unfocusedLineColorProperty());
+        unfocusedLine.endXProperty().bind(comboBox.widthProperty());
+        unfocusedLine.setSmooth(true);
+        unfocusedLine.setManaged(false);
+
+        focusedLine = new Line();
+        focusedLine.getStyleClass().add("focused-line");
+        focusedLine.setManaged(false);
+        focusedLine.strokeWidthProperty().bind(comboBox.lineStrokeWidthProperty());
+        focusedLine.strokeLineCapProperty().bind(comboBox.lineStrokeCapProperty());
+        focusedLine.strokeProperty().bind(comboBox.lineColorProperty());
+        focusedLine.setSmooth(true);
+        focusedLine.endXProperty().bind(comboBox.widthProperty());
+        focusedLine.setScaleX(0.0);
+        focusedLine.setManaged(false);
 
         MFXFontIcon warnIcon = new MFXFontIcon("mfx-exclamation-triangle", Color.RED);
         MFXIconWrapper warnWrapper = new MFXIconWrapper(warnIcon, 10);
 
-        validate = new Label("", warnWrapper);
+        validate = new MFXLabel();
+        validate.setLeadingIcon(warnWrapper);
         validate.getStyleClass().add("validate-label");
+        validate.getStylesheets().setAll(comboBox.getUserAgentStylesheet());
         validate.textProperty().bind(comboBox.getValidator().validatorMessageProperty());
-        validate.setFont(Font.font(padding));
-        validate.setGraphicTextGap(padding / 2);
+        validate.setGraphicTextGap(padding);
         validate.setVisible(false);
+        validate.setManaged(false);
+
+        if (comboBox.isValidated() && comboBox.getValidator().isInitControlValidation()) {
+            validate.setVisible(!comboBox.isValid());
+        }
 
-        getChildren().addAll(line, focusLine, validate);
+        getChildren().addAll(unfocusedLine, focusedLine, validate);
 
         setListeners();
     }
@@ -87,7 +98,7 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
     //================================================================================
 
     /**
-     * Adds listeners for: line, focus, disabled and validator properties.
+     * Adds listeners for: line, focus, disabled, validator properties and validate label's text.
      * <p>
      * Validator: when the control is not focused, and of course if {@code isValidated} is true,
      * all the conditions in the validator are evaluated and if one is false the {@code validate} label is shown.
@@ -101,28 +112,10 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
         MFXLegacyComboBox<T> comboBox = (MFXLegacyComboBox<T>) getSkinnable();
         MFXDialogValidator validator = comboBox.getValidator();
 
-        comboBox.lineColorProperty().addListener((observable, oldValue, newValue) -> {
-            if (!newValue.equals(oldValue)) {
-                focusLine.setStroke(newValue);
-            }
-        });
-
-        comboBox.unfocusedLineColorProperty().addListener((observable, oldValue, newValue) -> {
-            if (!newValue.equals(oldValue)) {
-                line.setStroke(newValue);
-            }
-        });
-
-        comboBox.lineStrokeWidthProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue.doubleValue() != oldValue.doubleValue()) {
-                line.setStrokeWidth(newValue.doubleValue());
-                focusLine.setStrokeWidth(newValue.doubleValue() * 1.3);
-            }
-        });
-
         comboBox.focusedProperty().addListener((observable, oldValue, newValue) -> {
             if (!newValue && comboBox.isValidated()) {
-                validate.setVisible(!validator.isValid());
+                comboBox.getValidator().update();
+                validate.setVisible(!comboBox.isValid());
             }
 
             if (comboBox.isAnimateLines()) {
@@ -131,9 +124,9 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
             }
 
             if (newValue) {
-                focusLine.setScaleX(1.0);
+                focusedLine.setScaleX(1.0);
             } else {
-                focusLine.setScaleX(0.0);
+                focusedLine.setScaleX(0.0);
             }
         });
 
@@ -149,19 +142,21 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
             }
         });
 
-        validator.addChangeListener((observable, oldValue, newValue) -> {
+        validator.addListener((observable, oldValue, newValue) -> {
             if (comboBox.isValidated()) {
                 validate.setVisible(!newValue);
             }
         });
-        validate.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> validator.showModal(comboBox.getScene().getWindow()));
+
+        validate.textProperty().addListener(invalidated -> comboBox.requestLayout());
+        validate.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> validator.showModal(comboBox.getScene().getWindow()));
     }
 
     /**
      * Builds and play the lines animation if {@code animateLines} is true.
      */
     private void buildAndPlayAnimation(boolean focused) {
-        ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(400), focusLine);
+        ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(350), focusedLine);
         if (focused) {
             scaleTransition.setFromX(0.0);
             scaleTransition.setToX(1.0);
@@ -169,7 +164,7 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
             scaleTransition.setFromX(1.0);
             scaleTransition.setToX(0.0);
         }
-        scaleTransition.setInterpolator(MFXAnimationFactory.getInterpolatorV1());
+        scaleTransition.setInterpolator(MFXAnimationFactory.getInterpolatorV2());
         scaleTransition.play();
     }
 
@@ -180,13 +175,23 @@ public class MFXLegacyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
     protected void layoutChildren(double x, double y, double w, double h) {
         super.layoutChildren(x, y, w, h);
 
-        final double size = padding / 2.5;
-        final double tx = -((w - line.getEndX()) / 2);
+        double lw = snapSizeX(
+                LabelUtils.computeLabelWidth(validate.getFont(), validate.getText()) +
+                        (validate.getLeadingIcon() != null ? validate.getLeadingIcon().getBoundsInParent().getWidth() : 0.0) +
+                        (validate.getTrailingIcon() != null ? validate.getTrailingIcon().getBoundsInParent().getWidth() : 0.0) +
+                        (validate.getGraphicTextGap() * 2) +
+                        20.0
+        );
+        double lh = snapSizeY(LabelUtils.computeTextHeight(validate.getFont(), validate.getText()));
+        double lx = w - lw;
+        double ly = h + (padding * 0.7);
+
+        if (lw > w) {
+            lx = -((lw - w) / 2.0);
+        }
 
-        focusLine.setTranslateY(h);
-        line.setTranslateY(h);
-        validate.resize(w * 1.5, h - size);
-        validate.setTranslateY(focusLine.getTranslateY() + size);
-        validate.setTranslateX(tx);
+        validate.resizeRelocate(lx, ly, lw, lh);
+        focusedLine.relocate(0, h);
+        unfocusedLine.relocate(0, h);
     }
 }

+ 105 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/BindingUtils.java

@@ -0,0 +1,105 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.utils;
+
+import javafx.beans.binding.*;
+import javafx.beans.property.*;
+
+/**
+ * Utils class to convert bindings and expressions to properties.
+ * <p>
+ * Very useful when working with validators since they only accept properties and,
+ * in my opinion, it is much easier to create the conditions using the JavaFX {@link Bindings} class.
+ */
+public class BindingUtils {
+
+    private BindingUtils() {}
+
+    /**
+     * Creates a new {@link IntegerProperty} and binds it to the given binding/expression.
+     */
+    public static IntegerProperty toProperty(IntegerExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        IntegerProperty property = new SimpleIntegerProperty();
+        property.bind(expression);
+        return property;
+    }
+
+    /**
+     * Creates a new {@link LongProperty} and binds it to the given binding/expression.
+     */
+    public static LongProperty toProperty(LongExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        LongProperty property = new SimpleLongProperty();
+        property.bind(expression);
+        return property;
+    }
+
+    /**
+     * Creates a new {@link FloatProperty} and binds it to the given binding/expression.
+     */
+    public static FloatProperty toProperty(FloatExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        FloatProperty property = new SimpleFloatProperty();
+        property.bind(expression);
+        return property;
+    }
+
+    /**
+     * Creates a new {@link DoubleProperty} and binds it to the given binding/expression.
+     */
+    public static DoubleProperty toProperty(DoubleExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        DoubleProperty property = new SimpleDoubleProperty();
+        property.bind(expression);
+        return property;
+    }
+
+    /**
+     * Creates a new {@link BooleanProperty} and binds it to the given binding/expression.
+     */
+    public static BooleanProperty toProperty(BooleanExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        BooleanProperty property = new SimpleBooleanProperty();
+        property.bind(expression);
+        return property;
+    }
+
+    /**
+     * Creates a new {@link StringProperty} and binds it to the given binding/expression.
+     */
+    public static StringProperty toProperty(StringExpression expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        StringProperty property = new SimpleStringProperty();
+        property.bind(expression);
+        return property;
+    }
+}

+ 97 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/DialogUtils.java

@@ -0,0 +1,97 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.controls.MFXDialog;
+import io.github.palexdev.materialfx.controls.MFXExceptionDialog;
+import io.github.palexdev.materialfx.controls.MFXStageDialog;
+import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
+import javafx.stage.Modality;
+import javafx.stage.Window;
+
+/**
+ * Utils class to quickly get modal {@link MFXStageDialog}s and other specialized dialogs.
+ */
+public class DialogUtils {
+
+    private DialogUtils() {
+    }
+
+    /**
+     * Calls {@link MFXDialogFactory#buildDialog(DialogType, String, String)} with the specified arguments.
+     *
+     * @param type    the dialog type
+     * @param title   the dialog title
+     * @param message the dialog content
+     * @return the newly created MFXDialog
+     */
+    public static MFXDialog getDialog(DialogType type, String title, String message) {
+        return MFXDialogFactory.buildDialog(type, title, message);
+    }
+
+    /**
+     * Creates a new {@link MFXStageDialog} with the given owner and arguments.
+     *
+     * @param window  the owner
+     * @param type    the dialog type
+     * @param title   the dialog title
+     * @param message the dialog content
+     * @return the newly created MFXStageDialog
+     */
+    public static MFXStageDialog getStageDialog(Window window, DialogType type, String title, String message) {
+        MFXStageDialog stageDialog = new MFXStageDialog(type, title, message);
+        stageDialog.setOwner(window);
+        stageDialog.setModality(Modality.APPLICATION_MODAL);
+        return stageDialog;
+    }
+
+    /**
+     * Creates a new {@link MFXStageDialog} with the given owner and {@link MFXDialog}.
+     *
+     * @param window the owner
+     * @param dialog the MFXDialog
+     * @return the newly created MFXStageDialog
+     */
+    public static MFXStageDialog getStageDialog(Window window, MFXDialog dialog) {
+        MFXStageDialog stageDialog = new MFXStageDialog(dialog);
+        stageDialog.setOwner(window);
+        stageDialog.setModality(Modality.APPLICATION_MODAL);
+        return stageDialog;
+    }
+
+    /**
+     * Creates a new {@link MFXStageDialog} which shows a {@link MFXExceptionDialog}
+     * with the given owner, title and exception.
+     *
+     * @param window the owner
+     * @param title  the dialog title
+     * @param th     the exception
+     * @return the newly created MFXStageDialog
+     */
+    public static MFXStageDialog getExceptionDialog(Window window, String title, Throwable th) {
+        MFXExceptionDialog dialog = new MFXExceptionDialog();
+        dialog.setTitle(title);
+        dialog.setException(th);
+        MFXStageDialog stageDialog = new MFXStageDialog(dialog);
+        stageDialog.setOwner(window);
+        stageDialog.setModality(Modality.APPLICATION_MODAL);
+        return stageDialog;
+    }
+}

+ 41 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ExceptionUtils.java

@@ -0,0 +1,41 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.utils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Little utils class to convert a throwable stack trace to a String.
+ */
+public class ExceptionUtils {
+    private static final StringWriter sw = new StringWriter();
+
+    private ExceptionUtils() {}
+
+    /**
+     * Converts the given exception stack trace to a String
+     * by using a {@link StringWriter} and a {@link PrintWriter}.
+     */
+    public static String getStackTraceString(Throwable ex) {
+        sw.flush();
+        ex.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+}

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoaderUtils.java

@@ -117,7 +117,7 @@ public class LoaderUtils {
     }
 
     /**
-     * Sets the location for the given fxmlLoader and loads the fmxl file.
+     * Sets the location for the given fxmlLoader and loads the fxml file.
      *
      * @param   fxmlURL the fxml file to load
      * @return  the loaded object hierarchy from the fxml
@@ -128,7 +128,7 @@ public class LoaderUtils {
     }
 
     /**
-     * Sets the location and the controller factory for the given fxmlLoader and loads the fmxl file.
+     * Sets the location and the controller factory for the given fxmlLoader and loads the fxml file.
      *
      * @param   fxmlURL the fxml file to load
      * @param   controllerFactory the controller object to set

+ 2 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -252,12 +252,11 @@ public class NodeUtils {
     public static double computeYOffset(double height, double contentHeight, VPos vpos) {
 
         switch (vpos) {
-            case TOP:
-                return 0;
             case CENTER:
                 return (height - contentHeight) / 2;
             case BOTTOM:
                 return height - contentHeight;
+            case TOP:
             default:
                 return 0;
         }
@@ -429,7 +428,7 @@ public class NodeUtils {
     }
 
     /*
-     * Simple utitilty function to return the 'opposite' value of a given HPos, taking
+     * Simple utility function to return the 'opposite' value of a given HPos, taking
      * into account the current VPos value. This is used to try and avoid overlapping.
      */
     private static HPos getHPosOpposite(HPos hpos, VPos vpos) {

+ 30 - 51
materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java

@@ -21,39 +21,35 @@ package io.github.palexdev.materialfx.validation;
 import io.github.palexdev.materialfx.controls.MFXStageDialog;
 import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
-import io.github.palexdev.materialfx.utils.StringUtils;
-import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
-import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
 import javafx.geometry.Pos;
 import javafx.scene.control.Label;
 import javafx.stage.Modality;
 import javafx.stage.Window;
 
-import java.util.HashMap;
-import java.util.Map;
-
 /**
- * This is a concrete implementation of a validator.
+ * This is an extension of the {@link MFXPriorityValidator}, basically adds the capability
+ * to shown a dialog showing all the unmet conditions of the validator using the {@link #getUnmetMessages()} method.
  * <p>
- * This validator has a string message associated with every boolean property in its base class.
- * It can show a {@link MFXStageDialog} containing all warning messages.
+ * The dialog used is a {@link MFXStageDialog} so it can also be modal by calling
+ * {@link #showModal(Window)} and specifying the owner.
  */
-public class MFXDialogValidator extends AbstractMFXValidator {
+public class MFXDialogValidator extends MFXPriorityValidator {
     //================================================================================
     // Properties
     //================================================================================
-    private final Map<BooleanProperty, String> messagesMap = new HashMap<>();
     private final ObjectProperty<DialogType> dialogType = new SimpleObjectProperty<>(DialogType.WARNING);
-    private String title;
+    private final StringProperty title = new SimpleStringProperty();
     private MFXStageDialog stageDialog;
 
     //================================================================================
     // Constructors
     //================================================================================
     public MFXDialogValidator(String title) {
-        this.title = title;
+        setTitle(title);
         initialize();
     }
 
@@ -68,6 +64,12 @@ public class MFXDialogValidator extends AbstractMFXValidator {
                 label.setAlignment(Pos.CENTER);
             }
         });
+
+        title.addListener((observable, oldValue, newValue) -> {
+            if (stageDialog != null) {
+                stageDialog.getDialog().setTitle(newValue);
+            }
+        });
     }
 
     /**
@@ -75,12 +77,12 @@ public class MFXDialogValidator extends AbstractMFXValidator {
      */
     public void show() {
         if (stageDialog == null) {
-            stageDialog = new MFXStageDialog(dialogType.get(), title, "");
+            stageDialog = new MFXStageDialog(dialogType.get(), getTitle(), "");
             Label label = (Label) stageDialog.getDialog().lookup(".content-label");
             label.setAlignment(Pos.CENTER);
         }
 
-        stageDialog.getDialog().setContent(getMessages());
+        stageDialog.getDialog().setContent(getUnmetMessages());
         stageDialog.setOwner(null);
         stageDialog.setModality(Modality.NONE);
         stageDialog.setCenterInOwner(false);
@@ -95,12 +97,12 @@ public class MFXDialogValidator extends AbstractMFXValidator {
      */
     public void showModal(Window owner) {
         if (stageDialog == null) {
-            stageDialog = new MFXStageDialog(dialogType.get(), title, "");
+            stageDialog = new MFXStageDialog(dialogType.get(), getTitle(), "");
             Label label = (Label) stageDialog.getDialog().lookup(".content-label");
             label.setAlignment(Pos.CENTER);
         }
 
-        stageDialog.getDialog().setContent(getMessages());
+        stageDialog.getDialog().setContent(getUnmetMessages());
         stageDialog.setOwner(owner);
         stageDialog.setModality(Modality.WINDOW_MODAL);
         stageDialog.setCenterInOwner(true);
@@ -109,43 +111,13 @@ public class MFXDialogValidator extends AbstractMFXValidator {
 
     }
 
-    /**
-     * Adds a new boolean condition to the list with the corresponding message in case it is false.
-     *
-     * @param property The new boolean condition
-     * @param message  The message to show in case it is false
-     */
-    public void add(BooleanProperty property, String message) {
-        super.conditions.add(property);
-        this.messagesMap.put(property, message);
-    }
-
-    /**
-     * Removes the given property and the corresponding message from the list.
-     */
-    public void remove(BooleanProperty property) {
-        messagesMap.remove(property);
-        super.conditions.remove(property);
-    }
-
-    /**
-     * Checks the messages list and if the corresponding boolean condition is false
-     * adds the message to the {@code StringBuilder}.
-     */
-    public String getMessages() {
-        StringBuilder sb = new StringBuilder();
-        for (BooleanProperty property : messagesMap.keySet()) {
-            if (!property.get()) {
-                sb.append(messagesMap.get(property)).append(",\n");
-            }
-        }
-        return StringUtils.replaceLast(sb.toString(), ",", ".");
-    }
-
     public DialogType getDialogType() {
         return dialogType.get();
     }
 
+    /**
+     * Specifies the dialog's type.
+     */
     public ObjectProperty<DialogType> dialogTypeProperty() {
         return dialogType;
     }
@@ -155,10 +127,17 @@ public class MFXDialogValidator extends AbstractMFXValidator {
     }
 
     public String getTitle() {
+        return title.get();
+    }
+
+    /**
+     * Specifies the dialog's title.
+     */
+    public StringProperty titleProperty() {
         return title;
     }
 
     public void setTitle(String title) {
-        this.title = title;
+        this.title.set(title);
     }
 }

+ 128 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXPriorityValidator.java

@@ -0,0 +1,128 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.validation;
+
+import io.github.palexdev.materialfx.utils.StringUtils;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.MapProperty;
+import javafx.beans.property.SimpleMapProperty;
+import javafx.collections.FXCollections;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * This is a concrete implementation of {@link AbstractMFXValidator}.
+ * <p>
+ * The idea of this is to have a validator which automatically updates its message according to the
+ * state of the conditions.
+ * <p>
+ * A {@link MapProperty} is used to map the added conditions to a user defined message/string,
+ */
+public class MFXPriorityValidator extends AbstractMFXValidator {
+    //================================================================================
+    // Properties
+    //================================================================================
+    protected final MapProperty<BooleanProperty, String> messagesMap;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXPriorityValidator() {
+        messagesMap = new SimpleMapProperty<>(FXCollections.observableMap(new LinkedHashMap<>()));
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        validatorMessageProperty().bind(Bindings.createStringBinding(
+                this::findMessage,
+                valid, messagesMap)
+        );
+    }
+
+    /**
+     * Finds the first property in the {@link MapProperty} which is false and returns the associated message.
+     * <p></p>
+     * If nothing is found then proceeds to evaluate all the dependencies' maps.
+     * In case nothing is found even in the dependencies then returns an empty string.
+     */
+    private String findMessage() {
+        String message = messagesMap.entrySet().stream()
+                .filter(entry -> !entry.getKey().get())
+                .findFirst()
+                .map(Map.Entry::getValue)
+                .orElse("");
+
+        if (!message.isEmpty()) {
+            return message;
+        }
+
+        List<MFXPriorityValidator> dependencies = getDependencies().stream()
+                .filter(validator -> !validator.isValid())
+                .filter(validator -> validator instanceof MFXPriorityValidator)
+                .map(validator -> (MFXPriorityValidator) validator)
+                .collect(Collectors.toList());
+
+        return dependencies.stream().map(dependency -> dependency.messagesMap.entrySet().stream()
+                .filter(entry -> !entry.getKey().get())
+                .findFirst()
+                .map(Map.Entry::getValue)
+                .orElse("")).filter(dependencyMessage -> !dependencyMessage.isEmpty()).findFirst().orElse("");
+    }
+
+    /**
+     * Adds a new boolean condition to the list with the corresponding message in case it is false.
+     *
+     * @param property The new boolean condition
+     * @param message  The message to show in case it is false
+     */
+    public void add(BooleanProperty property, String message) {
+        super.conditions.add(property);
+        this.messagesMap.put(property, message);
+    }
+
+    /**
+     * Removes the given property and the corresponding message from the list.
+     */
+    public void remove(BooleanProperty property) {
+        messagesMap.remove(property);
+        super.conditions.remove(property);
+    }
+
+    /**
+     * Checks the messages list and if the corresponding boolean condition is false
+     * adds the message to the {@code StringBuilder}.
+     */
+    public String getUnmetMessages() {
+        StringBuilder sb = new StringBuilder();
+        for (BooleanProperty property : messagesMap.keySet()) {
+            if (!property.get()) {
+                sb.append(messagesMap.get(property)).append(",\n");
+            }
+        }
+        return StringUtils.replaceLast(sb.toString(), ",", ".");
+    }
+}

+ 120 - 18
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java

@@ -21,15 +21,28 @@ package io.github.palexdev.materialfx.validation.base;
 import io.github.palexdev.materialfx.beans.binding.BooleanListBinding;
 import io.github.palexdev.materialfx.utils.StringUtils;
 import javafx.beans.InvalidationListener;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.*;
 import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 
 /**
  * Base class for all validators.
+ * <p></p>
+ * Defines the common properties every validator should have, such as:
+ * a message that reflects the validator's state, a list of conditions that must be met
+ * in order for the state to be valid and a way to chain multiple validators considered
+ * as "dependencies".
+ * <p></p>
+ * Those conditions are evaluated by a {@link BooleanListBinding} and doesn't always represent
+ * the validator's state. In fact the validator's state is represented by another property, {@link #validProperty()}
+ * which takes into account all the validator's dependencies too.
+ * <p>
+ * The valid property is bound to the {@link BooleanListBinding} but when a change occurs in the dependencies list
+ * then its value is updated by the {@link #update()} method, which temporarily un-bounds the property and re-computes
+ * its value.
  *
  * @see IMFXValidator
  * @see BooleanListBinding
@@ -38,17 +51,59 @@ public abstract class AbstractMFXValidator implements IMFXValidator {
     //================================================================================
     // Properties
     //================================================================================
-    protected ObservableList<BooleanProperty> conditions = FXCollections.observableArrayList();
-    protected BooleanListBinding validation = new BooleanListBinding(conditions);
-    private final StringProperty validatorMessage = new SimpleStringProperty("Validation failed!");
+    protected final StringProperty validatorMessage = new SimpleStringProperty("Validation failed!");
+    protected final ObservableList<BooleanProperty> conditions;
+    protected final ObservableList<AbstractMFXValidator> dependencies;
+    protected final BooleanListBinding listBinding;
+    protected final ReadOnlyBooleanWrapper valid;
+    protected boolean initControlValidation = false;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public AbstractMFXValidator() {
+        conditions = FXCollections.observableArrayList();
+        dependencies = FXCollections.observableArrayList();
+        listBinding = new BooleanListBinding(conditions);
+        valid = new ReadOnlyBooleanWrapper();
+        addListeners();
+    }
+
+    private void addListeners() {
+        valid.bind(listBinding);
+        dependencies.addListener((InvalidationListener) invalidated -> update());
+    }
 
     //================================================================================
     // Methods
     //================================================================================
+
+    /**
+     * Updates the {@link #validProperty()} value as follows:
+     * <p>
+     * Unbinds the property, then defines a new {@link BooleanBinding} variable
+     * instantiated as the {@link BooleanListBinding}, ({@code BooleanBinding binding = listBinding}),
+     * then for each dependency applies the {@link BooleanBinding#and(ObservableBooleanValue)} method on
+     * that variable with the dependency valid property as the argument.
+     * <p>
+     * At the end binds again the valid property to the new {@link BooleanBinding} variable.
+     */
+    public void update() {
+        valid.unbind();
+        BooleanBinding binding = listBinding;
+        for (AbstractMFXValidator dependency : dependencies) {
+            binding = binding.and(dependency.validProperty());
+        }
+        valid.bind(binding);
+    }
+
     public String getValidatorMessage() {
         return validatorMessage.get();
     }
 
+    /**
+     * Specifies the validator's message.
+     */
     public StringProperty validatorMessageProperty() {
         return validatorMessage;
     }
@@ -57,8 +112,36 @@ public abstract class AbstractMFXValidator implements IMFXValidator {
         this.validatorMessage.set(validatorMessage);
     }
 
-    public BooleanListBinding validationProperty() {
-        return validation;
+    /**
+     * @return the dependencies list
+     */
+    public ObservableList<AbstractMFXValidator> getDependencies() {
+        return dependencies;
+    }
+
+    /**
+     * Adds the specified dependencies to the list.
+     */
+    public void addDependencies(AbstractMFXValidator... dependencies) {
+        this.dependencies.addAll(dependencies);
+    }
+
+    /**
+     * Removes the specifies dependencies from the list.
+     * <p>
+     * Note: to remove all dependencies if you don't have their instance you can also
+     * get the dependencies list with {@link #getDependencies()} and then call the clear() method.
+     */
+    public void removeDependencies(AbstractMFXValidator... dependencies) {
+        this.dependencies.removeAll(dependencies);
+    }
+
+    public boolean isInitControlValidation() {
+        return initControlValidation;
+    }
+
+    public void setInitControlValidation(boolean initControlValidation) {
+        this.initControlValidation = initControlValidation;
     }
 
     //================================================================================
@@ -66,31 +149,50 @@ public abstract class AbstractMFXValidator implements IMFXValidator {
     //================================================================================
     @Override
     public boolean isValid() {
-        return this.validation.get();
+        return this.valid.get();
+    }
+
+    /**
+     * Specifies the validator's state.
+     */
+    public ReadOnlyBooleanProperty validProperty() {
+        return valid.getReadOnlyProperty();
     }
 
+    /**
+     * Wrapper method to add an {@link InvalidationListener} to the valid property of the validator.
+     */
     @Override
-    public void addInvalidationListener(InvalidationListener listener) {
-        this.validation.addListener(listener);
+    public void addListener(InvalidationListener listener) {
+        this.valid.addListener(listener);
     }
 
+    /**
+     * Wrapper method to add a {@link ChangeListener} to the valid property of the validator.
+     */
     @Override
-    public void addChangeListener(ChangeListener<? super Boolean> listener) {
-        this.validation.addListener(listener);
+    public void addListener(ChangeListener<? super Boolean> listener) {
+        this.valid.addListener(listener);
     }
 
+    /**
+     * Wrapper method to remove an {@link InvalidationListener} from the valid property of the validator.
+     */
     @Override
-    public void removeInvalidationListener(InvalidationListener listener) {
-        this.validation.removeListener(listener);
+    public void removeListener(InvalidationListener listener) {
+        this.valid.removeListener(listener);
     }
 
+    /**
+     * Wrapper method to remove a {@link ChangeListener} from the valid property of the validator.
+     */
     @Override
-    public void removeChangeListener(ChangeListener<? super Boolean> listener) {
-        this.validation.removeListener(listener);
+    public void removeListener(ChangeListener<? super Boolean> listener) {
+        this.valid.removeListener(listener);
     }
 
     /**
-     * Returns all booleans as a string.
+     * Returns all the boolean properties as a string.
      */
     @Override
     public String toString() {

+ 5 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/IMFXValidator.java

@@ -18,18 +18,16 @@
 
 package io.github.palexdev.materialfx.validation.base;
 
-import io.github.palexdev.materialfx.beans.binding.BooleanListBinding;
 import javafx.beans.InvalidationListener;
 import javafx.beans.value.ChangeListener;
 
 /**
- * Interface for all validators, most of these methods are wrappers
- * for {@link BooleanListBinding}
+ * Interface for all validators.
  */
 public interface IMFXValidator {
     boolean isValid();
-    void addInvalidationListener(InvalidationListener invalidationListener);
-    void addChangeListener(ChangeListener<? super Boolean> changeListener);
-    void removeInvalidationListener(InvalidationListener invalidationListener);
-    void removeChangeListener(ChangeListener<? super Boolean> changeListener);
+    void addListener(InvalidationListener invalidationListener);
+    void addListener(ChangeListener<? super Boolean> changeListener);
+    void removeListener(InvalidationListener invalidationListener);
+    void removeListener(ChangeListener<? super Boolean> changeListener);
 }

+ 56 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/Validated.java

@@ -0,0 +1,56 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.validation.base;
+
+import javafx.scene.Node;
+
+import java.util.function.Supplier;
+
+/**
+ * Interface that specifies the methods all validated controls should have.
+ *
+ * @param <T> The validator type, extends {@link AbstractMFXValidator}
+ */
+public interface Validated<T extends AbstractMFXValidator> {
+    /**
+     * @return the validator instance
+     */
+    T getValidator();
+
+    /**
+     * Replaces the control's default validator with a user's specified one.
+     * <p></p>
+     * <b>
+     * N.B: This method must be called before the control is laid out in the scene,
+     * otherwise the validation system will most likely be broken.
+     * <p>
+     * A good usage would be immediately after the constructor or in the initialize block of controllers.
+     * </b>
+     *
+     * @return the control's instance
+     */
+    Node installValidator(Supplier<T> validatorSupplier);
+
+    /**
+     * @return true if the validator is null or its state is valid. False if its state is invalid
+     */
+    default boolean isValid() {
+        return (getValidator() == null || getValidator().isValid());
+    }
+}

+ 28 - 2
materialfx/src/main/resources/io/github/palexdev/materialfx/css/legacy/mfx-combobox.css

@@ -17,8 +17,9 @@
  */
 
 .mfx-legacy-combo-box {
+    -mfx-line-color: rgb(50, 120, 220);
+    -mfx-unfocused-line-color: #4d4d4d;
     -fx-prompt-text-fill: #4d4d4d;
-
     -fx-padding: -1 -1 -1 -5;
 }
 
@@ -56,5 +57,30 @@
 }
 
 .mfx-legacy-combo-box .validate-label {
-    -fx-text-fill: #D34336;
+    -mfx-line-color: transparent;
+    -mfx-unfocused-line-color: transparent;
+    -fx-background-color: transparent;
+    -mfx-font-family: "Open Sans SemiBold";
+    -mfx-font-size: 11;
+    -mfx-text-fill: #EF6E6B;
+}
+
+.mfx-legacy-combo-box:invalid {
+    -mfx-line-color: #EF6E6B;
+    -mfx-unfocused-line-color: #EF6E6B;
+    -fx-prompt-text-fill: #EF6E6B;
+    -fx-text-fill: #EF6E6B;
+}
+
+.mfx-legacy-combo-box:invalid .text-field {
+    -fx-prompt-text-fill: #EF6E6B;
+    -fx-text-fill: #EF6E6B;
+}
+
+.mfx-legacy-combo-box:invalid .list-cell {
+    -fx-text-fill: #EF6E6B;
+}
+
+.mfx-legacy-combo-box:invalid > .arrow-button > .arrow {
+    -fx-background-color: #EF6E6B;
 }

+ 22 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-colors.css

@@ -0,0 +1,22 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+* {
+    -mfx-charcoal: #445055;
+    -mfx-red: #EF6E6B;
+}

+ 55 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-stepper.css

@@ -0,0 +1,55 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "fonts.css";
+@import "mfx-colors.css";
+
+.mfx-stepper {
+    -mfx-base-color: #7F0FFF;
+    -mfx-alt-color: #BEBEBE;
+    -mfx-bar-background: #F8F8FF;
+    -mfx-progress-color: -mfx-base-color;
+}
+
+.mfx-stepper .buttons-box .mfx-button {
+    -mfx-depth-level: level1;
+    -fx-background-color: white;
+    -fx-border-color: -mfx-charcoal;
+    -fx-background-radius: 20;
+    -fx-border-radius: 20;
+    -fx-font-family: 'Open Sans SemiBold';
+    -fx-font-size: 13;
+}
+
+.mfx-stepper .buttons-box .mfx-button .text {
+    -fx-fill: -mfx-charcoal;
+}
+
+.mfx-stepper .buttons-box .mfx-button:focused {
+    -fx-background-color: -mfx-base-color;
+    -fx-border-color: -mfx-base-color;
+}
+
+.mfx-stepper .buttons-box .mfx-button:focused .text {
+    -fx-fill: white;
+}
+
+.mfx-stepper .buttons-box .mfx-button .ripple-generator {
+    -mfx-ripple-color: derive(-mfx-base-color, 30%);
+    -mfx-ripple-radius: 60px;
+}

+ 79 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-steppertoggle.css

@@ -0,0 +1,79 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "fonts.css";
+
+.mfx-stepper-toggle {
+    -mfx-icon-color-selected: white;
+}
+
+.mfx-stepper-toggle .mfx-label {
+    -fx-background-color: transparent;
+    -mfx-font-family: 'Open Sans SemiBold';
+    -mfx-font-size: 14px;
+    -mfx-text-fill: -mfx-alt-color;
+}
+
+.mfx-stepper-toggle #circle {
+    -fx-fill: -mfx-bar-background;
+    -fx-stroke: -mfx-bar-background;
+    -fx-stroke-width: 2px;
+}
+
+.mfx-stepper-toggle .mfx-font-icon {
+    -mfx-color: -mfx-base-color;
+}
+
+.mfx-stepper-toggle:selected #circle {
+    -fx-fill: -mfx-base-color;
+    -fx-stroke: -mfx-base-color;
+}
+
+.mfx-stepper-toggle:selected .mfx-font-icon {
+    -mfx-color: white;
+}
+
+.mfx-stepper-toggle:selected .mfx-label {
+    -mfx-text-fill: -mfx-base-color;
+}
+
+.mfx-stepper-toggle:completed #circle {
+    -fx-fill: white;
+    -fx-stroke: -mfx-base-color;
+}
+
+.mfx-stepper-toggle:completed .mfx-font-icon {
+    -mfx-color: -mfx-alt-color;
+}
+
+.mfx-stepper-toggle:completed .mfx-label {
+    -mfx-text-fill: -mfx-alt-color;
+}
+
+.mfx-stepper-toggle:error #circle {
+    -fx-fill: white;
+    -fx-stroke: -mfx-red;
+}
+
+.mfx-stepper-toggle:error .mfx-font-icon {
+    -mfx-color: -mfx-red;
+}
+
+.mfx-stepper-toggle:error .mfx-label {
+    -mfx-text-fill: -mfx-red;
+}

+ 15 - 2
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-textfield.css

@@ -19,6 +19,8 @@
 .mfx-text-field {
     -fx-prompt-text-fill: #61606C;
     -fx-padding: 0.333333em 0 0.333333em 0;
+    -mfx-line-color: rgb(50, 150, 205);
+    -mfx-unfocused-line-color: rgb(77, 77, 77);
 }
 
 .mfx-text-field,
@@ -27,6 +29,17 @@
 }
 
 .mfx-text-field .validate-label {
-    -fx-font-family: "Open Sans SemiBold";
-    -fx-font-size: 11;
+    -mfx-line-color: transparent;
+    -mfx-unfocused-line-color: transparent;
+    -fx-background-color: transparent;
+    -mfx-font-family: "Open Sans SemiBold";
+    -mfx-font-size: 11;
+    -mfx-text-fill: #EF6E6B;
+}
+
+.mfx-text-field:invalid {
+    -fx-text-fill: #EF6E6B;
+    -fx-prompt-text-fill: #EF6E6B;
+    -mfx-line-color: #EF6E6B;
+    -mfx-unfocused-line-color: #EF6E6B;
 }