فهرست منبع

Version 11.3.2
Added notifications, scroll pane and tooltips
Added new utils classes, LabelUtils, LoggingUtils, StringUtils
Added custom implementation of a circular fifo queue, CircularQueue
Added missing documentation here and there
Added logging capabilities, using log4j2
MFXHLoader and MFXVLoader, threads timeout decreased, thread names set to "MFXHLoaderThread" and "MFXVLoaderThread"
MFXCheckboxSkin removed unnecessary variables

HexToRGBColor renamed to ColorUtils, fixed rgba method and added a new method to get random colors
Loader renamed to LoaderUtils
NodeUtils, added method to make a region circular
MFXResourcesManager added new SVGPath

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

PAlex404 4 سال پیش
والد
کامیت
b946fcffc5
50فایلهای تغییر یافته به همراه1875 افزوده شده و 95 حذف شده
  1. 1 0
      .gitignore
  2. 2 2
      README.md
  3. 1 1
      build.gradle
  4. 6 0
      demo/build.gradle
  5. 2 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java
  6. 130 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/NotificationsController.java
  7. 34 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ScrollPaneDemoController.java
  8. 2 5
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TogglesController.java
  9. 2 2
      demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.css
  10. 1 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/notifications_demo.css
  11. 42 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/notifications_demo.fxml
  12. 12 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpane_demo.css
  13. 49 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/scrollpane_demo.fxml
  14. BIN
      demo/src/main/resources/logo.ico
  15. 2 0
      materialfx/build.gradle
  16. 1 1
      materialfx/gradle.properties
  17. 2 1
      materialfx/src/main/java/io/github/palexdev/materialfx/MFXResourcesManager.java
  18. 64 0
      materialfx/src/main/java/io/github/palexdev/materialfx/collections/CircularQueue.java
  19. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java
  20. 6 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDialog.java
  21. 9 8
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXHLoader.java
  22. 175 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotification.java
  23. 224 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXScrollPane.java
  24. 3 4
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleButton.java
  25. 12 9
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java
  26. 86 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTooltip.java
  27. 8 7
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXVLoader.java
  28. 224 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/SimpleMFXNotificationPane.java
  29. 5 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXDialog.java
  30. 70 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXNotificationPane.java
  31. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXAnimationFactory.java
  32. 6 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXDialogFactory.java
  33. 4 5
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXStageDialogFactory.java
  34. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/DepthLevel.java
  35. 6 1
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/RippleGenerator.java
  36. 6 0
      materialfx/src/main/java/io/github/palexdev/materialfx/notifications/NotificationPos.java
  37. 106 0
      materialfx/src/main/java/io/github/palexdev/materialfx/notifications/NotificationsManager.java
  38. 212 0
      materialfx/src/main/java/io/github/palexdev/materialfx/notifications/PositionManager.java
  39. 8 15
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCheckboxSkin.java
  40. 45 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/ColorUtils.java
  41. 0 24
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/HexToRGBColor.java
  42. 48 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LabelUtils.java
  43. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoaderUtils.java
  44. 48 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoggingUtils.java
  45. 36 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java
  46. 53 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java
  47. 8 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/ToggleButtonsUtil.java
  48. 3 0
      materialfx/src/main/java/module-info.java
  49. 15 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-notification.css
  50. 89 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-scrollpane.css

+ 1 - 0
.gitignore

@@ -10,5 +10,6 @@ out/
 !gradle-wrapper.jar
 
 # Others
+demo/src/main/java/io/github/palexdev/materialfx/demo/TestDemo.java
 demo/src/test
 materialfx/src/test

+ 2 - 2
README.md

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

+ 1 - 1
build.gradle

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

+ 6 - 0
demo/build.gradle

@@ -22,4 +22,10 @@ application {
 
 jlink {
     imageZip = file("$buildDir/$rootProject.name-$rootProject.version" + '.zip')
+    launcher {
+        name = 'MaterialFX Demo'
+    }
+    jpackage {
+        imageOptions = ['--icon', 'src/main/resources/logo.ico']
+    }
 }

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

@@ -55,6 +55,8 @@ public class DemoController implements Initializable {
         hLoader.addItem(1, "CHECKBOXES", new MFXToggleNode("CHECKBOXES"), MFXResourcesLoader.load("checkboxes_demo.fxml"));
         hLoader.addItem(2, "TOGGLES", new MFXToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_demo.fxml"));
         hLoader.addItem(3, "DIALOGS", new MFXToggleNode("DIALOGS"), MFXResourcesLoader.load("dialogs_demo.fxml"), controller -> new DialogsController(demoPane));
+        hLoader.addItem(4, "NOTIFICATIONS", new MFXToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml"));
+        hLoader.addItem(5, "SCROLLPANE", new MFXToggleNode("SCROLLPANE"), MFXResourcesLoader.load("scrollpane_demo.fxml"));
         hLoader.setDefault("BUTTONS");
 
         /*

+ 130 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/NotificationsController.java

@@ -0,0 +1,130 @@
+package io.github.palexdev.materialfx.demo.controllers;
+
+import io.github.palexdev.materialfx.controls.MFXDialog;
+import io.github.palexdev.materialfx.controls.MFXNotification;
+import io.github.palexdev.materialfx.controls.SimpleMFXNotificationPane;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXDialog;
+import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
+import io.github.palexdev.materialfx.notifications.NotificationPos;
+import io.github.palexdev.materialfx.notifications.NotificationsManager;
+import javafx.fxml.FXML;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javafx.util.Duration;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.util.Random;
+
+public class NotificationsController {
+    private final Random random = new Random(System.currentTimeMillis());
+
+    private final String title = "MaterialFX Notification System";
+    private final String dummy =
+            "Lorem Ipsum is simply dummy text of the printing and typesetting industry. " +
+                    "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, " +
+                    "when an unknown printer took a galley of type and scrambled it to make a type specimen book. " +
+                    "It has survived not only five centuries, but also the leap into electronic typesetting, " +
+                    "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, " +
+                    "and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
+
+    @FXML
+    void showTopLeft() {
+        NotificationPos pos = NotificationPos.TOP_LEFT;
+        showNotification(pos);
+    }
+
+    @FXML
+    void showTopCenter() {
+        NotificationPos pos = NotificationPos.TOP_CENTER;
+        showNotification(pos);
+    }
+
+    @FXML
+    void showTopRight() {
+        NotificationPos pos = NotificationPos.TOP_RIGHT;
+        showNotification(pos);
+    }
+
+    @FXML
+    void showBottomLeft() {
+        NotificationPos pos = NotificationPos.BOTTOM_LEFT;
+        showNotification(pos);
+    }
+
+    @FXML
+    void showBottomCenter() {
+        NotificationPos pos = NotificationPos.BOTTOM_CENTER;
+        showNotification(pos);
+    }
+
+    @FXML
+    void showBottomRight() {
+        NotificationPos pos = NotificationPos.BOTTOM_RIGHT;
+        showNotification(pos);
+    }
+
+    private void showNotification(NotificationPos pos) {
+        MFXNotification notification = buildNotification();
+        NotificationsManager.send(pos, notification);
+    }
+
+    private MFXNotification buildNotification() {
+        Region template = getRandomTemplate();
+        MFXNotification notification = new MFXNotification(template, true, true);
+        notification.setHideAfterDuration(Duration.seconds(3));
+
+        if (template instanceof SimpleMFXNotificationPane) {
+            SimpleMFXNotificationPane pane = (SimpleMFXNotificationPane) template;
+            pane.setCloseHandler(closeEvent -> notification.hideNotification());
+        } else {
+            MFXDialog dialog = (MFXDialog) template;
+            dialog.setCloseHandler(closeEvent -> notification.hideNotification());
+        }
+
+        return notification;
+    }
+
+    private Region getRandomTemplate() {
+        final int rand = random.nextInt(4);
+
+        switch (rand) {
+            case 0:
+                FontIcon icon1 = new FontIcon("fas-info-circle");
+                icon1.setIconColor(Color.LIGHTBLUE);
+                icon1.setIconSize(15);
+                return new SimpleMFXNotificationPane(
+                        icon1,
+                        "Dummy Notification",
+                        title,
+                        dummy
+                );
+            case 1:
+                FontIcon icon2 = new FontIcon("fas-cocktail");
+                icon2.setIconColor(Color.GREEN);
+                icon2.setIconSize(15);
+                return new SimpleMFXNotificationPane(
+                        icon2,
+                        "Fast Food",
+                        title,
+                        "Hello username, your order is on the way!"
+                );
+            case 2:
+                FontIcon icon3 = new FontIcon("fab-whatsapp");
+                icon3.setIconColor(Color.GREEN);
+                icon3.setIconSize(15);
+                return new SimpleMFXNotificationPane(
+                        icon3,
+                        "Whatsapp Notification",
+                        title,
+                        "Hi Mark, it's been ages since we last spoke!\nHow are you?"
+                );
+            case 3:
+                AbstractMFXDialog dialog = MFXDialogFactory.buildDialog(DialogType.WARNING, "Warning Dialog as Notification", "Disk space is running low, better watch out...");
+                dialog.setVisible(true);
+                return dialog;
+            default:
+                return null;
+        }
+    }
+}

+ 34 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ScrollPaneDemoController.java

@@ -0,0 +1,34 @@
+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;
+
+public class ScrollPaneDemoController {
+
+    @FXML
+    private MFXScrollPane scrollPaneV;
+
+    @FXML
+    private MFXScrollPane scrollPaneVH;
+
+    @FXML
+    void setRandomTrackColor() {
+        scrollPaneV.setTrackColor(ColorUtils.getRandomColor());
+        scrollPaneVH.setTrackColor(ColorUtils.getRandomColor());
+    }
+
+    @FXML
+    void setRandomThumbColor() {
+        scrollPaneV.setThumbColor(ColorUtils.getRandomColor());
+        scrollPaneVH.setThumbColor(ColorUtils.getRandomColor());
+    }
+
+    @FXML
+    void setRandomThumbHoverColor() {
+        scrollPaneV.setThumbHoverColor(ColorUtils.getRandomColor());
+        scrollPaneVH.setThumbHoverColor(ColorUtils.getRandomColor());
+    }
+
+}

+ 2 - 5
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TogglesController.java

@@ -1,20 +1,17 @@
 package io.github.palexdev.materialfx.demo.controllers;
 
 import io.github.palexdev.materialfx.controls.MFXToggleButton;
+import io.github.palexdev.materialfx.utils.ColorUtils;
 import javafx.fxml.FXML;
-import javafx.scene.paint.Color;
-
-import java.util.Random;
 
 public class TogglesController {
-    private final Random random = new Random(System.currentTimeMillis());
 
     @FXML
     private MFXToggleButton toggleButton;
 
     @FXML
     private void handleButtonClick() {
-        toggleButton.setToggleColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
+        toggleButton.setToggleColor(ColorUtils.getRandomColor());
         toggleButton.setSelected(false);
     }
 }

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

@@ -32,12 +32,12 @@
     -fx-border-insets: -1;
     -fx-opacity: 0.6;
     -mfx-shape: rectangle;
-    -mfx-size: 30px;
+    -mfx-size: 27px;
 }
 
 #hLoader .mfx-toggle-node .text {
     -fx-font-family: 'Open Sans SemiBold';
-    -fx-font-size: 12.5;
+    -fx-font-size: 9;
     -fx-fill: white;
     -fx-opacity: 0.5;
 }

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

@@ -0,0 +1 @@
+@import url("common.css");

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

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+<?import io.github.palexdev.materialfx.controls.*?>
+<?import javafx.geometry.Insets?>
+<?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="@notifications_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.NotificationsController">
+   <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Notifications Positions"
+          StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="16.0"/>
+      </StackPane.margin>
+   </Label>
+   <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" prefHeight="50.0" prefWidth="200.0"
+         spacing="35.0" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="60.0"/>
+      </StackPane.margin>
+      <MFXButton buttonType="RAISED" onAction="#showTopLeft" rippleColor="#c8c8c8" rippleRadius="30.0"
+                 text="TOP LEFT"/>
+      <MFXButton buttonType="RAISED" onAction="#showTopCenter" rippleColor="#c8c8c8" rippleRadius="30.0"
+                 text="TOP CENTER"/>
+      <MFXButton buttonType="RAISED" onAction="#showTopRight" rippleColor="#c8c8c8" rippleRadius="30.0"
+                 text="TOP RIGHT"/>
+   </HBox>
+   <HBox alignment="CENTER" maxHeight="-Infinity" prefHeight="50.0" prefWidth="200.0" spacing="35.0"
+         StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="120.0"/>
+      </StackPane.margin>
+      <MFXButton buttonType="RAISED" onAction="#showBottomLeft" rippleColor="#c8c8c8" rippleRadius="35.0"
+                 text="BOTTOM LEFT"/>
+      <MFXButton buttonType="RAISED" onAction="#showBottomCenter" rippleColor="#c8c8c8" rippleRadius="35.0"
+                 text="BOTTOM CENTER"/>
+      <MFXButton buttonType="RAISED" onAction="#showBottomRight" rippleColor="#c8c8c8" rippleRadius="35.0"
+                 text="BOTTOM RIGHT"/>
+   </HBox>
+</StackPane>

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

@@ -0,0 +1,12 @@
+@import url("common.css");
+
+.label {
+    -fx-background-color: white;
+    -fx-background-radius: 0;
+    -fx-border-color: transparent;
+    -fx-border-radius: 0;
+    -fx-border-width: 0;
+    -fx-font-family: System;
+    -fx-font-size: 12;
+    -fx-text-fill: black;
+}

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

@@ -0,0 +1,49 @@
+<?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>

BIN
demo/src/main/resources/logo.ico


+ 2 - 0
materialfx/build.gradle

@@ -18,6 +18,8 @@ compileJava   {
 dependencies {
     testImplementation('junit:junit:4.13')
     implementation 'com.vanniktech:gradle-maven-publish-plugin:0.13.0'
+    implementation 'org.apache.logging.log4j:log4j-api:2.14.0'
+    implementation 'org.apache.logging.log4j:log4j-core:2.14.0'
 }
 
 javadoc {

+ 1 - 1
materialfx/gradle.properties

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

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

@@ -40,7 +40,8 @@ public class MFXResourcesManager {
         EXCLAMATION("M 51.2 8 c 23.7242 0 43.2 19.215 43.2 43.2 c 0 23.8582 -19.322 43.2 -43.2 43.2 c -23.8488 0 -43.2 -19.3124 -43.2 -43.2 c 0 -23.8406 19.3204 -43.2 43.2 -43.2 m 0 -6.4 C 23.8086 1.6 1.6 23.8166 1.6 51.2 c 0 27.3994 22.2086 49.6 49.6 49.6 s 49.6 -22.2006 49.6 -49.6 C 100.8 23.8166 78.5914 1.6 51.2 1.6 z m -2.298 24 h 4.5958 c 1.3646 0 2.4548 1.1364 2.398 2.5 l -1.4 33.6 c -0.0536 1.2856 -1.1112 2.3 -2.398 2.3 h -1.7958 c -1.2866 0 -2.3444 -1.0146 -2.398 -2.3 l -1.4 -33.6 c -0.0566 -1.3636 1.0334 -2.5 2.398 -2.5 z M 51.2 68 c -3.0928 0 -5.6 2.5072 -5.6 5.6 s 2.5072 5.6 5.6 5.6 s 5.6 -2.5072 5.6 -5.6 s -2.5072 -5.6 -5.6 -5.6 z"),
         EXCLAMATION_TRIANGLE("M 54.04 32 h 7.1 c 0.68 0 1.22 0.56 1.2 1.24 l -1.5 39.2 c -0.02 0.64 -0.56 1.16 -1.2 1.16 h -4.1 c -0.64 0 -1.18 -0.5 -1.2 -1.16 l -1.5 -39.2 c -0.02 -0.68 0.52 -1.24 1.2 -1.24 z M 57.6 77.6 c -3.1 0 -5.6 2.5 -5.6 5.6 s 2.5 5.6 5.6 5.6 s 5.6 -2.5 5.6 -5.6 s -2.5 -5.6 -5.6 -5.6 z m 56.3 10.4 L 65.92 4.8 c -3.68 -6.4 -12.94 -6.4 -16.64 0 L 1.3 88 c -3.68 6.38 0.92 14.4 8.32 14.4 H 105.6 c 7.36 0 12 -8 8.3 -14.4 z M 105.6 96 H 9.6 c -2.46 0 -4 -2.66 -2.78 -4.8 l 48 -83.2 c 1.22 -2.12 4.32 -2.14 5.54 0 l 48 83.2 c 1.24 2.12 -0.3 4.8 -2.76 4.8 z"),
         INFO("M 51.2 8 c 23.7242 0 43.2 19.215 43.2 43.2 c 0 23.8582 -19.322 43.2 -43.2 43.2 c -23.8488 0 -43.2 -19.3124 -43.2 -43.2 c 0 -23.8406 19.3204 -43.2 43.2 -43.2 m 0 -6.4 C 23.8086 1.6 1.6 23.8166 1.6 51.2 c 0 27.3994 22.2086 49.6 49.6 49.6 s 49.6 -22.2006 49.6 -49.6 C 100.8 23.8166 78.5914 1.6 51.2 1.6 z m -7.2 68.8 h 2.4 V 46.4 h -2.4 c -1.3254 0 -2.4 -1.0746 -2.4 -2.4 v -1.6 c 0 -1.3254 1.0746 -2.4 2.4 -2.4 h 9.6 c 1.3254 0 2.4 1.0746 2.4 2.4 v 28 h 2.4 c 1.3254 0 2.4 1.0746 2.4 2.4 v 1.6 c 0 1.3254 -1.0746 2.4 -2.4 2.4 h -14.4 c -1.3254 0 -2.4 -1.0746 -2.4 -2.4 v -1.6 c 0 -1.3254 1.0746 -2.4 2.4 -2.4 z m 7.2 -48 c -3.5346 0 -6.4 2.8654 -6.4 6.4 s 2.8654 6.4 6.4 6.4 s 6.4 -2.8654 6.4 -6.4 s -2.8654 -6.4 -6.4 -6.4 z"),
-        X("M 48.544 51.2 l 20.014 -20.014 c 2.456 -2.456 2.456 -6.438 0 -8.896 l -4.448 -4.448 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 35.2 37.856 L 15.186 17.842 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 1.842 22.29 c -2.456 2.456 -2.456 6.438 0 8.896 L 21.856 51.2 L 1.842 71.214 c -2.456 2.456 -2.456 6.438 0 8.896 l 4.448 4.448 c 2.456 2.456 6.44 2.456 8.896 0 L 35.2 64.544 l 20.014 20.014 c 2.456 2.456 6.44 2.456 8.896 0 l 4.448 -4.448 c 2.456 -2.456 2.456 -6.438 0 -8.896 L 48.544 51.2 z");
+        X("M 48.544 51.2 l 20.014 -20.014 c 2.456 -2.456 2.456 -6.438 0 -8.896 l -4.448 -4.448 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 35.2 37.856 L 15.186 17.842 c -2.456 -2.456 -6.438 -2.456 -8.896 0 L 1.842 22.29 c -2.456 2.456 -2.456 6.438 0 8.896 L 21.856 51.2 L 1.842 71.214 c -2.456 2.456 -2.456 6.438 0 8.896 l 4.448 4.448 c 2.456 2.456 6.44 2.456 8.896 0 L 35.2 64.544 l 20.014 20.014 c 2.456 2.456 6.44 2.456 8.896 0 l 4.448 -4.448 c 2.456 -2.456 2.456 -6.438 0 -8.896 L 48.544 51.2 z"),
+        ANGLE_DOWN("M151.5 347.8L3.5 201c-4.7-4.7-4.7-12.3 0-17l19.8-19.8c4.7-4.7 12.3-4.7 17 0L160 282.7l119.7-118.5c4.7-4.7 12.3-4.7 17 0l19.8 19.8c4.7 4.7 4.7 12.3 0 17l-148 146.8c-4.7 4.7-12.3 4.7-17 0z");
 
         private final String svgPath;
 

+ 64 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/collections/CircularQueue.java

@@ -0,0 +1,64 @@
+package io.github.palexdev.materialfx.collections;
+
+import java.util.LinkedList;
+
+/**
+ * This is the implementation of a circular FIFO queue.
+ * When the maximum size is reached the oldest element is removed and replaced
+ * by the new one.
+ */
+public class CircularQueue<E> extends LinkedList<E> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private int size;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public CircularQueue(int size) {
+        super();
+        this.size = size;
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Sets the maximum size of the queue and removes exceeding elements
+     * if the specified size is lesser than the number of elements.
+     * @param size The new desired size
+     * @throws IllegalArgumentException if the desired size is 0
+     */
+    public void setSize(int size) {
+        if (size == 0) {
+            throw new IllegalArgumentException("Size cannot be 0!!");
+        }
+
+        if (size < super.size()) {
+            for (int i = 0; i < (super.size() - size); i++) {
+                super.remove();
+            }
+        }
+        this.size = size;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    /**
+     * Adds the specified element to the queue and if it is full removes the oldest element
+     * and then adds the new one.
+     * <p></p>
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean add(E e) {
+        if (super.size() == this.size) {
+            super.remove();
+        }
+        return super.add(e);
+    }
+}

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

@@ -24,7 +24,7 @@ import java.util.List;
  * This is the implementation of a button following Google's material design guidelines in JavaFX.
  * <p>
  * Extends {@code Button}, redefines the style class to "mfx-button" for usage in CSS and
- * includes a {@code RippleGenerator} to generate ripple effect on click.
+ * includes a {@code RippleGenerator} to generate ripple effects on click.
  */
 public class MFXButton extends Button {
     //================================================================================
@@ -71,7 +71,7 @@ public class MFXButton extends Button {
         setRippleColor(Color.rgb(190, 190, 190));
     }
 
-//================================================================================
+    //================================================================================
     // Ripple properties
     //================================================================================
 
@@ -154,7 +154,7 @@ public class MFXButton extends Button {
     }
 
     //================================================================================
-    // Stylesheet properties
+    // Styleable Properties
     //================================================================================
 
     /**

+ 6 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDialog.java

@@ -10,6 +10,12 @@ import javafx.scene.layout.AnchorPane;
 import javafx.scene.layout.Pane;
 import javafx.util.Duration;
 
+/**
+ * This is the implementation of a dialog following Google's material design guidelines in JavaFX.
+ * <p>
+ * It's a concrete implementation of {@code AbstractMFXDialog} and redefines the style class to "mfx-dialog"
+ * for usage in CSS.
+ */
 public class MFXDialog extends AbstractMFXDialog {
     //================================================================================
     // Properties

+ 9 - 8
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXHLoader.java

@@ -2,7 +2,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.beans.MFXLoadItem;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.utils.Loader;
+import io.github.palexdev.materialfx.utils.LoaderUtils;
 import io.github.palexdev.materialfx.utils.ToggleButtonsUtil;
 import javafx.application.Platform;
 import javafx.beans.property.BooleanProperty;
@@ -81,11 +81,12 @@ public class MFXHLoader extends HBox {
         this.executor = new ThreadPoolExecutor(
                 2,
                 4,
-                20,
+                10,
                 TimeUnit.SECONDS,
                 new LinkedBlockingDeque<>(),
                 runnable -> {
                     Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                    t.setName("MFXHLoaderThread");
                     t.setDaemon(true);
                     return t;
                 }
@@ -156,8 +157,8 @@ public class MFXHLoader extends HBox {
      * @param fxmlFile The given fxml file
      */
     public void addItem(int index, ToggleButton button, URL fxmlFile) {
-        Loader.checkFxmlFile(fxmlFile);
-        addItem(index, Loader.generateKey(fxmlFile), button, fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
+        addItem(index, LoaderUtils.generateKey(fxmlFile), button, fxmlFile);
     }
 
     /**
@@ -170,7 +171,7 @@ public class MFXHLoader extends HBox {
      * @param fxmlFile The given fxml file
      */
     public void addItem(int index, String key, ToggleButton button, URL fxmlFile) {
-        Loader.checkFxmlFile(fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
         this.getChildren().add(button);
         button.setToggleGroup(toggleGroup);
         ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);
@@ -187,8 +188,8 @@ public class MFXHLoader extends HBox {
      * @param controllerFactory The given controller factory
      */
     public void addItem(int index, ToggleButton button, URL fxmlFile, Callback<Class<?>, Object> controllerFactory) {
-        Loader.checkFxmlFile(fxmlFile);
-        addItem(index, Loader.generateKey(fxmlFile), button, fxmlFile, controllerFactory);
+        LoaderUtils.checkFxmlFile(fxmlFile);
+        addItem(index, LoaderUtils.generateKey(fxmlFile), button, fxmlFile, controllerFactory);
     }
 
     /**
@@ -202,7 +203,7 @@ public class MFXHLoader extends HBox {
      * @param controllerFactory The given controller factory
      */
     public void addItem(int index, String key, ToggleButton button, URL fxmlFile, Callback<Class<?>, Object> controllerFactory) {
-        Loader.checkFxmlFile(fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
         this.getChildren().add(button);
         button.setToggleGroup(toggleGroup);
         ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);

+ 175 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotification.java

@@ -0,0 +1,175 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import javafx.animation.Animation;
+import javafx.animation.PauseTransition;
+import javafx.animation.Timeline;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.scene.layout.Region;
+import javafx.stage.Popup;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+/**
+ * This is the implementation of a popup notification in JavaFX.
+ * <p>
+ * Extends {@code Popup}, provides animations for showing and closing and allows to
+ * close automatically the notification after some specified time.
+ */
+public class MFXNotification extends Popup {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private Region content;
+    private boolean animate = false;
+    private final BooleanProperty hideAfter = new SimpleBooleanProperty(false);
+
+    private Timeline inAnimation;
+    private Timeline outAnimation;
+    private PauseTransition hideAfterTransition;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXNotification(Region content) {
+        setAutoFix(false);
+        this.content = content;
+        this.getContent().add(content);
+        initialize();
+    }
+
+    public MFXNotification(Region content, boolean animate) {
+        this(content);
+        this.animate = animate;
+    }
+
+    public MFXNotification(Region content, boolean animate, boolean hideAfter) {
+        this(content, animate);
+        this.hideAfter.set(hideAfter);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        this.inAnimation = MFXAnimationFactory.FADE_IN.build(content, 600);
+        this.outAnimation = MFXAnimationFactory.FADE_OUT.build(content, 600);
+        this.hideAfterTransition = new PauseTransition(Duration.seconds(4));
+
+        this.hideAfter.addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                addMouseHandlers();
+            } else {
+                removeMouseHandlers();
+            }
+        });
+    }
+
+    /**
+     * Closes the notification, plays out animation if requested.
+     *  <p>
+     *  <b>Note: this method should be used rather than Popup's hide() method</b>
+     */
+    public void hideNotification() {
+        if (animate) {
+            outAnimation.setOnFinished(event -> super.hide());
+            outAnimation.play();
+        } else {
+            super.hide();
+        }
+    }
+
+    /**
+     * If the notification is set to hide automatically this method is called.
+     * <p>
+     * Adds MouseEntered and MouseExited handlers to stop/restart the
+     * close countdown when mouse is on/off the notification content.
+     */
+    private void addMouseHandlers() {
+        this.content.setOnMouseEntered(event -> {
+            outAnimation.stop();
+            hideAfterTransition.stop();
+            this.content.setOpacity(1.0);
+        });
+        this.content.setOnMouseExited(event -> {
+            if (hideAfterTransition.getStatus().equals(Animation.Status.STOPPED)) {
+                hideAfterTransition.playFromStart();
+            }
+        });
+    }
+
+    /**
+     * If the notification is set to not hide automatically this method is called.
+     * Removes the handlers for MouseEntered and MouseExited
+     */
+    private void removeMouseHandlers() {
+        this.content.setOnMouseEntered(null);
+        this.content.setOnMouseExited(null);
+    }
+
+    /**
+     * Returns the notification's content
+     */
+    public Region getNotificationContent() {
+        return content;
+    }
+
+    /**
+     * Sets the notification's content and re-initializes the object.
+     */
+    public void setContent(Region content) {
+        this.content = content;
+        this.getContent().add(content);
+        initialize();
+    }
+
+    public void setAnimate(boolean animate) {
+        this.animate = animate;
+    }
+
+    public void setHideAfter(boolean hideAfter) {
+        this.hideAfter.set(hideAfter);
+    }
+
+    public void setHideAfterDuration(Duration hideAfterDuration) {
+        this.hideAfterTransition.setDuration(hideAfterDuration);
+    }
+
+    public void setInAnimation(Timeline inAnimation) {
+        this.inAnimation = inAnimation;
+    }
+
+    public void setOutAnimation(Timeline outAnimation) {
+        this.outAnimation = outAnimation;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    /**
+     * Shows the notification on screen, plays in animation if requested,
+     * starts the close countdown if it's set to hide automatically.
+     * @param ownerWindow The owner of the popup. This must not be null.
+     * @param anchorX The x position of the popup anchor in screen coordinates
+     * @param anchorY The y position of the popup anchor in screen coordinates
+     * @throws NullPointerException if content is null
+     */
+    @Override
+    public void show(Window ownerWindow, double anchorX, double anchorY) {
+        if (content == null) {
+            throw new NullPointerException("Notification content is null!!");
+        }
+
+        if (animate) {
+            inAnimation.play();
+        }
+        super.show(ownerWindow, anchorX, anchorY);
+
+        if (hideAfter.get()) {
+            hideAfterTransition.setOnFinished(event -> hideNotification());
+            hideAfterTransition.play();
+        }
+    }
+}

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

@@ -0,0 +1,224 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.EventHandler;
+import javafx.geometry.Bounds;
+import javafx.scene.Node;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.util.Duration;
+
+import java.util.function.Function;
+
+/**
+ * This is the implementation of a scroll pane following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code ScrollPane} and redefines the style class to "mfx-scroll-pane" for usage in CSS.
+ */
+public class MFXScrollPane extends ScrollPane {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-scroll-pane";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-scrollpane.css").toString();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXScrollPane() {
+        initialize();
+    }
+
+    public MFXScrollPane(Node content) {
+        super(content);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        addListeners();
+    }
+
+    //================================================================================
+    // Style 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));
+
+    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);
+    }
+
+    /**
+     * Adds listeners for colors change 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());
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+    private static void customScrolling(ScrollPane scrollPane, DoubleProperty scrollDirection, Function<Bounds, Double> sizeFunc) {
+        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
+        final double[] pushes = {1};
+        final double[] derivatives = new double[frictions.length];
+
+        Timeline timeline = new Timeline();
+        final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
+        final EventHandler<ScrollEvent> scrollHandler = event -> {
+            if (event.getEventType() == ScrollEvent.SCROLL) {
+                int direction = event.getDeltaY() > 0 ? -1 : 1;
+                for (int i = 0; i < pushes.length; i++) {
+                    derivatives[i] += direction * pushes[i];
+                }
+                if (timeline.getStatus() == Animation.Status.STOPPED) {
+                    timeline.play();
+                }
+                event.consume();
+            }
+        };
+        if (scrollPane.getContent().getParent() != null) {
+            scrollPane.getContent().getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
+            scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
+        }
+        scrollPane.getContent().parentProperty().addListener((o,oldVal, newVal)->{
+            if (oldVal != null) {
+                oldVal.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
+                oldVal.removeEventHandler(ScrollEvent.ANY, scrollHandler);
+            }
+            if (newVal != null) {
+                newVal.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
+                newVal.addEventHandler(ScrollEvent.ANY, scrollHandler);
+            }
+        });
+        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
+            for (int i = 0; i < derivatives.length; i++) {
+                derivatives[i] *= frictions[i];
+            }
+            for (int i = 1; i < derivatives.length; i++) {
+                derivatives[i] += derivatives[i - 1];
+            }
+            double dy = derivatives[derivatives.length - 1];
+            double size = sizeFunc.apply(scrollPane.getContent().getLayoutBounds());
+            scrollDirection.set(Math.min(Math.max(scrollDirection.get() + dy / size, 0), 1));
+            if (Math.abs(dy) < 0.001) {
+                timeline.stop();
+            }
+        }));
+        timeline.setCycleCount(Animation.INDEFINITE);
+    }
+
+    /**
+     * Adds smooth vertical scrolling to the specified scroll pane.
+     * <p>
+     * <b>Note: not recommended for small scroll panes</b>
+     */
+    public static void smoothVScrolling(ScrollPane scrollPane) {
+        customScrolling(scrollPane, scrollPane.vvalueProperty(), Bounds::getHeight);
+    }
+
+    /**
+     * Adds smooth horizontal scrolling to the specified scroll pane.
+     * <p>
+     * <b>Note: not recommended for small scroll panes</b>
+     */
+    public static void smoothHScrolling(ScrollPane scrollPane) {
+        customScrolling(scrollPane, scrollPane.hvalueProperty(), Bounds::getWidth);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+}

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

@@ -60,7 +60,7 @@ public class MFXToggleButton extends ToggleButton {
     }
 
     //================================================================================
-    // Styleable properties
+    // Styleable Properties
     //================================================================================
 
     /**
@@ -249,14 +249,13 @@ public class MFXToggleButton extends ToggleButton {
         }
     }
 
-    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
     //================================================================================
     // Override Methods
     //================================================================================
-
     @Override
     protected Skin<?> createDefaultSkin() {
         return new MFXToggleButtonSkin(this);
@@ -269,6 +268,6 @@ public class MFXToggleButton extends ToggleButton {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return getClassCssMetaData();
+        return this.getControlCssMetaDataList();
     }
 }

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

@@ -3,6 +3,7 @@ package io.github.palexdev.materialfx.controls;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.enums.ToggleNodeShape;
 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;
@@ -16,11 +17,17 @@ import javafx.scene.layout.CornerRadii;
 import javafx.scene.layout.Region;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
-import javafx.scene.shape.Circle;
 import javafx.util.Duration;
 
 import java.util.List;
 
+/**
+ * This control is basically a {@code ToggleButton} but it is mostly used to contain graphic rather than text.
+ * It's also possible to make it appear circular for a modern like design.
+ * <p>
+ * Extends {@code ToggleButton}, redefines the style class to "mfx-toggle-node" for usage in CSS and
+ * includes a {@code RippleGenerator} to generate ripple effects on click.
+ */
 public class MFXToggleNode extends ToggleButton {
     //================================================================================
     // Properties
@@ -109,14 +116,11 @@ public class MFXToggleNode extends ToggleButton {
      */
     private void clip() {
         setClip(null);
-        Circle circle = new Circle(getSize() * 0.5);
-        circle.centerXProperty().bind(widthProperty().divide(2.0));
-        circle.centerYProperty().bind(heightProperty().divide(2.0));
-        setClip(circle);
+        NodeUtils.makeRegionCircular(this);
     }
 
     //================================================================================
-    // Styleable properties
+    // Styleable Properties
     //================================================================================
 
     /**
@@ -251,14 +255,13 @@ public class MFXToggleNode extends ToggleButton {
         }
     }
 
-    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
         return StyleableProperties.cssMetaDataList;
     }
 
     //================================================================================
     // Override Methods
     //================================================================================
-
     @Override
     protected Skin<?> createDefaultSkin() {
         ToggleButtonSkin skin = new ToggleButtonSkin(this);
@@ -278,6 +281,6 @@ public class MFXToggleNode extends ToggleButton {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return getClassCssMetaData();
+        return this.getControlCssMetaDataList();
     }
 }

+ 86 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTooltip.java

@@ -0,0 +1,86 @@
+package io.github.palexdev.materialfx.controls;
+
+import javafx.animation.PauseTransition;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.scene.Node;
+import javafx.scene.control.Tooltip;
+import javafx.util.Duration;
+
+/**
+ * Workaround class to make JavaFX's Tooltips remain open as long as the mouse
+ * in on the Tooltip's node.
+ */
+public class MFXTooltip extends Tooltip {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private double duration = 3600000;
+    private final BooleanProperty isHoveringPrimary = new SimpleBooleanProperty(false);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTooltip() {
+        initialize();
+    }
+
+    public MFXTooltip(String text) {
+        super(text);
+        initialize();
+    }
+
+    public MFXTooltip(String text, Node node) {
+        this(text);
+        isHoveringTargetPrimary(node);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        isHoveringPrimary.addListener((observable, oldValue, newValue) -> {
+            if (!newValue) {
+                hide();
+            }
+        });
+    }
+
+    /**
+     * Registers the MouseEntered and MouseExited handlers on the given node.
+     * @param node The Tooltip's node
+     */
+    public void isHoveringTargetPrimary(Node node) {
+        node.setOnMouseEntered(e -> isHoveringPrimary.set(true));
+        node.setOnMouseExited(e -> isHoveringPrimary.set(false));
+    }
+
+    public void setDuration(double duration) {
+        this.duration = duration;
+    }
+
+    public BooleanProperty isHoveringPrimaryProperty() {
+        return isHoveringPrimary;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    /**
+     * Override of Tooltip's hide method. If the mouse is on the Tooltip's node
+     * then a {@code PauseTransition} with a very long duration is started,
+     * on finish it hides the Tooltip. As soon as the mouse exit the node
+     * the Tooltip is being hidden.
+     */
+    @Override
+    public void hide() {
+        if (isHoveringPrimary.get()) {
+            PauseTransition pauseTransition = new PauseTransition(Duration.millis(duration));
+            pauseTransition.setOnFinished(event -> super.hide());
+            pauseTransition.play();
+        } else {
+            super.hide();
+        }
+    }
+}

+ 8 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXVLoader.java

@@ -2,7 +2,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.beans.MFXLoadItem;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.utils.Loader;
+import io.github.palexdev.materialfx.utils.LoaderUtils;
 import io.github.palexdev.materialfx.utils.ToggleButtonsUtil;
 import javafx.application.Platform;
 import javafx.beans.property.BooleanProperty;
@@ -86,6 +86,7 @@ public class MFXVLoader extends VBox {
                 new LinkedBlockingDeque<>(),
                 runnable -> {
                     Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                    t.setName("MFXVLoaderThread");
                     t.setDaemon(true);
                     return t;
                 }
@@ -156,8 +157,8 @@ public class MFXVLoader extends VBox {
      * @param fxmlFile The given fxml file
      */
     public void addItem(int index, ToggleButton button, URL fxmlFile) {
-        Loader.checkFxmlFile(fxmlFile);
-        addItem(index, Loader.generateKey(fxmlFile), button, fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
+        addItem(index, LoaderUtils.generateKey(fxmlFile), button, fxmlFile);
     }
 
     /**
@@ -170,7 +171,7 @@ public class MFXVLoader extends VBox {
      * @param fxmlFile The given fxml file
      */
     public void addItem(int index, String key, ToggleButton button, URL fxmlFile) {
-        Loader.checkFxmlFile(fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
         this.getChildren().add(button);
         button.setToggleGroup(toggleGroup);
         ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);
@@ -187,8 +188,8 @@ public class MFXVLoader extends VBox {
      * @param controllerFactory The given controller factory
      */
     public void addItem(int index, ToggleButton button, URL fxmlFile, Callback<Class<?>, Object> controllerFactory) {
-        Loader.checkFxmlFile(fxmlFile);
-        addItem(index, Loader.generateKey(fxmlFile), button, fxmlFile, controllerFactory);
+        LoaderUtils.checkFxmlFile(fxmlFile);
+        addItem(index, LoaderUtils.generateKey(fxmlFile), button, fxmlFile, controllerFactory);
     }
 
     /**
@@ -202,7 +203,7 @@ public class MFXVLoader extends VBox {
      * @param controllerFactory The given controller factory
      */
     public void addItem(int index, String key, ToggleButton button, URL fxmlFile, Callback<Class<?>, Object> controllerFactory) {
-        Loader.checkFxmlFile(fxmlFile);
+        LoaderUtils.checkFxmlFile(fxmlFile);
         this.getChildren().add(button);
         button.setToggleGroup(toggleGroup);
         ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);

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

@@ -0,0 +1,224 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesManager.SVGResources;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXNotificationPane;
+import io.github.palexdev.materialfx.utils.LoggingUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.SVGPath;
+
+/**
+ * This class extends {@code AbstractMFXNotificationPane} and it serves as an
+ * example of a basic pane for a {@code MFXNotification}.
+ */
+public class SimpleMFXNotificationPane extends AbstractMFXNotificationPane {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane headerNode;
+    private final Label headerLabel;
+    private final Label titleLabel;
+    private final MFXScrollPane contentScroll;
+    private final Label contentLabel;
+
+    private final MFXButton closeButton;
+    private final MFXButton okButton;
+    private final HBox buttonsBox;
+
+    private EventHandler<MouseEvent> closeHandler;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public SimpleMFXNotificationPane(String header, String title, String content) {
+        this(null, header, title, content);
+    }
+
+    public SimpleMFXNotificationPane(Node icon, String header, String title, String content) {
+        // Header
+        headerNode = new StackPane();
+        headerLabel = new Label();
+        headerLabel.textProperty().bind(headerProperty);
+        headerLabel.getStyleClass().add("header-label");
+        headerLabel.setGraphic(icon);
+        headerLabel.setGraphicTextGap(7);
+        headerLabel.setPadding(new Insets(15, 15, 0, 15));
+        headerProperty.set(header);
+
+        closeButton = new MFXButton("");
+        closeButton.setPrefSize(18, 18);
+        closeButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        SVGPath x = SVGResources.X.getSvgPath();
+        x.setScaleX(0.14);
+        x.setScaleY(0.14);
+        closeButton.setGraphic(x);
+        closeButton.setRippleRadius(12);
+        closeButton.setRippleColor(Color.rgb(255, 0, 0, 0.1));
+
+        NodeUtils.makeRegionCircular(closeButton);
+
+        headerNode.getChildren().addAll(headerLabel, closeButton);
+        StackPane.setMargin(headerLabel, new Insets(0, 0, 10, 0));
+        StackPane.setAlignment(headerLabel, Pos.CENTER_LEFT);
+        StackPane.setAlignment(closeButton, Pos.TOP_RIGHT);
+        StackPane.setMargin(closeButton, new Insets(6, 6, 8, 8));
+
+        // Title
+        titleLabel = new Label();
+        titleLabel.textProperty().bind(titleProperty);
+        titleLabel.getStyleClass().add("title-label");
+        titleLabel.setPadding(new Insets(0, 0, 0, 15));
+        titleProperty.set(title);
+
+        // Content
+        contentScroll = new MFXScrollPane();
+        contentScroll.setFitToWidth(true);
+        contentScroll.setPrefSize(200, 200);
+
+        contentLabel = new Label();
+        contentLabel.textProperty().bind(contentProperty);
+        contentLabel.getStyleClass().add("content-label");
+        contentLabel.setWrapText(true);
+        contentProperty.set(content);
+
+        contentScroll.setContent(contentLabel);
+        contentScroll.setPadding(new Insets(5, 10, 5, 10));
+        VBox.setMargin(contentScroll, new Insets(5, 10, 5, 10));
+
+        // Buttons
+        buttonsBox = new HBox();
+        buttonsBox.getStyleClass().add("buttons-box");
+        buttonsBox.setAlignment(Pos.CENTER_RIGHT);
+        buttonsBox.setSpacing(20);
+        okButton = new MFXButton("OK");
+        okButton.setPrefWidth(50);
+        buttonsBox.getChildren().add(okButton);
+        VBox.setMargin(buttonsBox, new Insets(2, 10, 2, 0));
+
+        getChildren().addAll(headerNode, titleLabel, contentScroll, buttonsBox);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setPrefSize(360, 160);
+        setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+    }
+
+    /**
+     * Adds the specified button the the HBox at the bottom of the VBox.
+     */
+    public void addButton(Button button) {
+        this.buttonsBox.getChildren().add(button);
+    }
+
+    /**
+     * Adds the specified button to the HBox at the bottom of the VBox at the specified index.
+     */
+    public void addButton(int index, Button button) {
+        try {
+            this.buttonsBox.getChildren().add(index, button);
+        } catch (IndexOutOfBoundsException ex) {
+            LoggingUtils.logException(
+                    "Could not add button at index:" + index +
+                            ", list size is:" + this.buttonsBox.getChildren().size(),
+                    ex
+            );
+        }
+    }
+
+    /**
+     *  Since this class has no references to {@code MFXNotification} because they are two distinct and separate concepts,
+     *  the close button action must be set after instantiating a {@code MFXNotification}.
+     */
+    public void setCloseHandler(EventHandler<MouseEvent> closeHandler) {
+        if (this.closeHandler != null) {
+            this.closeButton.removeEventHandler(MouseEvent.MOUSE_PRESSED, this.closeHandler);
+        }
+
+        this.closeHandler = closeHandler;
+        this.closeButton.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
+    }
+
+    public StackPane getHeaderNode() {
+        return headerNode;
+    }
+
+    public Label getHeaderLabel() {
+        return headerLabel;
+    }
+
+    public Label getTitleLabel() {
+        return titleLabel;
+    }
+
+    public MFXScrollPane getContentScroll() {
+        return contentScroll;
+    }
+
+    public Label getContentLabel() {
+        return contentLabel;
+    }
+
+    public MFXButton getCloseButton() {
+        return closeButton;
+    }
+
+    public MFXButton getOkButton() {
+        return okButton;
+    }
+
+    public HBox getButtonsBox() {
+        return buttonsBox;
+    }
+
+    /*
+     * Unused code.
+     * Before using the scroll pane for the content label the header had an extra button,
+     * an expand button similar to Android's notifications, that button was set to be visible
+     * only if the content was truncated and on click the prefHeight was incremented by the specified value with
+     * a Transition, however the problem with this approach was the PositionManager system because as you can see in the above code,
+     * if the content was still truncated at the end of the transition the method was executed again and again until the isTruncated property
+     * was false. The PositionManager had two extra methods, repositionNotifications and buildRepositionAnimation with the expandValue as parameter,
+     * the reposition method had to be recalled every time too with the same frequency as the expandNotificationMethod but as you can see this class
+     * has no references to PositionManager because of course they are two distinct and separate concepts.
+     *
+     * I don't want to delete this code because I still believe it can be implemented in some way, but in the end I opted for a scroll pane
+     * because it was way easier.
+     */
+    /*
+    public void expandNotification(double expandValue) {
+        final double currHeight = getPrefHeight();
+        Transition expand = new Transition() {
+            {
+                setCycleDuration(Duration.millis(0.1));
+            }
+
+            @Override
+            protected void interpolate(double frac) {
+                setPrefHeight(currHeight + (expandValue * frac));
+            }
+        };
+        expand.setOnFinished(event -> {
+            if (isTruncated.get()) {
+                expandNotification(expandValue);
+            }
+        });
+        expand.play();
+    }
+    */
+}

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

@@ -13,6 +13,11 @@ import javafx.scene.layout.BorderPane;
 import javafx.scene.layout.Pane;
 import javafx.scene.layout.Region;
 
+/**
+ * Base class for a material dialog.
+ * <p>
+ * Extends {@code BorderPane}.
+ */
 public abstract class AbstractMFXDialog extends BorderPane {
     //================================================================================
     // Properties

+ 70 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXNotificationPane.java

@@ -0,0 +1,70 @@
+package io.github.palexdev.materialfx.controls.base;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.layout.VBox;
+
+/**
+ * Base class for a material notification content pane.
+ * <p>
+ * Extends {@code VBox} and redefines the style class to "mfx-notification" for usage in CSS.
+ */
+public abstract class AbstractMFXNotificationPane extends VBox {
+    //================================================================================
+    // Properties
+    //================================================================================
+    protected final String STYLE_CLASS = "mfx-notification";
+    protected final String STYLESHEET = MFXResourcesLoader.load("css/mfx-notification.css").toString();
+
+    protected final StringProperty headerProperty = new SimpleStringProperty("");
+    protected final StringProperty titleProperty = new SimpleStringProperty("");
+    protected final StringProperty contentProperty = new SimpleStringProperty("");
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    public String getHeaderProperty() {
+        return headerProperty.get();
+    }
+
+    public StringProperty headerPropertyProperty() {
+        return headerProperty;
+    }
+
+    public void setHeaderProperty(String headerProperty) {
+        this.headerProperty.set(headerProperty);
+    }
+
+    public String getTitleProperty() {
+        return titleProperty.get();
+    }
+
+    public StringProperty titlePropertyProperty() {
+        return titleProperty;
+    }
+
+    public void setTitleProperty(String titleProperty) {
+        this.titleProperty.set(titleProperty);
+    }
+
+    public String getContentProperty() {
+        return contentProperty.get();
+    }
+
+    public StringProperty contentPropertyProperty() {
+        return contentProperty;
+    }
+
+    public void setContentProperty(String contentProperty) {
+        this.contentProperty.set(contentProperty);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

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

@@ -8,7 +8,7 @@ import javafx.scene.Node;
 import javafx.util.Duration;
 
 /**
- * Convenience factory for various animations applied to {@code Node}s
+ * Convenience factory for various animations applied to {@code Node}s.
  *
  * @see Timeline
  */

+ 6 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXDialogFactory.java

@@ -6,6 +6,7 @@ import io.github.palexdev.materialfx.controls.MFXDialog;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXDialog;
 import io.github.palexdev.materialfx.controls.enums.ButtonType;
 import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.geometry.Insets;
 import javafx.geometry.Pos;
 import javafx.scene.control.Label;
@@ -143,8 +144,8 @@ public class MFXDialogFactory {
         headerNode.setStyle("-fx-background-color: " + color + ";\n");
 
         SVGPath closeSvg = SVGResources.X.getSvgPath();
-        closeSvg.setScaleX(0.2);
-        closeSvg.setScaleY(0.2);
+        closeSvg.setScaleX(0.17);
+        closeSvg.setScaleY(0.17);
         closeSvg.setFill(Color.WHITE);
 
         if (dialog.getType() != null && dialog.getType().equals(DialogType.GENERIC)) {
@@ -156,10 +157,12 @@ public class MFXDialogFactory {
         closeButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         closeButton.setGraphic(closeSvg);
         closeButton.setRippleRadius(15);
-        closeButton.setRippleColor(Color.rgb(230, 230, 230, 0.7));
+        closeButton.setRippleColor(Color.rgb(255, 0, 0, 0.1));
         closeButton.setRippleInDuration(Duration.millis(500));
         closeButton.setButtonType(ButtonType.FLAT);
 
+        NodeUtils.makeRegionCircular(closeButton);
+
         StackPane.setAlignment(closeButton, Pos.TOP_RIGHT);
         StackPane.setMargin(closeButton, new Insets(7, 7, 0, 0));
         headerNode.getChildren().addAll(icon, closeButton);

+ 4 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXStageDialogFactory.java

@@ -9,7 +9,7 @@ import javafx.stage.Stage;
 import javafx.stage.StageStyle;
 
 /**
- * Factory class to build {@code MFXStageDialog}s
+ * Factory class to build {@code MFXStageDialog}s.
  */
 public class MFXStageDialogFactory {
 
@@ -18,7 +18,7 @@ public class MFXStageDialogFactory {
     //================================================================================
 
     /**
-     * Builds a MFXStageDialog from type, title and content
+     * Builds a MFXStageDialog from type, title and content.
      * @param type The dialog type
      * @param title The dialog's title
      * @param content The dialog's content
@@ -45,7 +45,7 @@ public class MFXStageDialogFactory {
     }
 
     /**
-     * Builds a MFXStageDialog from an AbstractMFXDialog or subclasses
+     * Builds a MFXStageDialog from an AbstractMFXDialog or subclasses.
      * @param dialog The dialog
      * @return The MFXStageDialog's stage
      */
@@ -64,8 +64,7 @@ public class MFXStageDialogFactory {
     }
 
     /**
-     * Creates a TRANSPARENT {@code Scene} however it doesn't seem to work
-     * so the dialog is clipped with a {@code Rectangle} to keep round corners
+     * Creates a TRANSPARENT {@code Scene}.
      * @param pane The dialog
      * @return The MFXStageDialog scene
      */

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

@@ -3,7 +3,7 @@ package io.github.palexdev.materialfx.effects;
 import javafx.scene.paint.Color;
 
 /**
- * Enumerator which defines 5 levels of {@code DropShadow} effects from {@code LEVEL1} to {@code LEVEL5}.
+ * Enumerator which defines 6 levels of {@code DropShadow} effects from {@code LEVEL0} to {@code LEVEL5}.
  */
 public enum DepthLevel {
     LEVEL0(Color.rgb(0, 0, 0, 0), 0, 0, 0, 0),

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

@@ -17,6 +17,11 @@ import java.util.List;
 
 import static io.github.palexdev.materialfx.effects.MFXDepthManager.shadowOf;
 
+/**
+ * Convenience class for creating highly customizable ripple effects.
+ * <p>
+ * Extends {@code Group} and sets the style class to "ripple-generator" for usage in CSS.
+ */
 public class RippleGenerator extends Group {
     //================================================================================
     // Properties
@@ -266,7 +271,7 @@ public class RippleGenerator extends Group {
     }
 
     //================================================================================
-    // Stylesheet properties
+    // Stylesheet Properties
     //================================================================================
     private static class StyleableProperties {
         private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;

+ 6 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/notifications/NotificationPos.java

@@ -0,0 +1,6 @@
+package io.github.palexdev.materialfx.notifications;
+
+public enum NotificationPos {
+    TOP_LEFT, TOP_CENTER, TOP_RIGHT,
+    BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT
+}

+ 106 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/notifications/NotificationsManager.java

@@ -0,0 +1,106 @@
+package io.github.palexdev.materialfx.notifications;
+
+import io.github.palexdev.materialfx.collections.CircularQueue;
+import io.github.palexdev.materialfx.controls.MFXNotification;
+import javafx.geometry.Rectangle2D;
+import javafx.stage.Screen;
+import javafx.stage.Window;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class is a notification system, its job is to manage the incoming notifications
+ * by sending them to the correct position. It also keeps track of the sent notifications
+ * by storing them in a {@link CircularQueue} with the default size of 20.
+ */
+public class NotificationsManager {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final Rectangle2D screenBounds;
+    private static final Window window;
+    private static final Map<NotificationPos, PositionManager> notifications = new HashMap<>();
+    private static final CircularQueue<MFXNotification> notificationsHistory = new CircularQueue<>(20);
+
+    //================================================================================
+    // Init
+    //================================================================================
+    static {
+        screenBounds = Screen.getPrimary().getVisualBounds();
+        window = getWindow();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Sends a {@code MFXNotification} to the designated {@code PositionManager}
+     * @param pos The notifications' position on screen
+     * @param notification The notification
+     */
+    public static void send(NotificationPos pos, MFXNotification notification) {
+        notifications.computeIfAbsent(pos, notificationPos -> new PositionManager(screenBounds, window, notificationPos));
+        notifications.get(pos).show(notification);
+        notificationsHistory.add(notification);
+    }
+
+    /**
+     * Sends a {@code MFXNotification} to the designated {@code PositionManager} with the specified spacing.
+     * @param pos The notifications' position on screen
+     * @param notification The notification
+     * @param spacing The number of pixels between each shown notification and from screen's left and right borders
+     */
+    public static void send(NotificationPos pos, MFXNotification notification, double spacing) {
+        notifications.computeIfAbsent(pos, notificationPos -> new PositionManager(screenBounds, window, notificationPos));
+        notifications.get(pos).setSpacing(spacing).show(notification);
+        notificationsHistory.add(notification);
+    }
+
+    /**
+     * Sends a {@code MFXNotification} to the designated {@code PositionManager} with the specified limit.
+     * @param pos The notifications' position on screen
+     * @param notification The notification
+     * @param limit The maximum number of notifications to show, if limit is exceeded they will be queued
+     */
+    public static void send(NotificationPos pos, MFXNotification notification, int limit) {
+        notifications.computeIfAbsent(pos, notificationPos -> new PositionManager(screenBounds, window, notificationPos));
+        notifications.get(pos).setLimit(limit).show(notification);
+        notificationsHistory.add(notification);
+    }
+
+    /**
+     * Sends a {@code MFXNotification} to the designated {@code PositionManager} with the specified spacing and limit.
+     * @param pos The notifications' position on screen
+     * @param notification The notification
+     * @param spacing The number of pixels between each shown notification and from screen's left and right borders
+     * @param limit The maximum number of notifications to show, if limit is exceeded they will be queued
+     */
+    public static void send(NotificationPos pos, MFXNotification notification, double spacing, int limit) {
+        notifications.computeIfAbsent(pos, notificationPos -> new PositionManager(screenBounds, window, notificationPos));
+        notifications.get(pos).setSpacing(spacing).setLimit(limit).show(notification);
+        notificationsHistory.add(notification);
+    }
+
+    public static PositionManager getPositionManager(NotificationPos pos) {
+        return notifications.get(pos);
+    }
+
+    public CircularQueue<MFXNotification> getNotificationsHistory() {
+        return notificationsHistory;
+    }
+
+    public static void setHistoryLimit(int size) {
+        notificationsHistory.setSize(size);
+    }
+
+    private static Window getWindow() {
+        for (Window w : Window.getWindows()) {
+            if (w.isFocused()) {
+                return w;
+            }
+        }
+        return null;
+    }
+}

+ 212 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/notifications/PositionManager.java

@@ -0,0 +1,212 @@
+package io.github.palexdev.materialfx.notifications;
+
+import io.github.palexdev.materialfx.controls.MFXNotification;
+import io.github.palexdev.materialfx.utils.LoggingUtils;
+import javafx.animation.Interpolator;
+import javafx.animation.Transition;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.geometry.Rectangle2D;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * Support class for the {@code NotificationManager}.
+ * <p>
+ * Each {@code PositionManager} is responsible for showing the incoming notifications according to its position ({@link #pos}),
+ * and according to its limit ({@link #limit}), which by default is 3. Each notification is distant from each other and from
+ * the left and right borders according to the {@link #spacing} property which by default is 15.
+ * <p>
+ * To simulate the behavior of a queue, a {@link ThreadPoolExecutor} and a {@link Semaphore} are used,
+ * each notification show is committed to a JavaFX's {@link Task} and before any other operation a permit is acquired
+ * from the semaphore. The semaphore has a number of permits equals to the {@link #limit} property of this class,
+ * when the limit is reached and there are no more permits available all other sent notifications are queued and
+ * waiting for a semaphore {@code release()}. The semaphore is released every time a notification is being hidden.
+ */
+public class PositionManager {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final ThreadPoolExecutor service;
+    private final Semaphore semaphore;
+    private final Rectangle2D screenBounds;
+    private final Window owner;
+
+    private final List<MFXNotification> notifications = new ArrayList<>();
+    private double spacing = 15;
+    private int limit = 3;
+
+    private final NotificationPos pos;
+    private double anchorX;
+    private double anchorY;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public PositionManager(Rectangle2D screenBounds, Window owner, NotificationPos pos) {
+        this.service = new ThreadPoolExecutor(
+                1,
+                2,
+                2,
+                TimeUnit.SECONDS,
+                new LinkedBlockingDeque<>(),
+                runnable -> {
+                    Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                    t.setName("MFXNotificationsThread - " + pos.name());
+                    t.setDaemon(true);
+                    return t;
+                }
+        );
+        this.service.allowCoreThreadTimeOut(true);
+        this.semaphore = new Semaphore(limit);
+
+        this.screenBounds = screenBounds;
+        this.owner = owner;
+        this.pos = pos;
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Shows the specified notification on screen.
+     * <p>
+     * The show mechanism uses a {@link ThreadPoolExecutor} with JavaFX's {@link Task}s and
+     * a {@link Semaphore} to make threads wait when the notifications limit is reached,
+     * in a sense it simulates the operation of a queue.
+     * <p>
+     * After the new notification has been added to the list, all others notifications are repositioned,
+     * then anchorX and anchorY are calculated according to the {@link #pos} property.
+     * <p>
+     * After that the notification's show method is called on the JavaFX's thread with the specified owner and anchors.
+     * On hidden the notification is removed from the list and the semaphore is released.
+     * <p></p>
+     * On failed task, logs the exception.
+     *
+     * @param newNotification The notification to show
+     */
+    public void show(MFXNotification newNotification) {
+        Task<Void> showTask = new Task<>() {
+            @Override
+            protected Void call() throws Exception {
+                semaphore.acquire();
+
+                notifications.add(newNotification);
+                repositionNotifications(newNotification);
+
+                newNotification.setOnHidden(event -> {
+                    notifications.remove(newNotification);
+                    semaphore.release();
+                });
+                computePosition(newNotification);
+                Platform.runLater(() -> newNotification.show(owner, anchorX, anchorY));
+
+                return null;
+            }
+        };
+        showTask.setOnFailed(event -> LoggingUtils.logException(showTask.getException()));
+        service.submit(showTask);
+    }
+
+    public PositionManager setSpacing(double spacing) {
+        this.spacing = spacing;
+        return this;
+    }
+
+    public PositionManager setLimit(int limit) {
+        this.limit = limit;
+        this.semaphore.release(limit);
+        return this;
+    }
+
+    /**
+     * Repositions every notification in the list, except the most recent one, with a {@code Transition} animation.
+     */
+    private void repositionNotifications(MFXNotification newNotification) {
+        for (int i = 0; i < notifications.indexOf(newNotification); i++) {
+            MFXNotification oldNotification = notifications.get(i);
+            buildRepositionAnimation(newNotification, oldNotification).play();
+        }
+    }
+
+    /**
+     * Builds the repositioning animation.
+     * <p>
+     * The new anchorY is calculated using the current value and the new notification's content prefHeight.
+     * <b>Note: this works only if the notification's content has it's pref height set</b>
+     * @param newNotification The new notification
+     * @param oldNotification The already showing notification
+     * @return The animation
+     */
+    private Transition buildRepositionAnimation(MFXNotification newNotification, MFXNotification oldNotification) {
+        final double notificationHeight = newNotification.getNotificationContent().getPrefHeight();
+        final double oldAnchorY = oldNotification.getAnchorY();
+
+        switch (pos) {
+            case BOTTOM_LEFT:
+            case BOTTOM_CENTER:
+            case BOTTOM_RIGHT:
+                return new Transition() {
+                    {
+                        setCycleDuration(Duration.millis(350));
+                        setInterpolator(Interpolator.EASE_BOTH);
+                    }
+
+                    @Override
+                    protected void interpolate(double frac) {
+                        final double newAnchorY = (oldAnchorY - notificationHeight) - (spacing * frac);
+                        oldNotification.setAnchorY(newAnchorY);
+                    }
+                };
+            default:
+                return new Transition() {
+                    {
+                        setCycleDuration(Duration.millis(350));
+                        setInterpolator(Interpolator.EASE_BOTH);
+                    }
+
+                    @Override
+                    protected void interpolate(double frac) {
+                        final double newAnchorY = (oldAnchorY + notificationHeight) + (spacing * frac);
+                        oldNotification.setAnchorY(newAnchorY);
+                    }
+                };
+        }
+    }
+
+    /**
+     * Computes the notification coordinates according to the {@link #pos} property.
+     */
+    private void computePosition(MFXNotification notification) {
+        switch (pos) {
+            case TOP_LEFT:
+            case TOP_CENTER:
+            case TOP_RIGHT:
+                anchorY = spacing;
+                break;
+            case BOTTOM_LEFT:
+            case BOTTOM_CENTER:
+            case BOTTOM_RIGHT:
+                anchorY = screenBounds.getHeight() - notification.getNotificationContent().getPrefHeight() - spacing;
+                break;
+        }
+
+        switch (pos) {
+            case TOP_LEFT:
+            case BOTTOM_LEFT:
+                anchorX = spacing;
+                break;
+            case TOP_RIGHT:
+            case BOTTOM_RIGHT:
+                anchorX = screenBounds.getWidth() - notification.getNotificationContent().getPrefWidth() - spacing;
+                break;
+            default:
+                anchorX = (screenBounds.getWidth() / 2) - (notification.getNotificationContent().getPrefWidth() / 2);
+        }
+    }
+}

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

@@ -10,7 +10,6 @@ import javafx.scene.control.skin.CheckBoxSkin;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.*;
 import javafx.scene.paint.Color;
-import javafx.scene.shape.Circle;
 import javafx.scene.shape.SVGPath;
 import javafx.util.Duration;
 
@@ -26,10 +25,8 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
     private final StackPane mark;
     private final RippleGenerator rippleGenerator;
 
-    private final double rippleContainerWidth = 30;
-    private final double rippleContainerHeight = 30;
-    private final double boxWidth = 26;
-    private final double boxHeight = 26;
+    private final double rippleContainerSize = 30;
+    private final double boxSize = 26;
 
     private final double labelOffset = 2;
 
@@ -41,20 +38,16 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
 
         // Contains the ripple generator and the box
         rippleContainer = new AnchorPane();
-        rippleContainer.setPrefSize(rippleContainerWidth, rippleContainerHeight);
+        rippleContainer.setPrefSize(rippleContainerSize, rippleContainerSize);
         rippleContainer.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         rippleContainer.getStyleClass().setAll("ripple-container");
 
         // To make ripple container appear like a Circle
-        Circle circle = new Circle();
-        circle.setCenterX(rippleContainerWidth / 2);
-        circle.setCenterY(rippleContainerHeight / 2);
-        circle.setRadius(rippleContainerWidth * 0.6);
-        rippleContainer.setClip(circle);
+        NodeUtils.makeRegionCircular(rippleContainer, rippleContainerSize * 0.55);
 
-        // Contains the mark which is a SVG path defined in css
+        // Contains the mark which is a SVG path defined in CSS
         box = new StackPane();
-        box.setPrefSize(boxWidth, boxHeight);
+        box.setPrefSize(boxSize, boxSize);
         box.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
         box.getStyleClass().setAll("box");
         box.setBorder(new Border(new BorderStroke(
@@ -167,8 +160,8 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
      */
     private void centerBox() {
         final double offsetPercentage = 3;
-        final double vInset = ((rippleContainerHeight - boxHeight) / 2) * offsetPercentage;
-        final double hInset = ((rippleContainerWidth - boxWidth) / 2) * offsetPercentage;
+        final double vInset = ((rippleContainerSize - boxSize) / 2) * offsetPercentage;
+        final double hInset = ((rippleContainerSize - boxSize) / 2) * offsetPercentage;
         AnchorPane.setTopAnchor(box, vInset);
         AnchorPane.setRightAnchor(box, hInset);
         AnchorPane.setBottomAnchor(box, vInset);

+ 45 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ColorUtils.java

@@ -0,0 +1,45 @@
+package io.github.palexdev.materialfx.utils;
+
+import javafx.scene.paint.Color;
+
+import java.util.Random;
+
+/**
+ * Utils class for JavaFX's {@code Color}s and CSS.
+ */
+public class ColorUtils {
+    private static final Random random = new Random(System.currentTimeMillis());
+
+    private ColorUtils() {
+    }
+
+    /**
+     * Converts a JavaFX's {@code Color} to CSS corresponding rgb function.
+     * @return the rgb function as a string
+     */
+    public static String rgb(Color color) {
+        return String.format("rgb(%d, %d, %d)",
+                (int) (255 * color.getRed()),
+                (int) (255 * color.getGreen()),
+                (int) (255 * color.getBlue()));
+    }
+
+    /**
+     * Converts a JavaFX's {@code Color} to CSS corresponding rgba function.
+     * @return the rgba function as a string
+     */
+    public static String rgba(Color color) {
+        return String.format("rgba(%d, %d, %d, %s)",
+                (int) (255 * color.getRed()),
+                (int) (255 * color.getGreen()),
+                (int) (255 * color.getBlue()),
+                color.getOpacity());
+    }
+
+    /**
+     * Generates a random {@code Color} using java.util.Random.
+     */
+    public static Color getRandomColor() {
+        return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256));
+    }
+}

+ 0 - 24
materialfx/src/main/java/io/github/palexdev/materialfx/utils/HexToRGBColor.java

@@ -1,24 +0,0 @@
-package io.github.palexdev.materialfx.utils;
-
-import javafx.scene.paint.Color;
-
-public class HexToRGBColor {
-
-    private HexToRGBColor() {
-    }
-
-    public static String rgb(Color color) {
-        return String.format("#%02x%02x%02x",
-                (int) (255 * color.getRed()),
-                (int) (255 * color.getGreen()),
-                (int) (255 * color.getBlue()));
-    }
-
-    public static String rgba(Color color) {
-        return String.format("rgba(%d, %d, %d, %f)",
-                (int) (255 * color.getRed()),
-                (int) (255 * color.getGreen()),
-                (int) (255 * color.getBlue()),
-                color.getOpacity());
-    }
-}

+ 48 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/LabelUtils.java

@@ -0,0 +1,48 @@
+package io.github.palexdev.materialfx.utils;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.scene.control.Label;
+import javafx.scene.text.Text;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Utils class for JavaFX's {@code Label}s.
+ */
+public class LabelUtils {
+
+    private LabelUtils() {
+    }
+
+    /**
+     * Checks if the text of the specified {@code Label} is truncated.
+     * @param label The specified label
+     */
+    public static boolean isLabelTruncated(Label label) {
+        AtomicBoolean isTruncated = new AtomicBoolean(false);
+
+        label.needsLayoutProperty().addListener((observable, oldValue, newValue) -> {
+            String originalString = label.getText();
+            Text textNode = (Text) label.lookup(".text");
+            String actualString = textNode.getText();
+            isTruncated.set(!actualString.isEmpty() && !originalString.equals(actualString));
+        });
+        return isTruncated.get();
+    }
+
+    /**
+     * Registers a listener to the specified {@code Label} which checks if the text
+     * is truncated and updates the specified boolean property accordingly.
+     * @param isTruncated The boolean property to change
+     * @param label The specified label
+     */
+    public static void registerTruncatedLabelListener(BooleanProperty isTruncated, Label label) {
+        label.needsLayoutProperty().addListener((observable, oldValue, newValue) -> {
+            String originalString = label.getText();
+            Text textNode = (Text) label.lookup(".text");
+            String actualString = textNode.getText();
+
+            isTruncated.set(!actualString.isEmpty() && !originalString.equals(actualString));
+        });
+    }
+}

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

@@ -5,9 +5,9 @@ import java.net.URL;
 /**
  * Convenience class to avoid duplicated code in {@code MFXHLoader} and {@code MFXVLoader} classes
  */
-public class Loader {
+public class LoaderUtils {
 
-    private Loader() {
+    private LoaderUtils() {
     }
 
     /**

+ 48 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoggingUtils.java

@@ -0,0 +1,48 @@
+package io.github.palexdev.materialfx.utils;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Utils class for logging.
+ */
+public class LoggingUtils {
+    public static final Logger logger = LogManager.getLogger("MaterialFX - " + LoggingUtils.class.getSimpleName());
+    private static final Level EXCEPTION = Level.forName("EXCEPTION", 150);
+    private static final StringWriter sw = new StringWriter();
+
+    private LoggingUtils() {
+    }
+
+    /**
+     * Gets the stacktrace of a {@code Throwable} as a String.
+     * @param ex The throwable/exception
+     * @return the stacktrace as a String
+     */
+    public static String getStackTraceString(Throwable ex) {
+        sw.flush();
+        ex.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    /**
+     * Logs the given {@code Throwable}'s stacktrace to the console.
+     * @param ex The throwable/exception
+     */
+    public static void logException(Throwable ex) {
+        logger.log(EXCEPTION, getStackTraceString(ex));
+    }
+
+    /**
+     * Logs the given {@code Throwable}'s exception to the console and adds the given String at the beginning.
+     * @param msg The extra message you want to log
+     * @param ex The throwable/exception
+     */
+    public static void logException(String msg, Throwable ex) {
+        logger.log(EXCEPTION, msg + "\n" + getStackTraceString(ex));
+    }
+}

+ 36 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -9,6 +9,7 @@ import javafx.scene.layout.Background;
 import javafx.scene.layout.BackgroundFill;
 import javafx.scene.layout.Region;
 import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -86,6 +87,41 @@ public class NodeUtils {
         return false;
     }
 
+    /**
+     * Makes the given region circular.
+     * <p>
+     * <b>Notice: the region's pref width and height must be set and be equals</b>
+     * @param region The given region
+     */
+    public static void makeRegionCircular(Region region) {
+        Circle circle = new Circle(region.getPrefWidth() / 2.0);
+        circle.centerXProperty().bind(region.widthProperty().divide(2.0));
+        circle.centerYProperty().bind(region.heightProperty().divide(2.0));
+        try {
+            region.setClip(circle);
+        } catch (IllegalArgumentException ex) {
+            LoggingUtils.logException("Could not set region's clip to make it circular", ex);
+        }
+    }
+
+    /**
+     * Makes the given region circular with the specified radius.
+     * <p>
+     * <b>Notice: the region's pref width and height must be set and be equals</b>
+     * @param region The given region
+     * @param radius The wanted radius
+     */
+    public static void makeRegionCircular(Region region, double radius) {
+        Circle circle = new Circle(radius);
+        circle.centerXProperty().bind(region.widthProperty().divide(2.0));
+        circle.centerYProperty().bind(region.heightProperty().divide(2.0));
+        try {
+            region.setClip(circle);
+        } catch (IllegalArgumentException ex) {
+            LoggingUtils.logException("Could not set region's clip to make it circular", ex);
+        }
+    }
+
     /* The next two methods are copied from com.sun.javafx.scene.control.skin.Utils class
      * It's a private module, so to avoid adding exports and opens I copied them
      */

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

@@ -0,0 +1,53 @@
+package io.github.palexdev.materialfx.utils;
+
+/**
+ * Utils class for {@code String}s.
+ */
+public class StringUtils {
+    public static final String EMPTY = "";
+    public static final int INDEX_NOT_FOUND = -1;
+
+    /**
+     * 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
+     */
+    public static String difference(final String str1, final String str2) {
+        if (str1 == null) {
+            return str2;
+        }
+        if (str2 == null) {
+            return str1;
+        }
+        final int at = indexOfDifference(str1, str2);
+        if (at == INDEX_NOT_FOUND) {
+            return EMPTY;
+        }
+        return str2.substring(at);
+    }
+
+    /**
+     * Finds the index at which two {@code CharSequence}s differ.
+     * @param cs1 The first sequence
+     * @param cs2 The second sequence
+     */
+    public static int indexOfDifference(final CharSequence cs1, final CharSequence cs2) {
+        if (cs1 == cs2) {
+            return INDEX_NOT_FOUND;
+        }
+        if (cs1 == null || cs2 == null) {
+            return 0;
+        }
+        int i;
+        for (i = 0; i < cs1.length() && i < cs2.length(); ++i) {
+            if (cs1.charAt(i) != cs2.charAt(i)) {
+                break;
+            }
+        }
+        if (i < cs2.length() || i < cs1.length()) {
+            return i;
+        }
+        return INDEX_NOT_FOUND;
+    }
+}

+ 8 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ToggleButtonsUtil.java

@@ -7,6 +7,9 @@ import javafx.scene.control.ToggleButton;
 import javafx.scene.control.ToggleGroup;
 import javafx.scene.input.MouseEvent;
 
+/**
+ * Utils class for {@code ToggleButton}s.
+ */
 public class ToggleButtonsUtil {
 
     private static final EventHandler<MouseEvent> consumeMouseEventFilter = (MouseEvent mouseEvent) -> {
@@ -21,6 +24,11 @@ public class ToggleButtonsUtil {
         ((ToggleButton) toggle).addEventFilter(MouseEvent.MOUSE_CLICKED, consumeMouseEventFilter);
     }
 
+    /**
+     * Adds a handler to the given {@code ToggleGroup} to make sure there's always at least
+     * one {@code ToggleButton} selected.
+     * @param toggleGroup The given ToggleGroup
+     */
     public static void addAlwaysOneSelectedSupport(final ToggleGroup toggleGroup) {
         toggleGroup.getToggles().addListener((ListChangeListener.Change<? extends Toggle> c) -> {
             while (c.next()) {

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

@@ -3,14 +3,17 @@ module MaterialFX.materialfx.main {
     requires javafx.fxml;
     requires javafx.graphics;
     requires java.desktop;
+    requires org.apache.logging.log4j;
 
     exports io.github.palexdev.materialfx;
     exports io.github.palexdev.materialfx.beans;
+    exports io.github.palexdev.materialfx.collections;
     exports io.github.palexdev.materialfx.controls;
     exports io.github.palexdev.materialfx.controls.base;
     exports io.github.palexdev.materialfx.controls.enums;
     exports io.github.palexdev.materialfx.controls.factories;
     exports io.github.palexdev.materialfx.effects;
+    exports io.github.palexdev.materialfx.notifications;
     exports io.github.palexdev.materialfx.skins;
     exports io.github.palexdev.materialfx.utils;
 }

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

@@ -0,0 +1,15 @@
+.mfx-notification {
+    -fx-background-color: white;
+}
+
+.mfx-notification .title-label {
+    -fx-font-size: 12.5;
+    -fx-font-weight: bold;
+}
+
+.mfx-button {
+    -fx-background-color: white;
+    -fx-background-radius: 20;
+    -fx-border-radius: 20;
+}
+

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

@@ -0,0 +1,89 @@
+.mfx-scroll-pane {
+    -mfx-track-color: rgb(230, 230, 230);
+    -mfx-thumb-color: rgb(137, 137, 137);
+    -mfx-thumb-hover-color: rgb(89, 88, 91);
+
+    -fx-background-color: white;
+}
+
+.mfx-scroll-pane * {
+    -fx-background-color: white;
+}
+
+.mfx-scroll-pane:focused {
+    -fx-background-color: white;
+}
+
+.mfx-scroll-pane > .corner {
+    -fx-background-color: transparent ;
+}
+
+.mfx-scroll-pane .scroll-bar:horizontal .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+    -fx-background-insets: 3.5 4 3.5 4;
+}
+
+.mfx-scroll-pane .scroll-bar:vertical .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+    -fx-background-insets: 4 3.5 4 3.5;
+}
+
+.mfx-scroll-pane .scroll-bar:horizontal .increment-button,
+.mfx-scroll-pane .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-scroll-pane .scroll-bar:vertical .increment-button,
+.mfx-scroll-pane .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-scroll-pane .scroll-bar .increment-arrow,
+.mfx-scroll-pane .scroll-bar .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.15em 0.0;
+}
+
+.mfx-scroll-pane .scroll-bar:horizontal .increment-arrow,
+.mfx-scroll-pane .scroll-bar:horizontal .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+.mfx-scroll-pane .scroll-bar:vertical .increment-arrow,
+.mfx-scroll-pane .scroll-bar:vertical .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+.mfx-scroll-pane .scroll-bar:horizontal .thumb,
+.mfx-scroll-pane .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-scroll-pane .scroll-bar:horizontal .thumb:hover,
+.mfx-scroll-pane .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;
+}
+
+.mfx-scroll-pane .scroll-bar:horizontal .thumb:pressed,
+.mfx-scroll-pane .scroll-bar:vertical .thumb:pressed {
+    -fx-background-color: -mfx-thumb-hover-color;
+    -fx-background-insets: 1.5, 0.0, 0.0;
+    -fx-background-radius: 2.0em;
+}