浏览代码

Version 11.5.5-staging
Added 2 new controls: MFXDatePicker and MFXTextField.

Added custom implementation of DateCell, MFXDateCell.
Added MaterialFX font, FontHandler, FontResources and MFXFontIcon for better managing SVG resources.
Added new methods to NodeUtils, used for positioning MFXDatePicker's popup.
Added new method to StringUtils.
Added new method to MFXResourcesLoader.
Added new SVG resources to MFXResourcesManager.

Fixed getClassCssMetaDataList() method in many controls, if I'm not wrong it should be static.
MFXButton, moved RippleGenerator initialization in separate protected method for overriding.
Ripples clip, using factory pattern which is more flexible than just an enum.
RippleGenerator, use clip from factory for background animation instead of Rectangle.
MFXListViewSkin, fixed scrollbars not being initially hidden if hideScrollBars is set to true.
AbstractMFXValidator, always initialize properties.
MFXDialogValidator, use mapping for messages rather than relying on list indexes.
Fixed font.css
Fixed MFXScrollpane's bars not being completely hidden.

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

PAlex404 4 年之前
父节点
当前提交
0090c31ac1
共有 53 个文件被更改,包括 3272 次插入156 次删除
  1. 23 0
      .run/MaterialFX [build].run.xml
  2. 23 0
      .run/MaterialFX [clean].run.xml
  3. 23 0
      .run/MaterialFX [materialfx_uploadArchives].run.xml
  4. 23 0
      .run/MaterialFX [run].run.xml
  5. 1 1
      build.gradle
  6. 32 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DatePickersDemoController.java
  7. 8 6
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java
  8. 2 1
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DialogsController.java
  9. 43 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextfieldsDemoController.java
  10. 7 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/datepickers_demo.css
  11. 21 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/textfields_demo.css
  12. 19 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/customcss/custom-datepicker.css
  13. 53 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/datepickers_demo.fxml
  14. 57 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml
  15. 1 1
      materialfx/gradle.properties
  16. 5 0
      materialfx/src/main/java/io/github/palexdev/materialfx/MFXResourcesLoader.java
  17. 2 0
      materialfx/src/main/java/io/github/palexdev/materialfx/MFXResourcesManager.java
  18. 17 10
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java
  19. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckbox.java
  20. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXComboBox.java
  21. 113 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDateCell.java
  22. 411 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDatePicker.java
  23. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListCell.java
  24. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java
  25. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXRadioButton.java
  26. 287 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java
  27. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleButton.java
  28. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java
  29. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/SimpleMFXNotificationPane.java
  30. 63 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/RippleClipTypeFactory.java
  31. 4 37
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleClipType.java
  32. 20 18
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleGenerator.java
  33. 26 0
      materialfx/src/main/java/io/github/palexdev/materialfx/font/FontHandler.java
  34. 41 0
      materialfx/src/main/java/io/github/palexdev/materialfx/font/FontResources.java
  35. 167 0
      materialfx/src/main/java/io/github/palexdev/materialfx/font/MFXFontIcon.java
  36. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCheckboxSkin.java
  37. 50 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDateCellSkin.java
  38. 920 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java
  39. 8 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java
  40. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRadioButtonSkin.java
  41. 189 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java
  42. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXToggleButtonSkin.java
  43. 347 9
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java
  44. 11 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java
  45. 9 20
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java
  46. 7 2
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java
  47. 1 0
      materialfx/src/main/java/module-info.java
  48. 27 28
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/fonts.css
  49. 167 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-datepicker-content.css
  50. 5 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-datepicker.css
  51. 8 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-scrollpane.css
  52. 9 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-textfield.css
  53. 二进制
      materialfx/src/main/resources/io/github/palexdev/materialfx/fonts/materialfx-resources.ttf

+ 23 - 0
.run/MaterialFX [build].run.xml

@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="MaterialFX [build]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="build" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 23 - 0
.run/MaterialFX [clean].run.xml

@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="MaterialFX [clean]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="clean" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 23 - 0
.run/MaterialFX [materialfx_uploadArchives].run.xml

@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="MaterialFX [materialfx:uploadArchives]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="materialfx:uploadArchives" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 23 - 0
.run/MaterialFX [run].run.xml

@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="MaterialFX [run]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="run" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 1 - 1
build.gradle

@@ -4,7 +4,7 @@ plugins {
 }
 
 group 'io.github.palexdev'
-version '11.4.4'
+version '11.5.5-staging'
 
 repositories {
     mavenCentral()

+ 32 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DatePickersDemoController.java

@@ -0,0 +1,32 @@
+package io.github.palexdev.materialfx.demo.controllers;
+
+import io.github.palexdev.materialfx.controls.MFXDatePicker;
+import io.github.palexdev.materialfx.demo.MFXResourcesLoader;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.geometry.Insets;
+import javafx.scene.layout.StackPane;
+
+import java.net.URL;
+import java.time.LocalDate;
+import java.util.ResourceBundle;
+
+public class DatePickersDemoController implements Initializable {
+
+    @FXML
+    private MFXDatePicker customPicker;
+
+    @FXML
+    private StackPane pane;
+
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        String css = MFXResourcesLoader.load("customcss/custom-datepicker.css").toString();
+        customPicker.getContent().getStylesheets().add(css);
+
+        MFXDatePicker initialized = new MFXDatePicker(LocalDate.now());
+        initialized.setColorText(true);
+        pane.getChildren().add(initialized);
+        StackPane.setMargin(initialized, new Insets(10, 0, 0, 0));
+    }
+}

+ 8 - 6
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java

@@ -146,12 +146,14 @@ public class DemoController implements Initializable {
         vLoader.addItem(0, "BUTTONS", new MFXToggleNode("BUTTONS"), MFXResourcesLoader.load("buttons_demo.fxml"));
         vLoader.addItem(1, "CHECKBOXES", new MFXToggleNode("CHECKBOXES"), MFXResourcesLoader.load("checkboxes_demo.fxml"));
         vLoader.addItem(2, "COMBOBOXES", new MFXToggleNode("COMBOBOXES"), MFXResourcesLoader.load("combo_boxes_demo.fxml"));
-        vLoader.addItem(3, "DIALOGS", new MFXToggleNode("DIALOGS"), MFXResourcesLoader.load("dialogs_demo.fxml"), controller -> new DialogsController(demoPane));
-        vLoader.addItem(4, "LISTVIEWS", new MFXToggleNode("LISTVIEWS"), MFXResourcesLoader.load("listviews_demo.fxml"));
-        vLoader.addItem(5, "NOTIFICATIONS", new MFXToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml"));
-        vLoader.addItem(6, "RADIOBUTTONS", new MFXToggleNode("RADIOBUTTONS"), MFXResourcesLoader.load("radio_buttons_demo.fxml"));
-        vLoader.addItem(7, "SCROLLPANES", new MFXToggleNode("SCROLLPANES"), MFXResourcesLoader.load("scrollpanes_demo.fxml"));
-        vLoader.addItem(8, "TOGGLES", new MFXToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_demo.fxml"));
+        vLoader.addItem(3, "DATEPICKERS", new MFXToggleNode("DATEPICKERS"), MFXResourcesLoader.load("datepickers_demo.fxml"));
+        vLoader.addItem(4, "DIALOGS", new MFXToggleNode("DIALOGS"), MFXResourcesLoader.load("dialogs_demo.fxml"), controller -> new DialogsController(demoPane));
+        vLoader.addItem(5, "LISTVIEWS", new MFXToggleNode("LISTVIEWS"), MFXResourcesLoader.load("listviews_demo.fxml"));
+        vLoader.addItem(6, "NOTIFICATIONS", new MFXToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml"));
+        vLoader.addItem(7, "RADIOBUTTONS", new MFXToggleNode("RADIOBUTTONS"), MFXResourcesLoader.load("radio_buttons_demo.fxml"));
+        vLoader.addItem(8, "SCROLLPANES", new MFXToggleNode("SCROLLPANES"), MFXResourcesLoader.load("scrollpanes_demo.fxml"));
+        vLoader.addItem(9, "TEXTFIELDS", new MFXToggleNode("TEXTFIELDS"), MFXResourcesLoader.load("textfields_demo.fxml"));
+        vLoader.addItem(10, "TOGGLES", new MFXToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_demo.fxml"));
         vLoader.setDefault("BUTTONS");
 
         // Others

+ 2 - 1
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DialogsController.java

@@ -6,6 +6,7 @@ import io.github.palexdev.materialfx.controls.base.AbstractMFXDialog;
 import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
+import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.scene.layout.Pane;
@@ -78,7 +79,7 @@ public class DialogsController implements Initializable {
         this.animateDialog = MFXDialogFactory.buildDialog(DialogType.INFO, "", text);
         this.animateDialog.setAnimateIn(true);
         this.animateDialog.setAnimateOut(true);
-        this.pane.getChildren().addAll(dialog, animateDialog);
+        Platform.runLater(() -> this.pane.getChildren().addAll(dialog, animateDialog));
     }
 
     @Override

+ 43 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextfieldsDemoController.java

@@ -0,0 +1,43 @@
+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 javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Label;
+
+import java.net.URL;
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.ResourceBundle;
+
+public class TextfieldsDemoController implements Initializable {
+
+    @FXML
+    private MFXTextField validated;
+
+    @FXML
+    private MFXCheckbox checkbox;
+
+    @FXML
+    private MFXDatePicker picker;
+
+    @FXML
+    private Label label;
+
+    @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)));
+        validated.getValidator().add(checkboxValidation, "Checkbox must be selected");
+        validated.getValidator().add(datePickerValidation, "Selected date must be 03/10/1911");
+        validated.setIsValidated(true);
+
+        label.visibleProperty().bind(validated.getValidator().validationProperty());
+    }
+}

+ 7 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/datepickers_demo.css

@@ -0,0 +1,7 @@
+#customPicker {
+    -mfx-main-color: salmon;
+    -mfx-line-color: salmon;
+    -mfx-color-text: true;
+    -mfx-close-on-day-selected: false;
+    -mfx-animate-calendar: false;
+}

+ 21 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/textfields_demo.css

@@ -0,0 +1,21 @@
+.mfx-text-field {
+    -fx-font-family: "Open Sans Regular";
+}
+
+.label {
+    -fx-font-family: "Open Sans SemiBold";
+}
+
+#colors {
+    -fx-prompt-text-fill: #000e8b;
+    -fx-text-fill: #000e8b;
+    -mfx-unfocused-line-color: #000e8b;
+    -mfx-line-color: darkorange;
+
+    -mfx-text-limit: 10;
+}
+
+#colors:focused {
+    -fx-prompt-text-fill: darkorange;
+    -fx-text-fill: darkorange;
+}

+ 19 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/customcss/custom-datepicker.css

@@ -0,0 +1,19 @@
+.years-button .ripple-generator {
+    -mfx-ripple-color: #ff4100;
+}
+
+.month-back-button .ripple-generator {
+    -mfx-ripple-color: #ff4100;
+}
+
+.month-forward-button .ripple-generator {
+    -mfx-ripple-color: #ff4100;
+}
+
+.day-cell .ripple-generator {
+    -mfx-ripple-color: #ff4100;
+}
+
+.year-cell .ripple-generator {
+    -mfx-ripple-color: #ff4100;
+}

+ 53 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/datepickers_demo.fxml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import io.github.palexdev.materialfx.controls.*?>
+<?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/datepickers_demo.css" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.DatePickersDemoController">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Date Pickers" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="20.0" />
+      </StackPane.margin>
+   </Label>
+   <MFXDatePicker StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="300.0" top="70.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <MFXDatePicker closeOnDaySelected="false" colorText="true" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="70.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <MFXDatePicker disable="true" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="300.0" top="70.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Customization" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="130.0" />
+      </StackPane.margin>
+   </Label>
+   <MFXDatePicker lineColor="#ff9000b2" pickerColor="#17c500">
+      <StackPane.margin>
+         <Insets right="150.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <MFXDatePicker id="customPicker" fx:id="customPicker">
+      <StackPane.margin>
+         <Insets left="150.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Initialized">
+      <StackPane.margin>
+         <Insets top="110.0" />
+      </StackPane.margin>
+   </Label>
+   <StackPane fx:id="pane" alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="110.0" prefWidth="250.0" StackPane.alignment="BOTTOM_CENTER">
+      <StackPane.margin>
+         <Insets bottom="10.0" />
+      </StackPane.margin>
+   </StackPane>
+</StackPane>

+ 57 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import io.github.palexdev.materialfx.controls.*?>
+<?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/11.0.1" 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" />
+        </StackPane.margin>
+    </Label>
+   <MFXTextField alignment="CENTER" maxWidth="-Infinity" prefWidth="120.0" text="MFXTextField" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="300.0" top="70.0" />
+      </StackPane.margin></MFXTextField>
+   <MFXTextField alignment="CENTER" maxWidth="-Infinity" prefWidth="120.0" promptText="Text Limit" textLimit="5" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="70.0" />
+      </StackPane.margin>
+   </MFXTextField>
+   <MFXTextField alignment="CENTER" disable="true" maxWidth="-Infinity" prefWidth="120.0" text="Disabled" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="300.0" top="70.0" />
+      </StackPane.margin>
+   </MFXTextField>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Customization" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <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>
+   <MFXCheckbox fx:id="checkbox" checkedColor="#00e240" markType="VARIANT9" text="CheckBox Validation" StackPane.alignment="BOTTOM_LEFT">
+      <StackPane.margin>
+         <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
+      </StackPane.margin>
+   </MFXCheckbox>
+   <MFXDatePicker fx:id="picker" colorText="true" lineColor="#00c133b2" pickerColor="#00c133" StackPane.alignment="BOTTOM_CENTER">
+      <StackPane.margin>
+         <Insets bottom="30.0" />
+      </StackPane.margin>
+   </MFXDatePicker>
+   <Label fx:id="label" alignment="CENTER" minHeight="-Infinity" minWidth="-Infinity" prefHeight="25.0" prefWidth="100.0" text="VALID" textFill="#00c133" visible="false" StackPane.alignment="BOTTOM_RIGHT">
+      <StackPane.margin>
+         <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
+      </StackPane.margin>
+   </Label>
+</StackPane>

+ 1 - 1
materialfx/gradle.properties

@@ -1,6 +1,6 @@
 GROUP=io.github.palexdev
 POM_ARTIFACT_ID=materialfx
-VERSION_NAME=11.4.4
+VERSION_NAME=11.5.5-staging
 
 POM_NAME=materialfx
 POM_DESCRIPTION=Material Desgin components for JavaFX

+ 5 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/MFXResourcesLoader.java

@@ -1,5 +1,6 @@
 package io.github.palexdev.materialfx;
 
+import java.io.InputStream;
 import java.net.URL;
 
 /**
@@ -13,4 +14,8 @@ public class MFXResourcesLoader {
     public static URL load(String path) {
         return MFXResourcesLoader.class.getResource(path);
     }
+
+    public static InputStream loadStream(String name) {
+        return MFXResourcesLoader.class.getResourceAsStream(name);
+    }
 }

+ 2 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/MFXResourcesManager.java

@@ -43,6 +43,8 @@ public class MFXResourcesManager {
         EXCLAMATION_TRIANGLE("M 54.04 32 h 7.1 c 0.68 0 1.22 0.56 1.2 1.24 l -1.5 39.2 c -0.02 0.64 -0.56 1.16 -1.2 1.16 h -4.1 c -0.64 0 -1.18 -0.5 -1.2 -1.16 l -1.5 -39.2 c -0.02 -0.68 0.52 -1.24 1.2 -1.24 z M 57.6 77.6 c -3.1 0 -5.6 2.5 -5.6 5.6 s 2.5 5.6 5.6 5.6 s 5.6 -2.5 5.6 -5.6 s -2.5 -5.6 -5.6 -5.6 z m 56.3 10.4 L 65.92 4.8 c -3.68 -6.4 -12.94 -6.4 -16.64 0 L 1.3 88 c -3.68 6.38 0.92 14.4 8.32 14.4 H 105.6 c 7.36 0 12 -8 8.3 -14.4 z M 105.6 96 H 9.6 c -2.46 0 -4 -2.66 -2.78 -4.8 l 48 -83.2 c 1.22 -2.12 4.32 -2.14 5.54 0 l 48 83.2 c 1.24 2.12 -0.3 4.8 -2.76 4.8 z"),
         INFO("M 51.2 8 c 23.7242 0 43.2 19.215 43.2 43.2 c 0 23.8582 -19.322 43.2 -43.2 43.2 c -23.8488 0 -43.2 -19.3124 -43.2 -43.2 c 0 -23.8406 19.3204 -43.2 43.2 -43.2 m 0 -6.4 C 23.8086 1.6 1.6 23.8166 1.6 51.2 c 0 27.3994 22.2086 49.6 49.6 49.6 s 49.6 -22.2006 49.6 -49.6 C 100.8 23.8166 78.5914 1.6 51.2 1.6 z m -7.2 68.8 h 2.4 V 46.4 h -2.4 c -1.3254 0 -2.4 -1.0746 -2.4 -2.4 v -1.6 c 0 -1.3254 1.0746 -2.4 2.4 -2.4 h 9.6 c 1.3254 0 2.4 1.0746 2.4 2.4 v 28 h 2.4 c 1.3254 0 2.4 1.0746 2.4 2.4 v 1.6 c 0 1.3254 -1.0746 2.4 -2.4 2.4 h -14.4 c -1.3254 0 -2.4 -1.0746 -2.4 -2.4 v -1.6 c 0 -1.3254 1.0746 -2.4 2.4 -2.4 z m 7.2 -48 c -3.5346 0 -6.4 2.8654 -6.4 6.4 s 2.8654 6.4 6.4 6.4 s 6.4 -2.8654 6.4 -6.4 s -2.8654 -6.4 -6.4 -6.4 z"),
         X("M 48.544 51.2 l 20.014 -20.014 c 2.456 -2.456 2.456 -6.438 0 -8.896 l -4.448 -4.448 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 35.2 37.856 L 15.186 17.842 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 1.842 22.29 c -2.456 2.456 -2.456 6.438 0 8.896 L 21.856 51.2 L 1.842 71.214 c -2.456 2.456 -2.456 6.438 0 8.896 l 4.448 4.448 c 2.456 2.456 6.44 2.456 8.896 0 L 35.2 64.544 l 20.014 20.014 c 2.456 2.456 6.44 2.456 8.896 0 l 4.448 -4.448 c 2.456 -2.456 2.456 -6.438 0 -8.896 L 48.544 51.2 z"),
+        ANGLE_LEFT("M238.475 475.535l7.071-7.07c4.686-4.686 4.686-12.284 0-16.971L50.053 256 245.546 60.506c4.686-4.686 4.686-12.284 0-16.971l-7.071-7.07c-4.686-4.686-12.284-4.686-16.97 0L10.454 247.515c-4.686 4.686-4.686 12.284 0 16.971l211.051 211.05c4.686 4.686 12.284 4.686 16.97-.001z"),
+        ANGLE_RIGHT("M17.525 36.465l-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L205.947 256 10.454 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L34.495 36.465c-4.686-4.687-12.284-4.687-16.97 0z"),
         ANGLE_DOWN("M151.5 347.8L3.5 201c-4.7-4.7-4.7-12.3 0-17l19.8-19.8c4.7-4.7 12.3-4.7 17 0L160 282.7l119.7-118.5c4.7-4.7 12.3-4.7 17 0l19.8 19.8c4.7 4.7 4.7 12.3 0 17l-148 146.8c-4.7 4.7-12.3 4.7-17 0z");
 
         private final String svgPath;

+ 17 - 10
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java

@@ -146,14 +146,26 @@ public class MFXButton extends Button {
         rippleGenerator.setOutDuration(rippleOutDuration);
     }
 
+    public RippleGenerator getRippleGenerator() {
+        return rippleGenerator;
+    }
+
+    protected void setupRippleGenerator() {
+        this.getChildren().add(0, rippleGenerator);
+        this.setOnMousePressed(event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+    }
+
     private void setBindings() {
         rippleColor.bind(rippleGenerator.rippleColorProperty());
         rippleRadius.bind(rippleGenerator.rippleRadiusProperty());
         rippleInDuration.bind(rippleGenerator.inDurationProperty());
         rippleOutDuration.bind(rippleGenerator.outDurationProperty());
     }
-
-    //================================================================================
+//================================================================================
     // Styleable Properties
     //================================================================================
 
@@ -234,7 +246,7 @@ public class MFXButton extends Button {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -244,12 +256,7 @@ public class MFXButton extends Button {
     @Override
     protected Skin<?> createDefaultSkin() {
         MFXButtonSkin skin = new MFXButtonSkin(this);
-        this.getChildren().add(0, rippleGenerator);
-        this.setOnMousePressed(event -> {
-            rippleGenerator.setGeneratorCenterX(event.getX());
-            rippleGenerator.setGeneratorCenterY(event.getY());
-            rippleGenerator.createRipple();
-        });
+        setupRippleGenerator();
         return skin;
     }
 
@@ -260,6 +267,6 @@ public class MFXButton extends Button {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXButton.getControlCssMetaDataList();
     }
 }

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckbox.java

@@ -151,8 +151,8 @@ public class MFXCheckbox extends CheckBox {
         }
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
-        return MFXCheckbox.StyleableProperties.cssMetaDataList;
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
     }
 
     //================================================================================
@@ -171,7 +171,7 @@ public class MFXCheckbox extends CheckBox {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXCheckbox.getControlCssMetaDataList();
     }
 
 }

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

@@ -299,7 +299,7 @@ public class MFXComboBox<T> extends ComboBox<T> {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -318,6 +318,6 @@ public class MFXComboBox<T> extends ComboBox<T> {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXComboBox.getControlCssMetaDataList();
     }
 }

+ 113 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDateCell.java

@@ -0,0 +1,113 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import io.github.palexdev.materialfx.skins.MFXDateCellSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.css.PseudoClass;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.DateCell;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+
+/**
+ * Custom implementation of a {@code DateCell} for easily distinguish selected dates and
+ * current dates. Includes ripple effects.
+ */
+public class MFXDateCell extends DateCell {
+    private final String STYLE_CLASS = "mfx-date-cell";
+
+    private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selectedDate");
+    private static final PseudoClass CURRENT_DAY_PSEUDO_CLASS = PseudoClass.getPseudoClass("current");
+
+    private final BooleanProperty selectedDate = new SimpleBooleanProperty(false);
+    private final BooleanProperty current = new SimpleBooleanProperty(false);
+
+    private final RippleGenerator rippleGenerator = new RippleGenerator(this);
+
+    private boolean drawGraphic = false;
+
+    public MFXDateCell() {
+        initialize();
+    }
+
+    public MFXDateCell(String text) {
+        setText(text);
+        initialize();
+    }
+
+    public MFXDateCell(String text, boolean drawGraphic) {
+        setText(text);
+        this.drawGraphic = drawGraphic;
+        initialize();
+    }
+
+    private void initialize() {
+        rippleGenerator.setRippleColor(Color.rgb(220, 220, 220, 0.6));
+        getStyleClass().setAll(STYLE_CLASS);
+        addListeners();
+    }
+
+    private void addListeners() {
+        selectedDate.addListener(invalidate -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, selectedDate.get()));
+        current.addListener(invalidate -> pseudoClassStateChanged(CURRENT_DAY_PSEUDO_CLASS, current.get()));
+
+        selectedDate.addListener((observable, oldValue, newValue) -> {
+            if (getGraphic() != null) {
+                getGraphic().setVisible(!newValue || !current.get());
+            }
+        });
+
+        current.addListener((observable, oldValue, newValue) -> {
+            if (newValue && !selectedDate.get() && drawGraphic) {
+                Circle circle = new Circle(getPrefWidth() / 3.5);
+                circle.setFill(Color.TRANSPARENT);
+                circle.getStyleClass().add("cell-stroke");
+
+                setContentDisplay(ContentDisplay.CENTER);
+                setGraphic(circle);
+            }
+        });
+
+        addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+    }
+
+    public boolean isSelectedDate() {
+        return selectedDate.get();
+    }
+
+    public BooleanProperty selectedDateProperty() {
+        return selectedDate;
+    }
+
+    public void setSelectedDate(boolean selectedDate) {
+        this.selectedDate.set(selectedDate);
+    }
+
+    public boolean isCurrent() {
+        return current.get();
+    }
+
+    public BooleanProperty currentProperty() {
+        return current;
+    }
+
+    public void setCurrent(boolean current) {
+        this.current.set(current);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXDateCellSkin(this);
+    }
+
+}

+ 411 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDatePicker.java

@@ -0,0 +1,411 @@
+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.scene.control.DatePicker;
+import javafx.scene.control.Label;
+import javafx.scene.control.PopupControl;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Line;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * This is the implementation of a date picker following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code VBox}, redefines the style class to "mfx-date-picker" for usage in CSS.
+ * <p>A few notes:</p>
+ * <p>
+ * Extends {@code VBox} rather than extending {@code DatePicker} because JavaFX's date picker code is a huge mess
+ * and also bad designed.
+ * Rather than using a {@code ComboBox} this control uses a simple {@code Label} with a {@code MFXFontIcon}.
+ * <p>
+ * The {@code Label} value is bound to the {@code DatePicker} value and it's formatted using the set {@link #dateFormatter}.
+ * <p>
+ * To get the selected date use {@link #getDate()}.
+ * <p>
+ * You can also retrieve the instance of the {@code DatePicker} by using {@link #getDatePicker()},
+ * however I don't recommend it since this control doesn't use anything other than its value.
+ */
+public class MFXDatePicker extends VBox {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXDatePicker> FACTORY = new StyleablePropertyFactory<>(VBox.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-date-picker";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-datepicker.css").toString();
+
+    private final DatePicker datePicker;
+    private final ObjectProperty<DateTimeFormatter> dateFormatter = new SimpleObjectProperty<>(DateTimeFormatter.ofPattern("dd/M/yyyy"));
+
+    private Label value;
+    private MFXFontIcon calendar;
+    private Line line;
+    private PopupControl popup;
+    private MFXDatePickerContent datePickerContent;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXDatePicker() {
+        this.datePicker = new DatePicker();
+        initialize();
+    }
+
+    public MFXDatePicker(LocalDate localDate) {
+        this.datePicker = new DatePicker(localDate);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+
+        setMinWidth(92);
+        setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
+
+        value = new Label("");
+        value.getStyleClass().setAll("value");
+        value.setMinWidth(64);
+        calendar = new MFXFontIcon("mfx-calendar-semi-black");
+        calendar.getStyleClass().add("calendar-icon");
+        calendar.setColor((Color) getPickerColor());
+        calendar.setSize(20);
+        StackPane pane = new StackPane(value, calendar);
+        pane.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.setStroke(getLineColor());
+        line.setStartX(-3);
+        line.endXProperty().bind(pane.widthProperty().add(6));
+        line.translateYProperty().bind(heightProperty().add(5));
+
+        popup = new PopupControl();
+        datePickerContent = new MFXDatePickerContent(datePicker.getValue(), getDateFormatter());
+        popup.getScene().setRoot(datePickerContent);
+        popup.setAutoHide(true);
+
+        getChildren().addAll(pane, line);
+        addListeners();
+
+        if (datePicker.getValue() != null) {
+            value.setText(datePicker.getValue().format(getDateFormatter()));
+        }
+
+        datePickerContent.updateColor((Color) getPickerColor());
+    }
+
+    /**
+     * Adds listeners to date picker content currentDateProperty, to {@link #dateFormatter}, to {@link #pickerColor},
+     * to {@link #lineColor}, to {@link #colorText} and disabled property.
+     * <p>
+     * Adds event handler to calendar icon.
+     * <p>
+     * Binds date picker content animateCalendarProperty to {@link #animateCalendar}
+     */
+    private void addListeners() {
+        calendar.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            if (!popup.isShowing()) {
+                Point2D point = NodeUtils.pointRelativeTo(this, datePickerContent, HPos.CENTER, VPos.BOTTOM, 0, 0, true);
+                popup.show(this, snapPositionX(point.getX() - 4), snapPositionY(point.getY() + 2));
+            } else {
+                popup.hide();
+            }
+        });
+
+        datePickerContent.currentDateProperty().addListener((observable, oldValue, newValue) -> {
+            datePicker.setValue(newValue);
+            value.setText(newValue.format(datePickerContent.getDateFormatter()));
+        });
+
+        datePickerContent.currentDateProperty().addListener((observable, oldValue, newValue) -> {
+            if (!isCloseOnDaySelected()) {
+                return;
+            }
+
+            if (oldValue.getYear() == newValue.getYear() ||
+                    oldValue.getMonth() == newValue.getMonth()) {
+                if (oldValue.getDayOfMonth() != newValue.getDayOfMonth()) {
+                    popup.hide();
+                }
+            }
+        });
+
+        dateFormatter.addListener((observable, oldValue, newValue) -> {
+            LocalDate date = LocalDate.parse(value.getText(), oldValue);
+            value.setText(date.format(newValue));
+            datePickerContent.setDateFormatter(newValue);
+        });
+
+        pickerColor.addListener((observable, oldValue, newValue) -> {
+            Color color;
+            if (newValue instanceof Color) {
+                color = (Color) newValue;
+            } else {
+                throw new IllegalStateException("Paint values are not supported, change it to Color");
+            }
+
+            calendar.setColor(color);
+            datePickerContent.updateColor(color);
+            if (isColorText()) {
+                value.setTextFill(newValue);
+            } else {
+                value.setTextFill(Color.BLACK);
+            }
+        });
+        lineColor.addListener((observable, oldValue, newValue) -> line.setStroke(newValue));
+        colorText.addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                value.setTextFill(getPickerColor());
+            } else {
+                value.setTextFill(Color.BLACK);
+            }
+        });
+
+        datePickerContent.animateCalendarProperty().bind(animateCalendar);
+
+        disabledProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                line.setStroke(Color.LIGHTGRAY);
+                calendar.setColor(Color.LIGHTGRAY);
+            } else {
+                line.setStroke(getLineColor());
+                calendar.setColor((Color) getPickerColor());
+            }
+        });
+    }
+
+    public MFXDatePickerContent getContent() {
+        return datePickerContent;
+    }
+
+    public DateTimeFormatter getDateFormatter() {
+        return dateFormatter.get();
+    }
+
+    public ObjectProperty<DateTimeFormatter> dateFormatterProperty() {
+        return dateFormatter;
+    }
+
+    public void setDateFormatter(DateTimeFormatter dateFormatter) {
+        this.dateFormatter.set(dateFormatter);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+
+    /**
+     * Specifies the main color of the date picker and its content.
+     */
+    private final StyleableObjectProperty<Paint> pickerColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.PICKER_COLOR,
+            this,
+            "pickerColor",
+            Color.rgb(98, 0, 238)
+    );
+
+    /**
+     * Specifies the line color of the date picker
+     */
+    private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_COLOR,
+            this,
+            "lineColor",
+            Color.rgb(98, 0, 238, 0.7)
+    );
+
+    /**
+     * Specifies if the date picker text should be colored too.
+     */
+    private final StyleableBooleanProperty colorText = new SimpleStyleableBooleanProperty(
+            StyleableProperties.COLOR_TEXT,
+            this,
+            "colorText",
+            false
+    );
+
+    /**
+     * Specifies if the date picker popup should close on day selected.
+     */
+    private final StyleableBooleanProperty closeOnDaySelected = new SimpleStyleableBooleanProperty(
+            StyleableProperties.CLOSE_ON_DAY_SELECTED,
+            this,
+            "closeOnDaySelected",
+            true
+    );
+
+    /**
+     * Specifies if the month change should be animated.
+     */
+    private final StyleableBooleanProperty animateCalendar = new SimpleStyleableBooleanProperty(
+            StyleableProperties.ANIMATE_CALENDAR,
+            this,
+            "animateCalendar",
+            true
+    );
+
+    public Paint getPickerColor() {
+            return pickerColor.get();
+    }
+
+    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);
+    }
+
+    public Paint getLineColor() {
+        return lineColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> lineColorProperty() {
+        return lineColor;
+    }
+
+    public void setLineColor(Paint lineColor) {
+        this.lineColor.set(lineColor);
+    }
+
+    public boolean isColorText() {
+        return colorText.get();
+    }
+
+    public StyleableBooleanProperty colorTextProperty() {
+        return colorText;
+    }
+
+    public void setColorText(boolean colorText) {
+        this.colorText.set(colorText);
+    }
+
+    public boolean isCloseOnDaySelected() {
+        return closeOnDaySelected.get();
+    }
+
+    public StyleableBooleanProperty closeOnDaySelectedProperty() {
+        return closeOnDaySelected;
+    }
+
+    public void setCloseOnDaySelected(boolean closeOnDaySelected) {
+        this.closeOnDaySelected.set(closeOnDaySelected);
+    }
+
+    public boolean isAnimateCalendar() {
+        return animateCalendar.get();
+    }
+
+    public StyleableBooleanProperty animateCalendarProperty() {
+        return animateCalendar;
+    }
+
+    public void setAnimateCalendar(boolean animateCalendar) {
+        this.animateCalendar.set(animateCalendar);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXDatePicker, Paint> PICKER_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-main-color",
+                        MFXDatePicker::pickerColorProperty,
+                        Color.rgb(98, 0, 238)
+                );
+
+        private static final CssMetaData<MFXDatePicker, Paint> LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-line-color",
+                        MFXDatePicker::lineColorProperty,
+                        Color.rgb(90, 0, 238, 0.7)
+                );
+
+        private static final CssMetaData<MFXDatePicker, Boolean> COLOR_TEXT =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-color-text",
+                        MFXDatePicker::colorTextProperty,
+                        false
+                );
+
+        private static final CssMetaData<MFXDatePicker, Boolean> CLOSE_ON_DAY_SELECTED =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-close-on-day-selected",
+                        MFXDatePicker::closeOnDaySelectedProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXDatePicker, Boolean> ANIMATE_CALENDAR =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-animate-calendar",
+                        MFXDatePicker::animateCalendarProperty,
+                        true
+                );
+
+        static {
+            cssMetaDataList = List.of(PICKER_COLOR, LINE_COLOR, COLOR_TEXT, CLOSE_ON_DAY_SELECTED, ANIMATE_CALENDAR);
+        }
+
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+        return MFXDatePicker.getControlCssMetaDataList();
+    }
+
+    //================================================================================
+    // Wrapper Methods
+    //================================================================================
+    public DatePicker getDatePicker() {
+        return datePicker;
+    }
+
+    public LocalDate getDate() {
+        return datePicker.getValue();
+    }
+}

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

@@ -173,7 +173,7 @@ public class MFXListCell<T> extends ListCell<T> {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -187,7 +187,7 @@ public class MFXListCell<T> extends ListCell<T> {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXListCell.getControlCssMetaDataList();
     }
 
     /**

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

@@ -233,7 +233,7 @@ public class MFXListView<T> extends ListView<T> {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -252,6 +252,6 @@ public class MFXListView<T> extends ListView<T> {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXListView.getControlCssMetaDataList();
     }
 }

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

@@ -193,7 +193,7 @@ public class MFXRadioButton extends RadioButton {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -212,6 +212,6 @@ public class MFXRadioButton extends RadioButton {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXRadioButton.getControlCssMetaDataList();
     }
 }

+ 287 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java

@@ -0,0 +1,287 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.skins.MFXTextFieldSkin;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import javafx.css.*;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TextField;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a TextField restyled to comply with modern standards.
+ * <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>
+ */
+public class MFXTextField extends TextField {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXTextField> FACTORY = new StyleablePropertyFactory<>(TextField.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-text-field";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-textfield.css").toString();
+
+    private MFXDialogValidator validator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTextField() {
+        this("");
+    }
+
+    public MFXTextField(String text) {
+        super(text);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setupValidator();
+
+        textProperty().addListener((observable, oldValue, newValue) -> {
+            int limit = getTextLimit();
+            if (limit == -1) {
+                return;
+            }
+
+            if (newValue.length() > limit) {
+                String s = newValue.substring(0, limit);
+                setText(s);
+            }
+        });
+    }
+
+    /**
+     * 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
+    //================================================================================
+
+    private final StyleableIntegerProperty textLimit = new SimpleStyleableIntegerProperty(
+            StyleableProperties.TEXT_LIMIT,
+            this,
+            "maxLength",
+            -1
+    );
+
+    /**
+     * Specifies the line's color when the control is focused.
+     */
+    private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_COLOR,
+            this,
+            "lineColor",
+            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,
+            "unfocusedLineColor",
+            Color.rgb(77, 77, 77)
+    );
+
+    /**
+     * Specifies the lines' width.
+     */
+    private final StyleableDoubleProperty lineStrokeWidth = new SimpleStyleableDoubleProperty(
+            StyleableProperties.LINE_STROKE_WIDTH,
+            this,
+            "lineStrokeWidth",
+            1.5
+    );
+
+    /**
+     * Specifies if the lines switch between focus/un-focus should be animated.
+     */
+    private final StyleableBooleanProperty animateLines = new SimpleStyleableBooleanProperty(
+            StyleableProperties.ANIMATE_LINES,
+            this,
+            "animateLines",
+            true
+    );
+
+    /**
+     * Specifies if validation is required for the control.
+     */
+    private final StyleableBooleanProperty validated = new SimpleStyleableBooleanProperty(
+            StyleableProperties.IS_VALIDATED,
+            this,
+            "isValidated",
+            false
+    );
+
+    public int getTextLimit() {
+        return textLimit.get();
+    }
+
+    public StyleableIntegerProperty textLimitProperty() {
+        return textLimit;
+    }
+
+    public void setTextLimit(int textLimit) {
+        this.textLimit.set(textLimit);
+    }
+
+    public Paint getLineColor() {
+        return lineColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> lineColorProperty() {
+        return lineColor;
+    }
+
+    public void setLineColor(Paint lineColor) {
+        this.lineColor.set(lineColor);
+    }
+
+    public Paint getUnfocusedLineColor() {
+        return unfocusedLineColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unfocusedLineColorProperty() {
+        return unfocusedLineColor;
+    }
+
+    public void setUnfocusedLineColor(Paint unfocusedLineColor) {
+        this.unfocusedLineColor.set(unfocusedLineColor);
+    }
+
+    public double getLineStrokeWidth() {
+        return lineStrokeWidth.get();
+    }
+
+    public StyleableDoubleProperty lineStrokeWidthProperty() {
+        return lineStrokeWidth;
+    }
+
+    public void setLineStrokeWidth(double lineStrokeWidth) {
+        this.lineStrokeWidth.set(lineStrokeWidth);
+    }
+
+    public boolean isAnimateLines() {
+        return animateLines.get();
+    }
+
+    public StyleableBooleanProperty animateLinesProperty() {
+        return animateLines;
+    }
+
+    public void setAnimateLines(boolean animateLines) {
+        this.animateLines.set(animateLines);
+    }
+
+    public boolean isValidated() {
+        return validated.get();
+    }
+
+    public StyleableBooleanProperty isValidatedProperty() {
+        return validated;
+    }
+
+    public void setIsValidated(boolean isValidated) {
+        this.validated.set(isValidated);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXTextField, Number> TEXT_LIMIT =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-text-limit",
+                        MFXTextField::textLimitProperty,
+                        -1
+                );
+
+        private static final CssMetaData<MFXTextField, Paint> LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-line-color",
+                        MFXTextField::lineColorProperty,
+                        Color.rgb(50, 150, 205)
+                );
+
+        private static final CssMetaData<MFXTextField, Paint> UNFOCUSED_LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unfocused-line-color",
+                        MFXTextField::unfocusedLineColorProperty,
+                        Color.rgb(77, 77, 77)
+                );
+
+        private final static CssMetaData<MFXTextField, Number> LINE_STROKE_WIDTH =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-line-stroke-width",
+                        MFXTextField::lineStrokeWidthProperty,
+                        1.5
+                );
+
+        private static final CssMetaData<MFXTextField, Boolean> ANIMATE_LINES =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-animate-lines",
+                        MFXTextField::animateLinesProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXTextField, Boolean> IS_VALIDATED =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-validate",
+                        MFXTextField::isValidatedProperty,
+                        false
+                );
+
+        static {
+            cssMetaDataList = List.of(TEXT_LIMIT, LINE_COLOR, UNFOCUSED_LINE_COLOR, LINE_STROKE_WIDTH, IS_VALIDATED);
+        }
+
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXTextFieldSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXTextField.getControlCssMetaDataList();
+    }
+}

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

@@ -249,7 +249,7 @@ public class MFXToggleButton extends ToggleButton {
         }
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -268,6 +268,6 @@ public class MFXToggleButton extends ToggleButton {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXToggleButton.getControlCssMetaDataList();
     }
 }

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

@@ -255,7 +255,7 @@ public class MFXToggleNode extends ToggleButton {
         }
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
@@ -281,6 +281,6 @@ public class MFXToggleNode extends ToggleButton {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return this.getControlCssMetaDataList();
+        return MFXToggleNode.getControlCssMetaDataList();
     }
 }

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

@@ -191,7 +191,7 @@ public class SimpleMFXNotificationPane extends AbstractMFXNotificationPane {
      * Before using the scroll pane for the content label the header had an extra button,
      * an expand button similar to Android's notifications, that button was set to be visible
      * only if the content was truncated and on click the prefHeight was incremented by the specified value with
-     * a Transition, however the problem with this approach was the PositionManager system because as you can see in the above code,
+     * a Transition, however the problem with this approach was the PositionManager system because as you can see in the following code,
      * if the content was still truncated at the end of the transition the method was executed again and again until the isTruncated property
      * was false. The PositionManager had two extra methods, repositionNotifications and buildRepositionAnimation with the expandValue as parameter,
      * the reposition method had to be recalled every time too with the same frequency as the expandNotificationMethod but as you can see this class

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

@@ -0,0 +1,63 @@
+package io.github.palexdev.materialfx.controls.factories;
+
+import io.github.palexdev.materialfx.effects.RippleClipType;
+import javafx.scene.layout.Region;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
+
+/**
+ * Convenience class for building Ripple clip shapes.
+ */
+public class RippleClipTypeFactory {
+    private RippleClipType rippleClipType = RippleClipType.NOCLIP;
+    private double arcW = 0;
+    private double arcH = 0;
+
+    public RippleClipTypeFactory() {
+    }
+
+    public RippleClipTypeFactory(RippleClipType rippleClipType) {
+        this.rippleClipType = rippleClipType;
+    }
+
+    public RippleClipTypeFactory(RippleClipType rippleClipType, double arcW, double arcH) {
+        this.rippleClipType = rippleClipType;
+        this.arcW = arcW;
+        this.arcH = arcH;
+    }
+
+    public Shape build(Region region) {
+        double w = region.getWidth() - 0.5;
+        double h = region.getHeight() - 0.5;
+
+        switch (rippleClipType) {
+            case CIRCLE:
+                double radius = Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2)) / 2;
+                Circle circle = new Circle(radius);
+                circle.setTranslateX(w / 2);
+                circle.setTranslateY(h / 2);
+                return circle;
+            case RECTANGLE:
+                return new Rectangle(w, h);
+            case ROUNDED_RECTANGLE:
+                Rectangle rectangle = new Rectangle(w, h);
+                rectangle.setArcWidth(arcW);
+                rectangle.setArcHeight(arcH);
+                return rectangle;
+            default:
+                return null;
+        }
+    }
+
+    public RippleClipTypeFactory setArcs(double arcW, double arcH) {
+        this.arcW = arcW;
+        this.arcH = arcH;
+        return this;
+    }
+
+    public RippleClipTypeFactory setRippleClipType(RippleClipType rippleClipType) {
+        this.rippleClipType = rippleClipType;
+        return this;
+    }
+}

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

@@ -1,41 +1,8 @@
 package io.github.palexdev.materialfx.effects;
 
-import javafx.scene.layout.Region;
-import javafx.scene.shape.Circle;
-import javafx.scene.shape.Rectangle;
-import javafx.scene.shape.Shape;
-
 public enum RippleClipType {
-    CIRCLE {
-        @Override
-        public Shape buildClip(Region region) {
-            double radius = Math.sqrt(Math.pow(region.getWidth(), 2) + Math.pow(region.getHeight(), 2)) / 2;
-            Circle circle = new Circle(radius);
-            circle.setTranslateX(region.getWidth() / 2);
-            circle.setTranslateY(region.getHeight() / 2);
-            return circle;
-        }
-    },
-    RECTANGLE {
-        @Override
-        public Shape buildClip(Region region) {
-            return new Rectangle(region.getWidth(), region.getHeight());
-        }
-    },
-    NOCLIP {
-        @Override
-        public Shape buildClip(Region region) {
-            return null;
-        }
-    };
-
-    public static Circle buildClip(double radius) {
-        return new Circle(radius);
-    }
-
-    public static Rectangle buildClip(double width, double height) {
-        return new Rectangle(width, height);
-    }
-
-    public abstract Shape buildClip(Region region);
+    CIRCLE,
+    RECTANGLE,
+    ROUNDED_RECTANGLE,
+    NOCLIP,
 }

+ 20 - 18
materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleGenerator.java

@@ -1,5 +1,6 @@
 package io.github.palexdev.materialfx.effects;
 
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
 import javafx.animation.*;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
@@ -10,7 +11,7 @@ import javafx.scene.effect.DropShadow;
 import javafx.scene.layout.Region;
 import javafx.scene.paint.Color;
 import javafx.scene.shape.Circle;
-import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
 import javafx.util.Duration;
 
 import java.util.List;
@@ -31,16 +32,17 @@ public class RippleGenerator extends Group {
 
     private final Region region;
 
-    private RippleClipType rippleClipType = RippleClipType.RECTANGLE;
+    private RippleClipTypeFactory rippleClipTypeFactory = new RippleClipTypeFactory(RippleClipType.RECTANGLE);
     private DepthLevel level = null;
     private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825, 0.3025, 0.0875, 0.9975);
     //private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.1, 0.50, 0.3, 0.85);
-    private final StyleableObjectProperty<Color> rippleColor = new StyleableObjectProperty<>(Color.ROYALBLUE)
-    {
-        @Override public CssMetaData<RippleGenerator, Color> getCssMetaData() { return StyleableProperties.RIPPLE_COLOR; }
-        @Override public Object getBean() { return this; }
-        @Override public String getName() { return "rippleColor"; }
-    };
+    private final StyleableObjectProperty<Color> rippleColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.RIPPLE_COLOR,
+            this,
+            "rippleColor",
+            Color.ROYALBLUE
+    );
+
     private final StyleableDoubleProperty rippleRadius = new SimpleStyleableDoubleProperty(
             StyleableProperties.RIPPLE_RADIUS,
             this,
@@ -82,14 +84,14 @@ public class RippleGenerator extends Group {
         this.level = shadowLevel;
     }
 
-    public RippleGenerator(Region region, RippleClipType rippleClipType) {
+    public RippleGenerator(Region region, RippleClipTypeFactory factory) {
         this(region);
-        this.rippleClipType = rippleClipType;
+        this.rippleClipTypeFactory = factory;
     }
 
-    public RippleGenerator(Region region, DepthLevel shadowLevel, RippleClipType rippleClipType) {
+    public RippleGenerator(Region region, DepthLevel shadowLevel, RippleClipTypeFactory factory) {
         this(region, shadowLevel);
-        this.rippleClipType = rippleClipType;
+        this.rippleClipTypeFactory = factory;
     }
 
     //================================================================================
@@ -106,7 +108,7 @@ public class RippleGenerator extends Group {
         getChildren().add(ripple);
 
         if (animateBackground.get()) {
-            Rectangle fillRect = new Rectangle(region.getWidth(), region.getHeight());
+            Shape fillRect = rippleClipTypeFactory.build(region);
             fillRect.setFill(rippleColor.get());
             fillRect.setOpacity(0);
             getChildren().add(0, fillRect);
@@ -131,8 +133,8 @@ public class RippleGenerator extends Group {
         this.generatorCenterY = generatorCenterY;
     }
 
-    public void setRippleClipType(RippleClipType rippleClipType) {
-        this.rippleClipType = rippleClipType;
+    public void setRippleClipTypeFactory(RippleClipTypeFactory rippleClipTypeFactory) {
+        this.rippleClipTypeFactory = rippleClipTypeFactory;
     }
 
     public Color getRippleColor() {
@@ -217,7 +219,7 @@ public class RippleGenerator extends Group {
         private Ripple(double centerX, double centerY) {
             super(centerX, centerY, 0, Color.TRANSPARENT);
             setFill(rippleColor.get());
-            setClip(rippleClipType.buildClip(region));
+            setClip(rippleClipTypeFactory.build(region));
             buildAnimation();
         }
 
@@ -302,12 +304,12 @@ public class RippleGenerator extends Group {
 
     }
 
-    public List<CssMetaData<? extends Styleable, ?>> getGroupCssMetaDataList() {
+    public static List<CssMetaData<? extends Styleable, ?>> getGroupCssMetaDataList() {
         return RippleGenerator.StyleableProperties.cssMetaDataList;
     }
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
-        return this.getGroupCssMetaDataList();
+        return RippleGenerator.getGroupCssMetaDataList();
     }
 }

+ 26 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/font/FontHandler.java

@@ -0,0 +1,26 @@
+package io.github.palexdev.materialfx.font;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import javafx.scene.text.Font;
+
+/**
+ * Handler for MaterialFX font resources.
+ */
+public class FontHandler {
+    private static final Font resources;
+
+    private FontHandler() {
+    }
+
+    static {
+        resources = Font.loadFont(MFXResourcesLoader.loadStream("fonts/materialfx-resources.ttf"), 10);
+    }
+
+    public static Font getResources() {
+        return resources;
+    }
+
+    public static char getCode(String description) {
+        return FontResources.findByDescription(description).getCode();
+    }
+}

+ 41 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/font/FontResources.java

@@ -0,0 +1,41 @@
+package io.github.palexdev.materialfx.font;
+
+/**
+ * Enumerator class for MaterialFX font resources.
+ */
+public enum FontResources {
+    CALENDAR_BLACK("mfx-calendar-black", '\uE904'),
+    CALENDAR_SEMI_BLACK("mfx-calendar-semi-black", '\uE905'),
+    CALENDAR_WHITE("mfx-calendar-white", '\uE906'),
+    CHEVRON_DOWN("mfx-chevron-down", '\uE900'),
+    CHEVRON_LEFT("mfx-chevron-left", '\uE901'),
+    CHEVRON_RIGHT("mfx-chevron-right", '\uE902'),
+    CHEVRON_UP("mfx-chevron-right", '\uE903')
+    ;
+
+    public static FontResources findByDescription(String description) {
+        for (FontResources font : values()) {
+            if (font.getDescription().equals(description)) {
+                return font;
+            }
+        }
+        throw new IllegalArgumentException("Icon description '" + description + "' is invalid!");
+    }
+
+    private final String description;
+    private final char code;
+
+    FontResources(String description, char code) {
+        this.description = description;
+        this.code = code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public char getCode() {
+        return code;
+    }
+
+}

+ 167 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/font/MFXFontIcon.java

@@ -0,0 +1,167 @@
+package io.github.palexdev.materialfx.font;
+
+import javafx.css.*;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+
+import java.util.List;
+
+/**
+ * Class used for MaterialFX font icon resources.
+ */
+public class MFXFontIcon extends Text {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXFontIcon> FACTORY = new StyleablePropertyFactory<>(Text.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-font-icon";
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXFontIcon(String description) {
+        this(description, 10);
+    }
+
+    public MFXFontIcon(String description, double size) {
+        this(description, size, Color.rgb(117, 117, 117));
+    }
+
+    public MFXFontIcon(String description, double size, Color color) {
+        initialize();
+
+        setDescription(description);
+        setFont(FontHandler.getResources());
+        setSize(size);
+        setColor(color);
+
+        setText(String.valueOf(FontHandler.getCode(description)));
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().setAll(STYLE_CLASS);
+
+        sizeProperty().addListener((observable, oldValue, newValue) -> {
+            Font font = getFont();
+            setFont(Font.font(font.getFamily(), newValue.doubleValue()));
+        });
+
+        colorProperty().addListener((observable, oldValue, newValue) -> setFill(newValue));
+
+        descriptionProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null) {
+                final char character = FontHandler.getCode(newValue);
+                setText(String.valueOf(character));
+            }
+        });
+    }
+
+    /**
+     * Specifies the icon code of the icon.
+     */
+    private final StyleableStringProperty description = new SimpleStyleableStringProperty(
+            StyleableProperties.DESCRIPTION,
+            this,
+            "description"
+    );
+
+    /**
+     * Specifies the size of the icon.
+     */
+    private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
+            StyleableProperties.SIZE,
+            this,
+            "size",
+            10.0
+    );
+
+    /**
+     * Specifies the color of the icon.
+     */
+    private final StyleableObjectProperty<Color> color = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.COLOR,
+            this,
+            "color",
+            Color.rgb(117, 117, 117)
+    );
+
+    public String getDescription() {
+        return description.get();
+    }
+
+    public StyleableStringProperty descriptionProperty() {
+        return description;
+    }
+
+    public void setDescription(String code) {
+        this.description.set(code);
+    }
+
+    public double getSize() {
+        return size.get();
+    }
+
+    public StyleableDoubleProperty sizeProperty() {
+        return size;
+    }
+
+    public void setSize(double size) {
+        this.size.set(size);
+    }
+
+    public Color getColor() {
+        return color.get();
+    }
+
+    public StyleableObjectProperty<Color> colorProperty() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        this.color.set(color);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    public static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXFontIcon, String> DESCRIPTION =
+                FACTORY.createStringCssMetaData(
+                        "-mfx-icon-code",
+                        MFXFontIcon::descriptionProperty
+                );
+
+        private static final CssMetaData<MFXFontIcon, Number> SIZE =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-size",
+                        MFXFontIcon::sizeProperty,
+                        10
+                );
+
+        private static final CssMetaData<MFXFontIcon, Color> COLOR =
+                FACTORY.createColorCssMetaData(
+                        "-mfx-color",
+                        MFXFontIcon::colorProperty,
+                        Color.rgb(117, 117, 117)
+                );
+
+        static {
+            cssMetaDataList = List.of(DESCRIPTION, SIZE, COLOR);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+        return MFXFontIcon.getClassCssMetaDataList();
+    }
+}

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

@@ -1,7 +1,7 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXCheckbox;
-import io.github.palexdev.materialfx.effects.RippleClipType;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.geometry.Insets;
@@ -66,7 +66,7 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
         mark.getStyleClass().setAll("mark");
         box.getChildren().add(mark);
 
-        rippleGenerator = new RippleGenerator(rippleContainer, RippleClipType.NOCLIP);
+        rippleGenerator = new RippleGenerator(rippleContainer, new RippleClipTypeFactory());
         rippleGenerator.setRippleRadius(18);
         rippleGenerator.setInDuration(Duration.millis(400));
         rippleGenerator.setAnimateBackground(false);

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

@@ -0,0 +1,50 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXDateCell;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.effects.RippleClipType;
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import javafx.scene.control.skin.DateCellSkin;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Duration;
+
+/**
+ * This is the implementation of the {@code Skin} associated with every {@code MFXDateCell}.
+ * <p>
+ * This is necessary to make the {@code RippleGenerator work properly}.
+ */
+public class MFXDateCellSkin extends DateCellSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+
+    private final RippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXDateCellSkin(MFXDateCell dateCell) {
+        super(dateCell);
+
+        rippleGenerator = new RippleGenerator(dateCell, new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(15, 15));
+        rippleGenerator.setOutDuration(Duration.millis(500));
+        dateCell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+
+        updateChildren();
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected void updateChildren() {
+        super.updateChildren();
+        if (rippleGenerator != null) {
+            getChildren().add(0, rippleGenerator);
+        }
+    }
+}

+ 920 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java

@@ -0,0 +1,920 @@
+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.MFXDateCell;
+import io.github.palexdev.materialfx.controls.MFXScrollPane;
+import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.LoggingUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import io.github.palexdev.materialfx.utils.StringUtils;
+import javafx.animation.*;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.Separator;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.util.Duration;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.TextStyle;
+import java.time.temporal.WeekFields;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static java.time.temporal.ChronoUnit.MONTHS;
+
+/**
+ * This class is the beating heart of every {@code MFXDatePicker}.
+ * <p>
+ * Extends {@code 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.
+ * <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.
+ * <p>
+ * That said, this class has almost nothing to do with that one. The code is simpler and much more organized but most
+ * importantly it's well documented.
+ */
+public class MFXDatePickerContent extends VBox {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-datepicker-content";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-datepicker-content.css").toString();
+
+    private final double DEFAULT_WIDTH = 300;
+    private final double DEFAULT_HEIGHT = 380;
+    private final Insets DEFAULT_INSETS = new Insets(8, 10, 8, 10);
+
+    private final int daysPerWeek = 7;
+    private final List<MFXDateCell> days = new ArrayList<>();
+    private final List<MFXDateCell> dayNameCells = new ArrayList<>();
+    private final List<MFXDateCell> yearsList = new ArrayList<>();
+
+    private final ObjectProperty<LocalDate> currentDate = new SimpleObjectProperty<>(LocalDate.now());
+    private final ObjectProperty<YearMonth> yearMonth = new SimpleObjectProperty<>(YearMonth.of(getCurrentDate().getYear(), getCurrentDate().getMonth()));
+
+    private final VBox header;
+    private final StackPane yearMonthPane;
+    private final Separator separator;
+
+    private Label label;
+    private Label selectedDate;
+    private Label month;
+    private Label year;
+
+    private final StackPane holder;
+    private GridPane calendar;
+    private GridPane years;
+    private MFXScrollPane yearsScroll;
+    private StackPane yearsButton;
+    private StackPane monthBackButton;
+    private StackPane monthForwardButton;
+    private StackPane inputButton;
+
+    private Timeline yearsOpen;
+    private Timeline yearsClose;
+    private Timeline calendarTransition;
+
+    private final ObjectProperty<MFXDateCell> lastSelectedDayCell = new SimpleObjectProperty<>(null);
+    private MFXDateCell lastSelectedYearCell = null;
+    private MFXDateCell currYearCell = null;
+
+    private final BooleanProperty validInput = new SimpleBooleanProperty(true);
+    private MFXTextField inputField;
+    private boolean keyInput = false;
+
+    // Date formatters
+    private final ObjectProperty<DateTimeFormatter> dateFormatter = new SimpleObjectProperty<>(DateTimeFormatter.ofPattern("d/M/yyyy"));
+    private final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc");
+
+    private final BooleanProperty animateCalendar = new SimpleBooleanProperty();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXDatePickerContent() {
+        this(LocalDate.now(), DateTimeFormatter.ofPattern("d/M/yyyy"));
+    }
+
+    public MFXDatePickerContent(LocalDate localDate, DateTimeFormatter dateTimeFormatter) {
+        getStyleClass().add(STYLE_CLASS);
+        getStylesheets().setAll(STYLESHEET);
+        //setStyle("-fx-border-color: red");
+        setPrefSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
+        setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+
+        setDateFormatter(dateTimeFormatter);
+
+        buildButtons();
+        buildField();
+
+        header = buildHeader();
+        yearMonthPane = buildYearMonthPane();
+        separator = buildSeparator();
+
+        getChildren().addAll(
+                header,
+                yearMonthPane,
+                separator
+        );
+
+        holder = new StackPane(buildCalendar(), buildScroll());
+        holder.getStyleClass().add("holder");
+        //holder.setStyle("-fx-border-color: blue");
+        getChildren().add(holder);
+
+        initialize();
+
+        if (localDate != null) {
+            setCurrentDate(localDate);
+            setYearMonth(YearMonth.of(getCurrentDate().getYear(), getCurrentDate().getMonth()));
+            lastSelectedDayCell.set(
+                    days.stream()
+                            .filter(day -> day.getText().equals(Integer.toString(getCurrentDate().getDayOfMonth())))
+                            .findFirst()
+                            .orElse(null)
+            );
+            lastSelectedDayCell.get().setSelectedDate(true);
+            lastSelectedYearCell = yearsList.stream()
+                    .filter(year -> year.getText().equals(Integer.toString(getYearMonth().getYear())))
+                    .findFirst()
+                    .orElse(null);
+            if (lastSelectedYearCell != null) {
+                lastSelectedYearCell.setSelectedDate(true);
+                goToYear();
+            }
+        }
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        createYearCells();
+        createDayNameCells();
+        createDayCells();
+        populateYears();
+        populateCalendar();
+
+        buildAnimations();
+        yearsScroll.setOpacity(0);
+        yearsScroll.setVisible(false);
+
+        //selectedDate.setText(getCurrentDate().format(getDateFormatter()));
+        month.setText(StringUtils.titleCaseWord(getYearMonth().getMonth().getDisplayName(TextStyle.FULL, getLocale())));
+        year.setText(String.valueOf(getYearMonth().getYear()));
+
+        behaviorListeners();
+    }
+
+    //================================================================================
+    // [Behavior || Create] Methods
+    //================================================================================
+
+    /**
+     * Creates the MFXDateCells which will populate {@link #years}.
+     */
+    private void createYearCells() {
+        yearsList.clear();
+
+        int currYear = LocalDate.now().getYear();
+
+        int i;
+        for (i = currYear - 120; i <= currYear + 120; i++) {
+            MFXDateCell cell = new MFXDateCell(Integer.toString(i));
+            cell.getStyleClass().add("year-cell");
+            cell.setPrefSize(65, 25);
+            cell.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+            cell.setAlignment(Pos.CENTER);
+
+            if (i == LocalDate.now().getYear()) {
+                currYearCell = cell;
+                cell.setCurrent(true);
+                cell.setSelectedDate(false);
+            }
+
+            cell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+                if (lastSelectedYearCell != null) {
+                    lastSelectedYearCell.setSelectedDate(false);
+                }
+
+                lastSelectedYearCell = cell;
+                lastSelectedYearCell.setSelectedDate(true);
+
+                setYearMonth(getYearMonth().withYear(Integer.parseInt(lastSelectedYearCell.getText())));
+
+                if (lastSelectedDayCell.get() == null) {
+                    selectDay();
+                }
+            });
+
+            yearsList.add(cell);
+        }
+    }
+
+    /**
+     * Creates the MFXDateCells which will populate the first row of {@link #calendar}.
+     * They contain the first letter of each day name.
+     */
+    private void createDayNameCells() {
+        dayNameCells.clear();
+
+        int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue();
+        LocalDate date = LocalDate.of(2009, 7, 12 + firstDayOfWeek);
+
+        int i;
+        for (i = 0; i < daysPerWeek; i++) {
+            MFXDateCell cell = new MFXDateCell();
+            cell.getStyleClass().add("day-name-cell");
+            cell.setAlignment(Pos.CENTER);
+
+            String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS));
+            if (weekDayNameFormatter.getLocale() == java.util.Locale.CHINA) {
+                name = name.substring(name.length() - 1).toUpperCase();
+            } else {
+                name = name.substring(0, 1).toUpperCase();
+            }
+            cell.setText(name);
+
+            dayNameCells.add(cell);
+        }
+    }
+
+    /**
+     * Creates the MFXDateCells that will populate the {@link #calendar}.
+     * 42 cells are created because the grid is 6x7 at max.
+     * Each cell has its text set by default to "null" then starting from the
+     * first day index calculated with {@link #firstDayIndex()} the text is set from 1 to monthLength.
+     * The cells which still contains "null" are not visible, that's how the grid is built.
+     *
+     */
+    private void createDayCells() {
+        days.clear();
+
+        int day = LocalDate.now().getDayOfMonth();
+
+        int i;
+        for (i = 1; i <= 42; i++) {
+            MFXDateCell cell = new MFXDateCell("null", true);
+            cell.getStyleClass().add("day-cell");
+            cell.setPrefSize(45, 45);
+            cell.setAlignment(Pos.CENTER);
+            NodeUtils.makeRegionCircular(cell, 14);
+
+            cell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+                if (lastSelectedDayCell.get() != null) {
+                    lastSelectedDayCell.get().setSelectedDate(false);
+                }
+                lastSelectedDayCell.set(cell);
+                lastSelectedDayCell.get().setSelectedDate(true);
+
+                if (lastSelectedYearCell == null) {
+                    selectYear();
+                }
+            });
+
+            days.add(cell);
+        }
+
+        int offset = 0;
+        int firstDayIndex = firstDayIndex();
+        if (firstDayIndex == 7) {
+            offset = 1;
+        }
+
+        int index = firstDayIndex - 1 + offset;
+        int monthLength = getYearMonth().getMonth().length(getYearMonth().isLeapYear());
+        int cnt = 1;
+        for (i = index; i < monthLength + index; i++) {
+            MFXDateCell cell = days.get(i);
+            cell.setText(Integer.toString(cnt));
+
+            if (day == i &&
+                    LocalDate.now().getMonth().equals(getYearMonth().getMonth()) &&
+                    LocalDate.now().getYear() == getYearMonth().getYear()) {
+                cell.setCurrent(true);
+            }
+
+            cnt++;
+        }
+    }
+
+    //================================================================================
+    // Behavior || Populate
+    //================================================================================
+
+    /**
+     * Populates the {@link #years} grid with the previously created years cells.
+     */
+    private void populateYears() {
+        years.getChildren().clear();
+        years.getColumnConstraints().clear();
+
+        int nCols = 4;
+        ColumnConstraints columnConstraints = new ColumnConstraints();
+        columnConstraints.setPercentWidth(100);
+        columnConstraints.setHalignment(HPos.CENTER);
+
+        int i;
+        for (i = 0; i < nCols; i++) {
+            years.getColumnConstraints().add(columnConstraints);
+        }
+
+        int col = 0;
+        int row = 0;
+        for (MFXDateCell cell : yearsList) {
+            if (col == 4) {
+                col = 0;
+                row++;
+            }
+            years.add(cell, col, row);
+            col++;
+        }
+    }
+
+    /**
+     * Populates the {@link #calendar} grid with the previously created days cells.
+     */
+    private void populateCalendar() {
+        calendar.getChildren().clear();
+        calendar.getColumnConstraints().clear();
+
+        ColumnConstraints columnConstraints = new ColumnConstraints();
+        columnConstraints.setPercentWidth(100);
+        columnConstraints.setHalignment(HPos.CENTER);
+
+        int i;
+        int j;
+        for (i = 0; i < daysPerWeek; i++) {
+            calendar.getColumnConstraints().add(columnConstraints);
+            calendar.add(dayNameCells.get(i), i, 0);
+        }
+
+        for (i = 0; i < 6; i++) {
+            for (j = 0; j < 7; j++) {
+                MFXDateCell cell = days.get(i * 7 + j);
+                if (!cell.getText().equals("null")) {
+                    calendar.add(cell, j, i + 1);
+                }
+            }
+        }
+    }
+
+    //================================================================================
+    // Behavior
+    //================================================================================
+
+    /**
+     * Core of this class.
+     * This method represents the behavior of the picker.
+     * <p>
+     * Adds listeners to {@link #yearMonth} property so when it changes the calendar grid is refreshed {@link #refresh()},
+     * to {@link #lastSelectedDayCell} property so when it changes the {@link #currentDate} is updated,
+     * to {@link #currentDate} property so when it changes the text of {@link #selectedDate} is updated,
+     * to {@link #dateFormatter} property so when it changes the text of {@link #selectedDate} is reformatted.
+     */
+    private void behaviorListeners() {
+        yearMonth.addListener((observable, oldValue, newValue) -> {
+            month.setText(StringUtils.titleCaseWord(newValue.getMonth().getDisplayName(TextStyle.FULL, getLocale())));
+            year.setText(String.valueOf(newValue.getYear()));
+            setCurrentDate(getCurrentDate().withYear(newValue.getYear()).withMonth(newValue.getMonthValue()));
+
+            refresh();
+
+            if (lastSelectedDayCell.get() != null) {
+                MFXDateCell day = days.stream()
+                        .filter(cell -> cell.getText().equals(lastSelectedDayCell.get().getText()))
+                        .findFirst()
+                        .orElse(null);
+                if (day != null) {
+                    lastSelectedDayCell.set(day);
+                    day.setSelectedDate(true);
+                }
+            }
+        });
+
+        lastSelectedDayCell.addListener((observable, oldValue, newValue) -> {
+            LocalDate date = getCurrentDate().withDayOfMonth(Integer.parseInt(newValue.getText()));
+            if (date.equals(getCurrentDate())) {
+                setCurrentDate(LocalDate.EPOCH);
+            }
+            setCurrentDate(date);
+        });
+
+        currentDate.addListener((observable, oldValue, newValue) -> selectedDate.setText(newValue.format(getDateFormatter())));
+
+        dateFormatter.addListener((observable, oldValue, newValue) -> selectedDate.setText(getCurrentDate().format(newValue)));
+    }
+
+    /**
+     * Recreates the day cells and repopulates the calendar.
+     * Called every time the year or the month change.
+     */
+    public void refresh() {
+        createDayCells();
+        populateCalendar();
+    }
+
+    /**
+     * Called when {@link #inputButton} is pressed.
+     * Switched between mouse and keyboard input.
+     */
+    private void changeInput() {
+        keyInput = !keyInput;
+
+        if (keyInput) {
+            yearMonthPane.setVisible(false);
+            separator.setVisible(false);
+            setPrefHeight(210);
+            holder.getChildren().setAll(inputField);
+            label.setText("INPUT DATE");
+        } else {
+            yearMonthPane.setVisible(true);
+            separator.setVisible(true);
+            setPrefHeight(DEFAULT_HEIGHT);
+            holder.getChildren().setAll(calendar, yearsScroll);
+            label.setText("SELECT DATE");
+        }
+    }
+
+    //================================================================================
+    // Layout
+    //================================================================================
+
+    /**
+     * Creates all the buttons (yearsButton, monthBackButton, monthForwardButton, inputButton).
+     */
+    private void buildButtons() {
+        MFXFontIcon chevronDown = new MFXFontIcon("mfx-chevron-down", 13);
+        yearsButton = new StackPane(chevronDown);
+        yearsButton.getStyleClass().add("years-button");
+        yearsButton.setPrefSize(20, 20);
+        yearsButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        NodeUtils.makeRegionCircular(yearsButton);
+        StackPane.setMargin(chevronDown, new Insets(0.3, 0, 0, 0));
+        RippleGenerator rgYB = new RippleGenerator(yearsButton);
+        yearsButton.getChildren().add(0, rgYB);
+        yearsButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rgYB.setGeneratorCenterX(yearsButton.getWidth() / 2);
+            rgYB.setGeneratorCenterY(yearsButton.getHeight() / 2);
+            rgYB.createRipple();
+
+            animateYears();
+            goToYear();
+        });
+
+        MFXFontIcon chevronLeft = new MFXFontIcon("mfx-chevron-left", 13);
+        monthBackButton = new StackPane(chevronLeft);
+        monthBackButton.getStyleClass().add("month-back-button");
+        monthBackButton.setPrefSize(20, 20);
+        monthBackButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        NodeUtils.makeRegionCircular(monthBackButton);
+        RippleGenerator rgMB = new RippleGenerator(monthBackButton);
+        monthBackButton.getChildren().add(0, rgMB);
+        monthBackButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rgMB.setGeneratorCenterX(monthBackButton.getWidth() / 2);
+            rgMB.setGeneratorCenterY(monthBackButton.getHeight() / 2);
+            rgMB.createRipple();
+
+            changeMonth(false);
+        });
+
+        MFXFontIcon chevronRight = new MFXFontIcon("mfx-chevron-right", 13);
+        monthForwardButton = new StackPane(chevronRight);
+        monthForwardButton.getStyleClass().add("month-forward-button");
+        monthForwardButton.setPrefSize(20, 20);
+        monthForwardButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        NodeUtils.makeRegionCircular(monthForwardButton);
+        RippleGenerator rgMF = new RippleGenerator(monthForwardButton);
+        monthForwardButton.getChildren().add(0, rgMF);
+        monthForwardButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rgMF.setGeneratorCenterX(monthForwardButton.getWidth() / 2);
+            rgMF.setGeneratorCenterY(monthForwardButton.getHeight() / 2);
+            rgMF.createRipple();
+
+            changeMonth(true);
+        });
+
+        MFXFontIcon calendar = new MFXFontIcon("mfx-calendar-semi-black", 17);
+        inputButton = new StackPane(calendar);
+        inputButton.getStyleClass().add("change-input-button");
+        inputButton.setPrefSize(35, 35);
+        inputButton.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        inputButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        Tooltip tooltip = new Tooltip("Switches between mouse input and keyboard input");
+        Tooltip.install(inputButton, tooltip);
+        NodeUtils.makeRegionCircular(inputButton);
+        RippleGenerator rgIB = new RippleGenerator(inputButton);
+        rgIB.setInDuration(Duration.millis(500));
+        inputButton.getChildren().add(0, rgIB);
+        inputButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rgIB.setGeneratorCenterX(inputButton.getWidth() / 2);
+            rgIB.setGeneratorCenterY(inputButton.getHeight() / 2);
+            rgIB.createRipple();
+
+            changeInput();
+        });
+    }
+
+    /**
+     * Calls {@link #buildButtons()} and then build the header.
+     */
+    private VBox buildHeader() {
+        buildButtons();
+
+        label = new Label("SELECT DATE");
+        label.setTextFill(Color.WHITE);
+        VBox.setMargin(label, DEFAULT_INSETS);
+
+        selectedDate = new Label();
+        selectedDate.setId("selected-date");
+        selectedDate.setTextFill(Color.WHITE);
+
+        StackPane.setMargin(selectedDate, DEFAULT_INSETS);
+        StackPane.setAlignment(selectedDate, Pos.CENTER_LEFT);
+        StackPane.setMargin(inputButton, DEFAULT_INSETS);
+        StackPane.setAlignment(inputButton, Pos.CENTER_RIGHT);
+
+        StackPane stackPane = new StackPane(selectedDate, inputButton);
+
+        VBox header = new VBox(label, stackPane);
+        header.getStyleClass().add("header");
+        return header;
+    }
+
+    /**
+     * Builds the month-year pane.
+     */
+    private StackPane buildYearMonthPane() {
+        // Month-Years
+        month = new Label();
+        month.getStyleClass().add("month-label");
+        year = new Label();
+        year.getStyleClass().add("year-label");
+
+        HBox monthYearBox = new HBox(10, month, year, yearsButton);
+        monthYearBox.setPrefSize(Region.USE_PREF_SIZE, Region.USE_COMPUTED_SIZE);
+        monthYearBox.setPrefWidth(150);
+        monthYearBox.setMaxWidth(Region.USE_PREF_SIZE);
+        monthYearBox.setAlignment(Pos.CENTER_LEFT);
+        StackPane.setAlignment(monthYearBox, Pos.CENTER_LEFT);
+
+        // Backward-Forward
+        HBox bBox = new HBox(36, monthBackButton, monthForwardButton);
+        bBox.setPrefSize(Region.USE_PREF_SIZE, Region.USE_COMPUTED_SIZE);
+        bBox.setPrefWidth(150);
+        bBox.setMaxWidth(Region.USE_PREF_SIZE);
+        bBox.setAlignment(Pos.CENTER_RIGHT);
+        StackPane.setAlignment(bBox, Pos.CENTER_RIGHT);
+
+        StackPane monthYearPane = new StackPane(monthYearBox, bBox);
+        monthYearPane.getStyleClass().add("month-year-pane");
+        monthYearPane.setPadding(DEFAULT_INSETS);
+        return monthYearPane;
+    }
+
+    /**
+     * Builds the separator.
+     */
+    private Separator buildSeparator() {
+        Separator separator = new Separator(Orientation.HORIZONTAL);
+        separator.setPrefSize(280, 5);
+        separator.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        separator.setTranslateX(10);
+        VBox.setMargin(separator, new Insets(0, 0, 10, 0));
+        return separator;
+    }
+
+    /**
+     * Builds the calendar.
+     */
+    private GridPane buildCalendar() {
+        calendar = new GridPane();
+        calendar.getStyleClass().add("calendar");
+        calendar.setVgap(10);
+        //calendar.setStyle("-fx-border-color: red");
+
+        return calendar;
+    }
+
+    /**
+     * Builds the years grid.
+     */
+    private GridPane buildYears() {
+        years = new GridPane();
+        years.getStyleClass().add("years");
+        years.setPadding(DEFAULT_INSETS);
+        years.setHgap(10);
+        years.setVgap(10);
+        //years.setStyle("-fx-border-color: red");
+
+        return years;
+    }
+
+    /**
+     * Builds the scrollpane which holds the years grid.
+     */
+    private MFXScrollPane buildScroll() {
+        yearsScroll = new MFXScrollPane(buildYears());
+        yearsScroll.getStyleClass().add("years-scrollpane");
+        yearsScroll.setFitToWidth(true);
+        MFXScrollPane.smoothVScrolling(yearsScroll);
+
+        return yearsScroll;
+    }
+
+    /**
+     * Builds the text field used for keyboard input.
+     */
+    private void buildField() {
+        inputField = new MFXTextField();
+        inputField.setId("input-field");
+        inputField.setFont(Font.loadFont(MFXResourcesLoader.loadStream("fonts/OpenSans/OpenSans-SemiBold.ttf"), 16));
+        inputField.prefWidthProperty().bind(this.prefWidthProperty().divide(2.0));
+        inputField.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        inputField.setPromptText("dd/M/yyyy");
+        inputField.setAlignment(Pos.CENTER);
+        inputField.setTextLimit(10);
+
+        inputField.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+            if (event.getCode() == KeyCode.ENTER) {
+                LocalDate date;
+                try {
+                    date = LocalDate.parse(inputField.getText(), getDateFormatter());
+                    setCurrentDate(date);
+                    setYearMonth(YearMonth.of(date.getYear(), date.getMonth()));
+                    validInput.set(true);
+                    selectedDate.setText(date.format(getDateFormatter()));
+                    setCurrentDate(LocalDate.parse(selectedDate.getText(), getDateFormatter()));
+                } catch (DateTimeParseException ex) {
+                    LoggingUtils.logException(ex);
+                    inputField.getValidator().add(validInput, ex.getMessage());
+                    validInput.set(false);
+                }
+            }
+        });
+
+        inputField.getValidator().setValidatorMessage("Invalid Date");
+        inputField.getValidator().add(validInput, "Invalid Date");
+        inputField.setIsValidated(true);
+    }
+
+    /**
+     * Updated the css main color and input field line color.
+     */
+    public void updateColor(Color color) {
+        setStyle("-mfx-main-color: " + ColorUtils.rgb(color) + ";\n");
+        inputField.setLineColor(color);
+    }
+
+    //================================================================================
+    // Animations
+    //================================================================================
+
+    /**
+     * Builds the animations played when the years grid is opened/closed.
+     */
+    private void buildAnimations() {
+        yearsOpen = new Timeline(
+                new KeyFrame(Duration.ZERO, event -> {
+                    calendar.setVisible(false);
+                    yearsScroll.setVisible(true);
+                }),
+                new KeyFrame(Duration.millis(150), new KeyValue(yearsButton.rotateProperty(), -180, Interpolator.EASE_OUT)),
+                new KeyFrame(Duration.millis(400), new KeyValue(yearsScroll.opacityProperty(), 1.0, Interpolator.EASE_BOTH))
+        );
+
+        yearsClose = new Timeline(
+                new KeyFrame(Duration.millis(150), new KeyValue(yearsButton.rotateProperty(), 0, Interpolator.EASE_OUT)),
+                new KeyFrame(Duration.millis(250), new KeyValue(yearsScroll.opacityProperty(), 0.0, Interpolator.EASE_BOTH))
+        );
+        yearsClose.setOnFinished(event -> {
+            yearsScroll.setVisible(false);
+            calendar.setVisible(true);
+        });
+    }
+
+    /**
+     * Plays the animations for when years grid is opened/closed.
+     */
+    private void animateYears() {
+        boolean isOpen = yearsScroll.isVisible();
+        if (isOpen) {
+            yearsClose.play();
+        } else {
+            yearsOpen.play();
+        }
+    }
+
+    /**
+     * PLays the animation of month switching.
+     */
+    private void animateCalendar(boolean forward) {
+        int offset = (forward ? -1 : 1);
+
+        MFXSnapshotWrapper screen = new MFXSnapshotWrapper(calendar);
+        ImageView img = (ImageView) screen.getGraphic();
+        img.fitWidthProperty().bind(calendar.widthProperty().subtract(1));
+        img.fitHeightProperty().bind(calendar.heightProperty().subtract(1));
+        holder.getChildren().add(img);
+
+        Rectangle clip = new Rectangle(img.getFitWidth(), img.getFitHeight());
+        holder.setClip(clip);
+
+        calendarTransition = new Timeline(
+                new KeyFrame(Duration.millis(200),
+                        new KeyValue(img.translateXProperty(), offset * holder.getWidth(), Interpolator.EASE_OUT))
+        );
+        calendarTransition.setOnFinished(event -> {
+            holder.getChildren().remove(img);
+            holder.setClip(null);
+        });
+        calendarTransition.play();
+    }
+
+    //================================================================================
+    // Utility Methods
+    //================================================================================
+
+    /**
+     * Finds the index of the first day of the week from {@link #yearMonth}.
+     */
+    private int firstDayIndex() {
+        DayOfWeek fd = getYearMonth().atDay(1).getDayOfWeek();
+        return fd.getValue();
+    }
+
+    /**
+     * Switches month back or forward and updates {@link #yearMonth} accordingly;
+     */
+    private void changeMonth(boolean forward) {
+        if (calendarTransition != null && calendarTransition.getStatus() != Animation.Status.STOPPED) {
+            return;
+        }
+
+        if (!yearsScroll.isVisible() && animateCalendar.get()) {
+            animateCalendar(forward);
+        }
+
+        if (forward) {
+            setYearMonth(getYearMonth().plus(1, MONTHS));
+        } else {
+            setYearMonth(getYearMonth().minus(1, MONTHS));
+        }
+
+        if (lastSelectedDayCell.get() == null) {
+            selectDay();
+        }
+        if (lastSelectedYearCell == null) {
+            selectYear();
+        }
+
+        if (
+                lastSelectedDayCell.get() != null &&
+                        getYearMonth().getMonth().length(getYearMonth().isLeapYear()) <
+                                Integer.parseInt(lastSelectedDayCell.get().getText())
+        ) {
+            lastSelectedDayCell.get().setSelectedDate(false);
+
+            MFXDateCell cell = days.stream()
+                    .filter(dayCell -> dayCell.getText().equals("1"))
+                    .findFirst()
+                    .orElse(null);
+
+            if (cell != null) {
+                cell.setSelectedDate(true);
+                lastSelectedDayCell.set(cell);
+            }
+        }
+    }
+
+    private void selectYear() {
+        yearsList.stream()
+                .filter(year -> year.getText().equals(Integer.toString(getYearMonth().getYear())))
+                .findFirst()
+                .ifPresent(year -> {
+                    lastSelectedYearCell = year;
+                    lastSelectedYearCell.setSelectedDate(true);
+                });
+    }
+
+    private void selectDay() {
+        if (getYearMonth().getMonth().length(getYearMonth().isLeapYear()) < LocalDate.now().getDayOfMonth()) {
+            days.stream()
+                    .filter(day -> day.getText().equals("1"))
+                    .findFirst()
+                    .ifPresent(day -> {
+                        lastSelectedDayCell.set(day);
+                        lastSelectedDayCell.get().setSelectedDate(true);
+                    });
+        } else {
+            days.stream()
+                    .filter(day -> day.getText().equals(Integer.toString(getCurrentDate().getDayOfMonth())))
+                    .findFirst()
+                    .ifPresent(day -> {
+                        lastSelectedDayCell.set(day);
+                        lastSelectedDayCell.get().setSelectedDate(true);
+                    });
+        }
+    }
+
+    /**
+     * Moves the scrollpane to the last selected year if not null otherwise to the current year.
+     */
+    private void goToYear() {
+        MFXDateCell cell;
+        if (lastSelectedYearCell != null) {
+            cell = lastSelectedYearCell;
+        } else {
+            cell = currYearCell;
+        }
+
+        double contentHeight = yearsScroll.getContent().getBoundsInLocal().getHeight();
+        double nodePos = cell.getBoundsInParent().getMinY();
+        double vScroll = yearsScroll.getVmax() * (nodePos / contentHeight);
+        yearsScroll.setVvalue(vScroll);
+    }
+
+    private Locale getLocale() {
+        return Locale.getDefault(Locale.Category.FORMAT);
+    }
+
+    public MFXTextField getInputField() {
+        return inputField;
+    }
+
+    public LocalDate getCurrentDate() {
+        return currentDate.get();
+    }
+
+    public ObjectProperty<LocalDate> currentDateProperty() {
+        return currentDate;
+    }
+
+    public void setCurrentDate(LocalDate currentDate) {
+        this.currentDate.set(currentDate);
+    }
+
+    public YearMonth getYearMonth() {
+        return yearMonth.get();
+    }
+
+    public ObjectProperty<YearMonth> yearMonthProperty() {
+        return yearMonth;
+    }
+
+    public void setYearMonth(YearMonth yearMonth) {
+        this.yearMonth.set(yearMonth);
+    }
+
+    public MFXDateCell getLastSelectedDayCell() {
+        return lastSelectedDayCell.get();
+    }
+
+    public ObjectProperty<MFXDateCell> lastSelectedDayCellProperty() {
+        return lastSelectedDayCell;
+    }
+
+    public DateTimeFormatter getDateFormatter() {
+        return dateFormatter.get();
+    }
+
+    public ObjectProperty<DateTimeFormatter> dateFormatterProperty() {
+        return dateFormatter;
+    }
+
+    public void setDateFormatter(DateTimeFormatter dateFormatter) {
+        this.dateFormatter.set(dateFormatter);
+    }
+
+    public BooleanProperty animateCalendarProperty() {
+        return animateCalendar;
+    }
+}

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

@@ -20,7 +20,6 @@ import javafx.scene.layout.Region;
 import javafx.util.Duration;
 
 import java.util.Set;
-
 /**
  * This is the implementation of the {@code Skin} associated with every {@code MFXListView}.
  * <p>
@@ -134,6 +133,14 @@ public class MFXListViewSkin<T> extends ListViewSkin<T> {
             } else {
                 showBars.play();
             }
+            if (newValue &&
+                    hideBars.getStatus() != Animation.Status.RUNNING ||
+                    vBar.getOpacity() != 0 ||
+                    hBar.getOpacity() != 0
+            ) {
+                vBar.setOpacity(0.0);
+                hBar.setOpacity(0.0);
+            }
         });
 
         listView.depthLevelProperty().addListener((observable, oldValue, newValue) -> {

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

@@ -1,7 +1,7 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXRadioButton;
-import io.github.palexdev.materialfx.effects.RippleClipType;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
@@ -55,7 +55,7 @@ public class MFXRadioButtonSkin extends RadioButtonSkin {
         container = new StackPane();
         container.getStyleClass().add("radio-container");
 
-        rippleGenerator = new RippleGenerator(container, RippleClipType.NOCLIP);
+        rippleGenerator = new RippleGenerator(container, new RippleClipTypeFactory());
         rippleGenerator.setRippleRadius(radius * 1.2);
         rippleGenerator.setInDuration(Duration.millis(350));
         rippleGenerator.setAnimateBackground(false);

+ 189 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java

@@ -0,0 +1,189 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.MFXResourcesManager;
+import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import javafx.animation.FadeTransition;
+import javafx.scene.control.Label;
+import javafx.scene.control.skin.TextFieldSkin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.SVGPath;
+import javafx.scene.text.Font;
+import javafx.util.Duration;
+
+/**
+ * This is the implementation of the {@code Skin} associated with every {@code MFXTextField}.
+ */
+public class MFXTextFieldSkin extends TextFieldSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final double padding = 11;
+
+    private final Line line;
+    private final Line focusLine;
+    private final Label validate;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTextFieldSkin(MFXTextField textField) {
+        super(textField);
+
+        line = new Line();
+        line.getStyleClass().add("unfocused-line");
+        line.setStroke(textField.getUnfocusedLineColor());
+        line.setStrokeWidth(textField.getLineStrokeWidth());
+        line.setSmooth(true);
+
+        focusLine = new Line();
+        focusLine.getStyleClass().add("focused-line");
+        focusLine.setStroke(textField.getLineColor());
+        focusLine.setStrokeWidth(textField.getLineStrokeWidth());
+        focusLine.setSmooth(true);
+        focusLine.setOpacity(0.0);
+
+        line.endXProperty().bind(textField.widthProperty());
+        focusLine.endXProperty().bind(textField.widthProperty());
+        line.setManaged(false);
+        focusLine.setManaged(false);
+
+        StackPane stackPane = new StackPane();
+        stackPane.setPrefSize(1, 1);
+        stackPane.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        SVGPath warn = MFXResourcesManager.SVGResources.EXCLAMATION_TRIANGLE.getSvgPath();
+        warn.setScaleX((padding - 1) / 100);
+        warn.setScaleY((padding  - 1) / 100);
+        warn.setFill(Color.RED);
+        stackPane.getChildren().add(warn);
+
+        validate = new Label("", stackPane);
+        validate.getStyleClass().add("validate-label");
+        validate.textProperty().bind(textField.getValidator().validatorMessageProperty());
+        validate.setFont(Font.font(padding));
+        validate.setGraphicTextGap(padding);
+        validate.setVisible(false);
+
+        getChildren().addAll(line, focusLine, validate);
+
+        setListeners();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for: line, focus, disabled and validator properties.
+     * <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.
+     * The label text is bound to the {@code validatorMessage} property so if you want to change it you can do it
+     * by getting the instance with {@code getValidator()}.
+     * <p>
+     * There's also another listener to keep track of validator changes.
+     */
+    private void setListeners() {
+        MFXTextField textField = (MFXTextField) getSkinnable();
+        MFXDialogValidator validator = textField.getValidator();
+
+        textField.lineColorProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                focusLine.setStroke(newValue);
+            }
+        });
+
+        textField.unfocusedLineColorProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                line.setStroke(newValue);
+            }
+        });
+
+        textField.lineStrokeWidthProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.doubleValue() != oldValue.doubleValue()) {
+                line.setStrokeWidth(newValue.doubleValue());
+                focusLine.setStrokeWidth(newValue.doubleValue() * 1.3);
+            }
+        });
+
+        textField.focusedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue && textField.isValidated()) {
+                validate.setVisible(!validator.isValid());
+            }
+
+            if (textField.isAnimateLines()) {
+                buildAndPlayAnimation(newValue);
+                return;
+            }
+
+            if (newValue) {
+                focusLine.setOpacity(1.0);
+            } else {
+                focusLine.setOpacity(0.0);
+            }
+        });
+
+        textField.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            if (textField.isAnimateLines() && focusLine.getOpacity() != 1.0) {
+                buildAndPlayAnimation(true);
+                return;
+            }
+
+            focusLine.setOpacity(1.0);
+        });
+
+        textField.isValidatedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue) {
+                validate.setVisible(false);
+            }
+        });
+
+        textField.disabledProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                validate.setVisible(false);
+            }
+        });
+
+        validator.addChangeListener((observable, oldValue, newValue) -> {
+            if (textField.isValidated()) {
+                validate.setVisible(!newValue);
+            }
+        });
+
+        validate.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> validator.show());
+    }
+
+    /**
+     * Builds and play the lines animation if {@code animateLines} is true.
+     */
+    private void buildAndPlayAnimation(boolean focused) {
+        FadeTransition fadeTransition = new FadeTransition(Duration.millis(200), focusLine);
+        if (focused) {
+            fadeTransition.setFromValue(0.0);
+            fadeTransition.setToValue(1.0);
+        } else {
+            fadeTransition.setFromValue(1.0);
+            fadeTransition.setToValue(0.0);
+        }
+        fadeTransition.play();
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        final double size = padding / 2.5;
+
+        focusLine.setTranslateY(h + padding * 0.7);
+        line.setTranslateY(h + padding * 0.7);
+        validate.resize(w * 1.5, h - size);
+        validate.setTranslateY(focusLine.getTranslateY() + size);
+    }
+}

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

@@ -1,9 +1,9 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXToggleButton;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.effects.MFXDepthManager;
-import io.github.palexdev.materialfx.effects.RippleClipType;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
 import javafx.animation.Interpolator;
 import javafx.animation.KeyFrame;
@@ -64,7 +64,7 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
         container.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         container.setPrefSize(50, 40);
 
-        rippleGenerator = new RippleGenerator(container, RippleClipType.NOCLIP);
+        rippleGenerator = new RippleGenerator(container, new RippleClipTypeFactory());
         rippleGenerator.setAnimateBackground(false);
         rippleGenerator.setRippleColor((Color) (toggleButton.isSelected() ? toggleButton.getUnToggleLineColor() : toggleButton.getToggleLineColor()));
         rippleGenerator.setRippleRadius(circleRadius * 1.2);

+ 347 - 9
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -1,16 +1,18 @@
 package io.github.palexdev.materialfx.utils;
 
-import javafx.geometry.HPos;
-import javafx.geometry.Insets;
-import javafx.geometry.VPos;
+import javafx.geometry.*;
 import javafx.scene.Node;
-import javafx.scene.layout.AnchorPane;
-import javafx.scene.layout.Background;
-import javafx.scene.layout.BackgroundFill;
-import javafx.scene.layout.Region;
+import javafx.scene.Scene;
+import javafx.scene.layout.*;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Circle;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import javafx.util.FXPermission;
 
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -18,6 +20,7 @@ import java.util.List;
  * Utility class which provides convenience methods for working with Nodes
  */
 public class NodeUtils {
+    public static final FXPermission ACCESS_WINDOW_LIST_PERMISSION = new FXPermission("accessWindowList");
 
     private NodeUtils() {
     }
@@ -61,6 +64,13 @@ public class NodeUtils {
         region.setBackground(new Background(fills.toArray(BackgroundFill[]::new)));
     }
 
+    /**
+     * Sets the background of the given region to the given color.
+     */
+    public static void setBackground(Region region, Paint fill) {
+        region.setBackground(new Background(new BackgroundFill(fill, CornerRadii.EMPTY, Insets.EMPTY)));
+    }
+
     /**
      * Centers the specified node in an {@code AnchorPane}.
      */
@@ -94,7 +104,8 @@ public class NodeUtils {
      * @param region The given region
      */
     public static void makeRegionCircular(Region region) {
-        Circle circle = new Circle(region.getPrefWidth() / 2.0);
+        Circle circle = new Circle();
+        circle.radiusProperty().bind(region.widthProperty().divide(2.0));
         circle.centerXProperty().bind(region.widthProperty().divide(2.0));
         circle.centerYProperty().bind(region.heightProperty().divide(2.0));
         try {
@@ -122,7 +133,7 @@ public class NodeUtils {
         }
     }
 
-    /* The next two methods are copied from com.sun.javafx.scene.control.skin.Utils class
+    /* The following methods are copied from com.sun.javafx.scene.control.skin.Utils class
      * It's a private module, so to avoid adding exports and opens I copied them
      */
     public static double computeXOffset(double width, double contentWidth, HPos hpos) {
@@ -150,4 +161,331 @@ public class NodeUtils {
                 return 0;
         }
     }
+
+    public static Point2D pointRelativeTo(Node parent, Node node, HPos hpos,
+                                          VPos vpos, double dx, double dy, boolean reposition)
+    {
+        final double nodeWidth = node.getLayoutBounds().getWidth();
+        final double nodeHeight = node.getLayoutBounds().getHeight();
+        return pointRelativeTo(parent, nodeWidth, nodeHeight, hpos, vpos, dx, dy, reposition);
+    }
+
+    public static Point2D pointRelativeTo(Node parent, double anchorWidth,
+                                          double anchorHeight, HPos hpos, VPos vpos, double dx, double dy,
+                                          boolean reposition)
+    {
+        final Bounds parentBounds = getBounds(parent);
+        Scene scene = parent.getScene();
+        NodeOrientation orientation = parent.getEffectiveNodeOrientation();
+
+        if (orientation == NodeOrientation.RIGHT_TO_LEFT) {
+            if (hpos == HPos.LEFT) {
+                hpos = HPos.RIGHT;
+            } else if (hpos == HPos.RIGHT) {
+                hpos = HPos.LEFT;
+            }
+            dx *= -1;
+        }
+
+        double layoutX = positionX(parentBounds, anchorWidth, hpos) + dx;
+        final double layoutY = positionY(parentBounds, anchorHeight, vpos) + dy;
+
+        if (orientation == NodeOrientation.RIGHT_TO_LEFT && hpos == HPos.CENTER) {
+            if (scene.getWindow() instanceof Stage) {
+                layoutX = layoutX + parentBounds.getWidth() - anchorWidth;
+            } else {
+                layoutX = layoutX - parentBounds.getWidth() - anchorWidth;
+            }
+        }
+
+        if (reposition) {
+            return pointRelativeTo(parent, anchorWidth, anchorHeight, layoutX, layoutY, hpos, vpos);
+        } else {
+            return new Point2D(layoutX, layoutY);
+        }
+    }
+
+    /**
+     * This is the fallthrough function that most other functions fall into. It takes
+     * care specifically of the repositioning of the item such that it remains onscreen
+     * as best it can, given it's unique qualities.
+     *
+     * As will all other functions, this one returns a Point2D that represents an x,y
+     * location that should safely position the item onscreen as best as possible.
+     *
+     * Note that <code>width</code> and <height> refer to the width and height of the
+     * node/popup that is needing to be repositioned, not of the parent.
+     *
+     * Don't use the BASELINE vpos, it doesn't make sense and would produce wrong result.
+     */
+    public static Point2D pointRelativeTo(Object parent, double width,
+                                          double height, double screenX, double screenY, HPos hpos, VPos vpos)
+    {
+        double finalScreenX = screenX;
+        double finalScreenY = screenY;
+        final Bounds parentBounds = getBounds(parent);
+
+        // ...and then we get the bounds of this screen
+        final Screen currentScreen = getScreen(parent);
+        final Rectangle2D screenBounds =
+                hasFullScreenStage(currentScreen)
+                        ? currentScreen.getBounds()
+                        : currentScreen.getVisualBounds();
+
+        // test if this layout will force the node to appear outside
+        // of the screens bounds. If so, we must reposition the item to a better position.
+        // We firstly try to do this intelligently, so as to not overlap the parent if
+        // at all possible.
+        if (hpos != null) {
+            // Firstly we consider going off the right hand side
+            if ((finalScreenX + width) > screenBounds.getMaxX()) {
+                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
+            }
+
+            // don't let the node go off to the left of the current screen
+            if (finalScreenX < screenBounds.getMinX()) {
+                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
+            }
+        }
+
+        if (vpos != null) {
+            // don't let the node go off the bottom of the current screen
+            if ((finalScreenY + height) > screenBounds.getMaxY()) {
+                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos,vpos));
+            }
+
+            // don't let the node out of the top of the current screen
+            if (finalScreenY < screenBounds.getMinY()) {
+                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos,vpos));
+            }
+        }
+
+        // --- after all the moving around, we do one last check / rearrange.
+        // Unlike the check above, this time we are just fully committed to keeping
+        // the item on screen at all costs, regardless of whether or not that results
+        /// in overlapping the parent object.
+        if ((finalScreenX + width) > screenBounds.getMaxX()) {
+            finalScreenX -= (finalScreenX + width - screenBounds.getMaxX());
+        }
+        if (finalScreenX < screenBounds.getMinX()) {
+            finalScreenX = screenBounds.getMinX();
+        }
+        if ((finalScreenY + height) > screenBounds.getMaxY()) {
+            finalScreenY -= (finalScreenY + height - screenBounds.getMaxY());
+        }
+        if (finalScreenY < screenBounds.getMinY()) {
+            finalScreenY = screenBounds.getMinY();
+        }
+
+        return new Point2D(finalScreenX, finalScreenY);
+    }
+
+    private static double positionX(Bounds parentBounds, double width, HPos hpos) {
+        if (hpos == HPos.CENTER) {
+            // this isn't right, but it is needed for root menus to show properly
+            return parentBounds.getMinX();
+        } else if (hpos == HPos.RIGHT) {
+            return parentBounds.getMaxX();
+        } else if (hpos == HPos.LEFT) {
+            return parentBounds.getMinX() - width;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Utility function that returns the y-axis position that an object should be positioned at,
+     * given the parents screen bounds, the height of the object, and
+     * the required VPos.
+     *
+     * The BASELINE vpos doesn't make sense here, 0 is returned for it.
+     */
+    private static double positionY(Bounds parentBounds, double height, VPos vpos) {
+        if (vpos == VPos.BOTTOM) {
+            return parentBounds.getMaxY();
+        } else if (vpos == VPos.CENTER) {
+            return parentBounds.getMinY();
+        } else if (vpos == VPos.TOP) {
+            return parentBounds.getMinY() - height;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * To facilitate multiple types of parent object, we unfortunately must allow for
+     * Objects to be passed in. This method handles determining the bounds of the
+     * given Object. If the Object type is not supported, a default Bounds will be returned.
+     */
+    private static Bounds getBounds(Object obj) {
+        if (obj instanceof Node) {
+            final Node n = (Node)obj;
+            Bounds b = n.localToScreen(n.getLayoutBounds());
+            return b != null ? b : new BoundingBox(0, 0, 0, 0);
+        } else if (obj instanceof Window) {
+            final Window window = (Window)obj;
+            return new BoundingBox(window.getX(), window.getY(), window.getWidth(), window.getHeight());
+        } else {
+            return new BoundingBox(0, 0, 0, 0);
+        }
+    }
+
+    /*
+     * Simple utitilty 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) {
+        if (vpos == VPos.CENTER) {
+            if (hpos == HPos.LEFT){
+                return HPos.RIGHT;
+            } else if (hpos == HPos.RIGHT){
+                return HPos.LEFT;
+            } else if (hpos == HPos.CENTER){
+                return HPos.CENTER;
+            } else {
+                // by default center for now
+                return HPos.CENTER;
+            }
+        } else {
+            return HPos.CENTER;
+        }
+    }
+
+    /*
+     * Simple utitilty function to return the 'opposite' value of a given VPos, taking
+     * into account the current HPos value. This is used to try and avoid overlapping.
+     */
+    private static VPos getVPosOpposite(HPos hpos, VPos vpos) {
+        if (hpos == HPos.CENTER) {
+            if (vpos == VPos.BASELINE){
+                return VPos.BASELINE;
+            } else if (vpos == VPos.BOTTOM){
+                return VPos.TOP;
+            } else if (vpos == VPos.CENTER){
+                return VPos.CENTER;
+            } else if (vpos == VPos.TOP){
+                return VPos.BOTTOM;
+            } else {
+                // by default center for now
+                return VPos.CENTER;
+            }
+        } else {
+            return VPos.CENTER;
+        }
+    }
+
+    public static boolean hasFullScreenStage(final Screen screen) {
+        final List<Window> allWindows = AccessController.doPrivileged(
+                (PrivilegedAction<List<Window>>) Window::getWindows,
+                null,
+                ACCESS_WINDOW_LIST_PERMISSION);
+
+        for (final Window window : allWindows) {
+            if (window instanceof Stage) {
+                final Stage stage = (Stage) window;
+                if (stage.isFullScreen() && (getScreen(stage) == screen)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static Screen getScreen(Object obj) {
+        final Bounds parentBounds = getBounds(obj);
+
+        final Rectangle2D rect = new Rectangle2D(
+                parentBounds.getMinX(),
+                parentBounds.getMinY(),
+                parentBounds.getWidth(),
+                parentBounds.getHeight());
+
+        return getScreenForRectangle(rect);
+    }
+
+    public static Screen getScreenForRectangle(final Rectangle2D rect) {
+        final List<Screen> screens = Screen.getScreens();
+
+        final double rectX0 = rect.getMinX();
+        final double rectX1 = rect.getMaxX();
+        final double rectY0 = rect.getMinY();
+        final double rectY1 = rect.getMaxY();
+
+        Screen selectedScreen;
+
+        selectedScreen = null;
+        double maxIntersection = 0;
+        for (final Screen screen: screens) {
+            final Rectangle2D screenBounds = screen.getBounds();
+            final double intersection =
+                    getIntersectionLength(rectX0, rectX1,
+                            screenBounds.getMinX(),
+                            screenBounds.getMaxX())
+                            * getIntersectionLength(rectY0, rectY1,
+                            screenBounds.getMinY(),
+                            screenBounds.getMaxY());
+
+            if (maxIntersection < intersection) {
+                maxIntersection = intersection;
+                selectedScreen = screen;
+            }
+        }
+
+        if (selectedScreen != null) {
+            return selectedScreen;
+        }
+
+        selectedScreen = Screen.getPrimary();
+        double minDistance = Double.MAX_VALUE;
+        for (final Screen screen: screens) {
+            final Rectangle2D screenBounds = screen.getBounds();
+            final double dx = getOuterDistance(rectX0, rectX1,
+                    screenBounds.getMinX(),
+                    screenBounds.getMaxX());
+            final double dy = getOuterDistance(rectY0, rectY1,
+                    screenBounds.getMinY(),
+                    screenBounds.getMaxY());
+            final double distance = dx * dx + dy * dy;
+
+            if (minDistance > distance) {
+                minDistance = distance;
+                selectedScreen = screen;
+            }
+        }
+
+        return selectedScreen;
+    }
+
+    private static double getIntersectionLength(
+            final double a0, final double a1,
+            final double b0, final double b1) {
+        // (a0 <= a1) && (b0 <= b1)
+        return (a0 <= b0) ? getIntersectionLengthImpl(b0, b1, a1)
+                : getIntersectionLengthImpl(a0, a1, b1);
+    }
+
+    private static double getIntersectionLengthImpl(
+            final double v0, final double v1, final double v) {
+        // (v0 <= v1)
+        if (v <= v0) {
+            return 0;
+        }
+
+        return (v <= v1) ? v - v0 : v1 - v0;
+    }
+
+    private static double getOuterDistance(
+            final double a0, final double a1,
+            final double b0, final double b1) {
+        // (a0 <= a1) && (b0 <= b1)
+        if (a1 <= b0) {
+            return b0 - a1;
+        }
+
+        if (b1 <= a0) {
+            return b1 - a0;
+        }
+
+        return 0;
+    }
 }

+ 11 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java

@@ -67,4 +67,15 @@ public class StringUtils {
         return string.substring(0, index) + replacement
                 + string.substring(index + substring.length());
     }
+
+    public static String titleCaseWord(String str) {
+        if (str.length() > 0) {
+            int firstChar = str.codePointAt(0);
+            if (!Character.isTitleCase(firstChar)) {
+                str = new String(new int[] { Character.toTitleCase(firstChar) }, 0, 1) +
+                        str.substring(Character.offsetByCodePoints(str, 0, 1));
+            }
+        }
+        return str;
+    }
 }

+ 9 - 20
materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java

@@ -1,6 +1,5 @@
 package io.github.palexdev.materialfx.validation;
 
-import io.github.palexdev.materialfx.beans.binding.BooleanListBinding;
 import io.github.palexdev.materialfx.controls.MFXStageDialog;
 import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
@@ -9,14 +8,13 @@ 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.collections.FXCollections;
 import javafx.geometry.Pos;
 import javafx.scene.control.Label;
 import javafx.stage.Modality;
 import javafx.stage.Window;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * This is a concrete implementation of a validator.
@@ -28,7 +26,7 @@ public class MFXDialogValidator extends AbstractMFXValidator {
     //================================================================================
     // Properties
     //================================================================================
-    private final List<String> messages = new ArrayList<>();
+    private final Map<BooleanProperty, String> messagesMap = new HashMap<>();
     private final ObjectProperty<DialogType> dialogType = new SimpleObjectProperty<>(DialogType.WARNING);
     private String title;
     private MFXStageDialog stageDialog;
@@ -98,24 +96,15 @@ public class MFXDialogValidator extends AbstractMFXValidator {
      * @param message The message to show in case it is false
      */
     public void add(BooleanProperty property, String message) {
-        if (super.conditions == null || super.validation == null) {
-            super.conditions = FXCollections.observableArrayList(property);
-            super.validation = new BooleanListBinding(conditions);
-        } else {
-            super.conditions.add(property);
-        }
-
-        this.messages.add(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) {
-        int index = conditions.indexOf(property);
-        if (index != -1 && messages.size() > 0) {
-            messages.remove(index);
-        }
+        messagesMap.remove(property);
         super.conditions.remove(property);
     }
 
@@ -125,9 +114,9 @@ public class MFXDialogValidator extends AbstractMFXValidator {
      */
     public String getMessages() {
         StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < messages.size(); i++) {
-            if (!conditions.get(i).get()) {
-                sb.append(messages.get(i)).append(",\n");
+        for (BooleanProperty property : messagesMap.keySet()) {
+            if (!property.get()) {
+                sb.append(messagesMap.get(property)).append(",\n");
             }
         }
         return StringUtils.replaceLast(sb.toString(), ",", ".");

+ 7 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java

@@ -7,6 +7,7 @@ import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import javafx.beans.value.ChangeListener;
+import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 
 /**
@@ -18,8 +19,8 @@ public abstract class AbstractMFXValidator implements IMFXValidator {
     //================================================================================
     // Properties
     //================================================================================
-    protected ObservableList<BooleanProperty> conditions;
-    protected BooleanListBinding validation;
+    protected ObservableList<BooleanProperty> conditions = FXCollections.observableArrayList();
+    protected BooleanListBinding validation = new BooleanListBinding(conditions);
     private final StringProperty validatorMessage = new SimpleStringProperty("Validation failed!");
 
     //================================================================================
@@ -37,6 +38,10 @@ public abstract class AbstractMFXValidator implements IMFXValidator {
         this.validatorMessage.set(validatorMessage);
     }
 
+    public BooleanListBinding validationProperty() {
+        return validation;
+    }
+
     //================================================================================
     // Override Methods
     //================================================================================

+ 1 - 0
materialfx/src/main/java/module-info.java

@@ -14,6 +14,7 @@ module MaterialFX.materialfx.main {
     exports io.github.palexdev.materialfx.controls.enums;
     exports io.github.palexdev.materialfx.controls.factories;
     exports io.github.palexdev.materialfx.effects;
+    exports io.github.palexdev.materialfx.font;
     exports io.github.palexdev.materialfx.notifications;
     exports io.github.palexdev.materialfx.skins;
     exports io.github.palexdev.materialfx.utils;

+ 27 - 28
materialfx/src/main/resources/io/github/palexdev/materialfx/css/fonts.css

@@ -1,5 +1,5 @@
 @font-face {
-    font-family: 'Comfortaa';
+    font-family: 'Comfortaa Bold';
     font-weight: bold;
     font-style: normal;
     font-display: swap;
@@ -7,7 +7,7 @@
 }
 
 @font-face {
-    font-family: 'Comfortaa';
+    font-family: 'Comfortaa Light';
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
@@ -15,7 +15,7 @@
 }
 
 @font-face {
-    font-family: 'Comfortaa';
+    font-family: 'Comfortaa Medium';
     font-weight: 500px;
     font-style: normal;
     font-display: swap;
@@ -23,7 +23,7 @@
 }
 
 @font-face {
-    font-family: 'Comfortaa';
+    font-family: 'Comfortaa Regular';
     font-weight: normal;
     font-style: normal;
     font-display: swap;
@@ -31,7 +31,7 @@
 }
 
 @font-face {
-    font-family: 'Comfortaa';
+    font-family: 'Comfortaa SemiBold';
     font-weight: 600px;
     font-style: normal;
     font-display: swap;
@@ -39,7 +39,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans Bold';
     font-weight: bold;
     font-style: normal;
     font-display: swap;
@@ -47,7 +47,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans BoldItalic';
     font-weight: bold;
     font-style: italic;
     font-display: swap;
@@ -55,7 +55,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans ExtraBold';
     font-weight: 800px;
     font-style: normal;
     font-display: swap;
@@ -63,7 +63,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans ExtraBoldItalic';
     font-weight: 800px;
     font-style: italic;
     font-display: swap;
@@ -71,7 +71,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans Light';
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
@@ -79,7 +79,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans LightItalic';
     font-weight: 300px;
     font-style: italic;
     font-display: swap;
@@ -87,7 +87,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans Italic';
     font-weight: normal;
     font-style: italic;
     font-display: swap;
@@ -95,7 +95,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans SemiBold';
     font-weight: 600px;
     font-style: normal;
     font-display: swap;
@@ -103,7 +103,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans Regular';
     font-weight: normal;
     font-style: normal;
     font-display: swap;
@@ -111,7 +111,7 @@
 }
 
 @font-face {
-    font-family: 'Open Sans';
+    font-family: 'Open Sans SemiBoldItalic';
     font-weight: 600px;
     font-style: italic;
     font-display: swap;
@@ -119,7 +119,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Black';
     font-weight: 900px;
     font-style: normal;
     font-display: swap;
@@ -127,7 +127,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto BlackItalic';
     font-weight: 900px;
     font-style: italic;
     font-display: swap;
@@ -135,7 +135,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Bold';
     font-weight: bold;
     font-style: normal;
     font-display: swap;
@@ -143,7 +143,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto BoldItalic';
     font-weight: bold;
     font-style: italic;
     font-display: swap;
@@ -151,7 +151,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Light';
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
@@ -159,7 +159,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Italic';
     font-weight: normal;
     font-style: italic;
     font-display: swap;
@@ -167,7 +167,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto LightItalic';
     font-weight: 300px;
     font-style: italic;
     font-display: swap;
@@ -175,7 +175,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Medium';
     font-weight: 500px;
     font-style: normal;
     font-display: swap;
@@ -183,7 +183,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto MediumItalic';
     font-weight: 500px;
     font-style: italic;
     font-display: swap;
@@ -191,7 +191,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Regular';
     font-weight: normal;
     font-style: normal;
     font-display: swap;
@@ -199,7 +199,7 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto ThinItalic';
     font-weight: 100px;
     font-style: italic;
     font-display: swap;
@@ -207,10 +207,9 @@
 }
 
 @font-face {
-    font-family: 'Roboto';
+    font-family: 'Roboto Thin';
     font-weight: 100px;
     font-style: normal;
     font-display: swap;
     src: url('../fonts/Roboto/Roboto-Thin.ttf') format('truetype');
 }
-

+ 167 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-datepicker-content.css

@@ -0,0 +1,167 @@
+@import "fonts.css";
+
+* {
+    -fx-font-smoothing-type: gray;
+}
+
+.mfx-datepicker-content {
+    -mfx-main-color: #6200ee;
+}
+
+.mfx-datepicker-content {
+    -fx-background-color: white;
+    -fx-background-insets: 0 0 -3 0;
+    -fx-background-radius: 6 6 5 5;
+
+    -fx-border-color: transparent #d4d4d4 #d4d4d4 #d4d4d4;
+    -fx-border-width: 0.5;
+    -fx-border-radius: 5;
+    -fx-border-insets: 0 0 -3 0;
+}
+
+.header {
+    -fx-background-color: -mfx-main-color;
+    -fx-background-radius: 5 5 0 0;
+    -fx-background-insets: -0.5;
+}
+
+.header .label {
+    -fx-font-family: "Open Sans Bold";
+    -fx-font-size: 11.5;
+    -fx-font-smoothing-type: gray;
+}
+
+#selected-date {
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 25;
+}
+
+.month-year-pane {
+    -fx-background-color: white;
+}
+
+.years-button .ripple-generator {
+    -mfx-ripple-color: rgb(220, 220, 220);
+}
+
+.month-back-button .ripple-generator {
+    -mfx-ripple-color: rgb(220, 220, 220);
+}
+
+.month-forward-button .ripple-generator {
+    -mfx-ripple-color: rgb(220, 220, 220);
+}
+
+.change-input-button .ripple-generator {
+    -mfx-ripple-color: rgb(220, 220, 220);
+}
+
+.change-input-button .tooltip {
+    -fx-font-family: "Open Sans Regular";
+    -fx-background-color: rgba(255, 255, 255, 0.9);
+    -fx-text-fill: black;
+    -fx-font-smoothing-type: gray;
+}
+
+#input-field {
+    -fx-prompt-text-fill: rgb(77, 77, 77);
+}
+
+.holder {
+    -fx-background-insets: 0 0 -2 0;
+    -fx-background-radius: 0 0 5 5;
+}
+
+.calendar {
+    -fx-background-color: white;
+    -fx-background-radius: 0 0 5 5;
+}
+
+.years-scrollpane {
+    -mfx-track-color: transparent;
+    -mfx-thumb-color: derive(-mfx-main-color, 70%);
+    -mfx-thumb-hover-color: -mfx-main-color;
+}
+
+.years-scrollpane:focused {
+    -fx-border-color: transparent #d4d4d4 #d4d4d4 #d4d4d4;
+    -fx-border-width: 0.5;
+    -fx-border-radius: 0 0 5 5;
+    -fx-border-insets: -0.8 -0.8 -3 -0.8;
+}
+
+.years-scrollpane .scroll-bar {
+    -fx-background-color: transparent;
+    -fx-border-color: transparent;
+    -fx-focus-color: transparent;
+    -fx-background-insets: 100%;
+    -fx-border-insets: 100%;
+}
+
+.years {
+    -fx-background-radius: 0 0 5 5;
+}
+
+.day-name-cell {
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 14;
+    -fx-text-fill: rgb(130, 130, 130);
+}
+
+.day-cell {
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 11;
+    -fx-text-fill: rgb(68, 68, 68);
+}
+
+.day-cell .cell-stroke {
+    -fx-stroke: rgb(68, 68, 68);
+}
+
+.day-cell:current {
+    -fx-font-family: "Open Sans Regular";
+    -fx-font-size: 12.5;
+    -fx-text-fill: rgb(68, 68, 68);
+}
+
+.day-cell:selectedDate {
+    -fx-background-color: -mfx-main-color;
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 12.5;
+    -fx-text-fill: white;
+}
+
+.day-cell .ripple-generator {
+    -mfx-ripple-radius: 16px;
+    -mfx-ripple-color: rgba(211, 211, 211, 0.7);
+}
+
+.year-cell {
+    -fx-background-radius: 10;
+    -fx-border-radius: 10;
+    -fx-font-family: "Open Sans Regular";
+    -fx-font-size: 14;
+}
+
+.year-cell:current {
+    -fx-border-color: -mfx-main-color;
+    -fx-border-width: 0.8;
+    -fx-font-family: "Open Sans Regular";
+}
+
+.year-cell:selectedDate {
+    -fx-background-color: -mfx-main-color;
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 16;
+    -fx-text-fill: white;
+}
+
+.year-cell .ripple-generator {
+    -mfx-ripple-radius: 30px;
+    -mfx-ripple-color: rgba(211, 211, 211, 0.6);
+}
+
+.day-name-cell .ripple-generator {
+    -mfx-ripple-radius: 0;
+    -mfx-animate-background: false;
+}

+ 5 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-datepicker.css

@@ -0,0 +1,5 @@
+.value {
+    -fx-font-family: "Open Sans Regular";
+    -fx-font-size: 12.5;
+    -fx-font-smoothing-type: gray;
+}

+ 8 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-scrollpane.css

@@ -18,6 +18,14 @@
     -fx-background-color: transparent ;
 }
 
+.mfx-scroll-pane .scroll-bar {
+    -fx-background-color: transparent;
+    -fx-border-color: transparent;
+    -fx-focus-color: transparent;
+    -fx-background-insets: 100%;
+    -fx-border-insets: 100%;
+}
+
 .mfx-scroll-pane .scroll-bar:horizontal .track {
     -fx-background-color: -mfx-track-color;
     -fx-border-color: transparent;

+ 9 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-textfield.css

@@ -0,0 +1,9 @@
+.mfx-text-field {
+    -fx-prompt-text-fill: #61606C;
+    -fx-padding: 0.333333em 0 0.333333em 0;
+}
+
+.mfx-text-field,
+.mfx-text-field:focused {
+    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT;
+}

二进制
materialfx/src/main/resources/io/github/palexdev/materialfx/fonts/materialfx-resources.ttf