Эх сурвалжийг харах

Version 11.4.4
Added 3 new controls: MFXComboBox, MFXListView and MFXRadioButton.
Demo: added smooth scrolling, re-organized css files.

Added custom binding, BooleanListBinding to observe a list of boolean properties.
Added validation api.
Added new method to StringUtils.

MFXButton, depthLevel changed to Styleable property for usage in CSS.
MFXStageDialog ignore exception for owner and modality.
Refactored some skin classes to make use of getSkinnable().
MFXCheckboxSkin fixed updateColors() not being called in SceneBuilder if selected/indeterminate was checked.

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

PAlex404 4 жил өмнө
parent
commit
b56ba251f1
53 өөрчлөгдсөн 2856 нэмэгдсэн , 219 устгасан
  1. 2 2
      README.md
  2. 1 1
      build.gradle
  3. 81 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ComboBoxesDemoController.java
  4. 11 4
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java
  5. 124 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ListViewDemoController.java
  6. 11 2
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ScrollPaneDemoController.java
  7. 19 25
      demo/src/main/resources/io/github/palexdev/materialfx/demo/buttons_demo.fxml
  8. 4 3
      demo/src/main/resources/io/github/palexdev/materialfx/demo/checkboxes_demo.fxml
  9. 58 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/combo_boxes_demo.fxml
  10. 0 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/buttons_demo.css
  11. 0 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/checkboxes_demo.css
  12. 7 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/combo_boxes_demo.css
  13. 30 30
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/common.css
  14. 6 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/demo.css
  15. 0 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/info_dialog.css
  16. 30 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/listviews_demo.css
  17. 6 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/radio_buttons_demo.css
  18. 0 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/scrollpanes_demo.css
  19. 2 2
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/toggle_buttons_demo.css
  20. 7 13
      demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.fxml
  21. 3 3
      demo/src/main/resources/io/github/palexdev/materialfx/demo/dialogs_demo.fxml
  22. 1 1
      demo/src/main/resources/io/github/palexdev/materialfx/demo/info_dialog.fxml
  23. 57 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/listviews_demo.fxml
  24. 1 1
      demo/src/main/resources/io/github/palexdev/materialfx/demo/notifications_demo.fxml
  25. 60 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/radio_buttons_demo.fxml
  26. 0 49
      demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpane_demo.fxml
  27. 39 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpanes_demo.fxml
  28. 3 3
      demo/src/main/resources/io/github/palexdev/materialfx/demo/toggle_buttons_demo.fxml
  29. 1 1
      materialfx/gradle.properties
  30. 32 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXSnapshotWrapper.java
  31. 43 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/binding/BooleanListBinding.java
  32. 28 15
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java
  33. 323 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXComboBox.java
  34. 224 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListCell.java
  35. 257 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java
  36. 217 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXRadioButton.java
  37. 8 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStageDialog.java
  38. 5 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXAnimationFactory.java
  39. 32 12
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXButtonSkin.java
  40. 46 29
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCheckboxSkin.java
  41. 178 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  42. 227 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java
  43. 196 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRadioButtonSkin.java
  44. 22 21
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXToggleButtonSkin.java
  45. 17 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java
  46. 155 0
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java
  47. 79 0
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java
  48. 17 0
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/IMFXValidator.java
  49. 3 0
      materialfx/src/main/java/module-info.java
  50. 42 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-combobox.css
  51. 28 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listcell.css
  52. 113 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listview.css
  53. 0 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-radiobutton.css

+ 2 - 2
README.md

@@ -86,7 +86,7 @@ repositories {
 }
 
 dependencies {
-implementation 'io.github.palexdev:materialfx:11.3.3'
+implementation 'io.github.palexdev:materialfx:11.4.4'
 }
 ```
 ###### Maven
@@ -94,7 +94,7 @@ implementation 'io.github.palexdev:materialfx:11.3.3'
 <dependency>
   <groupId>io.github.palexdev</groupId>
   <artifactId>materialfx</artifactId>
-  <version>11.3.3</version>
+  <version>11.4.4</version>
 </dependency>
 ```
 

+ 1 - 1
build.gradle

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

+ 81 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ComboBoxesDemoController.java

@@ -0,0 +1,81 @@
+package io.github.palexdev.materialfx.demo.controllers;
+
+import io.github.palexdev.materialfx.controls.MFXCheckbox;
+import io.github.palexdev.materialfx.controls.MFXComboBox;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.net.URL;
+import java.util.List;
+import java.util.ResourceBundle;
+
+public class ComboBoxesDemoController implements Initializable {
+
+    @FXML
+    private MFXComboBox<String> standard;
+
+    @FXML
+    private MFXComboBox<String> lineColors;
+
+    @FXML
+    private MFXComboBox<String> editable;
+
+    @FXML
+    private MFXComboBox<Label> labels;
+
+    @FXML
+    private MFXComboBox<String> validated;
+
+    @FXML
+    private MFXComboBox<String> customized;
+
+    @FXML
+    private MFXCheckbox checkbox;
+
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        ObservableList<String> stringList = FXCollections.observableArrayList(List.of(
+                "String 0",
+                "String 1",
+                "String 2",
+                "String 3",
+                "String 4",
+                "String 5",
+                "String 6",
+                "String 7"
+        ));
+
+        ObservableList<Label> labelsList = FXCollections.observableArrayList(List.of(
+                new Label("Label 0", createIcon("fas-home")),
+                new Label("Label 1", createIcon("fas-star")),
+                new Label("Label 2", createIcon("fas-heart")),
+                new Label("Label 3", createIcon("fas-cocktail")),
+                new Label("Label 4", createIcon("fas-anchor")),
+                new Label("Label 5", createIcon("fas-bolt")),
+                new Label("Label 6", createIcon("fas-bug")),
+                new Label("Label 7", createIcon("fas-beer"))
+        ));
+
+        standard.setItems(stringList);
+        lineColors.setItems(stringList);
+        labels.setItems(labelsList);
+        editable.setItems(stringList);
+        validated.setItems(stringList);
+        customized.setItems(stringList);
+
+        editable.setEditable(true);
+        validated.getValidator().add(checkbox.selectedProperty(), "Checkbox is not selected!");
+    }
+
+    private FontIcon createIcon(String s) {
+        FontIcon icon = new FontIcon(s);
+        icon.setIconColor(Color.PURPLE);
+        icon.setIconSize(13);
+        return icon;
+    }
+}

+ 11 - 4
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java

@@ -46,6 +46,9 @@ public class DemoController implements Initializable {
     @FXML
     private StackPane navBar;
 
+    @FXML
+    private MFXScrollPane scrollPane;
+
     @FXML
     private MFXVLoader vLoader;
 
@@ -127,7 +130,7 @@ public class DemoController implements Initializable {
         StackPane.setAlignment(infoButton, Pos.BOTTOM_RIGHT);
         StackPane.setMargin(infoButton, new Insets(0, 8, 8, 0));
         StackPane.setAlignment(opNavButton, Pos.CENTER_LEFT);
-        StackPane.setMargin(opNavButton, new Insets(0, 0, 0, 9));
+        StackPane.setMargin(opNavButton, new Insets(0, 0, 0, 4));
 
         NodeUtils.makeRegionCircular(closeButton);
         NodeUtils.makeRegionCircular(minimizeButton);
@@ -142,13 +145,17 @@ public class DemoController implements Initializable {
         vLoader.setContentPane(contentPane);
         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, "TOGGLES", new MFXToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_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, "NOTIFICATIONS", new MFXToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml"));
-        vLoader.addItem(5, "SCROLLPANE", new MFXToggleNode("SCROLLPANE"), MFXResourcesLoader.load("scrollpane_demo.fxml"));
+        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.setDefault("BUTTONS");
 
         // Others
+        MFXScrollPane.smoothVScrolling(scrollPane);
         primaryStage.sceneProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue != null) {
                 Scene scene = primaryStage.getScene();

+ 124 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ListViewDemoController.java

@@ -0,0 +1,124 @@
+package io.github.palexdev.materialfx.demo.controllers;
+
+import io.github.palexdev.materialfx.controls.MFXButton;
+import io.github.palexdev.materialfx.controls.MFXListView;
+import io.github.palexdev.materialfx.effects.DepthLevel;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.geometry.Insets;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.paint.Color;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.net.URL;
+import java.util.List;
+import java.util.Random;
+import java.util.ResourceBundle;
+
+public class ListViewDemoController implements Initializable {
+    private final Random random = new Random(System.currentTimeMillis());
+
+    @FXML
+    private MFXListView<String> stringView;
+
+    @FXML
+    private MFXListView<Label> labelView;
+
+    @FXML
+    private MFXListView<HBox> hBoxView;
+
+    @FXML
+    private MFXListView<String> cssView;
+
+    @FXML
+    private MFXButton depthButton;
+
+    @FXML
+    private MFXButton colorsButton;
+
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        ObservableList<String> stringList = FXCollections.observableArrayList(List.of(
+                "String 0",
+                "String 1",
+                "String 2",
+                "String 3",
+                "String 4",
+                "String 5",
+                "String 6",
+                "String 7"
+        ));
+        stringView.setItems(stringList);
+
+        ObservableList<Label> labelsList = FXCollections.observableArrayList(List.of(
+                new Label("Label 0", createIcon("fas-home")),
+                new Label("Label 1", createIcon("fas-star")),
+                new Label("Label 2", createIcon("fas-heart")),
+                new Label("Label 3", createIcon("fas-cocktail")),
+                new Label("Label 4", createIcon("fas-anchor")),
+                new Label("Label 5", createIcon("fas-bolt")),
+                new Label("Label 6", createIcon("fas-bug")),
+                new Label("Label 7", createIcon("fas-beer"))
+        ));
+        labelView.setItems(labelsList);
+
+        ObservableList<HBox> hBoxesList = FXCollections.observableArrayList(List.of(
+                createHBox(0),
+                createHBox(1),
+                createHBox(2),
+                createHBox(3),
+                createHBox(4),
+                createHBox(5),
+                createHBox(6),
+                createHBox(7)
+        ));
+        hBoxView.setItems(hBoxesList);
+
+        cssView.setItems(stringList);
+        depthButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            DepthLevel level = cssView.getDepthLevel();
+            if (level.equals(DepthLevel.LEVEL0)) {
+                cssView.setDepthLevel(DepthLevel.LEVEL2);
+            } else {
+                cssView.setDepthLevel(DepthLevel.LEVEL0);
+            }
+        });
+        colorsButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            cssView.setTrackColor(ColorUtils.getRandomColor());
+            cssView.setThumbColor(ColorUtils.getRandomColor());
+            cssView.setThumbHoverColor(ColorUtils.getRandomColor());
+        });
+    }
+
+    private FontIcon createIcon(String s) {
+        FontIcon icon = new FontIcon(s);
+        icon.setIconColor(Color.PURPLE);
+        icon.setIconSize(13);
+        return icon;
+    }
+
+    private HBox createHBox(int index) {
+        HBox hBox = new HBox(20);
+        hBox.setPadding(new Insets(0, 10, 0, 10));
+        hBox.setPrefSize(150, 30);
+
+        FontIcon city = new FontIcon("fas-city");
+        city.setIconColor(Color.GOLD);
+        city.setIconSize(12);
+        Label label1 = new Label("City " + index, city);
+
+        FontIcon people = new FontIcon("fas-users");
+        people.setIconColor(Color.GOLD);
+        people.setIconSize(12);
+        Label label2 = new Label("Count: " + random.nextInt(2000000), people);
+
+        hBox.getChildren().addAll(label1, label2);
+        return hBox;
+    }
+
+}

+ 11 - 2
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ScrollPaneDemoController.java

@@ -4,8 +4,12 @@ package io.github.palexdev.materialfx.demo.controllers;
 import io.github.palexdev.materialfx.controls.MFXScrollPane;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
 
-public class ScrollPaneDemoController {
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class ScrollPaneDemoController implements Initializable {
 
     @FXML
     private MFXScrollPane scrollPaneV;
@@ -13,6 +17,12 @@ public class ScrollPaneDemoController {
     @FXML
     private MFXScrollPane scrollPaneVH;
 
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        MFXScrollPane.smoothVScrolling(scrollPaneV);
+        MFXScrollPane.smoothVScrolling(scrollPaneVH);
+    }
+
     @FXML
     void setRandomTrackColor() {
         scrollPaneV.setTrackColor(ColorUtils.getRandomColor());
@@ -30,5 +40,4 @@ public class ScrollPaneDemoController {
         scrollPaneV.setThumbHoverColor(ColorUtils.getRandomColor());
         scrollPaneVH.setThumbHoverColor(ColorUtils.getRandomColor());
     }
-
 }

+ 19 - 25
demo/src/main/resources/io/github/palexdev/materialfx/demo/buttons_demo.fxml

@@ -1,70 +1,64 @@
 <?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.*?>
 <?import javafx.scene.text.Font?>
 <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
-           prefWidth="600.0" stylesheets="@buttons_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
+           prefWidth="600.0" stylesheets="@css/buttons_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
     <MFXButton rippleRadius="40.0" styleClass="flat-button" StackPane.alignment="TOP_CENTER">
         <font>
-            <Font name="Comic Sans MS" size="14.0"/>
+            <Font name="Comic Sans MS" size="14.0" />
         </font>
         <StackPane.margin>
-            <Insets right="180.0" top="65.0"/>
+            <Insets right="180.0" top="65.0" />
         </StackPane.margin>
     </MFXButton>
-    <MFXButton rippleColor="#d5d5d5" rippleRadius="40.0" styleClass="raised-button" text="Button"
-               StackPane.alignment="TOP_CENTER">
+    <MFXButton rippleColor="#d5d5d5" rippleRadius="40.0" styleClass="raised-button" text="Button" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets right="180.0" top="165.0"/>
+            <Insets right="180.0" top="165.0" />
         </StackPane.margin>
     </MFXButton>
     <MFXButton rippleRadius="40.0" styleClass="flat-button" textFill="#d400ff" StackPane.alignment="TOP_CENTER">
         <font>
-            <Font name="Comic Sans MS" size="14.0"/>
+            <Font name="Comic Sans MS" size="14.0" />
         </font>
         <StackPane.margin>
-            <Insets top="65.0"/>
+            <Insets top="65.0" />
         </StackPane.margin>
     </MFXButton>
-    <MFXButton id="colored-raised" prefHeight="26.0" prefWidth="55.0" rippleColor="#d5d5d5" rippleRadius="40.0"
-               styleClass="raised-button" StackPane.alignment="TOP_CENTER">
+    <MFXButton id="colored-raised" prefHeight="26.0" prefWidth="55.0" rippleColor="#d5d5d5" rippleRadius="40.0" styleClass="raised-button" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="165.0"/>
+            <Insets top="165.0" />
         </StackPane.margin>
     </MFXButton>
     <MFXButton disable="true" rippleRadius="40.0" styleClass="flat-button" StackPane.alignment="TOP_CENTER">
         <font>
-            <Font name="Comic Sans MS" size="14.0"/>
+            <Font name="Comic Sans MS" size="14.0" />
         </font>
         <StackPane.margin>
-            <Insets left="180.0" top="65.0"/>
+            <Insets left="180.0" top="65.0" />
         </StackPane.margin>
     </MFXButton>
-    <MFXButton disable="true" rippleColor="#d5d5d5" rippleRadius="40.0" styleClass="raised-button"
-               StackPane.alignment="TOP_CENTER">
+    <MFXButton disable="true" rippleColor="#d5d5d5" rippleRadius="40.0" styleClass="raised-button" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets left="180.0" top="165.0"/>
+            <Insets left="180.0" top="165.0" />
         </StackPane.margin>
     </MFXButton>
-    <MFXButton id="custom-ripple" prefHeight="26.0" prefWidth="150.0" rippleRadius="50.0" styleClass="raised-button"
-               text="Custom Ripple" StackPane.alignment="CENTER">
+    <MFXButton id="custom-ripple" prefHeight="26.0" prefWidth="150.0" rippleRadius="50.0" styleClass="raised-button" text="Custom Ripple" StackPane.alignment="CENTER">
         <StackPane.margin>
-            <Insets top="60.0"/>
+            <Insets top="60.0" />
         </StackPane.margin>
     </MFXButton>
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Flat buttons" StackPane.alignment="TOP_CENTER">
+    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Flat buttons" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="20.0"/>
+            <Insets top="20.0" />
         </StackPane.margin>
     </Label>
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Raised buttons"
-           StackPane.alignment="TOP_CENTER">
+    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Raised buttons" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="120.0"/>
+            <Insets top="120.0" />
         </StackPane.margin>
     </Label>
 </StackPane>

+ 4 - 3
demo/src/main/resources/io/github/palexdev/materialfx/demo/checkboxes_demo.fxml

@@ -6,8 +6,9 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.paint.LinearGradient?>
 <?import javafx.scene.paint.Stop?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@checkboxes_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes color" StackPane.alignment="TOP_CENTER">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
+           prefWidth="600.0" stylesheets="@css/checkboxes_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
+    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes color" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
             <Insets top="20.0" />
         </StackPane.margin>
@@ -44,7 +45,7 @@
             <Insets />
         </opaqueInsets>
     </MFXCheckbox>
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes Mark Types" />
+    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes Mark Types" />
     <MFXCheckbox markType="CASPIAN" text="Caspian">
         <StackPane.margin>
             <Insets right="300.0" top="65.0" />

+ 58 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/combo_boxes_demo.fxml

@@ -0,0 +1,58 @@
+<?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/combo_boxes_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.ComboBoxesDemoController">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Combo Boxes" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="20.0" />
+      </StackPane.margin>
+   </Label>
+   <MFXComboBox fx:id="standard" animateLines="false" prefHeight="23.0" prefWidth="90.0" promptText="Standard" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="280.0" top="60.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXComboBox fx:id="lineColors" animateLines="false" lineColor="#58d726" prefHeight="23.0" prefWidth="90.0" promptText="Lines Colors" unfocusedLineColor="#b91212" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="60.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXComboBox disable="true" prefHeight="23.0" prefWidth="90.0" promptText="Disabled" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="280.0" top="60.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Customization" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="125.0" />
+      </StackPane.margin>
+   </Label>
+   <MFXComboBox fx:id="editable" prefHeight="23.0" prefWidth="90.0" promptText="Editable" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="120.0" top="170.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXComboBox fx:id="labels" prefHeight="23.0" prefWidth="90.0" promptText="Labels" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="350.0" top="170.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXComboBox fx:id="validated" lineColor="#ffdc00" prefHeight="23.0" prefWidth="90.0" promptText="Validated" validated="true" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="120.0" top="170.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXComboBox id="custom" fx:id="customized" prefHeight="23.0" prefWidth="90.0" promptText="CSS" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="350.0" top="170.0" />
+      </StackPane.margin>
+   </MFXComboBox>
+   <MFXCheckbox fx:id="checkbox" text="Validation!" StackPane.alignment="BOTTOM_LEFT">
+      <StackPane.margin>
+         <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+      </StackPane.margin>
+   </MFXCheckbox>
+</StackPane>

+ 0 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/buttons_demo.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/buttons_demo.css


+ 0 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/checkboxes_demo.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/checkboxes_demo.css


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

@@ -0,0 +1,7 @@
+#custom {
+    -mfx-line-color: #7F0FFF;
+    -mfx-unfocused-line-color: #E2CAFF;
+    -mfx-line-stroke-width: 2px;
+    -mfx-animate-lines: true;
+    -mfx-validate: true;
+}

+ 30 - 30
demo/src/main/resources/io/github/palexdev/materialfx/demo/common.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/common.css

@@ -3,7 +3,7 @@
     font-weight: bold;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Comfortaa/Comfortaa-Bold.ttf') format('truetype');
+    src: url('../fonts/Comfortaa/Comfortaa-Bold.ttf') format('truetype');
 }
 
 @font-face {
@@ -11,7 +11,7 @@
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Comfortaa/Comfortaa-Light.ttf') format('truetype');
+    src: url('../fonts/Comfortaa/Comfortaa-Light.ttf') format('truetype');
 }
 
 @font-face {
@@ -19,7 +19,7 @@
     font-weight: 500px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Comfortaa/Comfortaa-Medium.ttf') format('truetype');
+    src: url('../fonts/Comfortaa/Comfortaa-Medium.ttf') format('truetype');
 }
 
 @font-face {
@@ -27,7 +27,7 @@
     font-weight: normal;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Comfortaa/Comfortaa-Regular.ttf') format('truetype');
+    src: url('../fonts/Comfortaa/Comfortaa-Regular.ttf') format('truetype');
 }
 
 @font-face {
@@ -35,7 +35,7 @@
     font-weight: 600px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Comfortaa/Comfortaa-SemiBold.ttf') format('truetype');
+    src: url('../fonts/Comfortaa/Comfortaa-SemiBold.ttf') format('truetype');
 }
 
 @font-face {
@@ -43,7 +43,7 @@
     font-weight: bold;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-Bold.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-Bold.ttf') format('truetype');
 }
 
 @font-face {
@@ -51,7 +51,7 @@
     font-weight: bold;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-BoldItalic.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-BoldItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -59,7 +59,7 @@
     font-weight: 800px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-ExtraBold.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-ExtraBold.ttf') format('truetype');
 }
 
 @font-face {
@@ -67,7 +67,7 @@
     font-weight: 800px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-ExtraBoldItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -75,7 +75,7 @@
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-Light.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-Light.ttf') format('truetype');
 }
 
 @font-face {
@@ -83,7 +83,7 @@
     font-weight: 300px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-LightItalic.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-LightItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -91,7 +91,7 @@
     font-weight: normal;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-Italic.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-Italic.ttf') format('truetype');
 }
 
 @font-face {
@@ -99,7 +99,7 @@
     font-weight: 600px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-SemiBold.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-SemiBold.ttf') format('truetype');
 }
 
 @font-face {
@@ -107,7 +107,7 @@
     font-weight: normal;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-Regular.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-Regular.ttf') format('truetype');
 }
 
 @font-face {
@@ -115,7 +115,7 @@
     font-weight: 600px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/OpenSans/OpenSans-SemiBoldItalic.ttf') format('truetype');
+    src: url('../fonts/OpenSans/OpenSans-SemiBoldItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -123,7 +123,7 @@
     font-weight: 900px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Black.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype');
 }
 
 @font-face {
@@ -131,7 +131,7 @@
     font-weight: 900px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-BlackItalic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-BlackItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -139,7 +139,7 @@
     font-weight: bold;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Bold.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype');
 }
 
 @font-face {
@@ -147,7 +147,7 @@
     font-weight: bold;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-BoldItalic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-BoldItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -155,7 +155,7 @@
     font-weight: 300px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Light.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype');
 }
 
 @font-face {
@@ -163,7 +163,7 @@
     font-weight: normal;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Italic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Italic.ttf') format('truetype');
 }
 
 @font-face {
@@ -171,7 +171,7 @@
     font-weight: 300px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-LightItalic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-LightItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -179,7 +179,7 @@
     font-weight: 500px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Medium.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype');
 }
 
 @font-face {
@@ -187,7 +187,7 @@
     font-weight: 500px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-MediumItalic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-MediumItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -195,7 +195,7 @@
     font-weight: normal;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Regular.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype');
 }
 
 @font-face {
@@ -203,7 +203,7 @@
     font-weight: 100px;
     font-style: italic;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-ThinItalic.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-ThinItalic.ttf') format('truetype');
 }
 
 @font-face {
@@ -211,18 +211,18 @@
     font-weight: 100px;
     font-style: normal;
     font-display: swap;
-    src: url('fonts/Roboto/Roboto-Thin.ttf') format('truetype');
+    src: url('../fonts/Roboto/Roboto-Thin.ttf') format('truetype');
 }
 
-.label {
-    -fx-background-color: linear-gradient(to bottom right, #C01ADD 0%, #6A6AF8 100%);;
+#customLabel {
+    -fx-background-color: linear-gradient(to bottom right, #C01ADD 0%, #6A6AF8 100%);
     -fx-background-radius: 7;
     -fx-border-color: #7F0FFF;
     -fx-border-radius: 5;
     -fx-border-width: 2;
 }
 
-.label .text {
+#customLabel .text {
     -fx-font-family: "Open Sans Bold";
     -fx-font-size: 12.5;
     -fx-fill: white;

+ 6 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/demo.css

@@ -114,4 +114,10 @@
     -mfx-ripple-radius: 40px;
     -mfx-ripple-color: #e2caff;
     -mfx-animate-background: true;
+}
+
+#loaderScroll {
+    -mfx-track-color: #e6d7ff;
+    -mfx-thumb-color: #b885ff;
+    -mfx-thumb-hover-color: #9e47ff;
 }

+ 0 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/info_dialog.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/info_dialog.css


+ 30 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/listviews_demo.css

@@ -0,0 +1,30 @@
+#customView {
+    -mfx-hide-scrollbars: true;
+
+    -mfx-track-color: transparent;
+    -mfx-thumb-color: derive(salmon, 30%);
+    -mfx-thumb-hover-color: red;
+}
+
+#customView .mfx-list-cell {
+    -mfx-selected-color: salmon;
+    -mfx-hover-color: derive(salmon, 70%);
+}
+
+#customView .mfx-list-cell .ripple-generator {
+    -mfx-ripple-color: derive(red, 10%);
+}
+
+#label {
+    -fx-background-color: linear-gradient(to bottom right, #C01ADD 0%, #6A6AF8 100%);
+    -fx-background-radius: 7;
+    -fx-border-color: #7F0FFF;
+    -fx-border-radius: 5;
+    -fx-border-width: 2;
+}
+
+#label .text {
+    -fx-font-family: "Open Sans Bold";
+    -fx-font-size: 10;
+    -fx-fill: white;
+}

+ 6 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/radio_buttons_demo.css

@@ -0,0 +1,6 @@
+#cssButton {
+    -mfx-selected-color: green;
+    -mfx-unselected-color: darkred;
+    -mfx-selected-text-color: blue;
+    -mfx-unselected-text-color: darkred;
+}

+ 0 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpane_demo.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/scrollpanes_demo.css


+ 2 - 2
demo/src/main/resources/io/github/palexdev/materialfx/demo/toggle_buttons_demo.css → demo/src/main/resources/io/github/palexdev/materialfx/demo/css/toggle_buttons_demo.css

@@ -1,5 +1,5 @@
 #customRippleRadius .container .ripple-generator {
-    -mfx-ripple-radius: 15;
+    -mfx-ripple-radius: 15px;
 }
 
 .mfx-toggle-node {
@@ -7,6 +7,6 @@
 }
 
 .mfx-toggle-node .ripple-generator {
-    -mfx-ripple-radius: 15;
+    -mfx-ripple-radius: 15px;
     -mfx-ripple-color: #b3b3b3;
 }

+ 7 - 13
demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.fxml

@@ -1,32 +1,26 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-
 <?import io.github.palexdev.materialfx.controls.MFXScrollPane?>
 <?import io.github.palexdev.materialfx.controls.MFXVLoader?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.layout.*?>
-<StackPane id="demoPane" fx:id="demoPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
-           minWidth="-Infinity" prefHeight="432.0" prefWidth="768.0" stylesheets="@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.DemoController">
+<StackPane id="demoPane" fx:id="demoPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="432.0" prefWidth="768.0" stylesheets="@css/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.DemoController">
    <StackPane id="contentPane" fx:id="contentPane" prefHeight="207.0" prefWidth="441.0">
       <StackPane.margin>
-         <Insets bottom="15.0" left="20.0" right="20.0" top="15.0"/>
+         <Insets bottom="15.0" left="20.0" right="20.0" top="15.0" />
       </StackPane.margin>
    </StackPane>
-   <StackPane id="navBar" fx:id="navBar" maxWidth="-Infinity"
-              style="-fx-background-radius: 10; -fx-background-color: white;" translateX="-200.0"
-              StackPane.alignment="CENTER_LEFT">
+   <StackPane id="navBar" fx:id="navBar" maxWidth="-Infinity" style="-fx-background-radius: 10; -fx-background-color: white;" translateX="-200.0" StackPane.alignment="CENTER_LEFT">
       <StackPane.margin>
-         <Insets bottom="15.0" left="15.0" top="15.0"/>
+         <Insets bottom="15.0" left="15.0" top="15.0" />
       </StackPane.margin>
-      <MFXScrollPane fitToWidth="true" hbarPolicy="NEVER" prefWidth="150.0">
+      <MFXScrollPane id="loaderScroll" fx:id="scrollPane" fitToWidth="true" hbarPolicy="NEVER" prefWidth="150.0">
          <StackPane.margin>
-            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
+            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
          </StackPane.margin>
          <MFXVLoader fx:id="vLoader" alignment="TOP_CENTER" prefHeight="360.0" prefWidth="150.0" spacing="20.0">
             <padding>
-               <Insets bottom="10.0" left="5.0" right="5.0" top="10.0"/>
+               <Insets bottom="10.0" left="5.0" right="5.0" top="10.0" />
             </padding>
          </MFXVLoader>
       </MFXScrollPane>

+ 3 - 3
demo/src/main/resources/io/github/palexdev/materialfx/demo/dialogs_demo.fxml

@@ -5,7 +5,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
 <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.DialogsController">
-   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Pane Dialogs/Alerts" StackPane.alignment="TOP_CENTER">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Pane Dialogs/Alerts" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets top="16.0" />
       </StackPane.margin>
@@ -50,7 +50,7 @@
          <Insets left="300.0" top="100.0" />
       </StackPane.margin>
    </MFXButton>
-   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Stage Dialogs/Alerts">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Stage Dialogs/Alerts">
       <StackPane.margin>
          <Insets bottom="50.0" />
       </StackPane.margin>
@@ -75,7 +75,7 @@
          <Insets left="300.0" top="25.0" />
       </StackPane.margin>
    </MFXButton>
-   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Options" textAlignment="CENTER">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Options" textAlignment="CENTER">
       <StackPane.margin>
          <Insets top="120.0" />
       </StackPane.margin>

+ 1 - 1
demo/src/main/resources/io/github/palexdev/materialfx/demo/info_dialog.fxml

@@ -5,7 +5,7 @@
 <?import javafx.scene.control.*?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.TextFlow?>
-<MFXDialog maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@info_dialog.css" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.InfoController">
+<MFXDialog maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/info_dialog.css" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.InfoController">
    <top>
       <StackPane id="headerNode" prefHeight="80.0" prefWidth="600.0" BorderPane.alignment="CENTER">
          <Label id="materialfxLabel" alignment="CENTER" maxHeight="1.7976931348623157E308"

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

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import io.github.palexdev.materialfx.controls.*?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
+           prefWidth="730.0" stylesheets="@css/listviews_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.ListViewDemoController">
+   <padding>
+      <Insets left="20.0" right="20.0"/>
+   </padding>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="ListViews"
+          StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="20.0"/>
+      </StackPane.margin>
+   </Label>
+   <HBox alignment="TOP_CENTER" maxHeight="-Infinity" prefHeight="250.0" prefWidth="680.0" spacing="20.0">
+      <StackPane.margin>
+         <Insets top="-20.0"/>
+      </StackPane.margin>
+      <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="130.0"
+            spacing="10.0">
+         <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Standard"/>
+         <MFXListView fx:id="stringView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
+                      prefWidth="120.0"/>
+      </VBox>
+      <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="160.0"
+            spacing="10.0">
+         <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Labels"/>
+         <MFXListView fx:id="labelView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
+                      prefWidth="150.0"/>
+      </VBox>
+      <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="265.0"
+            spacing="10.0">
+         <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="HBoxes"/>
+         <MFXListView fx:id="hBoxView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
+                      prefWidth="265.0"/>
+      </VBox>
+      <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="135.0"
+            spacing="10.0">
+         <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="125.0" text="Customized and CSS"/>
+         <MFXListView id="customView" fx:id="cssView" maxHeight="-Infinity" maxWidth="-Infinity"
+                      prefHeight="200.0" prefWidth="110.0" stylesheets="@css/listviews_demo.css"/>
+      </VBox>
+   </HBox>
+   <VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="90.0" prefWidth="128.0"
+         spacing="10.0" StackPane.alignment="BOTTOM_RIGHT">
+      <StackPane.margin>
+         <Insets bottom="5.0"/>
+      </StackPane.margin>
+      <MFXButton fx:id="depthButton" prefWidth="70.0" text="3D"/>
+      <MFXButton fx:id="colorsButton" text="Change bars color"/>
+   </VBox>
+</StackPane>

+ 1 - 1
demo/src/main/resources/io/github/palexdev/materialfx/demo/notifications_demo.fxml

@@ -5,7 +5,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
 <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.NotificationsController">
-   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Notifications Positions" StackPane.alignment="TOP_CENTER">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Notifications Positions" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets top="16.0" />
       </StackPane.margin>

+ 60 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/radio_buttons_demo.fxml

@@ -0,0 +1,60 @@
+<?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/radio_buttons_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Radio Buttons"
+          StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="16.0"/>
+      </StackPane.margin>
+   </Label>
+   <MFXRadioButton text="Standard" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="100.0" top="60.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <MFXRadioButton disable="true" text="Disabled" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="100.0" top="60.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Customization"
+          StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="100.0"/>
+      </StackPane.margin>
+   </Label>
+   <MFXRadioButton selectedColor="#6bc4ff" selectedTextColor="BLACK" text="Colors 1" unSelectedColor="#ff8f8f"
+                   StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="350.0" top="145.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <MFXRadioButton selectedColor="#6bc4ff" selectedTextColor="#6bc4ff" text="Colors 2" unSelectedColor="#ff8f8f"
+                   unSelectedTextColor="#ff8f8f" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="180.0" top="145.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <MFXRadioButton selectedColor="#6bc4ff" selectedTextColor="#6bc4ff" text="Colors 3" unSelectedColor="#ffffff00"
+                   unSelectedTextColor="#ff8f8f" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="145.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <MFXRadioButton selectedColor="#33d348" selectedTextColor="BLACK" text="Colors 4" unSelectedColor="#c67aff"
+                   unSelectedTextColor="#f2f2f2" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="180.0" top="145.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+   <MFXRadioButton id="cssButton" text="CSS" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="350.0" top="145.0"/>
+      </StackPane.margin>
+   </MFXRadioButton>
+</StackPane>

+ 0 - 49
demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpane_demo.fxml

@@ -1,49 +0,0 @@
-<?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="@scrollpane_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.ScrollPaneDemoController">
-   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" stylesheets="@common.css" text="ScrollPane Preview"
-          StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets top="16.0"/>
-      </StackPane.margin>
-   </Label>
-   <MFXScrollPane fx:id="scrollPaneV" fitToWidth="true" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="290.0"
-                  prefWidth="290.0" styleClass="mfx-scroll-pane" StackPane.alignment="CENTER_LEFT">
-      <StackPane.margin>
-         <Insets left="5.0" top="5.0"/>
-      </StackPane.margin>
-      <padding>
-         <Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
-      </padding>
-      <Label text="Sometimes there isn't a good answer. No matter how you try to rationalize the outcome, it doesn't make sense. And instead of an answer, you are simply left with a question. Why?He took a sip of the drink. He wasn't sure whether he liked it or not, but at this moment it didn't matter. She had made it especially for him so he would have forced it down even if he had absolutely hated it. That's simply the way things worked. She made him a new-fangled drink each day and he took a sip of it and smiled, saying it was excellent.According to the caption on the bronze marker placed by the Multnomah Chapter of the Daughters of the American Revolution on May 12, 1939, “College Hall (is) the oldest building in continuous use for Educational purposes west of the Rocky Mountains. Here were educated men and women who have won recognition throughout the world in all the learned professions.”He heard the loud impact before he ever saw the result. It had been so loud that it had actually made him jump back in his seat. As soon as he recovered from the surprise, he saw the crack in the windshield. It seemed to be an analogy of the current condition of his life.There was something special about this little creature. Donna couldn't quite pinpoint what it was, but she knew with all her heart that it was true. It wasn't a matter of if she was going to try and save it, but a matter of how she was going to save it. She went back to the car to get a blanket and when she returned the creature was gone."
-             wrapText="true"/>
-   </MFXScrollPane>
-   <MFXScrollPane fx:id="scrollPaneVH" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="290.0" prefWidth="290.0"
-                  styleClass="mfx-scroll-pane" StackPane.alignment="CENTER_RIGHT">
-      <padding>
-         <Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
-      </padding>
-      <StackPane.margin>
-         <Insets right="5.0" top="5.0"/>
-      </StackPane.margin>
-      <Label prefWidth="400.0"
-             text="Sometimes there isn't a good answer. No matter how you try to rationalize the outcome, it doesn't make sense. And instead of an answer, you are simply left with a question. Why?He took a sip of the drink. He wasn't sure whether he liked it or not, but at this moment it didn't matter. She had made it especially for him so he would have forced it down even if he had absolutely hated it. That's simply the way things worked. She made him a new-fangled drink each day and he took a sip of it and smiled, saying it was excellent.According to the caption on the bronze marker placed by the Multnomah Chapter of the Daughters of the American Revolution on May 12, 1939, “College Hall (is) the oldest building in continuous use for Educational purposes west of the Rocky Mountains. Here were educated men and women who have won recognition throughout the world in all the learned professions.”He heard the loud impact before he ever saw the result. It had been so loud that it had actually made him jump back in his seat. As soon as he recovered from the surprise, he saw the crack in the windshield. It seemed to be an analogy of the current condition of his life.There was something special about this little creature. Donna couldn't quite pinpoint what it was, but she knew with all her heart that it was true. It wasn't a matter of if she was going to try and save it, but a matter of how she was going to save it. She went back to the car to get a blanket and when she returned the creature was gone."
-             wrapText="true"/>
-   </MFXScrollPane>
-   <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" prefHeight="34.0" prefWidth="200.0"
-         spacing="20.0" StackPane.alignment="BOTTOM_CENTER">
-      <StackPane.margin>
-         <Insets bottom="10.0"/>
-      </StackPane.margin>
-      <MFXButton buttonType="RAISED" onAction="#setRandomTrackColor" text="Random Track Color"/>
-      <MFXButton buttonType="RAISED" onAction="#setRandomThumbColor" text="Random Thumb Color"/>
-      <MFXButton buttonType="RAISED" onAction="#setRandomThumbHoverColor" text="Random Thumb Hover Color"/>
-   </HBox>
-</StackPane>

+ 39 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpanes_demo.fxml

@@ -0,0 +1,39 @@
+<?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/scrollpanes_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.ScrollPaneDemoController">
+   <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" stylesheets="@css/common.css" text="ScrollPane Preview" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="16.0" />
+      </StackPane.margin>
+   </Label>
+   <MFXScrollPane fx:id="scrollPaneV" fitToWidth="true" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="290.0" prefWidth="290.0" styleClass="mfx-scroll-pane" StackPane.alignment="CENTER_LEFT">
+      <StackPane.margin>
+         <Insets left="5.0" top="5.0" />
+      </StackPane.margin>
+      <padding>
+         <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+      </padding>
+      <Label text="Sometimes there isn't a good answer. No matter how you try to rationalize the outcome, it doesn't make sense. And instead of an answer, you are simply left with a question. Why?He took a sip of the drink. He wasn't sure whether he liked it or not, but at this moment it didn't matter. She had made it especially for him so he would have forced it down even if he had absolutely hated it. That's simply the way things worked. She made him a new-fangled drink each day and he took a sip of it and smiled, saying it was excellent.According to the caption on the bronze marker placed by the Multnomah Chapter of the Daughters of the American Revolution on May 12, 1939, “College Hall (is) the oldest building in continuous use for Educational purposes west of the Rocky Mountains. Here were educated men and women who have won recognition throughout the world in all the learned professions.”He heard the loud impact before he ever saw the result. It had been so loud that it had actually made him jump back in his seat. As soon as he recovered from the surprise, he saw the crack in the windshield. It seemed to be an analogy of the current condition of his life.There was something special about this little creature. Donna couldn't quite pinpoint what it was, but she knew with all her heart that it was true. It wasn't a matter of if she was going to try and save it, but a matter of how she was going to save it. She went back to the car to get a blanket and when she returned the creature was gone." wrapText="true" />
+   </MFXScrollPane>
+   <MFXScrollPane fx:id="scrollPaneVH" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="290.0" prefWidth="290.0" styleClass="mfx-scroll-pane" StackPane.alignment="CENTER_RIGHT">
+      <padding>
+         <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+      </padding>
+      <StackPane.margin>
+         <Insets right="5.0" top="5.0" />
+      </StackPane.margin>
+      <Label prefWidth="400.0" text="Sometimes there isn't a good answer. No matter how you try to rationalize the outcome, it doesn't make sense. And instead of an answer, you are simply left with a question. Why?He took a sip of the drink. He wasn't sure whether he liked it or not, but at this moment it didn't matter. She had made it especially for him so he would have forced it down even if he had absolutely hated it. That's simply the way things worked. She made him a new-fangled drink each day and he took a sip of it and smiled, saying it was excellent.According to the caption on the bronze marker placed by the Multnomah Chapter of the Daughters of the American Revolution on May 12, 1939, “College Hall (is) the oldest building in continuous use for Educational purposes west of the Rocky Mountains. Here were educated men and women who have won recognition throughout the world in all the learned professions.”He heard the loud impact before he ever saw the result. It had been so loud that it had actually made him jump back in his seat. As soon as he recovered from the surprise, he saw the crack in the windshield. It seemed to be an analogy of the current condition of his life.There was something special about this little creature. Donna couldn't quite pinpoint what it was, but she knew with all her heart that it was true. It wasn't a matter of if she was going to try and save it, but a matter of how she was going to save it. She went back to the car to get a blanket and when she returned the creature was gone." wrapText="true" />
+   </MFXScrollPane>
+   <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" prefHeight="34.0" prefWidth="200.0" spacing="20.0" StackPane.alignment="BOTTOM_CENTER">
+      <StackPane.margin>
+         <Insets bottom="10.0" />
+      </StackPane.margin>
+      <MFXButton buttonType="RAISED" onAction="#setRandomTrackColor" text="Random Track Color" />
+      <MFXButton buttonType="RAISED" onAction="#setRandomThumbColor" text="Random Thumb Color" />
+      <MFXButton buttonType="RAISED" onAction="#setRandomThumbHoverColor" text="Random Thumb Hover Color" />
+   </HBox>
+</StackPane>

+ 3 - 3
demo/src/main/resources/io/github/palexdev/materialfx/demo/toggle_buttons_demo.fxml

@@ -5,7 +5,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
 <?import org.kordamp.ikonli.javafx.FontIcon?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@toggle_buttons_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.TogglesController">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/toggle_buttons_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.TogglesController">
    <MFXToggleButton StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets right="320.0" top="60.0" />
@@ -21,7 +21,7 @@
          <Insets left="320.0" top="60.0" />
       </StackPane.margin>
    </MFXToggleButton>
-   <Label alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Buttons" StackPane.alignment="TOP_CENTER">
+   <Label id="customLabel" alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Buttons" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets top="20.0" />
       </StackPane.margin>
@@ -46,7 +46,7 @@
          <Insets left="320.0" top="140.0" />
       </StackPane.margin>
    </MFXToggleButton>
-   <Label alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Nodes">
+   <Label id="customLabel" alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Nodes">
       <StackPane.margin>
          <Insets top="20.0" />
       </StackPane.margin>

+ 1 - 1
materialfx/gradle.properties

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

+ 32 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXSnapshotWrapper.java

@@ -0,0 +1,32 @@
+package io.github.palexdev.materialfx.beans;
+
+import io.github.palexdev.materialfx.controls.MFXComboBox;
+import javafx.scene.Node;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+
+/**
+ * Class used in {@link MFXComboBox}, workaround for showing the item graphic if is is a node.
+ * <p>
+ * Makes a screenshot of the graphic node with transparent background.
+ * Then {@link #getGraphic()} should be used to get an ImageView node which contains the screenshot.
+ * <p></p>
+ * A little side note: since it is a screenshot the image may appear a little blurry compared to the real
+ * graphic, however it should be acceptable and I believe this is still better than having no graphic at all.
+ */
+public class MFXSnapshotWrapper {
+    private final WritableImage snapshot;
+
+    public MFXSnapshotWrapper(Node node) {
+        SnapshotParameters snapshotParameters = new SnapshotParameters();
+        snapshotParameters.setFill(Color.TRANSPARENT);
+        snapshotParameters.setDepthBuffer(true);
+        snapshot = node.snapshot(snapshotParameters, null);
+    }
+
+    public Node getGraphic() {
+        return new ImageView(snapshot);
+    }
+}

+ 43 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/binding/BooleanListBinding.java

@@ -0,0 +1,43 @@
+package io.github.palexdev.materialfx.beans.binding;
+
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+public class BooleanListBinding extends BooleanBinding {
+    private final ObservableList<BooleanProperty> boundList;
+    private final ListChangeListener<BooleanProperty> changeListener;
+    private BooleanProperty[] observedProperties;
+
+    public BooleanListBinding(ObservableList<BooleanProperty> boundList) {
+        this.boundList = boundList;
+        this.changeListener = c -> refreshBinding();
+        this.boundList.addListener(changeListener);
+        refreshBinding();
+    }
+
+    @Override
+    protected boolean computeValue() {
+        for (BooleanProperty bp: observedProperties) {
+            if (!bp.get()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public void dispose() {
+        boundList.removeListener(changeListener);
+        super.dispose();
+    }
+
+    private void refreshBinding() {
+        super.unbind(observedProperties);
+        observedProperties = boundList.toArray(new BooleanProperty[0]);
+        super.bind(observedProperties);
+        this.invalidate();
+    }
+}

+ 28 - 15
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java

@@ -162,7 +162,12 @@ public class MFXButton extends Button {
      * <p>
      * The {@code DropShadow} effect is used to make the control appear {@code RAISED}.
      */
-    private final ObjectProperty<DepthLevel> depthLevel = new SimpleObjectProperty<>(DepthLevel.LEVEL2);
+    private final StyleableObjectProperty<DepthLevel> depthLevel = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.DEPTH_LEVEL,
+            this,
+            "depthLevel",
+            DepthLevel.LEVEL2
+    );
 
     /**
      * Specifies the appearance of this control. According to material design there are two types of buttons:
@@ -178,28 +183,28 @@ public class MFXButton extends Button {
             ButtonType.FLAT
     );
 
-    public ButtonType getButtonType() {
-        return buttonType.get();
+    public DepthLevel getDepthLevel() {
+        return depthLevel.get();
     }
 
-    public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
-        return buttonType;
+    public StyleableObjectProperty<DepthLevel> depthLevelProperty() {
+        return depthLevel;
     }
 
-    public void setButtonType(ButtonType buttonType) {
-        this.buttonType.set(buttonType);
+    public void setDepthLevel(DepthLevel depthLevel) {
+        this.depthLevel.set(depthLevel);
     }
 
-    public DepthLevel getDepthLevel() {
-        return depthLevel.get();
+    public ButtonType getButtonType() {
+        return buttonType.get();
     }
 
-    public ObjectProperty<DepthLevel> depthLevelProperty() {
-        return depthLevel;
+    public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
+        return buttonType;
     }
 
-    public void setDepthLevel(DepthLevel depthLevel) {
-        this.depthLevel.set(depthLevel);
+    public void setButtonType(ButtonType buttonType) {
+        this.buttonType.set(buttonType);
     }
 
     //================================================================================
@@ -208,6 +213,14 @@ public class MFXButton extends Button {
     private static class StyleableProperties {
         private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
 
+        private static final CssMetaData<MFXButton, DepthLevel> DEPTH_LEVEL =
+                FACTORY.createEnumCssMetaData(
+                        DepthLevel.class,
+                        "-mfx-depth-level",
+                        MFXButton::depthLevelProperty,
+                        DepthLevel.LEVEL2
+                );
+
         private static final CssMetaData<MFXButton, ButtonType> BUTTON_TYPE =
                 FACTORY.createEnumCssMetaData(
                         ButtonType.class,
@@ -216,7 +229,7 @@ public class MFXButton extends Button {
                         ButtonType.FLAT);
 
         static {
-            cssMetaDataList = List.of(BUTTON_TYPE);
+            cssMetaDataList = List.of(DEPTH_LEVEL, BUTTON_TYPE);
         }
 
     }
@@ -230,7 +243,7 @@ public class MFXButton extends Button {
     //================================================================================
     @Override
     protected Skin<?> createDefaultSkin() {
-        MFXButtonSkin skin = new MFXButtonSkin(this, depthLevel.get());
+        MFXButtonSkin skin = new MFXButtonSkin(this);
         this.getChildren().add(0, rippleGenerator);
         this.setOnMousePressed(event -> {
             rippleGenerator.setGeneratorCenterX(event.getX());

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

@@ -0,0 +1,323 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
+import io.github.palexdev.materialfx.skins.MFXComboBoxSkin;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.ObservableList;
+import javafx.css.*;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Labeled;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.Skin;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a combo box following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code ComboBox}, redefines the style class to "mfx-combo-box" for usage in CSS and
+ * includes a {@link MFXDialogValidator}.
+ * <p></p>
+ * A few notes on features and usage:
+ * <p>
+ * If you check {@link ComboBox} documentation you will see a big warning about using nodes as content
+ * because the scenegraph only allows for Nodes to be in one place at a time.
+ * I found a workaround to this issue using {@link #snapshot(SnapshotParameters, WritableImage)}.
+ * Basically I make a "screenshot" of the graphic and then I use an {@code ImageView} to show it.
+ * <p>
+ * So let's say you have a combo box of labels with icons as graphic, when you select an item, it won't disappear anymore
+ * from the list because what you are seeing it's not the real graphic but a screenshot of it.
+ * <p>
+ * I recommend to use only nodes which are instances of {@code Labeled} since the {@code toString()} method is overridden
+ * to return the control's text.
+ * @see MFXSnapshotWrapper
+ */
+public class MFXComboBox<T> extends ComboBox<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXComboBox<?>> FACTORY = new StyleablePropertyFactory<>(ComboBox.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-combo-box";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-combobox.css").toString();
+
+    private MFXDialogValidator validator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXComboBox() {
+        initialize();
+    }
+
+    public MFXComboBox(ObservableList<T> observableList) {
+        super(observableList);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setCellFactory(listCell -> new MFXListCell<>() {
+            @Override
+            protected void updateItem(T item, boolean empty) {
+                super.updateItem(item, empty);
+
+                getChildren().remove(lookup(".ripple-generator"));
+            }
+        });
+
+        setButtonCell(new ListCell<>() {
+            {
+                valueProperty().addListener(observable -> {
+                    if (getValue() == null) {
+                        updateItem(null, true);
+                    }
+                });
+            }
+
+            @Override
+            protected void updateItem(T item, boolean empty) {
+                updateComboItem(this, item, empty);
+            }
+        });
+
+        setupValidator();
+    }
+
+    /**
+     * Defines the behavior of the button cell.
+     * <p>
+     * If it's empty or the item is null, shows the prompt text.
+     * <p>
+     * If the item is instanceof {@code Labeled} makes a "screenshot" of the graphic if not null,
+     * and gets item's text. Otherwise calls {@code toString()} on the item.
+     */
+    private void updateComboItem(ListCell<T> cell, T item, boolean empty) {
+
+        if (empty || item == null) {
+            cell.setGraphic(null);
+            cell.setText(getPromptText());
+            return;
+        }
+
+        if (item instanceof Labeled) {
+            Labeled nodeItem = (Labeled) item;
+            if (nodeItem.getGraphic() != null) {
+                cell.setGraphic(new MFXSnapshotWrapper(nodeItem.getGraphic()).getGraphic());
+            }
+            cell.setText(nodeItem.getText());
+        } else {
+            cell.setText(item.toString());
+        }
+    }
+
+    /**
+     * Configures the validator. If {@link #isValidated()} is true, by default shows a warning
+     * if no item is selected. The warning is showed as soon as the control is out of focus.
+     */
+    private void setupValidator() {
+        BooleanProperty validIndex = new SimpleBooleanProperty(false);
+        validIndex.bind(getSelectionModel().selectedIndexProperty().isNotEqualTo(-1));
+        validator = new MFXDialogValidator("Warning");
+        validator.add(validIndex, "Selected index is not valid");
+    }
+
+    /**
+     * Returns the validator instance of this control.
+     */
+    public MFXDialogValidator getValidator() {
+        return validator;
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+
+    /**
+     * Specifies the line's color when the control is focused.
+     */
+    private final StyleableObjectProperty<Paint> lineColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.LINE_COLOR,
+            this,
+            "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 isValidated = new SimpleStyleableBooleanProperty(
+            StyleableProperties.IS_VALIDATED,
+            this,
+            "isValidated",
+            false
+    );
+
+    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 isValidated.get();
+    }
+
+    public StyleableBooleanProperty isValidatedProperty() {
+        return isValidated;
+    }
+
+    public void setValidated(boolean isValidated) {
+        this.isValidated.set(isValidated);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXComboBox<?>, Paint> LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-line-color",
+                        MFXComboBox::lineColorProperty,
+                        Color.rgb(50, 150, 205)
+                );
+
+        private static final CssMetaData<MFXComboBox<?>, Paint> UNFOCUSED_LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unfocused-line-color",
+                        MFXComboBox::unfocusedLineColorProperty,
+                        Color.rgb(77, 77, 77)
+                );
+
+        private final static CssMetaData<MFXComboBox<?>, Number> LINE_STROKE_WIDTH =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-line-stroke-width",
+                        MFXComboBox::lineStrokeWidthProperty,
+                        1.5
+                );
+
+        private static final CssMetaData<MFXComboBox<?>, Boolean> ANIMATE_LINES =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-animate-lines",
+                        MFXComboBox::animateLinesProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXComboBox<?>, Boolean> IS_VALIDATED =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-validate",
+                        MFXComboBox::isValidatedProperty,
+                        false
+                );
+
+        static {
+            cssMetaDataList = List.of(LINE_COLOR, UNFOCUSED_LINE_COLOR, LINE_STROKE_WIDTH, IS_VALIDATED);
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXComboBoxSkin<>(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return this.getControlCssMetaDataList();
+    }
+}

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

@@ -0,0 +1,224 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.css.*;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ListCell;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.util.Duration;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a ListCell restyled to comply with modern standards.
+ * <p>
+ * Extends {@code ListCell}, redefines the style class to "mfx-list-cell" for usage in CSS,
+ * each cell has a {@code RippleGenerator} to generate ripple effects on click.
+ */
+public class MFXListCell<T> extends ListCell<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXListCell<?>> FACTORY = new StyleablePropertyFactory<>(ListCell.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-list-cell";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-listcell.css").toString();
+    private final RippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXListCell() {
+        rippleGenerator = new RippleGenerator(this);
+        rippleGenerator.setRippleColor(Color.rgb(50, 150, 255));
+        rippleGenerator.setInDuration(Duration.millis(400));
+
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setPadding(new Insets(8, 12, 8, 12));
+        addListeners();
+    }
+
+    /**
+     * Adds a listener to {@code listViewProperty} to bind the ripple radius to the
+     * listView width.
+     * <p>
+     * Adds listeners to {@code selectedProperty} and {@code hoverProperty} to set the background color
+     * according to {@link #selectedColor} and {@link #hoverColor}. When not selected the default color
+     * is white.
+     * <p>
+     * Adds a listener to the {@link #selectedColor} property in case of changes and the cell is selected.
+     */
+    private void addListeners() {
+        listViewProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null) {
+                rippleGenerator.rippleRadiusProperty().bind(newValue.widthProperty().divide(2.0));
+            }
+        });
+
+        selectedProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                NodeUtils.updateBackground(MFXListCell.this, getSelectedColor());
+            } else {
+                NodeUtils.updateBackground(MFXListCell.this, Color.WHITE);
+            }
+        });
+
+        hoverProperty().addListener((observable, oldValue, newValue) -> {
+            if (isSelected()) {
+                return;
+            }
+
+            if (newValue) {
+                if (getIndex() == 0) {
+                    setBackground(new Background(new BackgroundFill(getHoverColor(), CornerRadii.EMPTY, Insets.EMPTY)));
+                } else {
+                    NodeUtils.updateBackground(MFXListCell.this, getHoverColor());
+                }
+            } else {
+                NodeUtils.updateBackground(MFXListCell.this, Color.WHITE);
+            }
+        });
+
+        selectedColor.addListener((observableValue, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue) && isSelected()) {
+                NodeUtils.updateBackground(MFXListCell.this, newValue);
+            }
+        });
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+
+    /**
+     * Specifies the background color of the cell when it is selected.
+     */
+    private final StyleableObjectProperty<Paint> selectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_COLOR,
+            this,
+            "selectedColor",
+            Color.rgb(180, 180, 255)
+    );
+
+    /**
+     * Specifies the background color of the cell when the mouse is hover.
+     */
+    private final StyleableObjectProperty<Paint> hoverColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.HOVER_COLOR,
+            this,
+            "hoverColor",
+            Color.rgb(50, 150, 255, 0.2)
+    );
+
+    public Paint getSelectedColor() {
+        return selectedColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> selectedColorProperty() {
+        return selectedColor;
+    }
+
+    public void setSelectedColor(Paint selectedColor) {
+        this.selectedColor.set(selectedColor);
+    }
+
+    public Paint getHoverColor() {
+        return hoverColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> hoverColorProperty() {
+        return hoverColor;
+    }
+
+    public void setHoverColor(Paint hoverColor) {
+        this.hoverColor.set(hoverColor);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXListCell<?>, Paint> SELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-color",
+                        MFXListCell::selectedColorProperty,
+                        Color.rgb(180, 180, 255)
+                );
+
+        private static final CssMetaData<MFXListCell<?>, Paint> HOVER_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-hover-color",
+                        MFXListCell::hoverColorProperty,
+                        Color.rgb(50, 150, 255, 0.2)
+                );
+
+        static {
+            cssMetaDataList = List.of(SELECTED_COLOR, HOVER_COLOR);
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return this.getControlCssMetaDataList();
+    }
+
+    /**
+     * Overridden method to add the {@code RippleGenerator} and
+     * allow {@code Node}s.
+     */
+    @Override
+    protected void updateItem(T item, boolean empty) {
+        super.updateItem(item, empty);
+
+        if (empty || item == null) {
+            setGraphic(null);
+            setText(null);
+        } else {
+            if (item instanceof Node) {
+                Node nodeItem = (Node) item;
+                setGraphic(nodeItem);
+
+            } else {
+                setText(item.toString());
+            }
+
+            if (!getChildren().contains(rippleGenerator)) {
+                getChildren().add(0, rippleGenerator);
+            }
+
+            setOnMousePressed(mouseEvent -> {
+                rippleGenerator.setGeneratorCenterX(mouseEvent.getX());
+                rippleGenerator.setGeneratorCenterY(mouseEvent.getY());
+                rippleGenerator.createRipple();
+            });
+        }
+    }
+}

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

@@ -0,0 +1,257 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.effects.DepthLevel;
+import io.github.palexdev.materialfx.skins.MFXListViewSkin;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.css.*;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.util.Duration;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a ListView restyled to comply with modern standards.
+ * <p>
+ * Extends {@code ListView}, redefines the style class to "mfx-list-view for usage in CSS,
+ * for cells it uses {@link MFXListCell} by default.
+ */
+public class MFXListView<T> extends ListView<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXListView<?>> FACTORY = new StyleablePropertyFactory<>(ListView.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-list-view";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-listview.css").toString();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXListView() {
+        initialize();
+    }
+
+    public MFXListView(ObservableList<T> observableList) {
+        super(observableList);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setCellFactory(cell -> new MFXListCell<>());
+        addListeners();
+    }
+
+    /**
+     * Adds listeners for colors change to the scrollbars and calls setColors().
+     */
+    private void addListeners() {
+        this.trackColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+
+        this.thumbColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+
+        this.thumbHoverColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+    }
+
+    /**
+     *  Sets the CSS looked-up colors
+     */
+    private void setColors() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("-mfx-track-color: ").append(ColorUtils.rgb((Color) trackColor.get()))
+                .append(";\n-mfx-thumb-color: ").append(ColorUtils.rgb((Color) thumbColor.get()))
+                .append(";\n-mfx-thumb-hover-color: ").append(ColorUtils.rgb((Color) thumbHoverColor.get()))
+                .append(";");
+        setStyle(sb.toString());
+    }
+
+    //================================================================================
+    // ScrollBars Properties
+    //================================================================================
+
+    /**
+     * Specifies the color of the scrollbars' track.
+     */
+    private final ObjectProperty<Paint> trackColor = new SimpleObjectProperty<>(Color.rgb(132, 132, 132));
+
+    /**
+     * Specifies the color of the scrollbars' thumb.
+     */
+    private final ObjectProperty<Paint> thumbColor = new SimpleObjectProperty<>(Color.rgb(137, 137, 137));
+
+    /**
+     * Specifies the color of the scrollbars' thumb when mouse hover.
+     */
+    private final ObjectProperty<Paint> thumbHoverColor = new SimpleObjectProperty<>(Color.rgb(89, 88, 91));
+
+    /**
+     * Specifies the time after which the scrollbars are hidden.
+     */
+    private final ObjectProperty<Duration> hideAfter = new SimpleObjectProperty<>(Duration.seconds(1));
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+
+    /**
+     * Specifies if the scrollbars should be hidden when the mouse is not on the list.
+     */
+    private final StyleableBooleanProperty hideScrollBars = new SimpleStyleableBooleanProperty(
+            StyleableProperties.HIDE_SCROLLBARS,
+            this,
+            "hideScrollBars",
+            false
+    );
+
+    /**
+     * Specifies the shadow strength around the control.
+     */
+    private final StyleableObjectProperty<DepthLevel> depthLevel = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.DEPTH_LEVEL,
+            this,
+            "depthLevel",
+            DepthLevel.LEVEL2
+    );
+
+    public Paint getTrackColor() {
+        return trackColor.get();
+    }
+
+    public ObjectProperty<Paint> trackColorProperty() {
+        return trackColor;
+    }
+
+    public void setTrackColor(Paint trackColor) {
+        this.trackColor.set(trackColor);
+    }
+
+    public Paint getThumbColor() {
+        return thumbColor.get();
+    }
+
+    public ObjectProperty<Paint> thumbColorProperty() {
+        return thumbColor;
+    }
+
+    public void setThumbColor(Paint thumbColor) {
+        this.thumbColor.set(thumbColor);
+    }
+
+    public Paint getThumbHoverColor() {
+        return thumbHoverColor.get();
+    }
+
+    public ObjectProperty<Paint> thumbHoverColorProperty() {
+        return thumbHoverColor;
+    }
+
+    public void setThumbHoverColor(Paint thumbHoverColor) {
+        this.thumbHoverColor.set(thumbHoverColor);
+    }
+
+    public Duration getHideAfter() {
+        return hideAfter.get();
+    }
+
+    public ObjectProperty<Duration> hideAfterProperty() {
+        return hideAfter;
+    }
+
+    public void setHideAfter(Duration hideAfter) {
+        this.hideAfter.set(hideAfter);
+    }
+
+    public boolean isHideScrollBars() {
+        return hideScrollBars.get();
+    }
+
+    public StyleableBooleanProperty hideScrollBarsProperty() {
+        return hideScrollBars;
+    }
+
+    public void setHideScrollBars(boolean hideScrollBars) {
+        this.hideScrollBars.set(hideScrollBars);
+    }
+
+    public DepthLevel getDepthLevel() {
+        return depthLevel.get();
+    }
+
+    public StyleableObjectProperty<DepthLevel> depthLevelProperty() {
+        return depthLevel;
+    }
+
+    public void setDepthLevel(DepthLevel depthLevel) {
+        this.depthLevel.set(depthLevel);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXListView<?>, Boolean> HIDE_SCROLLBARS =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-hide-scrollbars",
+                        MFXListView::hideScrollBarsProperty,
+                        false
+                );
+
+        private static final CssMetaData<MFXListView<?>, DepthLevel> DEPTH_LEVEL =
+                FACTORY.createEnumCssMetaData(
+                        DepthLevel.class,
+                        "-mfx-depth-level",
+                        MFXListView::depthLevelProperty,
+                        DepthLevel.LEVEL2
+                );
+
+        static {
+            cssMetaDataList = List.of(HIDE_SCROLLBARS, DEPTH_LEVEL);
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXListViewSkin<>(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return this.getControlCssMetaDataList();
+    }
+}

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

@@ -0,0 +1,217 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.skins.MFXRadioButtonSkin;
+import javafx.css.*;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a radio button following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code RadioButton}, redefines the style class to "mfx-radio-button" for usage in CSS and
+ * includes a {@code RippleGenerator} to generate ripple effects on click.
+ */
+public class MFXRadioButton extends RadioButton {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXRadioButton> FACTORY = new StyleablePropertyFactory<>(RadioButton.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-radio-button";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-radiobutton.css").toString();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXRadioButton() {
+        setText("RadioButton");
+        initialize();
+    }
+
+    public MFXRadioButton(String s) {
+        super(s);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableObjectProperty<Paint> selectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_COLOR,
+            this,
+            "selectedColor",
+            Color.rgb(15, 157, 88)
+    );
+
+    private final StyleableObjectProperty<Paint> unSelectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNSELECTED_COLOR,
+            this,
+            "unSelectedColor",
+            Color.rgb(90, 90, 90)
+    );
+
+    private final StyleableObjectProperty<Paint> selectedTextColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_TEXT_COLOR,
+            this,
+            "selectedTextColor",
+            Color.rgb(15, 157, 88)
+    );
+
+    private final StyleableObjectProperty<Paint> unSelectedTextColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNSELECTED_TEXT_COLOR,
+            this,
+            "unSelectedTextColor",
+            Color.rgb(0, 0, 0)
+    );
+
+    private final StyleableBooleanProperty changeTextColor = new SimpleStyleableBooleanProperty(
+            StyleableProperties.CHANGE_TEXT_COLOR,
+            this,
+            "changeTextColor",
+            true
+    );
+
+    public Paint getSelectedColor() {
+        return selectedColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> selectedColorProperty() {
+        return selectedColor;
+    }
+
+    public void setSelectedColor(Paint selectedColor) {
+        this.selectedColor.set(selectedColor);
+    }
+
+    public Paint getUnSelectedColor() {
+        return unSelectedColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unSelectedColorProperty() {
+        return unSelectedColor;
+    }
+
+    public void setUnSelectedColor(Paint unSelectedColor) {
+        this.unSelectedColor.set(unSelectedColor);
+    }
+
+    public Paint getSelectedTextColor() {
+        return selectedTextColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> selectedTextColorProperty() {
+        return selectedTextColor;
+    }
+
+    public void setSelectedTextColor(Paint selectedTextColor) {
+        this.selectedTextColor.set(selectedTextColor);
+    }
+
+    public Paint getUnSelectedTextColor() {
+        return unSelectedTextColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unSelectedTextColorProperty() {
+        return unSelectedTextColor;
+    }
+
+    public void setUnSelectedTextColor(Paint unSelectedTextColor) {
+        this.unSelectedTextColor.set(unSelectedTextColor);
+    }
+
+    public boolean isChangeTextColor() {
+        return changeTextColor.get();
+    }
+
+    public StyleableBooleanProperty changeTextColorProperty() {
+        return changeTextColor;
+    }
+
+    public void setChangeTextColor(boolean changeTextColor) {
+        this.changeTextColor.set(changeTextColor);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXRadioButton, Paint> SELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-color",
+                        MFXRadioButton::selectedColorProperty,
+                        Color.rgb(15, 157, 88)
+                );
+
+        private static final CssMetaData<MFXRadioButton, Paint> UNSELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unselected-color",
+                        MFXRadioButton::unSelectedColorProperty,
+                        Color.rgb(90, 90, 90)
+                );
+
+        private static final CssMetaData<MFXRadioButton, Paint> SELECTED_TEXT_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-text-color",
+                        MFXRadioButton::selectedTextColorProperty,
+                        Color.rgb(15, 157, 88)
+                );
+
+        private static final CssMetaData<MFXRadioButton, Paint> UNSELECTED_TEXT_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unselected-text-color",
+                        MFXRadioButton::unSelectedTextColorProperty,
+                        Color.rgb(0, 0, 0)
+                );
+
+        private static final CssMetaData<MFXRadioButton, Boolean> CHANGE_TEXT_COLOR =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-change-text-color",
+                        MFXRadioButton::changeTextColorProperty,
+                        true
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    SELECTED_COLOR, UNSELECTED_COLOR,
+                    SELECTED_TEXT_COLOR, UNSELECTED_TEXT_COLOR,
+                    CHANGE_TEXT_COLOR
+            );
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXRadioButtonSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return this.getControlCssMetaDataList();
+    }
+}

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

@@ -151,7 +151,10 @@ public class MFXStageDialog {
      * @see Stage
      */
     public void setOwner(Window owner) {
-        this.dialogStage.initOwner(owner);
+        try {
+            this.dialogStage.initOwner(owner);
+        } catch (IllegalStateException ignored) {
+        }
     }
 
     /**
@@ -159,7 +162,10 @@ public class MFXStageDialog {
      * @see Stage
      */
     public void setModality(Modality modality) {
-        this.dialogStage.initModality(modality);
+        try {
+            this.dialogStage.initModality(modality);
+        } catch (IllegalStateException ignored) {
+        }
     }
 
     /**

+ 5 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXAnimationFactory.java

@@ -145,6 +145,11 @@ public enum MFXAnimationFactory {
     };
 
     private static final Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
+
+    public static Interpolator getInterpolator() {
+        return interpolator;
+    }
+
     private static void resetNode(Node node) {
         node.setTranslateX(0);
         node.setTranslateY(0);

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

@@ -1,6 +1,7 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
+import io.github.palexdev.materialfx.controls.enums.ButtonType;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.effects.MFXDepthManager;
 import javafx.scene.control.skin.ButtonSkin;
@@ -12,13 +13,10 @@ public class MFXButtonSkin extends ButtonSkin {
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXButtonSkin(MFXButton button, DepthLevel depthLevel) {
+    public MFXButtonSkin(MFXButton button) {
         super(button);
-
-        button.buttonTypeProperty().addListener(
-                (observable, oldValue, newValue) -> updateButtonType(button, depthLevel));
-
-        updateButtonType(button, depthLevel);
+        setListeners();
+        updateButtonType();
     }
 
     //================================================================================
@@ -26,18 +24,40 @@ public class MFXButtonSkin extends ButtonSkin {
     //================================================================================
 
     /**
-     * Changes the button type
+     * Adds listeners to: depthLevel and buttonType properties.
+     */
+    private void setListeners() {
+        MFXButton button = (MFXButton) getSkinnable();
+
+        button.depthLevelProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue) && button.getButtonType().equals(ButtonType.RAISED)) {
+                button.setEffect(MFXDepthManager.shadowOf(newValue));
+            }
+        });
+
+        button.buttonTypeProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                updateButtonType();
+            }
+        });
+
+    }
+
+    /**
+     * Changes the button type.
      */
-    private void updateButtonType(MFXButton button, DepthLevel depthLevel) {
+    private void updateButtonType() {
+        MFXButton button = (MFXButton) getSkinnable();
+
         switch (button.getButtonType()) {
             case RAISED: {
-                getSkinnable().setEffect(MFXDepthManager.shadowOf(depthLevel));
-                getSkinnable().setPickOnBounds(false);
+                button.setEffect(MFXDepthManager.shadowOf(button.getDepthLevel()));
+                button.setPickOnBounds(false);
                 break;
             }
             case FLAT: {
-                getSkinnable().setEffect(MFXDepthManager.shadowOf(DepthLevel.LEVEL0));
-                getSkinnable().setPickOnBounds(true);
+                button.setEffect(MFXDepthManager.shadowOf(DepthLevel.LEVEL0));
+                button.setPickOnBounds(true);
                 break;
             }
         }

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

@@ -33,8 +33,8 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXCheckboxSkin(MFXCheckbox control) {
-        super(control);
+    public MFXCheckboxSkin(MFXCheckbox checkbox) {
+        super(checkbox);
 
         // Contains the ripple generator and the box
         rippleContainer = new AnchorPane();
@@ -51,7 +51,7 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
         box.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         box.getStyleClass().setAll("box");
         box.setBorder(new Border(new BorderStroke(
-                control.getUncheckedColor(),
+                checkbox.getUncheckedColor(),
                 BorderStrokeStyle.SOLID,
                 new CornerRadii(2),
                 new BorderWidths(2.2)
@@ -74,8 +74,8 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
         rippleContainer.getChildren().addAll(rippleGenerator, box);
 
         updateChildren();
-        updateMarkType(control);
-        setListeners(control);
+        updateMarkType();
+        setListeners();
     }
 
     //================================================================================
@@ -84,52 +84,68 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
 
     /**
      * Adds listeners for: markType, selected, indeterminate, checked and unchecked coloros properties.
-     * @param control The MFXCheckbox associated to this skin
      */
-    private void setListeners(MFXCheckbox control) {
-        control.markTypeProperty().addListener(
-                (observable, oldValue, newValue) -> updateMarkType(control));
+    private void setListeners() {
+        MFXCheckbox checkbox = (MFXCheckbox) getSkinnable();
 
-        control.selectedProperty().addListener(
-                (observable, oldValue, newValue) -> updateColors(control)
+        checkbox.markTypeProperty().addListener(
+                (observable, oldValue, newValue) -> updateMarkType()
         );
 
-        control.indeterminateProperty().addListener(
-                (observable, oldValue, newValue) -> updateColors(control)
+        checkbox.selectedProperty().addListener(
+                (observable, oldValue, newValue) -> updateColors()
         );
 
-        control.checkedColorProperty().addListener(
-                (observable, oldValue, newValue) -> updateColors(control)
+        checkbox.indeterminateProperty().addListener(
+                (observable, oldValue, newValue) -> updateColors()
         );
 
-        control.uncheckedColorProperty().addListener(
-                (observable, oldValue, newValue) -> updateColors(control)
+        checkbox.checkedColorProperty().addListener(
+                (observable, oldValue, newValue) -> updateColors()
+        );
+
+        checkbox.uncheckedColorProperty().addListener(
+                (observable, oldValue, newValue) -> updateColors()
         );
 
         /* Listener on control but if the coordinates of the event are greater than then ripple container size
          * then the center of the ripple is set to the width and/or height of container
          */
-        control.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
+        checkbox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
             rippleGenerator.setGeneratorCenterX(Math.min(event.getX(), rippleContainer.getWidth()));
             rippleGenerator.setGeneratorCenterY(Math.min(event.getY(), rippleContainer.getHeight()));
             rippleGenerator.createRipple();
         });
+
+        /*
+         * Workaround
+         * When the control is created the Skin is still null, so if the CheckBox is set
+         * to be selected/indeterminate the animation won't be played. To fix this add a listener to the
+         * control's skinProperty, when the skin is not null and the CheckBox isSelected/isIndeterminate,
+         * play the animation.
+         */
+        checkbox.skinProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && (checkbox.isSelected() || checkbox.isIndeterminate())) {
+                updateColors();
+            }
+        });
     }
 
     /**
      * This method is called whenever one of the following properties changes:
      * {@code selectedProperty}, {@code indeterminateProperty}, {@code checkedColor} and {@code uncheckedColor} properties
-     * @param control The MFXCheckbox associated to this skin
      * @see NodeUtils
      */
-    private void updateColors(MFXCheckbox control) {
+    private void updateColors() {
+        MFXCheckbox checkbox = (MFXCheckbox) getSkinnable();
+
         final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
-        if (control.isIndeterminate()) {
-            NodeUtils.updateBackground(box, control.getCheckedColor(), new Insets(4));
-        } else if (control.isSelected()) {
-            NodeUtils.updateBackground(box, control.getCheckedColor(), Insets.EMPTY);
+        if (checkbox.isIndeterminate()) {
+            NodeUtils.updateBackground(box, checkbox.getCheckedColor(), new Insets(4));
+        } else if (checkbox.isSelected()) {
+            NodeUtils.updateBackground(box, checkbox.getCheckedColor(), Insets.EMPTY);
             box.setBorder(new Border(new BorderStroke(
-                    control.getCheckedColor(),
+                    checkbox.getCheckedColor(),
                     borderStroke.getTopStyle(),
                     borderStroke.getRadii(),
                     borderStroke.getWidths()
@@ -137,7 +153,7 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
         } else {
             NodeUtils.updateBackground(box, Color.TRANSPARENT);
             box.setBorder(new Border(new BorderStroke(
-                    control.getUncheckedColor(),
+                    checkbox.getUncheckedColor(),
                     borderStroke.getTopStyle(),
                     borderStroke.getRadii(),
                     borderStroke.getWidths()
@@ -147,11 +163,12 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
 
     /**
      * This method is called whenever the {@code markType} property changes.
-     * @param control The MFXCheckbox associated to this skin
      */
-    private void updateMarkType(MFXCheckbox control) {
+    private void updateMarkType() {
+        MFXCheckbox checkbox = (MFXCheckbox) getSkinnable();
+
         SVGPath svgPath = new SVGPath();
-        svgPath.setContent(control.getMarkType().getSvhPath());
+        svgPath.setContent(checkbox.getMarkType().getSvhPath());
         mark.setShape(svgPath);
     }
 

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

@@ -0,0 +1,178 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.MFXResourcesManager;
+import io.github.palexdev.materialfx.controls.MFXComboBox;
+import io.github.palexdev.materialfx.validation.MFXDialogValidator;
+import javafx.animation.FadeTransition;
+import javafx.scene.control.Label;
+import javafx.scene.control.skin.ComboBoxListViewSkin;
+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 MFXComboBox}.
+ */
+public class MFXComboBoxSkin<T> extends ComboBoxListViewSkin<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final double padding = 11;
+
+    private final Line line;
+    private final Line focusLine;
+    private final Label validate;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXComboBoxSkin(MFXComboBox<T> comboBox) {
+        super(comboBox);
+
+        line = new Line();
+        line.getStyleClass().add("unfocused-line");
+        line.setStroke(comboBox.getUnfocusedLineColor());
+        line.setStrokeWidth(comboBox.getLineStrokeWidth());
+        line.setSmooth(true);
+
+        focusLine = new Line();
+        focusLine.getStyleClass().add("focused-line");
+        focusLine.setStroke(comboBox.getLineColor());
+        focusLine.setStrokeWidth(comboBox.getLineStrokeWidth());
+        focusLine.setSmooth(true);
+        focusLine.setOpacity(0.0);
+
+        line.endXProperty().bind(comboBox.widthProperty());
+        focusLine.endXProperty().bind(comboBox.widthProperty());
+
+        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(comboBox.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 and an event handler to show a dialog if you click
+     * on the warning label.
+     */
+    private void setListeners() {
+        MFXComboBox<T> comboBox = (MFXComboBox<T>) getSkinnable();
+        MFXDialogValidator validator = comboBox.getValidator();
+
+        comboBox.lineColorProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                focusLine.setStroke(newValue);
+            }
+        });
+
+        comboBox.unfocusedLineColorProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                line.setStroke(newValue);
+            }
+        });
+
+        comboBox.lineStrokeWidthProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.doubleValue() != oldValue.doubleValue()) {
+                line.setStrokeWidth(newValue.doubleValue());
+                focusLine.setStrokeWidth(newValue.doubleValue() * 1.3);
+            }
+        });
+
+        comboBox.focusedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue && comboBox.isValidated()) {
+                validate.setVisible(!validator.isValid());
+            }
+
+            if (comboBox.isAnimateLines()) {
+                buildAndPlayAnimation(newValue);
+                return;
+            }
+
+            if (newValue) {
+                focusLine.setOpacity(1.0);
+            } else {
+                focusLine.setOpacity(0.0);
+            }
+        });
+
+        comboBox.isValidatedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue) {
+                validate.setVisible(false);
+            }
+        });
+
+        comboBox.disabledProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                validate.setVisible(false);
+            }
+        });
+
+        validator.addChangeListener((observable, oldValue, newValue) -> {
+            if (comboBox.isValidated()) {
+                validate.setVisible(!newValue);
+            }
+        });
+        validate.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> validator.showModal(comboBox.getScene().getWindow()));
+    }
+
+    /**
+     * Builds and play the lines animation if {@code animateLines} is true.
+     */
+    private void buildAndPlayAnimation(boolean focused) {
+        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);
+        line.setTranslateY(h);
+        validate.resize(w * 1.5, h - size);
+        validate.setTranslateY(focusLine.getTranslateY() + size);
+    }
+}

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

@@ -0,0 +1,227 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXListView;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.effects.MFXDepthManager;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.skin.ListViewSkin;
+import javafx.scene.control.skin.VirtualFlow;
+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>
+ * The most important thing this skin does is replacing the default scrollbars with new ones,
+ * this makes styling them a lot more easy.
+ */
+public class MFXListViewSkin<T> extends ListViewSkin<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final VirtualFlow<?> virtualFlow;
+
+    private final ScrollBar vBar;
+    private final ScrollBar hBar;
+
+    private final Timeline hideBars;
+    private final Timeline showBars;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXListViewSkin(final MFXListView<T> listView) {
+        super(listView);
+
+        virtualFlow = (VirtualFlow<?>) listView.lookup(".virtual-flow");
+        listView.setEffect(MFXDepthManager.shadowOf(listView.getDepthLevel()));
+
+        this.vBar = new ScrollBar();
+        this.hBar = new ScrollBar();
+        bindScrollBars(listView);
+        getChildren().addAll(vBar, hBar);
+
+        vBar.setManaged(false);
+        vBar.setOrientation(Orientation.VERTICAL);
+        vBar.getStyleClass().add("mfx-scroll-bar");
+        vBar.visibleProperty().bind(vBar.visibleAmountProperty().isNotEqualTo(0));
+
+        hBar.setManaged(false);
+        hBar.setOrientation(Orientation.HORIZONTAL);
+        hBar.getStyleClass().add("mfx-scroll-bar");
+        hBar.visibleProperty().bind(hBar.visibleAmountProperty().isNotEqualTo(0));
+
+        hideBars = new Timeline(
+                new KeyFrame(Duration.millis(400),
+                        new KeyValue(vBar.opacityProperty(), 0.0, MFXAnimationFactory.getInterpolator()),
+                        new KeyValue(hBar.opacityProperty(), 0.0, MFXAnimationFactory.getInterpolator()))
+        );
+        showBars = new Timeline(
+                new KeyFrame(Duration.millis(400),
+                        new KeyValue(vBar.opacityProperty(), 1.0, MFXAnimationFactory.getInterpolator()),
+                        new KeyValue(hBar.opacityProperty(), 1.0, MFXAnimationFactory.getInterpolator()))
+        );
+
+        setListeners();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for: mouseExited, mouseEntered, hideScrollBars, and depthLevel properties.
+     */
+    private void setListeners() {
+        MFXListView<T> listView = (MFXListView<T>) getSkinnable();
+
+        listView.setOnMouseExited(event -> {
+            if (listView.isHideScrollBars()) {
+                hideBars.setDelay(listView.getHideAfter());
+
+                if (hBar.isPressed()) {
+                    hBar.pressedProperty().addListener(new ChangeListener<>() {
+                        @Override
+                        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                            if (!newValue) {
+                                hideBars.play();
+                            }
+                            hBar.pressedProperty().removeListener(this);
+                        }
+                    });
+                    return;
+                }
+
+                if (vBar.isPressed()) {
+                    vBar.pressedProperty().addListener(new ChangeListener<>() {
+                        @Override
+                        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                            if (!newValue) {
+                                hideBars.play();
+                            }
+                            vBar.pressedProperty().removeListener(this);
+                        }
+                    });
+                    return;
+                }
+
+                hideBars.play();
+            }
+        });
+
+        listView.setOnMouseEntered(event -> {
+            if (hideBars.getStatus().equals(Animation.Status.RUNNING)) {
+                hideBars.stop();
+            }
+            showBars.play();
+        });
+
+        listView.hideScrollBarsProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                hideBars.play();
+            } else {
+                showBars.play();
+            }
+        });
+
+        listView.depthLevelProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                listView.setEffect(MFXDepthManager.shadowOf(listView.getDepthLevel()));
+            }
+        });
+    }
+
+    private void bindScrollBars(MFXListView<?> listView) {
+        final Set<Node> nodes = listView.lookupAll("VirtualScrollBar");
+        for (Node node : nodes) {
+            if (node instanceof ScrollBar) {
+                ScrollBar bar = (ScrollBar) node;
+                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
+                    bindScrollBars(vBar, bar);
+                } else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) {
+                    bindScrollBars(hBar, bar);
+                }
+            }
+        }
+    }
+
+    private void bindScrollBars(ScrollBar scrollBarA, ScrollBar scrollBarB) {
+        scrollBarA.valueProperty().bindBidirectional(scrollBarB.valueProperty());
+        scrollBarA.minProperty().bindBidirectional(scrollBarB.minProperty());
+        scrollBarA.maxProperty().bindBidirectional(scrollBarB.maxProperty());
+        scrollBarA.visibleAmountProperty().bindBidirectional(scrollBarB.visibleAmountProperty());
+        scrollBarA.unitIncrementProperty().bindBidirectional(scrollBarB.unitIncrementProperty());
+        scrollBarA.blockIncrementProperty().bindBidirectional(scrollBarB.blockIncrementProperty());
+    }
+
+    private double estimateHeight() {
+        double borderWidth = snapVerticalInsets();
+
+        double cellsHeight = 0;
+        for (int i = 0; i < virtualFlow.getCellCount(); i++) {
+            ListCell<?> cell = (ListCell<?>) virtualFlow.getCell(i);
+
+            cellsHeight += cell.getHeight();
+        }
+
+        return cellsHeight + borderWidth;
+    }
+
+    private double snapVerticalInsets() {
+        return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return 200;
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        final int itemsCount = getSkinnable().getItems().size();
+        if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
+            return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+        }
+
+        final double fixedCellSize = getSkinnable().getFixedCellSize();
+        double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
+                fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
+        double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+        if (height > computedHeight) {
+            height = computedHeight;
+        }
+
+        if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
+            return getSkinnable().getMaxHeight();
+        }
+
+        return height;
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        Insets insets = getSkinnable().getInsets();
+        final double prefWidth = vBar.prefWidth(-1);
+        vBar.resizeRelocate(w - prefWidth - insets.getRight(), insets.getTop(), prefWidth, h - insets.getTop() - insets.getBottom());
+
+        final double prefHeight = hBar.prefHeight(-1);
+        hBar.resizeRelocate(insets.getLeft(), h - prefHeight - insets.getBottom(), w - insets.getLeft() - insets.getRight(), prefHeight);
+    }
+}

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

@@ -0,0 +1,196 @@
+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.effects.RippleGenerator;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.scene.Cursor;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.skin.RadioButtonSkin;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+
+/**
+ *  This is the implementation of the {@code Skin} associated with every {@code MFXRadioButton}.
+ */
+public class MFXRadioButtonSkin extends RadioButtonSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane container;
+    private final Circle radio;
+    private final Circle dot;
+    private final double padding = 6;
+
+    private final RippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXRadioButtonSkin(MFXRadioButton radioButton) {
+        super(radioButton);
+
+        final double radius = 8;
+        radio = new Circle(radius);
+        radio.getStyleClass().setAll("radio");
+        radio.setStrokeWidth(2);
+        radio.setFill(Color.web("#f4f4f4"));
+        radio.setSmooth(true);
+
+        dot = new Circle(radius);
+        dot.getStyleClass().setAll("dot");
+        dot.fillProperty().bind(radioButton.selectedColorProperty());
+        dot.setScaleX(0);
+        dot.setScaleY(0);
+        dot.setSmooth(true);
+
+        container = new StackPane();
+        container.getStyleClass().add("radio-container");
+
+        rippleGenerator = new RippleGenerator(container, RippleClipType.NOCLIP);
+        rippleGenerator.setRippleRadius(radius * 1.2);
+        rippleGenerator.setInDuration(Duration.millis(350));
+        rippleGenerator.setAnimateBackground(false);
+
+        container.getChildren().addAll(rippleGenerator, radio, dot);
+
+        radioButton.setCursor(Cursor.HAND);
+        updateChildren();
+        updateColors();
+        radio.setStroke(radioButton.isSelected() ? radioButton.getSelectedColor() : radioButton.getUnSelectedColor());
+        setListeners();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds a listener to the selected property for animations, colors and ripples.
+     */
+    private void setListeners() {
+        MFXRadioButton radioButton = (MFXRadioButton) getSkinnable();
+
+        radioButton.selectedColorProperty().addListener((observable, oldValue, newValue) -> updateColors());
+        radioButton.selectedTextColorProperty().addListener((observable, oldValue, newValue) -> updateColors());
+        radioButton.unSelectedColorProperty().addListener((observable, oldValue, newValue) -> updateColors());
+        radioButton.unSelectedTextColorProperty().addListener((observable, oldValue, newValue) -> updateColors());
+
+        radioButton.selectedProperty().addListener((observableValue, oldValue, newValue) ->  {
+            buildAndPlayAnimation();
+            updateColors();
+            rippleGenerator.setGeneratorCenterX(container.getWidth() / 2.0 - 0.3);
+            rippleGenerator.setGeneratorCenterY(container.getHeight() / 2.0 - 0.3);
+            rippleGenerator.createRipple();
+        });
+
+        /*
+         * Workaround
+         * When the control is created the Skin is still null, so if the RadioButton is set
+         * to be selected the animation won't be played. To fix this add a listener to the
+         * control's skinProperty, when the skin is not null and the RadioButton isSelected,
+         * play the animation.
+         */
+        radioButton.skinProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && radioButton.isSelected()) {
+                buildAndPlayAnimation();
+            }
+        });
+    }
+
+    /**
+     * Changes the ripples color according to the selected property and
+     * the text color if {@code changeTextColor} property is true.
+     */
+    private void updateColors() {
+        final MFXRadioButton radioButton = (MFXRadioButton) getSkinnable();
+        Color selectedColor = (Color) radioButton.getSelectedColor();
+        Color unSelectedColor = (Color) radioButton.getUnSelectedColor();
+        rippleGenerator.setRippleColor(radioButton.isSelected() ? selectedColor : unSelectedColor);
+
+        if (radioButton.isChangeTextColor()) {
+            Color selectedTextColor = (Color) radioButton.getSelectedTextColor();
+            Color unSelectedTextColor = (Color) radioButton.getUnSelectedTextColor();
+
+            Text text = (Text) radioButton.lookup(".text");
+            String color = radioButton.isSelected() ? ColorUtils.rgb(selectedTextColor) : ColorUtils.rgb(unSelectedTextColor);
+            text.setStyle("-fx-fill: " + color + ";\n");
+        }
+    }
+
+    /**
+     * Builds and play the animation to show/hide the radio dot and change the stroke color.
+     */
+    private void buildAndPlayAnimation() {
+        final MFXRadioButton radioButton = (MFXRadioButton) getSkinnable();
+        final Duration duration = Duration.millis(200);
+
+        KeyValue keyValue1 = new KeyValue(dot.scaleXProperty(), radioButton.isSelected() ? 0.55 : 0, Interpolator.EASE_BOTH);
+        KeyValue keyValue2 = new KeyValue(dot.scaleYProperty(), radioButton.isSelected() ? 0.55 : 0, Interpolator.EASE_BOTH);
+        KeyValue keyValue3 = new KeyValue(radio.strokeProperty(), radioButton.isSelected() ? radioButton.getSelectedColor() : radioButton.getUnSelectedColor(), Interpolator.EASE_BOTH);
+
+        KeyFrame keyFrame = new KeyFrame(duration, keyValue1, keyValue2, keyValue3);
+        Timeline timeline = new Timeline(keyFrame);
+        timeline.play();
+    }
+
+    private void removeRadio() {
+        getChildren().removeIf(node -> node.getStyleClass().contains("radio"));
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected void updateChildren() {
+        super.updateChildren();
+        if (radio != null) {
+            removeRadio();
+            getChildren().addAll(rippleGenerator, container);
+        }
+    }
+
+    @Override
+    protected void layoutChildren(final double x, final double y, final double w, final double h) {
+        final RadioButton radioButton = getSkinnable();
+
+        final double contWidth = container.prefWidth(-1);
+        final double contHeight = container.prefHeight(-1);
+        final double computeWidth = Math.max(radioButton.prefWidth(-1), radioButton.minWidth(-1));
+        final double labelWidth = Math.min(computeWidth - contWidth, w - snapSizeX(contWidth));
+        final double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
+        final double maxHeight = Math.max(contHeight, labelHeight);
+        final double xOffset = NodeUtils.computeXOffset(w, labelWidth + computeWidth, radioButton.getAlignment().getHpos()) + x;
+        final double yOffset = NodeUtils.computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + y;
+
+        layoutLabelInArea(xOffset + contWidth + padding, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
+        container.resize(snapSizeX(contWidth), snapSizeY(contHeight));
+        positionInArea(container, xOffset, yOffset, contWidth, maxHeight, 0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos());
+    }
+
+    @Override
+    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computeMinWidth(height,
+                topInset,
+                rightInset,
+                bottomInset,
+                leftInset) + snapSizeX(radio.minWidth(-1)) + padding;
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefWidth(height,
+                topInset,
+                rightInset,
+                bottomInset,
+                leftInset) + snapSizeX(radio.prefWidth(-1)) + padding;
+    }
+}

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

@@ -35,13 +35,13 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXToggleButtonSkin(MFXToggleButton control) {
-        super(control);
+    public MFXToggleButtonSkin(MFXToggleButton toggleButton) {
+        super(toggleButton);
 
-         circleRadius = control.getSize();
+         circleRadius = toggleButton.getSize();
 
         line = new Line();
-        line.setStroke(control.isSelected() ? control.getToggleLineColor() : control.getUnToggleLineColor());
+        line.setStroke(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
         line.setStartX(0);
         line.setStartY(0);
         line.setEndX(circleRadius * 2 + 4);
@@ -51,7 +51,7 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
         line.setSmooth(true);
 
         circle = new Circle(circleRadius);
-        circle.setFill(control.isSelected() ? control.getToggleColor() : control.getUnToggleColor());
+        circle.setFill(toggleButton.isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
         circle.setTranslateX(-circleRadius);
         circle.setSmooth(true);
         circle.setEffect(MFXDepthManager.shadowOf(DepthLevel.LEVEL1));
@@ -66,15 +66,15 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
 
         rippleGenerator = new RippleGenerator(container, RippleClipType.NOCLIP);
         rippleGenerator.setAnimateBackground(false);
-        rippleGenerator.setRippleColor((Color) (control.isSelected() ? control.getUnToggleLineColor() : control.getToggleLineColor()));
+        rippleGenerator.setRippleColor((Color) (toggleButton.isSelected() ? toggleButton.getUnToggleLineColor() : toggleButton.getToggleLineColor()));
         rippleGenerator.setRippleRadius(circleRadius * 1.2);
         rippleGenerator.setInDuration(Duration.millis(400));
         rippleGenerator.setTranslateX(-circleRadius);
         container.getChildren().add(0, rippleGenerator);
 
-        control.setGraphic(container);
+        toggleButton.setGraphic(container);
 
-        setListeners(control);
+        setListeners();
     }
 
     //================================================================================
@@ -83,24 +83,25 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
 
     /**
      * Adds listeners for: selected, size and skin(workaround) properties.
-     * @param control The MFXToggleButton associated to this skin
      */
-    private void setListeners(MFXToggleButton control) {
-        control.selectedProperty().addListener((observable, oldValue, newValue) -> {
+    private void setListeners() {
+        MFXToggleButton toggleButton = (MFXToggleButton) getSkinnable();
+
+        toggleButton.selectedProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue) {
-                line.setStroke(control.getToggleLineColor());
-                rippleGenerator.setRippleColor((Color) control.getToggleLineColor());
-                circle.setFill(control.getToggleColor());
+                line.setStroke(toggleButton.getToggleLineColor());
+                rippleGenerator.setRippleColor((Color) toggleButton.getToggleLineColor());
+                circle.setFill(toggleButton.getToggleColor());
             } else {
-                line.setStroke(control.getUnToggleLineColor());
-                rippleGenerator.setRippleColor((Color) control.getUnToggleLineColor());
-                circle.setFill(control.getUnToggleColor());
+                line.setStroke(toggleButton.getUnToggleLineColor());
+                rippleGenerator.setRippleColor((Color) toggleButton.getUnToggleLineColor());
+                circle.setFill(toggleButton.getUnToggleColor());
             }
         });
 
-        control.selectedProperty().addListener((observable, oldValue, newValue) -> buildAndPlayAnimation(newValue));
+        toggleButton.selectedProperty().addListener((observable, oldValue, newValue) -> buildAndPlayAnimation(newValue));
 
-        control.sizeProperty().addListener((observable, oldValue, newValue) -> {
+        toggleButton.sizeProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue.doubleValue() < oldValue.doubleValue()) {
                 double translateX = newValue.doubleValue() + oldValue.doubleValue();
                 circle.setTranslateX(translateX + 2);
@@ -114,8 +115,8 @@ public class MFXToggleButtonSkin extends ToggleButtonSkin {
          * control's skinProperty, when the skin is not null and the ToggleButton isSelected,
          * play the animation.
          */
-        control.skinProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue != null && control.isSelected()) {
+        toggleButton.skinProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && toggleButton.isSelected()) {
                 buildAndPlayAnimation(true);
             }
         });

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

@@ -9,6 +9,7 @@ public class StringUtils {
 
     /**
      * Finds the difference between two {@code String}s.
+     *
      * @param str1 The first String
      * @param str2 The second String
      * @return the difference between the two given strings
@@ -29,6 +30,7 @@ public class StringUtils {
 
     /**
      * Finds the index at which two {@code CharSequence}s differ.
+     *
      * @param cs1 The first sequence
      * @param cs2 The second sequence
      */
@@ -50,4 +52,19 @@ public class StringUtils {
         }
         return INDEX_NOT_FOUND;
     }
+
+    /**
+     * Replaces the last occurrence of the given string with a new string.
+     * @param string The string to modify
+     * @param substring The last occurrence to find
+     * @param replacement The replacement
+     * @return The modified string
+     */
+    public static String replaceLast(String string, String substring, String replacement) {
+        int index = string.lastIndexOf(substring);
+        if (index == -1)
+            return string;
+        return string.substring(0, index) + replacement
+                + string.substring(index + substring.length());
+    }
 }

+ 155 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXDialogValidator.java

@@ -0,0 +1,155 @@
+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;
+import io.github.palexdev.materialfx.utils.StringUtils;
+import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.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;
+
+/**
+ * This is a concrete implementation of a validator.
+ * <p>
+ * This validator has a string message associated with every boolean property in its base class.
+ * It can show a {@link MFXStageDialog} containing all warning messages.
+ */
+public class MFXDialogValidator extends AbstractMFXValidator {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final List<String> messages = new ArrayList<>();
+    private final ObjectProperty<DialogType> dialogType = new SimpleObjectProperty<>(DialogType.WARNING);
+    private String title;
+    private MFXStageDialog stageDialog;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXDialogValidator(String title) {
+        this.title = title;
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        dialogType.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue) && stageDialog != null) {
+                MFXDialogFactory.convertToSpecific(newValue, stageDialog.getDialog());
+                Label label = (Label) stageDialog.getDialog().lookup(".content-label");
+                label.setAlignment(Pos.CENTER);
+            }
+        });
+    }
+
+    /**
+     * Shows this validator's dialog. The dialog is not modal and doesn't scrim the background.
+     */
+    public void show() {
+        if (stageDialog == null) {
+            stageDialog = new MFXStageDialog(dialogType.get(), title, "");
+            Label label = (Label) stageDialog.getDialog().lookup(".content-label");
+            label.setAlignment(Pos.CENTER);
+        }
+
+        stageDialog.getDialog().setContent(getMessages());
+        stageDialog.setOwner(null);
+        stageDialog.setModality(Modality.NONE);
+        stageDialog.setCenterInOwner(false);
+        stageDialog.setScrimBackground(false);
+        stageDialog.show();
+    }
+
+    /**
+     * Shows this validator's dialog. The dialog is modal and scrims the background.
+     * @param owner The dialog's owner.
+     */
+    public void showModal(Window owner) {
+        if (stageDialog == null) {
+            stageDialog = new MFXStageDialog(dialogType.get(), title, "");
+            Label label = (Label) stageDialog.getDialog().lookup(".content-label");
+            label.setAlignment(Pos.CENTER);
+        }
+
+        stageDialog.getDialog().setContent(getMessages());
+        stageDialog.setOwner(owner);
+        stageDialog.setModality(Modality.WINDOW_MODAL);
+        stageDialog.setCenterInOwner(true);
+        stageDialog.setScrimBackground(true);
+        stageDialog.show();
+
+    }
+
+    /**
+     * Adds a new boolean condition to the list with the corresponding message in case it is false.
+     * @param property The new boolean condition
+     * @param message The message to show in case it is false
+     */
+    public void add(BooleanProperty property, String message) {
+        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);
+    }
+
+    /**
+     * 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);
+        }
+        super.conditions.remove(property);
+    }
+
+    /**
+     * Checks the messages list and if the corresponding boolean condition is false
+     * adds the message to the {@code StringBuilder}.
+     */
+    public String getMessages() {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < messages.size(); i++) {
+            if (!conditions.get(i).get()) {
+                sb.append(messages.get(i)).append(",\n");
+            }
+        }
+        return StringUtils.replaceLast(sb.toString(), ",", ".");
+    }
+
+    public DialogType getDialogType() {
+        return dialogType.get();
+    }
+
+    public ObjectProperty<DialogType> dialogTypeProperty() {
+        return dialogType;
+    }
+
+    public void setDialogType(DialogType dialogType) {
+        this.dialogType.set(dialogType);
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+}

+ 79 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/AbstractMFXValidator.java

@@ -0,0 +1,79 @@
+package io.github.palexdev.materialfx.validation.base;
+
+import io.github.palexdev.materialfx.beans.binding.BooleanListBinding;
+import io.github.palexdev.materialfx.utils.StringUtils;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.collections.ObservableList;
+
+/**
+ * Base class for all validators.
+ * @see IMFXValidator
+ * @see BooleanListBinding
+ */
+public abstract class AbstractMFXValidator implements IMFXValidator {
+    //================================================================================
+    // Properties
+    //================================================================================
+    protected ObservableList<BooleanProperty> conditions;
+    protected BooleanListBinding validation;
+    private final StringProperty validatorMessage = new SimpleStringProperty("Validation failed!");
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    public String getValidatorMessage() {
+        return validatorMessage.get();
+    }
+
+    public StringProperty validatorMessageProperty() {
+        return validatorMessage;
+    }
+
+    public void setValidatorMessage(String validatorMessage) {
+        this.validatorMessage.set(validatorMessage);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public boolean isValid() {
+        return this.validation.get();
+    }
+
+    @Override
+    public void addInvalidationListener(InvalidationListener listener) {
+        this.validation.addListener(listener);
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener<? super Boolean> listener) {
+        this.validation.addListener(listener);
+    }
+
+    @Override
+    public void removeInvalidationListener(InvalidationListener listener) {
+        this.validation.removeListener(listener);
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener<? super Boolean> listener) {
+        this.validation.removeListener(listener);
+    }
+
+    /**
+     * Returns all booleans as a string.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("Values: ");
+        for (BooleanProperty bp : conditions) {
+            sb.append(bp.get()).append(", ");
+        }
+        return StringUtils.replaceLast(sb.toString(), ",", ".");
+    }
+}

+ 17 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/validation/base/IMFXValidator.java

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

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

@@ -7,6 +7,7 @@ module MaterialFX.materialfx.main {
 
     exports io.github.palexdev.materialfx;
     exports io.github.palexdev.materialfx.beans;
+    exports io.github.palexdev.materialfx.beans.binding;
     exports io.github.palexdev.materialfx.collections;
     exports io.github.palexdev.materialfx.controls;
     exports io.github.palexdev.materialfx.controls.base;
@@ -16,4 +17,6 @@ module MaterialFX.materialfx.main {
     exports io.github.palexdev.materialfx.notifications;
     exports io.github.palexdev.materialfx.skins;
     exports io.github.palexdev.materialfx.utils;
+    exports io.github.palexdev.materialfx.validation;
+    exports io.github.palexdev.materialfx.validation.base;
 }

+ 42 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-combobox.css

@@ -0,0 +1,42 @@
+.mfx-combo-box {
+    -fx-prompt-text-fill: #4d4d4d;
+
+    -fx-padding: -1 -1 -1 -5;
+}
+
+.mfx-combo-box:editable > .arrow-button,
+.mfx-combo-box > .arrow-button {
+    -fx-padding: 2;
+}
+
+.mfx-combo-box,
+.mfx-combo-box:focused,
+.mfx-combo-box:editable,
+.mfx-combo-box:editable:focused {
+    -fx-background-color: transparent, transparent, transparent, transparent;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+}
+
+.mfx-combo-box .text-field,
+.mfx-combo-box:focused .text-field,
+.mfx-combo-box:editable .text-field,
+.mfx-combo-box:editable:focused .text-field {
+    -fx-background-color: transparent, transparent, transparent, transparent;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+}
+
+.mfx-combo-box .combo-box-button-container {
+    -fx-background-color: transparent;
+}
+
+.mfx-combo-box > .arrow-button,
+.mfx-combo-box:editable > .arrow-button,
+.mfx-combo-box:editable:focused > .arrow-button {
+    -fx-background-color: transparent;
+}
+
+.mfx-combo-box .validate-label {
+    -fx-text-fill: #D34336;
+}

+ 28 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listcell.css

@@ -0,0 +1,28 @@
+.mfx-list-cell {
+    -fx-background-insets: 0.0;
+    -fx-text-fill: BLACK;
+
+    -mfx-hover-color: rgba(50, 150, 255, 0.2);
+}
+
+.mfx-list-cell:odd,
+.mfx-list-cell:even {
+    -fx-background-color: white;
+}
+
+.mfx-list-cell:filled:hover,
+.mfx-list-cell:selected .label {
+    -fx-text-fill: black;
+}
+
+.list-cell .text {
+    -fx-fill: -fx-text-background-color ;
+}
+
+.list-cell:selected .text {
+    -fx-fill: black;
+}
+
+.list-cell:hover .text {
+    -fx-fill: black;
+}

+ 113 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listview.css

@@ -0,0 +1,113 @@
+/* Base */
+.mfx-list-view {
+    -fx-focus-color: transparent;
+    -fx-faint-focus-color: transparent;
+/*    -fx-background-insets: 1;*/
+    -fx-border-color: transparent;
+}
+
+/* Look up colors */
+.mfx-list-view {
+    -mfx-selection-bar: transparent;
+    -mfx-selection-bar-non-focused: transparent;
+    -mfx-box-border: transparent;
+
+    -fx-selection-bar: -mfx-selection-bar;
+    -fx-selection-bar-non-focused: -mfx-selection-bar;
+    -fx-box-border: -mfx-box-border;
+
+    -mfx-track-color: rgb(230, 230, 230);
+    -mfx-thumb-color: rgb(137, 137, 137);
+    -mfx-thumb-hover-color: rgb(89, 88, 91);
+}
+
+/* Remove JavaFX crap */
+.mfx-list-view > .virtual-flow > .corner {
+    -fx-background-color: transparent ;
+}
+
+.mfx-list-view > .virtual-flow > .scroll-bar,
+.mfx-list-view > .virtual-flow > .scroll-bar .decrement-arrow,
+.mfx-list-view > .virtual-flow > .scroll-bar .increment-arrow,
+.mfx-list-view > .virtual-flow > .scroll-bar .decrement-button,
+.mfx-list-view > .virtual-flow > .scroll-bar .increment-button {
+    -fx-pref-width: 0;
+    -fx-pref-height: 0;
+}
+
+
+.mfx-list-view .scroll-bar:horizontal .increment-button,
+.mfx-list-view .scroll-bar:horizontal .decrement-button {
+    -fx-background-color: transparent;
+    -fx-background-radius: 0.0em;
+    -fx-padding: 0.0 0.0 10.0 0.0;
+}
+
+.mfx-list-view .scroll-bar:vertical .increment-button,
+.mfx-list-view .scroll-bar:vertical .decrement-button {
+    -fx-background-color: transparent;
+    -fx-background-radius: 0.0em;
+    -fx-padding: 0.0 10.0 0.0 0.0;
+
+}
+
+.mfx-list-view .scroll-bar .increment-arrow,
+.mfx-list-view .scroll-bar .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.15em 0.0;
+}
+
+.mfx-list-view .scroll-bar:horizontal .increment-arrow,
+.mfx-list-view .scroll-bar:horizontal .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+.mfx-list-view .scroll-bar:vertical .increment-arrow,
+.mfx-list-view .scroll-bar:vertical .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+/* Customize ScrollBars */
+.mfx-list-view .mfx-scroll-bar:horizontal .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+}
+
+.mfx-list-view .mfx-scroll-bar:vertical .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+}
+
+.mfx-list-view .mfx-scroll-bar .decrement-arrow,
+.mfx-list-view .mfx-scroll-bar .increment-arrow {
+    -fx-pref-width: 0;
+    -fx-pref-height: 0;
+}
+
+.mfx-list-view .mfx-scroll-bar {
+    -fx-background-color: transparent;
+    -fx-pref-width: 12;
+    -fx-pref-height: 12;
+    -fx-padding: 5 0.5 5 0.5;
+}
+
+.mfx-list-view .mfx-scroll-bar:horizontal .thumb,
+.mfx-list-view .mfx-scroll-bar:vertical .thumb {
+    -fx-background-color: -mfx-thumb-color;
+    -fx-background-insets: 2.0, 0.0, 0.0;
+    -fx-background-radius: 2.0em;
+}
+
+.mfx-list-view .mfx-scroll-bar:horizontal .thumb:hover,
+.mfx-list-view .mfx-scroll-bar:vertical .thumb:hover {
+    -fx-background-color: -mfx-thumb-hover-color;
+    -fx-background-insets: 1.5, 0.0, 0.0;
+    -fx-background-radius: 2.0em;
+}
+

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