Explorar el Código

:boom: Huge Update [Part 2]

Disclaimer: I recently switched to Linux for production because I was tired of Bugdows but because they use different line endings IntelliJ is showing me that all files are changed, also the ones with identical content. I re-normalized the project and added a .gitattributes file. I'm not sure how GitHub will visualize this commit, if it's messed sorry in advance... but don't worry, as always all changes are listed here 😉

:arrow_up: Upgraded VirtualizedFX to version 11.2.1

Demo
:construction: Temporarily "disabled" or non-functional some showcases
:white_check_mark: Added new tests

MaterialFX
Beans Package
:sparkles: Added convenience properties for Java's functions
:sparkles: Added convenience property to represent a NumberRange
:sparkles: Added many new beans
:recycle: NumberRange: implemented hashCode, equals and toString, added two new methods to convert a range on integers to a List or a Set
:truck: Renamed RipplePosition ti PositionBean and moved to this package

Collections Package
:sparkles: TransformableList is a new kind of ObservableList that combines JavaFX's FilteredList and SortedList functionalities into one
:sparkles: TransformableListWrapper is an ObservableList which wraps both the source list and the transformable list in the same class. This makes using TransformableLists less verbose as modifications to the source list can be made directly from this wrapper

Controls Package
:fire: Removed AbstractMFXNotificationPane
:fire: Removed MFXNotification
:fire: Removed SimpleMFXNotificationPane
:sparkles: Added a new cell to contain notifications
:sparkles: FilterPane, a new control that allows to build Predicate filters interactively
:sparkles: MFXNotificationCenter, a new control that allows to display multiple notifications. It is basically an icon that opens a popup which contains not only the list of notifications but also controls to manage them
:sparkles: MFXPopup, extension of PopupControl to easily set the popup's content and position it by using HPos and VPos enumerations. It also introduces a hover functionality
:sparkles: MFXSimpleNotification, a simple implementation of INotification

Effects Package
:sparkles: ConsumerTransition, an implementation of Transition that uses a consumer to perform some action when the interpolate method is called
:sparkles: Interpolators, a new enumerator that offers some new interpolators for JavaFX's animations

Enums Package
:recycle: Moved all MaterialFX enumerators to this top level package
:sparkles: ChainMode, a new enumerator mainly used by PredicateUtils to chain two predicates
:sparkles: NotificationCounterStyle, a new enumerator to specify MFXNotificationCenter's counter style
:sparkles: NotificationPos, a new enumerator to specify at which position a notification system should place the notification
:sparkles: NotificationState, a new enumerator to represent the read state of a notification

Factories Package
:recycle: Moved all MaterialFX factories to this top level package
:sparkles: InsetsFactory, a new factory tp build JavaFX's Insets objects

Filter Package
:boom: The filter API has been completely remade and now it's super flexible, super useful, super amazin haha. I won't describe it here as there are a LOT of new classes and concepts to be described so I recommend you to read AbstractFilter, FilterBean and BiPredicateBean documentations, usage examples can be also found in the demo (not yet at time of writing) and in the documentation of MFXFilterPane
:construction: MFXFilterDialog has been completely commented, will be reworked for the new API

Font Package
:sparkles: Added new resources

Notifications Package
:boom: The notification API has been completely remade. Now there are to notification systems, one is very similar to the old one but it is limited to one notification at a time at a given position. This restriction helper to keep the system simple and efficient. To show multiple notifications at one time I recommend the usage of MFXNotificationCenterSystem which uses a MFXNotificationCenter to show the notifications. Or, you could implement you own notification system since the notification API now offers some base classes to build on top.
AbstractMFXNotificationSystem and INotificationSystem specify the base features all notification systems should have, INotification specifies the base features all notifications should have

Skin Package
No notable change aside from new skins for the new controls and minor changes due to classes renamed/moved

Utils Package
:sparkles: FunctionalStringConverter, a functional alternative to JavaFX's StringConverter
:sparkles: ReusableScheduledExecutor, a wrapper class to make a ScheduledExecutorService reusable. To stop/restart a ScheduledExecutorService it's needed to keep a reference to the ScheduledFuture task but this often result in boilerplate code, this wrapper fixes this
:sparkles: EnumStringConverter, an implementation of JavaFX's StringConverter to work with enumerators
:sparkles: Added a new method to ExecutionUtils
:sparkles: FXCollectors, a class that contains some new collectors for JavaFX's observable collections
:sparkles: PredicateUtils, utils for Predicates
:sparkles: StringUtils, added a new method to convert an elapsed time in seconds to a String
:recycle: AnimationUtils, added some new methods to PauseTransitionBuilder and KeyFrames classes
:bug: ExceptionUtils, fixed getStackTraceString method as StringWriters are not reusable

Resources
:sparkles: Added new CSS files for new controls
:recycle: MFXColors.css, added a new color

Signed-off-by: Alessadro Parisi <alessandro.parisi406@gmail.com>
Signed-off-by: palexdev <alessandro.parisi406@gmail.com>
palexdev hace 3 años
padre
commit
160bc46473
Se han modificado 100 ficheros con 4176 adiciones y 1037 borrados
  1. 34 0
      .gitattributes
  2. 1 1
      .github/workflows/gradle.yml
  3. 7 23
      demo/libs/scenicview.jar
  4. 4 32
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/FontResourcesDemoController.java
  5. 1 1
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ProgressBarsDemoController.java
  6. 2 7
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ProgressSpinnersDemoController.java
  7. 3 7
      demo/src/main/java/io/github/palexdev/materialfx/demo/model/Machine.java
  8. 1 2
      demo/src/main/java/io/github/palexdev/materialfx/demo/model/Person.java
  9. 84 0
      demo/src/test/java/NotificationsTest.java
  10. 73 0
      demo/src/test/java/PopupTest.java
  11. 69 0
      demo/src/test/java/binding/BindingManagerTests.java
  12. 2 2
      demo/src/test/java/combobox/ComboBoxTest.java
  13. 42 0
      materialfx/gradle.properties
  14. 98 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/CustomBounds.java
  15. 104 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/FilterBean.java
  16. 40 1
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXLoaderBean.java
  17. 121 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/PopupPositionBean.java
  18. 86 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/PositionBean.java
  19. 85 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/TransitionPositionBean.java
  20. 62 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/NumberRangeProperty.java
  21. 14 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/base/ResettableProperty.java
  22. 15 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/BiFunctionProperty.java
  23. 14 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/BiPredicateProperty.java
  24. 13 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/ConsumerProperty.java
  25. 14 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/FunctionProperty.java
  26. 13 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/PredicateProperty.java
  27. 13 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/SupplierProperty.java
  28. 35 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/resettable/ResettableBooleanProperty.java
  29. 156 0
      materialfx/src/main/java/io/github/palexdev/materialfx/collections/CircularQueue.java
  30. 269 0
      materialfx/src/main/java/io/github/palexdev/materialfx/collections/ObservableStack.java
  31. 168 0
      materialfx/src/main/java/io/github/palexdev/materialfx/collections/TransformableListWrapper.java
  32. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXButton.java
  33. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckListView.java
  34. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXComboBox.java
  35. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXContextMenu.java
  36. 5 7
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXContextMenuItem.java
  37. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDialog.java
  38. 240 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterComboBox.java
  39. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXHLoader.java
  40. 5 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXIconWrapper.java
  41. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXLabel.java
  42. 0 194
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java
  43. 721 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotificationCenter.java
  44. 1 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXPasswordField.java
  45. 357 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXPopup.java
  46. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXProgressBar.java
  47. 147 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXScrollPane.java
  48. 4 4
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXSlider.java
  49. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStageDialog.java
  50. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepper.java
  51. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepperToggle.java
  52. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableRow.java
  53. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableSortModel.java
  54. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java
  55. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleButton.java
  56. 0 235
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/SimpleMFXNotificationPane.java
  57. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXDialog.java
  58. 0 88
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXListView.java
  59. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXToggleNode.java
  60. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXCheckTreeCell.java
  61. 224 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXNotificationCell.java
  62. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXSimpleTreeCell.java
  63. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableRowCell.java
  64. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java
  65. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyListCell.java
  66. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyListView.java
  67. 138 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyTableView.java
  68. 101 0
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/DepthLevel.java
  69. 10 9
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/MFXDepthManager.java
  70. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/RippleClipType.java
  71. 0 74
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/RipplePosition.java
  72. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/base/AbstractMFXRippleGenerator.java
  73. 5 5
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/base/IRipple.java
  74. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/ButtonType.java
  75. 22 0
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/ChainMode.java
  76. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/DialogType.java
  77. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/LoaderCacheLevel.java
  78. 10 0
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationCounterStyle.java
  79. 30 0
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationPos.java
  80. 8 0
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationState.java
  81. 12 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/SliderEnums.java
  82. 4 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/SortState.java
  83. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/StepperToggleState.java
  84. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/Styles.java
  85. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/TextPosition.java
  86. 53 0
      materialfx/src/main/java/io/github/palexdev/materialfx/factories/InsetsFactory.java
  87. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/factories/MFXAnimationFactory.java
  88. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/factories/MFXDialogFactory.java
  89. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/factories/MFXStageDialogFactory.java
  90. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/factories/RippleClipTypeFactory.java
  91. 51 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/BooleanFilter.java
  92. 59 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/DoubleFilter.java
  93. 66 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/EnumFilter.java
  94. 0 23
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/EvaluationMode.java
  95. 59 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/FloatFilter.java
  96. 0 27
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/IFilterable.java
  97. 59 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/IntegerFilter.java
  98. 59 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/LongFilter.java
  99. 0 195
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXEvaluationBox.java
  100. 23 33
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXFilterDialog.java

+ 34 - 0
.gitattributes

@@ -0,0 +1,34 @@
+# Java sources
+*.java          text diff=java
+*.kt            text diff=java
+*.groovy        text diff=java
+*.scala         text diff=java
+*.gradle        text diff=java
+*.gradle.kts    text diff=java
+
+# These files are text and should be normalized (Convert crlf => lf)
+*.css           text diff=css
+*.scss          text diff=css
+*.sass          text
+*.df            text
+*.htm           text diff=html
+*.html          text diff=html
+*.js            text
+*.jsp           text
+*.jspf          text
+*.jspx          text
+*.properties    text
+*.tld           text
+*.tag           text
+*.tagx          text
+*.xml           text
+
+# These files are binary and should be left untouched
+# (binary is a macro for -text -diff)
+*.class         binary
+*.dll           binary
+*.ear           binary
+*.jar           binary
+*.so            binary
+*.war           binary
+*.jks           binary

+ 1 - 1
.github/workflows/gradle.yml

@@ -24,7 +24,7 @@ dependencies {
     implementation 'org.kordamp.ikonli:ikonli-core:12.2.0'
     implementation 'org.kordamp.ikonli:ikonli-javafx:12.2.0'
     implementation 'org.kordamp.ikonli:ikonli-fontawesome5-pack:12.2.0'
-    implementation 'io.github.palexdev:virtualizedfx:11.1.3'
+    implementation 'io.github.palexdev:virtualizedfx:11.2.1'
     implementation project(':materialfx')
 }
 

+ 7 - 23
demo/libs/scenicview.jar

@@ -19,17 +19,13 @@
 package io.github.palexdev.materialfx.demo.controllers;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
-import io.github.palexdev.materialfx.controls.MFXNotification;
 import io.github.palexdev.materialfx.controls.MFXStageDialog;
-import io.github.palexdev.materialfx.controls.SimpleMFXNotificationPane;
 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.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
 import io.github.palexdev.materialfx.effects.DepthLevel;
-import io.github.palexdev.materialfx.notifications.NotificationPos;
-import io.github.palexdev.materialfx.notifications.NotificationsManager;
+import io.github.palexdev.materialfx.enums.ButtonType;
+import io.github.palexdev.materialfx.enums.DialogType;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.factories.MFXDialogFactory;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
@@ -37,9 +33,7 @@ import javafx.geometry.Insets;
 import javafx.geometry.Pos;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Pane;
-import javafx.scene.layout.Region;
 import javafx.stage.Modality;
-import javafx.util.Duration;
 
 import java.net.URL;
 import java.util.ResourceBundle;
@@ -270,9 +264,10 @@ public class DialogsController implements Initializable {
         action3.setDepthLevel(DepthLevel.LEVEL1);
         close.setDepthLevel(DepthLevel.LEVEL1);
 
-        action1.setOnAction(event -> NotificationsManager.send(NotificationPos.BOTTOM_RIGHT, createNotification("Action 1 Performed")));
+        // TODO remake
+/*        action1.setOnAction(event -> NotificationsManager.send(NotificationPos.BOTTOM_RIGHT, createNotification("Action 1 Performed")));
         action2.setOnAction(event -> NotificationsManager.send(NotificationPos.BOTTOM_RIGHT, createNotification("Action 2 Performed")));
-        action3.setOnAction(event -> NotificationsManager.send(NotificationPos.BOTTOM_RIGHT, createNotification("Action 3 Performed")));
+        action3.setOnAction(event -> NotificationsManager.send(NotificationPos.BOTTOM_RIGHT, createNotification("Action 3 Performed")));*/
         dialog.addCloseButton(close);
 
         HBox box = new HBox(20, action1, action2, action3, close);
@@ -280,15 +275,4 @@ public class DialogsController implements Initializable {
         box.setPadding(new Insets(20, 5, 20, 5));
         return box;
     }
-
-    private MFXNotification createNotification(String text) {
-        Region notificationPane = new SimpleMFXNotificationPane(
-                "Dialogs Actions Test",
-                "",
-                text
-        );
-        MFXNotification notification = new MFXNotification(notificationPane, true, true);
-        notification.setHideAfterDuration(Duration.seconds(3));
-        return notification;
-    }
 }

+ 4 - 32
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/FontResourcesDemoController.java

@@ -18,19 +18,8 @@
 
 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 io.github.palexdev.materialfx.enums.NotificationPos;
 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;
 
@@ -83,27 +72,10 @@ public class NotificationsController {
     }
 
     private void showNotification(NotificationPos pos) {
-        MFXNotification notification = buildNotification();
-        NotificationsManager.send(pos, notification);
+       // TODO remake
     }
 
-    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() {
+/*    private Region getRandomTemplate() {
         final int rand = random.nextInt(4);
 
         switch (rand) {
@@ -144,5 +116,5 @@ public class NotificationsController {
             default:
                 return null;
         }
-    }
+    }*/
 }

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.demo.controllers;
 
 import io.github.palexdev.materialfx.beans.NumberRange;
 import io.github.palexdev.materialfx.controls.MFXProgressBar;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.utils.AnimationUtils;
 import io.github.palexdev.materialfx.utils.AnimationUtils.KeyFrames;
 import javafx.animation.Animation;

+ 2 - 7
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ProgressSpinnersDemoController.java

@@ -18,13 +18,13 @@
 
 package io.github.palexdev.materialfx.demo.model;
 
-import io.github.palexdev.materialfx.filter.IFilterable;
 import javafx.beans.property.IntegerProperty;
 import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 
-public class FilterablePerson implements IFilterable {
+// TODO remove?
+public class FilterablePerson {
     private final StringProperty firstName = new SimpleStringProperty();
     private final StringProperty lastName = new SimpleStringProperty();
     private final StringProperty address = new SimpleStringProperty();
@@ -85,11 +85,6 @@ public class FilterablePerson implements IFilterable {
         this.age.set(age);
     }
 
-    @Override
-    public String toFilterString() {
-        return getFirstName() + getLastName() + getAddress() + getAge();
-    }
-
     @Override
     public String toString() {
         return getFirstName() + " " + getLastName();

+ 3 - 7
demo/src/main/java/io/github/palexdev/materialfx/demo/model/Machine.java

@@ -18,13 +18,14 @@
 
 package io.github.palexdev.materialfx.demo.model;
 
-import io.github.palexdev.materialfx.filter.IFilterable;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 
-public class Machine implements IFilterable {
+
+// TODO review?
+public class Machine {
 
     public enum State {
         ONLINE, OFFLINE
@@ -89,9 +90,4 @@ public class Machine implements IFilterable {
     public void setState(State state) {
         this.state.set(state);
     }
-
-    @Override
-    public String toFilterString() {
-        return getName() + " " + getIp() + " " + getOwner() + " " + getState().name();
-    }
 }

+ 1 - 2
demo/src/main/java/io/github/palexdev/materialfx/demo/model/Person.java

@@ -1,9 +1,8 @@
-import combobox.ComboBoxTest;
 import javafx.application.Application;
 
 public class Launcher {
 
     public static void main(String[] args) {
-        Application.launch(ComboBoxTest.class, args);
+        Application.launch(NotificationsTest.class, args);
     }
 }

+ 84 - 0
demo/src/test/java/NotificationsTest.java

@@ -0,0 +1,84 @@
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.MFXNotificationCenter;
+import io.github.palexdev.materialfx.enums.NotificationPos;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.notifications.MFXNotificationCenterSystem;
+import io.github.palexdev.materialfx.notifications.MFXNotificationSystem;
+import io.github.palexdev.materialfx.controls.MFXSimpleNotification;
+import io.github.palexdev.materialfx.notifications.base.INotification;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.RandomInstance;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import org.scenicview.ScenicView;
+
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+public class NotificationsTest extends Application {
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        StackPane stackPane = new StackPane();
+
+        MFXNotificationCenter notificationCenter = new MFXNotificationCenter();
+        IntStream.range(0, 100).forEach(i -> notificationCenter.getNotifications().add(createDummyNotification()));
+        stackPane.getChildren().add(notificationCenter);
+
+        MFXNotificationCenterSystem.instance()
+                .initOwner(primaryStage)
+                .setOpenOnNew(false)
+                .setCloseAutomatically(true)
+                .setPosition(NotificationPos.TOP_RIGHT);
+        MFXNotificationSystem.instance()
+                .initOwner(primaryStage)
+                .setPosition(NotificationPos.TOP_RIGHT);
+        stackPane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
+            switch (event.getCode()) {
+                case A -> MFXNotificationCenterSystem.instance().publish(createDummyNotification());
+                case C -> notificationCenter.stopNotificationsUpdater();
+                case S -> notificationCenter.startNotificationsUpdater(60, TimeUnit.SECONDS);
+                case T -> MFXNotificationSystem.instance().publish(createDummyNotification());
+                case P -> MFXNotificationCenterSystem.instance().delaySetPosition(NotificationPos.TOP_LEFT);
+            }
+        });
+
+        Scene scene = new Scene(stackPane, 800, 600);
+        primaryStage.setScene(scene);
+        primaryStage.show();
+
+        ScenicView.show(scene);
+    }
+
+    private INotification createDummyNotification() {
+        MFXLabel label = new MFXLabel("Random Label n." + RandomInstance.random.nextInt());
+        label.setLeadingIcon(MFXFontIcon.getRandomIcon(32, ColorUtils.getRandomColor()));
+        label.setAlignment(Pos.CENTER_LEFT);
+        label.setLineColor(Color.TRANSPARENT);
+        label.setUnfocusedLineColor(Color.TRANSPARENT);
+        label.setMaxWidth(Double.MAX_VALUE);
+        HBox.setHgrow(label, Priority.ALWAYS);
+
+        MFXLabel time = new MFXLabel();
+        time.setAlignment(Pos.CENTER_RIGHT);
+        time.setLineColor(Color.TRANSPARENT);
+        time.setUnfocusedLineColor(Color.TRANSPARENT);
+
+        HBox box = new HBox(label, time);
+        box.setMinSize(450, 100);
+        box.setStyle("-fx-background-color: white");
+        box.setAlignment(Pos.CENTER_LEFT);
+        MFXSimpleNotification notification = new MFXSimpleNotification(box);
+        notification.setOnUpdateElapsed((longElapsed, stringElapsed) -> Platform.runLater(() -> time.setText(stringElapsed)));
+        time.setText(notification.getTimeToStringConverter().apply(notification.getElapsedTime()));
+        return notification;
+    }
+}

+ 73 - 0
demo/src/test/java/PopupTest.java

@@ -0,0 +1,73 @@
+import io.github.palexdev.materialfx.controls.MFXButton;
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.MFXPopup;
+import io.github.palexdev.materialfx.enums.ButtonType;
+import io.github.palexdev.materialfx.effects.DepthLevel;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.controls.MFXSimpleNotification;
+import io.github.palexdev.materialfx.notifications.base.INotification;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.RandomInstance;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.geometry.HPos;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Scene;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+
+public class PopupTest extends Application {
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        StackPane stackPane = new StackPane();
+
+        MFXButton button = new MFXButton("SHOW");
+        button.setPrefSize(180, 36);
+        button.setButtonType(ButtonType.RAISED);
+        button.setDepthLevel(DepthLevel.LEVEL1);
+
+        button.setOnAction(event -> {
+                Region content = createDummyNotification().getContent();
+                MFXPopup popup = new MFXPopup(content);
+                popup.show(button, HPos.LEFT, VPos.BOTTOM, -0, -0);
+        });
+
+        stackPane.getChildren().add(button);
+        Scene scene = new Scene(stackPane, 800, 800);
+        primaryStage.setScene(scene);
+        primaryStage.show();
+    }
+
+    private INotification createDummyNotification() {
+        MFXLabel label = new MFXLabel("Random Label n." + RandomInstance.random.nextInt());
+        label.setLeadingIcon(MFXFontIcon.getRandomIcon(32, ColorUtils.getRandomColor()));
+        label.setAlignment(Pos.CENTER_LEFT);
+        label.setLineColor(Color.TRANSPARENT);
+        label.setUnfocusedLineColor(Color.TRANSPARENT);
+        label.setMaxWidth(Double.MAX_VALUE);
+        HBox.setHgrow(label, Priority.ALWAYS);
+
+        MFXLabel time = new MFXLabel();
+        time.setAlignment(Pos.CENTER_RIGHT);
+        time.setLineColor(Color.TRANSPARENT);
+        time.setUnfocusedLineColor(Color.TRANSPARENT);
+
+        HBox box = new HBox(label, time);
+        box.setMinSize(450, 100);
+        box.setStyle("-fx-background-color: white");
+        box.setAlignment(Pos.CENTER_LEFT);
+        MFXSimpleNotification notification = new MFXSimpleNotification(box);
+        notification.setOnUpdateElapsed((longElapsed, stringElapsed) -> Platform.runLater(() -> time.setText(stringElapsed)));
+        time.setText(notification.getTimeToStringConverter().apply(notification.getElapsedTime()));
+        box.setStyle("" +
+                "-fx-background-color: transparent;\n" +
+                "-fx-border-color: red");
+        return notification;
+    }
+}

+ 69 - 0
demo/src/test/java/binding/BindingManagerTests.java

@@ -0,0 +1,69 @@
+package collections;
+
+import io.github.palexdev.materialfx.collections.TransformableList;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testfx.framework.junit5.ApplicationExtension;
+
+import java.util.Comparator;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(ApplicationExtension.class)
+public class TransformableListTest {
+    private final ObservableList<String> source = FXCollections.observableArrayList("A", "B", "C", "D", "E");
+
+    @Test
+    public void sortTest1() {
+        TransformableList<String> transformed = new TransformableList<>(source);
+        transformed.setComparator(Comparator.reverseOrder(), true);
+
+        assertEquals(transformed.get(4), "A");
+        assertEquals(transformed.indexOf("E"), 0);
+        assertEquals(transformed.viewToSource(0), 4);
+        assertEquals(transformed.sourceToView(0), 4);
+    }
+
+    @Test
+    public void sortAndFilterTest1() {
+        TransformableList<String> transformed = new TransformableList<>(source);
+        transformed.setComparator(Comparator.reverseOrder(), true);
+        transformed.setPredicate(s -> s.equals("A") || s.equals("C") || s.equals("E"));
+
+        assertThrows(IndexOutOfBoundsException.class, () -> transformed.get(4));
+        assertEquals(transformed.get(1), "C");
+        assertEquals(transformed.indexOf("E"), 0);
+        assertEquals(transformed.viewToSource(1), 2);
+        assertEquals(transformed.sourceToView(1), -1);
+    }
+
+    @Test
+    public void testJavaFX1() {
+        SortedList<String> sorted = new SortedList<>(source);
+        sorted.setComparator(Comparator.reverseOrder());
+
+        assertEquals(sorted.get(4), "A");
+        assertEquals(sorted.indexOf("E"), 0);
+        assertEquals(sorted.getSourceIndex(0), 4);
+        assertEquals(sorted.getViewIndex(0), 4);
+    }
+
+    @Test
+    public void testJavaFX2() {
+        SortedList<String> sorted = new SortedList<>(source);
+        sorted.setComparator(Comparator.reverseOrder());
+
+        FilteredList<String> filtered = new FilteredList<>(sorted);
+        filtered.setPredicate(s -> s.equals("A") || s.equals("C") || s.equals("E"));
+
+        assertThrows(IndexOutOfBoundsException.class, () -> filtered.get(4));
+        assertEquals(filtered.get(1), "C");
+        assertEquals(filtered.indexOf("E"), 0);
+        assertEquals(filtered.getSourceIndex(1), 2);
+        assertTrue(filtered.getViewIndex(1) < 0);
+    }
+}

+ 2 - 2
demo/src/test/java/combobox/ComboBoxTest.java

@@ -23,7 +23,7 @@ dependencies {
     testImplementation('junit:junit:4.13.2')
     implementation 'com.vanniktech:gradle-maven-publish-plugin:0.18.0'
 
-    implementation 'io.github.palexdev:virtualizedfx:11.1.3'
+    implementation 'io.github.palexdev:virtualizedfx:11.2.1'
 }
 
 javadoc {
@@ -70,7 +70,7 @@ jar {
 shadowJar {
     mergeServiceFiles()
     dependencies {
-        include(dependency('io.github.palexdev:virtualizedfx:11.1.3'))
+        include(dependency('io.github.palexdev:virtualizedfx:11.2.1'))
     }
 }
 

+ 42 - 0
materialfx/gradle.properties

@@ -0,0 +1,42 @@
+package io.github.palexdev.materialfx.beans;
+
+import java.util.function.BiPredicate;
+
+/**
+ * A simple bean that wraps a {@link BiPredicate} and s String that represents
+ * the name for the predicate.
+ *
+ * @param <T> the type of the first argument to the predicate
+ * @param <U> the type of the second argument the predicate
+ */
+public class BiPredicateBean<T, U> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String name;
+    private final BiPredicate<T, U> predicate;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public BiPredicateBean(String name, BiPredicate<T, U> predicate) {
+        this.name = name;
+        this.predicate = predicate;
+    }
+
+    //================================================================================
+    // Getters
+    //================================================================================
+    public String name() {
+        return name;
+    }
+
+    public BiPredicate<T, U> predicate() {
+        return predicate;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}

+ 98 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/CustomBounds.java

@@ -0,0 +1,98 @@
+package io.github.palexdev.materialfx.beans;
+
+import io.github.palexdev.materialfx.notifications.MFXNotificationCenterSystem;
+import javafx.geometry.BoundingBox;
+import javafx.geometry.Bounds;
+
+/**
+ * JavaFX allows you to create custom {@code Bounds} objects, see {@link BoundingBox}, the thing is
+ * that it automatically computes the max X/Y/Z values. This can be quite unfortunate in some rare
+ * cases because maybe you need some kind of special bounds, this bean is specifically for those
+ * cases, it allows creating custom bounds.
+ * <p>
+ * An example of that is in the {@link MFXNotificationCenterSystem} class, there custom bounds
+ * are created to take into account the coordinates of the bell icon and the entire width/height of the
+ * notification center. Like I said tough, cases like that are quite rare.
+ */
+public class CustomBounds {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final double minX;
+    private final double minY;
+    private final double minZ;
+    private final double maxX;
+    private final double maxY;
+    private final double maxZ;
+    private final double width;
+    private final double height;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public CustomBounds(double minX, double minY, double maxX, double maxY, double width, double height) {
+        this(minX, minY, 0, maxX, maxY, 0, width, height);
+    }
+
+    public CustomBounds(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, double width, double height) {
+        this.minX = minX;
+        this.minY = minY;
+        this.minZ = minZ;
+        this.maxX = maxX;
+        this.maxY = maxY;
+        this.maxZ = maxZ;
+        this.width = width;
+        this.height = height;
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+    public static CustomBounds from(Bounds bounds) {
+        return new CustomBounds(
+                bounds.getMinX(),
+                bounds.getMinY(),
+                bounds.getMinZ(),
+                bounds.getMaxX(),
+                bounds.getMaxY(),
+                bounds.getMaxY(),
+                bounds.getWidth(),
+                bounds.getHeight()
+        );
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+    public double getMinX() {
+        return minX;
+    }
+
+    public double getMinY() {
+        return minY;
+    }
+
+    public double getMinZ() {
+        return minZ;
+    }
+
+    public double getMaxX() {
+        return maxX;
+    }
+
+    public double getMaxY() {
+        return maxY;
+    }
+
+    public double getMaxZ() {
+        return maxZ;
+    }
+
+    public double getWidth() {
+        return width;
+    }
+
+    public double getHeight() {
+        return height;
+    }
+}

+ 104 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/FilterBean.java

@@ -0,0 +1,104 @@
+package io.github.palexdev.materialfx.beans;
+
+import io.github.palexdev.materialfx.enums.ChainMode;
+import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+/**
+ * A simple bean that has all the necessary information to produce a {@link Predicate}
+ * for a given T object type.
+ * <p></p>
+ * It wraps the following data:
+ * <p> - A String which is the query
+ * <p> - An object of type {@link AbstractFilter}, which is effectively responsible for producing the {@link Predicate}
+ * <p> - A {@link BiPredicateBean}, which is used by {@link AbstractFilter}, see {@link AbstractFilter#predicateFor(String)} or {@link AbstractFilter#predicateFor(String, BiPredicate)}
+ * <p> - A {@link ChainMode} enumeration to specify how this filter should be combined with other filters
+ *
+ * @param <T> the type of objects to filter
+ * @param <U> the type of objects on which the {@link BiPredicate} operates
+ */
+public class FilterBean<T, U> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String query;
+    private final AbstractFilter<T, U> filter;
+    private final BiPredicateBean<U, U> predicateBean;
+    private ChainMode mode;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public FilterBean(String query, AbstractFilter<T, U> filter, BiPredicateBean<U, U> predicateBean) {
+        this(query, filter, predicateBean, ChainMode.OR);
+    }
+
+    public FilterBean(String query, AbstractFilter<T, U> filter, BiPredicateBean<U, U> predicateBean, ChainMode mode) {
+        this.query = query;
+        this.filter = filter;
+        this.predicateBean = predicateBean;
+        this.mode = mode;
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Calls {@link AbstractFilter#predicateFor(String)} with the query specified by this bean.
+     */
+    public Predicate<T> predicate() {
+        return filter.predicateFor(query);
+    }
+
+    /**
+     * @return the query, see {@link AbstractFilter} documentation for more info about the query
+     */
+    public String getQuery() {
+        return query;
+    }
+
+    /**
+     * @return the {@link AbstractFilter} specified by this bean
+     */
+    public AbstractFilter<T, U> getFilter() {
+        return filter;
+    }
+
+    /**
+     * Delegate for {@link AbstractFilter#name()}.
+     */
+    public String getFilterName() {
+        return filter.name();
+    }
+
+    /**
+     * @return the {@link BiPredicateBean} specified by this bean
+     */
+    public BiPredicateBean<U, U> getPredicateBean() {
+        return predicateBean;
+    }
+
+    /**
+     * Delegate for {@link BiPredicateBean#name()}.
+     */
+    public String getPredicateName() {
+        return predicateBean.name();
+    }
+
+    /**
+     * @return the {@link ChainMode} enumeration that specifies how this filter should be chained with other filters.
+     */
+    public ChainMode getMode() {
+        return mode;
+    }
+
+    /**
+     * Sets the chain mode to the specified one.
+     */
+    public void setMode(ChainMode mode) {
+        this.mode = mode;
+    }
+}

+ 40 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXLoaderBean.java

@@ -18,7 +18,8 @@
 
 package io.github.palexdev.materialfx.beans;
 
-import java.util.List;
+import java.util.*;
+import java.util.stream.IntStream;
 
 /**
  * Simple bean to represent a range of values from min to max.
@@ -43,14 +44,38 @@ public class NumberRange<T extends Number> {
     //================================================================================
     // Methods
     //================================================================================
+
+    /**
+     * @return the lower bound
+     */
     public T getMin() {
         return min;
     }
 
+    /**
+     * @return the upper bound
+     */
     public T getMax() {
         return max;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NumberRange<?> that = (NumberRange<?>) o;
+        return Objects.equals(min, that.min) && Objects.equals(max, that.max);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(min, max);
+    }
+
+    @Override
+    public String toString() {
+        return "Min[" + min + "], Max[" + max + "]";
+    }
     //================================================================================
     // Static Methods
     //================================================================================
@@ -124,4 +149,18 @@ public class NumberRange<T extends Number> {
     public static boolean inRangeOf(long val, List<NumberRange<Long>> ranges) {
         return ranges.stream().anyMatch(range -> inRangeOf(val, range));
     }
+
+    /**
+     * Expands a range of integers to a List of integers.
+     */
+    public static List<Integer> expandRange(NumberRange<Integer> range) {
+        return IntStream.rangeClosed(range.getMin(), range.getMax()).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+    }
+
+    /**
+     * Expands a range of integers to a Set of integers.
+     */
+    public static Set<Integer> expandRangeToSet(NumberRange<Integer> range) {
+        return IntStream.rangeClosed(range.getMin(), range.getMax()).collect(HashSet::new, HashSet::add, HashSet::addAll);
+    }
 }

+ 121 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/PopupPositionBean.java

@@ -0,0 +1,121 @@
+package io.github.palexdev.materialfx.beans;
+
+import io.github.palexdev.materialfx.controls.MFXPopup;
+import javafx.geometry.Bounds;
+import javafx.geometry.HPos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+
+/**
+ * A useful bean which gives info about a {@link MFXPopup}'s position and owner.
+ * <p></p>
+ * The purpose of this bean is to provide a way to communicate between the popup and its skin.
+ * The precise location of a popup cannot be computed when the show methods are called because the content
+ * has not been laid out yet, thus its sizes/bounds are 0. This changes when the skin is created, at that moment
+ * all info about the content are available so this bean is necessary to properly reposition and animate the popup.
+ */
+public class PopupPositionBean {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final Node owner;
+    private final Bounds ownerBounds;
+    private final PositionBean positionBean;
+    private final HPos hPos;
+    private final VPos vPos;
+    private final double xOffset;
+    private final double yOffset;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public PopupPositionBean(Node owner, PositionBean positionBean, HPos hPos, VPos vPos, double xOffset, double yOffset) {
+        this.owner = owner;
+        this.ownerBounds = owner.getLayoutBounds();
+        this.positionBean = positionBean;
+        this.hPos = hPos;
+        this.vPos = vPos;
+        this.xOffset = xOffset;
+        this.yOffset = yOffset;
+    }
+
+    /**
+     * @return the popup's owner
+     */
+    public Node getOwner() {
+        return owner;
+    }
+
+    /**
+     * @return the popup owner's bounds
+     */
+    public Bounds getOwnerBounds() {
+        return ownerBounds;
+    }
+
+    /**
+     * @return the popup owner's width
+     */
+    public double getOwnerWidth() {
+        return ownerBounds.getWidth();
+    }
+
+    /**
+     * @return the popup owner's height
+     */
+    public double getOwnerHeight() {
+        return ownerBounds.getHeight();
+    }
+
+    /**
+     * You should NOT rely on these coordinates since as of now
+     * they do not take into account the translations made by the skin.
+     *
+     * @return the initial computed coordinates of the popup
+     */
+    public PositionBean getPositionBean() {
+        return positionBean;
+    }
+
+    /**
+     * Delegate for {@link #getPositionBean()}.getX().
+     */
+    public double getX() {
+        return positionBean.getX();
+    }
+
+    /**
+     * Delegate for {@link #getPositionBean()}.getY().
+     */
+    public double getY() {
+        return positionBean.getY();
+    }
+
+    /**
+     * @return the specified {@link HPos}
+     */
+    public HPos getHPos() {
+        return hPos;
+    }
+
+    /**
+     * @return the specified {@link VPos}
+     */
+    public VPos getVPos() {
+        return vPos;
+    }
+
+    /**
+     * @return the specified x offset
+     */
+    public double getXOffset() {
+        return xOffset;
+    }
+
+    /**
+     * @return the specified y offset
+     */
+    public double getYOffset() {
+        return yOffset;
+    }
+}

+ 86 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/PositionBean.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 Parisi Alessandro
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ * MaterialFX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.beans;
+
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+
+/**
+ * Simple bean that keeps track of two coordinates, x and y.
+ * <p>
+ * Both are JavaFX properties to allow dynamic uses.
+ */
+public class PositionBean {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final DoubleProperty x = new SimpleDoubleProperty(0);
+    private final DoubleProperty y = new SimpleDoubleProperty(0);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public PositionBean() {
+    }
+
+    public PositionBean(double x, double y) {
+        setX(x);
+        setY(y);
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+    public static PositionBean of(double x, double y) {
+        return new PositionBean(x, y);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    public double getX() {
+        return x.get();
+    }
+
+    /**
+     * The x coordinate property.
+     */
+    public DoubleProperty xProperty() {
+        return x;
+    }
+
+    public void setX(double xPosition) {
+        this.x.set(xPosition);
+    }
+
+    public double getY() {
+        return y.get();
+    }
+
+    /**
+     * The y coordinate property
+     */
+    public DoubleProperty yProperty() {
+        return y;
+    }
+
+    public void setY(double yPosition) {
+        this.y.set(yPosition);
+    }
+}

+ 85 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/TransitionPositionBean.java

@@ -0,0 +1,85 @@
+package io.github.palexdev.materialfx.beans;
+
+import javafx.animation.Transition;
+
+// TODO documentation
+/**
+ * This is an extension of {@link PositionBean} to be used
+ * with {@link Transition}s that start from a point P(x, y) and
+ * end at a point P1(endX, endY).
+ * <p></p>
+ * A very basic example:
+ * <p>
+ * Let's say I want to move a point P from (x, y) to the left
+ * (x1, y) with an animation. The transition would probably look like this:
+ * <pre>
+ * {@code
+ *     double startX = ...;
+ *     double startY = ...;
+ *     double endX = ...;
+ *     double endY = startY; // The y coordinate doesn't change so it is equal to the start one
+ *     TransitionPositionBean position = TransitionPositionBean.of(startX, startY, endX, endY);
+ *     Transition move = new Transition() {
+ *             @Override
+ *             protected void interpolate(double frac) {
+ *                 p.setX(x - position.deltaX() * frac);
+ *             }
+ *      }
+ * }
+ * </pre>
+ */
+public class TransitionPositionBean extends PositionBean {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final double endX;
+    private final double endY;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public TransitionPositionBean(double x, double y, double endX, double endY) {
+        super(x, y);
+        this.endX = endX;
+        this.endY = endY;
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+    public static TransitionPositionBean of(double x, double y, double endX, double endY) {
+        return new TransitionPositionBean(x, y, endX, endY);
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+
+    /**
+     * @return the end x coordinate
+     */
+    public double getEndX() {
+        return endX;
+    }
+
+    /**
+     * @return the end y coordinate
+     */
+    public double getEndY() {
+        return endY;
+    }
+
+    /**
+     * @return the difference between the star x and end x coordinates
+     */
+    public double deltaX() {
+        return getX() - getEndX();
+    }
+
+    /**
+     * @return the difference between the start y and end y coordinates
+     */
+    public double deltaY() {
+        return getY() - getEndY();
+    }
+}

+ 62 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/NumberRangeProperty.java

@@ -0,0 +1,62 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.NumberRange;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link NumberRange}.
+ *
+ * @param <T> the range's number type
+ */
+public class NumberRangeProperty<T extends Number> extends SimpleObjectProperty<NumberRange<T>> {
+
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Convenience method to get the range's lower bound.
+     * Null if the range is null.
+     */
+    public T getMin() {
+        return get() == null ? null : get().getMin();
+    }
+
+    /**
+     * Convenience method to get the range's upper bound.
+     * Null if the range is null.
+     */
+    public T getMax() {
+        return get() == null ? null : get().getMin();
+    }
+
+    /**
+     * Convenience method to set a range with both min and max equal.
+     */
+    public void setRange(T value) {
+        set(NumberRange.of(value));
+    }
+
+    /**
+     * Convenience method to set a range with the given min and max values.
+     */
+    public void setRange(T min, T max) {
+        set(NumberRange.of(min, max));
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+
+    /**
+     * Overridden to check equality between ranges and return in case ranges are the same.
+     */
+    @Override
+    public void set(NumberRange<T> newValue) {
+        NumberRange<T> oldValue = get();
+        if (newValue.equals(oldValue)) return;
+        super.set(newValue);
+    }
+}

+ 14 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/base/ResettableProperty.java

@@ -0,0 +1,14 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link BiConsumer}.
+ *
+ * @param <T> the consumer's first argument
+ * @param <U> the consumer's second argument
+ */
+public class BiConsumerProperty<T, U> extends SimpleObjectProperty<BiConsumer<T, U>> {}

+ 15 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/BiFunctionProperty.java

@@ -0,0 +1,15 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.BiFunction;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link BiFunction}.
+ *
+ * @param <T> the function's first argument
+ * @param <U> the function's second argument
+ * @param <R> the function's return type
+ */
+public class BiFunctionProperty<T, U, R> extends SimpleObjectProperty<BiFunction<T, U, R>> {}

+ 14 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/BiPredicateProperty.java

@@ -0,0 +1,14 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.BiPredicate;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link BiPredicate}.
+ *
+ * @param <T> the predicate's first argument
+ * @param <U> the predicate's second argument
+ */
+public class BiPredicateProperty<T, U> extends SimpleObjectProperty<BiPredicate<T, U>> {}

+ 13 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/ConsumerProperty.java

@@ -0,0 +1,13 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.Consumer;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link Consumer}.
+ *
+ * @param <T> the consumer's input type
+ */
+public class ConsumerProperty<T> extends SimpleObjectProperty<Consumer<T>> {}

+ 14 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/FunctionProperty.java

@@ -0,0 +1,14 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.Function;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link Function}.
+ *
+ * @param <T> the function's input type
+ * @param <R> the function's return type
+ */
+public class FunctionProperty<T, R> extends SimpleObjectProperty<Function<T, R>> {}

+ 13 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/PredicateProperty.java

@@ -0,0 +1,13 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.Predicate;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link Predicate}.
+ *
+ * @param <T> the predicate's input type
+ */
+public class PredicateProperty<T> extends SimpleObjectProperty<Predicate<T>> {}

+ 13 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/functional/SupplierProperty.java

@@ -0,0 +1,13 @@
+package io.github.palexdev.materialfx.beans.properties.functional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.function.Supplier;
+
+/**
+ * Simply an {@link ObjectProperty} that wraps a {@link Supplier}.
+ *
+ * @param <T> the supplier's return type
+ */
+public class SupplierProperty<T> extends SimpleObjectProperty<Supplier<T>> {}

+ 35 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/resettable/ResettableBooleanProperty.java

@@ -0,0 +1,35 @@
+package io.github.palexdev.materialfx.collections;
+
+import java.util.Arrays;
+import java.util.List;
+
+class ChangeHelper {
+    ChangeHelper() {}
+
+    public static String addRemoveChangeToString(int from, int to, List<?> list, List<?> removed) {
+        StringBuilder sb = new StringBuilder();
+        if (removed.isEmpty()) {
+            sb.append(list.subList(from, to));
+            sb.append(" added at ").append(from);
+        } else {
+            sb.append(removed);
+            if (from == to) {
+                sb.append(" removed at ").append(from);
+            } else {
+                sb.append(" replaced by ");
+                sb.append(list.subList(from, to));
+                sb.append(" at ").append(from);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    public static String permChangeToString(int[] permutation) {
+        return "permutated by " + Arrays.toString(permutation);
+    }
+
+    public static String updateChangeToString(int from, int to) {
+        return "updated at range [" + from + ", " + to + ")";
+    }
+}

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

@@ -0,0 +1,156 @@
+package io.github.palexdev.materialfx.collections;
+
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+import java.util.Collections;
+import java.util.List;
+
+abstract class NonIterableChange<E> extends ListChangeListener.Change<E> {
+    private final int from;
+    private final int to;
+    private boolean invalid = true;
+    private static final int[] EMPTY_PERM = new int[0];
+
+    protected NonIterableChange(int from, int to, ObservableList<E> list) {
+        super(list);
+        this.from = from;
+        this.to = to;
+    }
+
+    public int getFrom() {
+        checkState();
+        return from;
+    }
+
+    public int getTo() {
+        checkState();
+        return to;
+    }
+
+    protected int[] getPermutation() {
+        checkState();
+        return EMPTY_PERM;
+    }
+
+    public boolean next() {
+        if (invalid) {
+            invalid = false;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void reset() {
+        invalid = true;
+    }
+
+    public void checkState() {
+        if (invalid) {
+            throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change.");
+        }
+    }
+
+    public String toString() {
+        boolean oldInvalid = invalid;
+        invalid = false;
+        String ret;
+        if (wasPermutated()) {
+            ret = ChangeHelper.permChangeToString(getPermutation());
+        } else if (wasUpdated()) {
+            ret = ChangeHelper.updateChangeToString(from, to);
+        } else {
+            ret = ChangeHelper.addRemoveChangeToString(from, to, getList(), getRemoved());
+        }
+
+        invalid = oldInvalid;
+        return "{ " + ret + " }";
+    }
+
+    public static class SimpleUpdateChange<E> extends NonIterableChange<E> {
+        public SimpleUpdateChange(int position, ObservableList<E> list) {
+            this(position, position + 1, list);
+        }
+
+        public SimpleUpdateChange(int from, int to, ObservableList<E> list) {
+            super(from, to, list);
+        }
+
+        public List<E> getRemoved() {
+            return Collections.emptyList();
+        }
+
+        public boolean wasUpdated() {
+            return true;
+        }
+    }
+
+    public static class SimplePermutationChange<E> extends NonIterableChange<E> {
+        private final int[] permutation;
+
+        public SimplePermutationChange(int from, int to, int[] permutation, ObservableList<E> list) {
+            super(from, to, list);
+            this.permutation = permutation;
+        }
+
+        public List<E> getRemoved() {
+            checkState();
+            return Collections.emptyList();
+        }
+
+        protected int[] getPermutation() {
+            checkState();
+            return permutation;
+        }
+    }
+
+    public static class SimpleAddChange<E> extends NonIterableChange<E> {
+        public SimpleAddChange(int from, int to, ObservableList<E> list) {
+            super(from, to, list);
+        }
+
+        public boolean wasRemoved() {
+            checkState();
+            return false;
+        }
+
+        public List<E> getRemoved() {
+            checkState();
+            return Collections.emptyList();
+        }
+    }
+
+    public static class SimpleRemovedChange<E> extends NonIterableChange<E> {
+        private final List<E> removed;
+
+        public SimpleRemovedChange(int from, int to, E removed, ObservableList<E> list) {
+            super(from, to, list);
+            this.removed = Collections.singletonList(removed);
+        }
+
+        public boolean wasRemoved() {
+            checkState();
+            return true;
+        }
+
+        public List<E> getRemoved() {
+            checkState();
+            return removed;
+        }
+    }
+
+    public static class GenericAddRemoveChange<E> extends NonIterableChange<E> {
+        private final List<E> removed;
+
+        public GenericAddRemoveChange(int from, int to, List<E> removed, ObservableList<E> list) {
+            super(from, to, list);
+            this.removed = removed;
+        }
+
+        public List<E> getRemoved() {
+            checkState();
+            return removed;
+        }
+    }
+}

+ 269 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/collections/ObservableStack.java

@@ -0,0 +1,269 @@
+package io.github.palexdev.materialfx.collections;
+
+import io.github.palexdev.materialfx.collections.NonIterableChange.GenericAddRemoveChange;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import javafx.collections.transformation.TransformationList;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * A {@code TransformableList} is a particular type of List which wraps another
+ * List called "source" and allows manipulations such as: filter and sort, retaining
+ * the original items' index.
+ * <p></p>
+ * Extends {@link TransformationList}, it's basically the same thing of a {@link FilteredList}
+ * and a {@link SortedList} but combined into one.
+ * <p></p>
+ * A more detailed (and hopefully more clear) explanation about the "indexes retention mentioned above":
+ * <p>
+ * Think of this List as a View for the source list. The underlying data provided by the source is
+ * not guaranteed to be what the user sees but the items' properties are maintained.
+ * Let's see a brief example:
+ * <pre>
+ * {@code
+ *     // Let's say I have this ObservableList
+ *     ObservableList<String> source = FXCollections.observableArrayList("A", "B", "C"):
+ *
+ *     // Now let's say I want to sort this list in reverse order (CBA) and that
+ *     // for some reason I still want A to be the element at index 0, B-1 and C-2
+ *     // This is exactly the purpose of the TransformableList...
+ *     TransformableList<String> transformed = new TransformableList<>(source);
+ *     transformed.setSorter(Comparator.reverseOrder());
+ *
+ *     // Now that the order is (CBA) let's see how the list behaves:
+ *     transformed.get(0); // Returns C
+ *     transformed.indexOf("C"); // Returns 2, the index is retrieved in the source list
+ *     transformed.viewToSource(0); // Returns 2, it maps an index of the transformed list to the index of the source list, at 0 we have C which is at index 2 in the source list
+ *     transformed.sourceToView(0); // Also returns 2, it maps an index of the source list to the index of the transformed list, at 0 we have C which is at index 2 in the transformed list
+ *
+ *     // To better see its behavior try to sort and filter the list at the same time.
+ *     // You'll notice that sometimes sourceToView will return a negative index because the item is not in the transformed list (after a filter operation)
+ * }
+ * </pre>
+ *
+ * Check {@link #computeIndexes()} documentation to see how indexes are calculated.
+ * <p></p>
+ * <b>IMPORTANT:</b> If using a reversed comparator please use {@link #setComparator(Comparator, boolean)} with 'true' as argument,
+ * as {@link #setComparator(Comparator)} will always assume it is a natural order comparator. This is needed to make {@link #sourceToView(int)}
+ * properly work as it uses a binary search algorithm to find the right index.
+ *
+ * @param <T> the items' type
+ */
+public class TransformableList<T> extends TransformationList<T, T> {
+    //================================================================================
+    // Constructors
+    //================================================================================
+    private final List<Integer> indexes = new ArrayList<>();
+    private boolean reversed = false;
+
+    private final ObjectProperty<Predicate<? super T>> predicate = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            update();
+        }
+    };
+
+    private final ObjectProperty<Comparator<? super T>> comparator = new SimpleObjectProperty<>() {
+        @Override
+        protected void invalidated() {
+            update();
+        }
+    };
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public TransformableList(ObservableList<? extends T> source) {
+        this(source, null);
+    }
+
+    public TransformableList(ObservableList<? extends T> source, Predicate<? super T> predicate) {
+        this(source, predicate, null);
+    }
+
+    public TransformableList(ObservableList<? extends T> source, Predicate<? super T> predicate, Comparator<? super T> comparator) {
+        super(source);
+        setPredicate(predicate);
+        setComparator(comparator);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Calls {@link #getSourceIndex(int)}, just with a different name to be more clear.
+     * <p></p>
+     * Maps an index of the transformed list, to the index of the source list.
+     */
+    public int viewToSource(int index) {
+        return getSourceIndex(index);
+    }
+
+    /**
+     * Calls {@link #getViewIndex(int)}, just with a different name to be more clear.
+     * <p></p>
+     * Maps an index of the source list, to the index of the transformed list.
+     */
+    public int sourceToView(int index) {
+        return getViewIndex(index);
+    }
+
+    /**
+     * Responsible for updating the transformed indexes when the
+     * predicate or the comparator change.
+     */
+    private void update() {
+        indexes.clear();
+        indexes.addAll(computeIndexes());
+        if (this.hasListeners()) {
+            this.fireChange(new GenericAddRemoveChange<>(0, size(), new ArrayList<>(this), this));
+        }
+    }
+
+    /**
+     * Core method of TransformableLists. This is responsible for computing
+     * the transformed indexes by creating a {@link SortedMap} and mapping every index from 0 to source size
+     * to its item. Before mapping, items are filtered with the given predicate, {@link #predicateProperty()}.
+     * Before returning, the map's entry set is sorted by its values with the given comparator, {@link #comparatorProperty()}.
+     * Finally, returns the map's key set, this set contains the transformed indexes, filtered and sorted.
+     */
+    private Collection<Integer> computeIndexes() {
+        Predicate<? super T> filter = this.getPredicate();
+        Comparator<? super T> sorter = this.getComparator();
+        SortedMap<Integer, T> sourceMap;
+        if (filter != null) {
+            sourceMap = IntStream.range(0, getSource().size())
+                    .filter((index) -> filter.test(getSource().get(index)))
+                    .collect(TreeMap::new, (map, index) -> map.put(index, getSource().get(index)), TreeMap::putAll);
+        } else {
+            sourceMap = IntStream.range(0, getSource().size())
+                    .collect(TreeMap::new, (map, index) -> map.put(index, getSource().get(index)), TreeMap::putAll);
+        }
+
+        return sorter != null ? sourceMap.entrySet().stream()
+                .sorted((o1, o2) -> sorter.compare(o1.getValue(), o2.getValue()))
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toList()) : sourceMap.keySet();
+    }
+
+    public Predicate<? super T> getPredicate() {
+        return this.predicate.get();
+    }
+
+    /**
+     * Specifies the predicate used to filter the source list.
+     */
+    public ObjectProperty<Predicate<? super T>> predicateProperty() {
+        return this.predicate;
+    }
+
+    public void setPredicate(Predicate<? super T> predicate) {
+        this.predicate.set(predicate);
+    }
+
+    public Comparator<? super T> getComparator() {
+        return this.comparator.get();
+    }
+
+    /**
+     * Specifies the comparator used to sort the source list.
+     *
+     * @see #setComparator(Comparator, boolean)
+     */
+    public ObjectProperty<Comparator<? super T>> comparatorProperty() {
+        return this.comparator;
+    }
+
+    public void setComparator(Comparator<? super T> comparator) {
+        this.reversed = false;
+        this.comparator.set(comparator);
+    }
+
+    /**
+     * This method is NECESSARY if using a reversed comparator,
+     * a special flag is set to true and {@link #sourceToView(int)} behaves accordingly.
+     */
+    public void setComparator(Comparator<? super T> sorter, boolean reversed) {
+        this.reversed = reversed;
+        this.comparator.set(sorter);
+    }
+
+    /**
+     * Specifies if a reversed comparator is being used.
+     */
+    public boolean isReversed() {
+        return reversed;
+    }
+
+    /**
+     * Communicates to the transformed list, specifically to {@link #getViewIndex(int)},
+     * if the list is sorted in reversed order.
+     */
+    public void setReversed(boolean reversed) {
+        this.reversed = reversed;
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+
+    /**
+     * {@inheritDoc}
+     * <p></p>
+     * Calls {@link #update()}.
+     */
+    @Override
+    protected void sourceChanged(ListChangeListener.Change<? extends T> c) {
+        beginChange();
+        update();
+        endChange();
+    }
+
+    /**
+     * @return the number of items in the transformable list
+     */
+    @Override
+    public int size() {
+        return indexes.size();
+    }
+
+    /**
+     * Retrieves and return the item at the given index in the transformable list.
+     * This means transformations due to {@link #predicateProperty()} or {@link #comparatorProperty()}
+     * are taken into account.
+     */
+    @Override
+    public T get(int index) {
+        if (index > size()) {
+            throw new IndexOutOfBoundsException(index);
+        } else {
+            return getSource().get(indexes.get(index));
+        }
+    }
+
+    @Override
+    public int getSourceIndex(int index) {
+        if (index > size()) {
+            throw new IndexOutOfBoundsException(index);
+        } else {
+            return indexes.get(index);
+        }
+    }
+
+    @Override
+    public int getViewIndex(int index) {
+        int viewIndex = reversed ?
+                Collections.binarySearch(indexes, index, Collections.reverseOrder()) :
+                Collections.binarySearch(indexes, index);
+        return viewIndex < 0 ? -1 : viewIndex;
+    }
+}

+ 168 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/collections/TransformableListWrapper.java

@@ -0,0 +1,168 @@
+package io.github.palexdev.materialfx.collections;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.ObjectProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+import java.util.*;
+import java.util.function.Predicate;
+
+@SuppressWarnings({"unchecked", "NullableProblems"})
+public class TransformableListWrapper<T> extends AbstractList<T> implements ObservableList<T> {
+    private final ObservableList<T> source;
+    private final TransformableList<T> transformableList;
+
+    public TransformableListWrapper(ObservableList<T> source) {
+        this.source = source;
+        this.transformableList = new TransformableList<>(source);
+    }
+
+    @Override
+    public void addListener(ListChangeListener<? super T> listener) {
+        transformableList.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(ListChangeListener<? super T> listener) {
+        transformableList.removeListener(listener);
+    }
+
+    @Override
+    public boolean add(T t) {
+        return source.add(t);
+    }
+
+    @Override
+    public T set(int index, T element) {
+        return source.set(index, element);
+    }
+
+    @Override
+    public void add(int index, T element) {
+        source.add(index, element);
+    }
+
+    @Override
+    public T remove(int index) {
+        return source.remove(index);
+    }
+
+    @Override
+    public int indexOf(Object o) {
+        return transformableList.indexOf(o);
+    }
+
+    @Override
+    public int lastIndexOf(Object o) {
+        return transformableList.lastIndexOf(o);
+    }
+
+    @Override
+    public void clear() {
+        source.clear();
+    }
+
+    @Override
+    public boolean addAll(int index, Collection<? extends T> c) {
+        return source.addAll(index, c);
+    }
+
+    @Override
+    public boolean addAll(T... elements) {
+        return source.addAll(elements);
+    }
+
+    @Override
+    public boolean setAll(T... elements) {
+        return source.setAll(elements);
+    }
+
+    @Override
+    public boolean setAll(Collection<? extends T> col) {
+        return source.setAll(col);
+    }
+
+    @Override
+    public boolean removeAll(T... elements) {
+        return source.removeAll(elements);
+    }
+
+    @Override
+    public boolean retainAll(T... elements) {
+        return source.retainAll(elements);
+    }
+
+    @Override
+    public void remove(int from, int to) {
+        source.remove(from, to);
+    }
+
+    @Override
+    public void addListener(InvalidationListener listener) {
+        transformableList.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(InvalidationListener listener) {
+        transformableList.removeListener(listener);
+    }
+
+    @Override
+    public T get(int index) {
+        return transformableList.get(index);
+    }
+
+    @Override
+    public int size() {
+        return transformableList.size();
+    }
+
+    public ObservableList<? extends T> getSource() {
+        return transformableList.getSource();
+    }
+
+    public int viewToSource(int index) {
+        return transformableList.viewToSource(index);
+    }
+
+    public int sourceToView(int index) {
+        return transformableList.sourceToView(index);
+    }
+
+    public Predicate<? super T> getPredicate() {
+        return transformableList.getPredicate();
+    }
+
+    public ObjectProperty<Predicate<? super T>> predicateProperty() {
+        return transformableList.predicateProperty();
+    }
+
+    public void setPredicate(Predicate<? super T> predicate) {
+        transformableList.setPredicate(predicate);
+    }
+
+    public Comparator<? super T> getComparator() {
+        return transformableList.getComparator();
+    }
+
+    public ObjectProperty<Comparator<? super T>> comparatorProperty() {
+        return transformableList.comparatorProperty();
+    }
+
+    public void setComparator(Comparator<? super T> comparator) {
+        transformableList.setComparator(comparator);
+    }
+
+    public void setComparator(Comparator<? super T> sorter, boolean reversed) {
+        transformableList.setComparator(sorter, reversed);
+    }
+
+    public boolean isReversed() {
+        return transformableList.isReversed();
+    }
+
+    public void setReversed(boolean reversed) {
+        transformableList.setReversed(reversed);
+    }
+}

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

@@ -19,10 +19,10 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.enums.ButtonType;
+import io.github.palexdev.materialfx.enums.ButtonType;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.skins.MFXButtonSkin;
 import javafx.beans.property.*;
 import javafx.css.*;
@@ -119,7 +119,7 @@ public class MFXButton extends Button {
         setRippleColor(Color.rgb(190, 190, 190));
         setRippleRadius(25);
         setComputeRadiusMultiplier(true);
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
     }
 
     public boolean isComputeRadiusMultiplier() {

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXToggleNode;
-import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.enums.TextPosition;
 import io.github.palexdev.materialfx.skins.MFXCircleToggleNodeSkin;
 import javafx.css.*;
 import javafx.scene.Node;

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.enums.DialogType;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.selection.ComboBoxSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXComboBoxSkin;
@@ -41,7 +41,7 @@ import javafx.scene.paint.Paint;
 import java.util.List;
 import java.util.function.Supplier;
 
-import static io.github.palexdev.materialfx.controls.enums.Styles.ComboBoxStyles;
+import static io.github.palexdev.materialfx.enums.Styles.ComboBoxStyles;
 
 /**
  * This is the implementation of a combo box following Google's material design guidelines in JavaFX.
@@ -229,7 +229,7 @@ public class MFXComboBox<T> extends Control implements Validated<MFXDialogValida
                         .addMenuItem(selectLast)
                         .addSeparator()
                         .addMenuItem(resetSelection)
-                        .install()
+                        .installAndGet()
         );
     }
 

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

@@ -365,9 +365,9 @@ public class MFXContextMenu extends VBox {
         }
 
         /**
-         * Installs the added items in the context menu.
+         * Installs the added items in the context menu and returns it.
          */
-        public MFXContextMenu install() {
+        public MFXContextMenu installAndGet() {
             contextMenu.setItems(items);
             return contextMenu;
         }

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXDatePickerContent;
 import io.github.palexdev.materialfx.utils.NodeUtils;
@@ -145,12 +145,10 @@ public class MFXDatePicker extends VBox {
         rippleGenerator.setClipSupplier(() -> null);
         rippleGenerator.setRadiusMultiplier(1.7);
         rippleGenerator.setRippleColor(Color.rgb(98, 0, 238, 0.3));
-        rippleGenerator.setRipplePositionFunction(event -> {
-            RipplePosition ripplePosition = new RipplePosition();
-            ripplePosition.setXPosition(calendar.getBoundsInParent().getCenterX());
-            ripplePosition.setYPosition(calendar.getBoundsInParent().getCenterY());
-            return ripplePosition;
-        });
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(
+                calendar.getBoundsInParent().getCenterX(),
+                calendar.getBoundsInParent().getCenterY()
+        ));
         getChildren().add(0, rippleGenerator);
     }
 

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

@@ -18,7 +18,7 @@
 
 package io.github.palexdev.materialfx.controls;
 
-import io.github.palexdev.materialfx.controls.enums.ButtonType;
+import io.github.palexdev.materialfx.enums.ButtonType;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.ExceptionUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;

+ 240 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterComboBox.java

@@ -0,0 +1,240 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.enums.ChainMode;
+import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.beans.FilterBean;
+import io.github.palexdev.materialfx.skins.MFXFilterPaneSkin;
+import io.github.palexdev.materialfx.utils.PredicateUtils;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+
+import java.util.function.Predicate;
+
+/**
+ * This control allows to produce a {@link Predicate} for a given object type
+ * interactively, meaning that the filter is assembled from the user choices.
+ * To produce a filter the user must choose the object's field, input/choose a query
+ * and a way to evaluate the object's field against the query.
+ * <p></p>
+ * From now on all code examples to better understand the functionalities of this control
+ * will use these POJO classes:
+ * <pre>
+ * {@code
+ *      public enum Gender {
+ *          MALE, FEMALE
+ *      }
+ *
+ *      public class Person {
+ *          private final String name;
+ *          private final int age;
+ *          private final Gender gender;
+ *          private final City city;
+ *
+ *          public Person(String name, int age, Gender gender, City city) {
+ *              this.name = name;
+ *              this.age = age;
+ *              this.gender = gender;
+ *              this.city = city;
+ *          }
+ *
+ *          public String name() {
+ *              return name;
+ *          }
+ *
+ *          public int age() {
+ *              return age;
+ *          }
+ *
+ *          public Gender gender() {
+ *              return gender;
+ *          }
+ *
+ *          public City city() {
+ *              return city;
+ *          }
+ *      }
+ *
+ *      public class City {
+ *          private final String name;
+ *          private final long population;
+ *
+ *          public City(String name, long population) {
+ *              this.name = name;
+ *              this.population = population;
+ *          }
+ *
+ *          public String name() {
+ *              return name;
+ *          }
+ *
+ *          public long population() {
+ *              return population;
+ *          }
+ *      }
+ * }
+ * </pre>
+ * <p></p>
+ * To specify on which fields to operate the filters must be added like this:
+ * <pre>
+ * {@code
+ *      MFXFilterPane<Person> fp = new MFXFilterPane<>();
+ *      AbstractFilter<Person, String> nameFilter = new StringFilter<>("Name", Person::name)
+ *      AbstractFilter<Person, Integer> ageFilter = new IntegerFilter<>("Age", Person:.age);
+ *
+ *      // MFXFilterPane is so powerful and versatile that you can also filter by nested objects, something like this for example...
+ *      AbstractFilter<Person, Long> populationFilter = new LongFilter<>("City Population", person -> person.city().population());
+ *
+ *      // It even works for enumerators...
+ *      AbstractFilter<Person, Enum<Gender>> genderFilter = new EnumFilter<>("Gender", Person::gender, Gender.class); // Note that the type is necessary
+ *
+ *      // Finally...
+ *      fp.getFilters().addAll(nameFilter, ageFilter, populationFilter, genderFilter);
+ * }
+ * </pre>
+ *
+ * <p></p>
+ * When a filter is created through the add button, a {@link FilterBean} is created and added to a list
+ * which holds the "active filters", {@link #getActiveFilters()}.
+ * <p>
+ * Note that the list is not unmodifiable, potentially, you could even add your own custom filters, the UI will be updated anyway.
+ * <p>
+ * As you can read in the {@link FilterBean} documentation, they can be chained according to the specified {@link FilterBean#getMode()},
+ * this is also interactive, meaning that when you build more than one filter, a node will appear between them, that node specifies
+ * the {@link ChainMode}, by clicking on it, you can switch between modes.
+ * <p></p>
+ * Once filters you have finished you can produce a filter by calling {@link #filter()}.
+ * <p>
+ * The control also offers to icons that are intended to produce a filter or reset the control,
+ * to set their behavior use {@link #setOnFilter(EventHandler)} and {@link #setOnReset(EventHandler)}.
+ *
+ * @param <T>
+ */
+public class MFXFilterPane<T> extends Control {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-filter-pane";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/MFXFilterPane.css");
+    private final StringProperty headerText = new SimpleStringProperty("Filters");
+    private final ObservableList<AbstractFilter<T, ?>> filters = FXCollections.observableArrayList();
+    private final ObservableList<FilterBean<T, ?>> activeFilters = FXCollections.observableArrayList();
+
+    private EventHandler<MouseEvent> onFilter = event -> {};
+    private EventHandler<MouseEvent> onReset = event -> {};
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXFilterPane() {
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        getStylesheets().add(STYLESHEET);
+    }
+
+    /**
+     * Builds a predicate from the list of built filters (active filters).
+     * <p></p>
+     * The {@link FilterBean} are chained by using {@link PredicateUtils#chain(Predicate, Predicate, ChainMode)}.
+     * <p></p>
+     * If the list is empty by default a predicate that always returns true is built.
+     */
+    public Predicate<T> filter() {
+        Predicate<T> filter = null;
+        ChainMode mode = null;
+
+        for (FilterBean<T, ?> activeFilter : activeFilters) {
+            if (filter == null) {
+                filter = activeFilter.predicate();
+                mode = activeFilter.getMode();
+                continue;
+            }
+
+            filter = PredicateUtils.chain(filter, activeFilter.predicate(), mode);
+            mode = activeFilter.getMode();
+        }
+
+        return filter != null ? filter : t -> true;
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+    public String getHeaderText() {
+        return headerText.get();
+    }
+
+    /**
+     * Specifies the text of the header.
+     */
+    public StringProperty headerTextProperty() {
+        return headerText;
+    }
+
+    public void setHeaderText(String headerText) {
+        this.headerText.set(headerText);
+    }
+
+    /**
+     * @return the list of {@link AbstractFilter}s. Each of them
+     * represents an object's field o  which the filter operates
+     */
+    public ObservableList<AbstractFilter<T, ?>> getFilters() {
+        return filters;
+    }
+
+    /**
+     * @return the list of built filters
+     */
+    public ObservableList<FilterBean<T, ?>> getActiveFilters() {
+        return activeFilters;
+    }
+
+    /**
+     * @return the action invoked when clicking on the filter icon
+     */
+    public EventHandler<MouseEvent> getOnFilter() {
+        return onFilter;
+    }
+
+    /**
+     * Sets the action to perform when the filter icon is clicked.
+     */
+    public void setOnFilter(EventHandler<MouseEvent> onFilter) {
+        this.onFilter = onFilter;
+    }
+
+    /**
+     * @return the action invoked when clicking on the reset icon
+     */
+    public EventHandler<MouseEvent> getOnReset() {
+        return onReset;
+    }
+
+    /**
+     * Sets the action to perform when the reset icon is clicked.
+     */
+    public void setOnReset(EventHandler<MouseEvent> onReset) {
+        this.onReset = onReset;
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXFilterPaneSkin<>(this);
+    }
+}

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

@@ -19,8 +19,8 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.beans.MFXLoaderBean;
-import io.github.palexdev.materialfx.controls.enums.LoaderCacheLevel;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.enums.LoaderCacheLevel;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.utils.LoaderUtils;
 import io.github.palexdev.materialfx.utils.ToggleButtonsUtil;
 import javafx.application.Platform;

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

@@ -19,7 +19,7 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.effects.ripple.base.IRippleGenerator;
 import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.ObjectProperty;
@@ -63,6 +63,8 @@ public class MFXIconWrapper extends StackPane {
         setSize(size);
     }
 
+    // TODO add constructor with MFXFontIcon description and replace everywhere
+
     //================================================================================
     // Methods
     //================================================================================
@@ -87,7 +89,7 @@ public class MFXIconWrapper extends StackPane {
      */
     public MFXIconWrapper defaultRippleGeneratorBehavior() {
         addRippleGenerator();
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
         addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
             if (event.getButton() == MouseButton.PRIMARY) {
                 rippleGenerator.generateRipple(event);
@@ -103,7 +105,7 @@ public class MFXIconWrapper extends StackPane {
      * @see IRippleGenerator
      * @see MFXCircleRippleGenerator
      */
-    public MFXIconWrapper rippleGeneratorBehavior(Function<MouseEvent, RipplePosition> positionFunction) {
+    public MFXIconWrapper rippleGeneratorBehavior(Function<MouseEvent, PositionBean> positionFunction) {
         addRippleGenerator();
         rippleGenerator.setRipplePositionFunction(positionFunction);
         addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {

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

@@ -34,7 +34,7 @@ import javafx.scene.text.Font;
 
 import java.util.List;
 
-import static io.github.palexdev.materialfx.controls.enums.Styles.LabelStyles;
+import static io.github.palexdev.materialfx.enums.Styles.LabelStyles;
 
 /**
  * This is the implementation of a label following Google's material design guidelines in JavaFX.

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

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

+ 721 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotificationCenter.java

@@ -0,0 +1,721 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.beans.properties.functional.FunctionProperty;
+import io.github.palexdev.materialfx.collections.TransformableListWrapper;
+import io.github.palexdev.materialfx.controls.cell.MFXNotificationCell;
+import io.github.palexdev.materialfx.enums.NotificationCounterStyle;
+import io.github.palexdev.materialfx.enums.NotificationState;
+import io.github.palexdev.materialfx.notifications.base.INotification;
+import io.github.palexdev.materialfx.selection.MultipleSelectionModel;
+import io.github.palexdev.materialfx.skins.MFXNotificationCenterSkin;
+import io.github.palexdev.materialfx.utils.ExecutionUtils;
+import io.github.palexdev.materialfx.utils.ListChangeProcessor;
+import io.github.palexdev.materialfx.utils.others.ReusableScheduledExecutor;
+import io.github.palexdev.virtualizedfx.beans.NumberRange;
+import io.github.palexdev.virtualizedfx.flow.simple.SimpleVirtualFlow;
+import io.github.palexdev.virtualizedfx.utils.ListChangeHelper;
+import io.github.palexdev.virtualizedfx.utils.ListChangeHelper.Change;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.LongBinding;
+import javafx.beans.property.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import static io.github.palexdev.materialfx.enums.NotificationCounterStyle.NUMBER;
+
+/**
+ * A quite complex but easy to use implementation of a modern notification center.
+ * <p></p>
+ * For the notifications it uses {@link TransformableListWrapper} as list implementation, this allows
+ * not only basic operations such additions, removals and replacements, but also filter and sort operations.
+ * <p></p>
+ * It's composed by an icon and a popup that contains the list of notifications.
+ * <p>
+ * A complete list of the features the notification center offers:
+ * <p> - Uses a virtual flow to show the notifications and have high performance
+ * <p> - Uses {@link MFXNotificationCell} as cells to contain the notifications, those special
+ * cells perfectly integrate with the selection mode feature of the notification center to show a checkbox when needed
+ * <p> - Has a {@link MultipleSelectionModel} to keep track of the selected notifications
+ * <p> - Has a property that keeps track of the number of unread notifications
+ * <p> - Has two styles for the unread counter: as a DOT, or as a dot with the NUMBER
+ * <p> - Allows to change the header's text
+ * <p> - Allows to toggle a "Do not disturb" mode
+ * <p> - Has a property that specifies whether the popup is showing or not
+ * <p> - Has a property that specifies whether the mouse is on the popup (it's bound, cannot be set, nor unbound)
+ * <p> - Allows to specify the space between the bell icon and the popup
+ * <p> - Allows to specify the popup's size. Must be greater than 0 and should always be larger than the bell's icon
+ * <p> - Has a context menu that opens when right-clicking on the virtual flow, by default it contains
+ * items to perform selection, filter and sort actions. It can also be disabled by setting a null factory, {@link #contextMenuFactoryProperty()}
+ * <p> - Has a flag to specify whether the notification center is animated or not
+ * <p> - Has a flag to specify whether visible notifications should be set as READ when the popup is shown
+ * <p> - Has a flag to specify whether notifications should be set as READ when they are dismissed
+ * <p> - Allows specifying the action to perform when the bell icon is pressed, by default inverts the value of the {@link #showingProperty()}
+ * to inform the popup that it should open/hide
+ * <p> Has an executor that runs every 60 seconds (by default, can be changed) that updates all the notifications.
+ * By update, I mean that {@link INotification#updateElapsed()} is called. The service can be stopped and started
+ * whenever desired, by default it is always started.
+ * <p></p>
+ * As you can see it as a LOT to offer, and to be honest it's not everything I wanted to implement, but I decided to
+ * restrain myself for now, as adding any other feature would add more and more complexity.
+ */
+public class MFXNotificationCenter extends Control {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-notification-center";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/MFXNotificationCenter.css");
+
+    private final TransformableListWrapper<INotification> notifications = new TransformableListWrapper<>(FXCollections.observableArrayList());
+
+    private final SimpleVirtualFlow<INotification, MFXNotificationCell> virtualFlow;
+    private final MultipleSelectionModel<INotification> selectionModel = new MultipleSelectionModel<>(notifications);
+
+    private final BooleanProperty selectionMode = new SimpleBooleanProperty(false);
+    private final ReadOnlyLongWrapper unreadCount = new ReadOnlyLongWrapper(0);
+    private final LongBinding unreadCountBinding;
+
+    private final ObjectProperty<NotificationCounterStyle> counterStyle = new SimpleObjectProperty<>(NUMBER);
+    private final StringProperty headerTextProperty = new SimpleStringProperty("Notifications");
+    private final BooleanProperty doNotDisturb = new SimpleBooleanProperty(false);
+    private final BooleanProperty showing = new SimpleBooleanProperty(false);
+
+    private final BooleanProperty popupHover = new SimpleBooleanProperty(false) {
+        @Override
+        public void unbind() {}
+    };
+    private final DoubleProperty popupSpacing = new SimpleDoubleProperty(10);
+    private final DoubleProperty popupWidth = new SimpleDoubleProperty(450);
+    private final DoubleProperty popupHeight = new SimpleDoubleProperty(550);
+
+    private MFXContextMenu contextMenu;
+    private final FunctionProperty<Node, MFXContextMenu> contextMenuFactory = new FunctionProperty<>() {
+        @Override
+        public void set(Function<Node, MFXContextMenu> newValue) {
+            if (contextMenu != null) {
+                contextMenu.dispose();
+                contextMenu = null;
+            }
+            if (newValue != null) {
+                contextMenu = newValue.apply(virtualFlow);
+            }
+            super.set(newValue);
+        }
+    };
+
+    private boolean animated = true;
+    private boolean markAsReadOnShow = false;
+    private boolean markAsReadOnDismiss = false;
+
+    private EventHandler<MouseEvent> onIconClicked = event -> setShowing(!isShowing());
+
+    private final ReusableScheduledExecutor notificationsUpdater;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXNotificationCenter() {
+        virtualFlow = SimpleVirtualFlow.Builder.create(
+                notifications,
+                notification -> new MFXNotificationCell(this, notification),
+                Orientation.VERTICAL
+        );
+
+        unreadCountBinding = Bindings.createLongBinding(() ->
+                notifications.stream()
+                        .filter(notification -> notification.getState() == NotificationState.UNREAD)
+                        .count(),
+                notifications
+        );
+
+        notificationsUpdater = new ReusableScheduledExecutor(Executors.newScheduledThreadPool(
+                1,
+                r -> {
+                    Thread thread = new Thread(r);
+                    thread.setDaemon(true);
+                    return thread;
+                }
+        ));
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        getStylesheets().add(STYLESHEET);
+        setPrefSize(400, 550);
+        defaultContextMenu();
+
+        unreadCount.bind(unreadCountBinding);
+        notifications.addListener((ListChangeListener<? super INotification>) change -> {
+            if (!selectionModel.getSelection().isEmpty()) {
+                if (change.getList().isEmpty()) {
+                    selectionModel.clearSelection();
+                } else {
+                    Change c = ListChangeHelper.processChange(change, NumberRange.of(0, Integer.MAX_VALUE));
+                    ListChangeProcessor updater = new ListChangeProcessor(selectionModel.getSelection().keySet());
+                    c.processReplacement((changed, removed) -> selectionModel.replaceSelection(changed.toArray(new Integer[0])));
+                    c.processAddition((from, to, added) -> {
+                        updater.computeAddition(added.size(), from);
+                        selectionModel.replaceSelection(updater.getIndexes().toArray(new Integer[0]));
+                    });
+                    c.processRemoval((from, to, removed) -> {
+                        updater.computeRemoval(removed, from);
+                        selectionModel.replaceSelection(updater.getIndexes().toArray(new Integer[0]));
+                    });
+                }
+            }
+        });
+
+        notifications.predicateProperty().addListener(invalidated -> {
+            setSelectionMode(false);
+            selectionModel.clearSelection();
+        });
+        notifications.comparatorProperty().addListener(invalidated -> {
+            setSelectionMode(false);
+            selectionModel.clearSelection();
+        });
+
+        showing.addListener((observable, oldValue, newValue) -> {
+            if (newValue && markAsReadOnShow) markVisibleNotificationsAs(NotificationState.READ);
+        });
+
+        startNotificationsUpdater(60, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Responsible for building and setting the default context menu.
+     */
+    protected void defaultContextMenu() {
+        setContextMenuFactory(owner -> {
+            MFXContextMenuItem selectAll = new MFXContextMenuItem("Select All");
+            selectAll.setAction(event -> {
+                        if (notifications.isEmpty()) return;
+                        NumberRange<Integer> indexes = NumberRange.of(0, notifications.size() - 1);
+                        selectionModel.replaceSelection(NumberRange.expandRange(indexes).toArray(Integer[]::new));
+                    }
+            );
+
+            MFXContextMenuItem selectRead = new MFXContextMenuItem("Select Read");
+            selectRead.setAction(event -> {
+                if (notifications.isEmpty()) return;
+                Integer[] indexes = IntStream.range(0, notifications.size())
+                        .filter(i -> notifications.get(i).getState() == NotificationState.READ)
+                        .boxed()
+                        .toArray(Integer[]::new);
+                selectionModel.replaceSelection(indexes);
+            });
+
+            MFXContextMenuItem selectUnread = new MFXContextMenuItem("Select Unread");
+            selectUnread.setAction(event -> {
+                if (notifications.isEmpty()) return;
+                Integer[] indexes = IntStream.range(0, notifications.size())
+                        .filter(i -> notifications.get(i).getState() == NotificationState.UNREAD)
+                        .boxed()
+                        .toArray(Integer[]::new);
+                selectionModel.replaceSelection(indexes);
+            });
+
+            MFXContextMenuItem clearSelection = new MFXContextMenuItem("Clear Selection");
+            clearSelection.setAction(event -> selectionModel.clearSelection());
+
+            MFXContextMenuItem sortByState = new MFXContextMenuItem("Sort By State");
+            sortByState.setAction(event -> notifications.setComparator(Comparator.comparing(INotification::getState)));
+
+            MFXContextMenuItem sortByTime = new MFXContextMenuItem("Sort By Time");
+            sortByTime.setAction(event -> notifications.setComparator(Comparator.comparing(INotification::getTime)));
+
+            MFXContextMenuItem reverseSort = new MFXContextMenuItem("Reverse Sort");
+            reverseSort.setAction(event -> {
+                if (notifications.getComparator() == null) return;
+                Comparator<? super INotification> comparator = notifications.getComparator();
+                notifications.setComparator(comparator.reversed());
+            });
+
+            MFXContextMenuItem filterRead = new MFXContextMenuItem("Filter By Read");
+            filterRead.setAction(event -> notifications.setPredicate(notification -> notification.getState() == NotificationState.READ));
+
+            MFXContextMenuItem filterUnread = new MFXContextMenuItem("Filter By Unread");
+            filterUnread.setAction(event -> notifications.setPredicate(notification -> notification.getState() == NotificationState.UNREAD));
+
+            MFXContextMenuItem clearFilter = new MFXContextMenuItem("Clear Filter");
+            clearFilter.setAction(event -> notifications.setPredicate(null));
+
+            MFXContextMenuItem clearSort = new MFXContextMenuItem("Clear Sort");
+            clearSort.setAction(event -> notifications.setComparator(null));
+
+
+            MFXContextMenu contextMenu = MFXContextMenu.Builder.build(owner)
+                    .addMenuItem(selectAll)
+                    .addMenuItem(selectRead)
+                    .addMenuItem(selectUnread)
+                    .addMenuItem(clearSelection)
+                    .addSeparator()
+                    .addMenuItem(sortByState)
+                    .addMenuItem(sortByTime)
+                    .addMenuItem(reverseSort)
+                    .addSeparator()
+                    .addMenuItem(filterRead)
+                    .addMenuItem(filterUnread)
+                    .addSeparator()
+                    .addMenuItem(clearSort)
+                    .addMenuItem(clearFilter)
+                    .installAndGet();
+
+            ExecutionUtils.executeWhen(
+                    getStylesheets(),
+                    () -> {
+                        String base = contextMenu.getUserAgentStylesheet(); // TODO change if making THAT change
+                        List<String> stylesheets = new ArrayList<>(List.of(base));
+                        stylesheets.addAll(getStylesheets());
+                        contextMenu.getStylesheets().setAll(stylesheets);
+                    },
+                    true,
+                    () -> true,
+                    false
+            );
+            return contextMenu;
+        });
+    }
+
+    /**
+     * Starts the notifications updater service to run the update task
+     * periodically, according to the given period and time unit.
+     *
+     * @see INotification#updateElapsed()
+     */
+    public void startNotificationsUpdater(long period, TimeUnit timeUnit) {
+        notificationsUpdater.scheduleAtFixedRate(
+                () -> notifications.forEach(INotification::updateElapsed),
+                0,
+                period,
+                timeUnit
+        );
+    }
+
+    /**
+     * Immediately stops the notifications updater service.
+     */
+    public void stopNotificationsUpdater() {
+        notificationsUpdater.cancelNow();
+    }
+
+    /**
+     * Sets all the given notifications' state to the given state.
+     * <p>
+     * At the end recomputes the number of unread notifications.
+     */
+    public void markNotificationsAs(NotificationState state, INotification... notifications) {
+        for (INotification notification : notifications) {
+            notification.setNotificationState(state);
+        }
+        unreadCountBinding.invalidate();
+    }
+
+    /**
+     * Sets all the visible notifications' state to the given state.
+     */
+    public void markVisibleNotificationsAs(NotificationState state) {
+        markNotificationsAs(
+                state,
+                getCells().values().stream()
+                        .map(MFXNotificationCell::getNotification)
+                        .toArray(INotification[]::new)
+        );
+    }
+
+    /**
+     * Sets all the selected notifications' state to the given state.
+     */
+    public void markSelectedNotificationsAs(NotificationState state) {
+        markNotificationsAs(state, selectionModel.getSelection().values().toArray(INotification[]::new));
+    }
+
+    /**
+     * Sets all the notifications' state to the given state.
+     */
+    public void markAllNotificationsAs(NotificationState state) {
+        markNotificationsAs(state, notifications.toArray(INotification[]::new));
+    }
+
+    /**
+     * Sets all the given notifications' state to READ, then removes them from the notifications list.
+     */
+    public void dismiss(INotification... notifications) {
+        if (markAsReadOnDismiss) {
+            markNotificationsAs(NotificationState.READ, notifications);
+        }
+        this.notifications.removeAll(notifications);
+    }
+
+    /**
+     * Sets all the visible notifications' state to READ, then removes them from the notifications list.
+     */
+    public void dismissVisible() {
+        dismiss(getCells().values().stream().map(MFXNotificationCell::getNotification).toArray(INotification[]::new));
+    }
+
+    /**
+     * Sets all the selected notifications' state to READ, then removes them from the notifications list.
+     */
+    public void dismissSelected() {
+        dismiss(getSelectionModel().getSelection().values().toArray(INotification[]::new));
+    }
+
+    /**
+     * Sets all the notifications' state to READ, then removes them from the notifications list.
+     */
+    public void dismissAll() {
+        dismiss(notifications.toArray(INotification[]::new));
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+
+    /**
+     * @return the list of notifications
+     */
+    public TransformableListWrapper<INotification> getNotifications() {
+        return notifications;
+    }
+
+    /**
+     * @return the selection model instance used to keep track of selected notifications
+     */
+    public MultipleSelectionModel<INotification> getSelectionModel() {
+        return selectionModel;
+    }
+
+    public boolean isSelectionMode() {
+        return selectionMode.get();
+    }
+
+    /**
+     * Specifies if the notification center is in selection mode.
+     * <p></p>
+     * By default this mode triggers {@link MFXNotificationCell} to show a checkbox for selection
+     */
+    public BooleanProperty selectionModeProperty() {
+        return selectionMode;
+    }
+
+    public void setSelectionMode(boolean selectionMode) {
+        this.selectionMode.set(selectionMode);
+    }
+
+    public long getUnreadCount() {
+        return unreadCount.get();
+    }
+
+    /**
+     * Specifies the number of unread notifications.
+     */
+    public ReadOnlyLongProperty unreadCountProperty() {
+        return unreadCount.getReadOnlyProperty();
+    }
+
+    public NotificationCounterStyle getCounterStyle() {
+        return counterStyle.get();
+    }
+
+    /**
+     * Specifies the style of the unread counter.
+     */
+    public ObjectProperty<NotificationCounterStyle> counterStyleProperty() {
+        return counterStyle;
+    }
+
+    public void setCounterStyle(NotificationCounterStyle counterStyle) {
+        this.counterStyle.set(counterStyle);
+    }
+
+    public String getHeaderTextProperty() {
+        return headerTextProperty.get();
+    }
+
+    /**
+     * Specifies the header's text.
+     */
+    public StringProperty headerTextPropertyProperty() {
+        return headerTextProperty;
+    }
+
+    public void setHeaderTextProperty(String headerTextProperty) {
+        this.headerTextProperty.set(headerTextProperty);
+    }
+
+    public boolean isDoNotDisturb() {
+        return doNotDisturb.get();
+    }
+
+    /**
+     * Specifies if the notification center is in "Do not disturb" mode.
+     */
+    public BooleanProperty doNotDisturbProperty() {
+        return doNotDisturb;
+    }
+
+    public void setDoNotDisturb(boolean doNotDisturb) {
+        this.doNotDisturb.set(doNotDisturb);
+    }
+
+    public boolean isShowing() {
+        return showing.get();
+    }
+
+    /**
+     * Specifies if the popup is shown/hidden.
+     * <p>
+     * Can also be used to control the popup.
+     */
+    public BooleanProperty showingProperty() {
+        return showing;
+    }
+
+    public void setShowing(boolean showing) {
+        this.showing.set(showing);
+    }
+
+    public boolean isPopupHover() {
+        return popupHover.get();
+    }
+
+    /**
+     * Specifies if the mouse is on the popup.
+     * <p></p>
+     * Despite being a Read-Write property, it is bound to the popup's hover property
+     * and cannot be unbound. Attempts to set this will always fail with an exception.
+     */
+    public BooleanProperty popupHoverProperty() {
+        return popupHover;
+    }
+
+    public double getPopupSpacing() {
+        return popupSpacing.get();
+    }
+
+    /**
+     * Specifies the space between the bell icon and the popup
+     */
+    public DoubleProperty popupSpacingProperty() {
+        return popupSpacing;
+    }
+
+    public void setPopupSpacing(double popupSpacing) {
+        this.popupSpacing.set(popupSpacing);
+    }
+
+    public double getPopupWidth() {
+        return popupWidth.get();
+    }
+
+    /**
+     * Specifies the popups' width.
+     */
+    public DoubleProperty popupWidthProperty() {
+        return popupWidth;
+    }
+
+    public void setPopupWidth(double popupWidth) {
+        this.popupWidth.set(popupWidth);
+    }
+
+    public double getPopupHeight() {
+        return popupHeight.get();
+    }
+
+    /**
+     * Specifies the popup's height.
+     */
+    public DoubleProperty popupHeightProperty() {
+        return popupHeight;
+    }
+
+    public void setPopupHeight(double popupHeight) {
+        this.popupHeight.set(popupHeight);
+    }
+
+    public Function<Node, MFXContextMenu> getContextMenuFactory() {
+        return contextMenuFactory.get();
+    }
+
+    /**
+     * Specifies the function used to produce a {@link MFXContextMenu}.
+     * <p></p>
+     * Setting this to null will remove the context menu.
+     */
+    public FunctionProperty<Node, MFXContextMenu> contextMenuFactoryProperty() {
+        return contextMenuFactory;
+    }
+
+    public void setContextMenuFactory(Function<Node, MFXContextMenu> contextMenuFactory) {
+        this.contextMenuFactory.set(contextMenuFactory);
+    }
+
+    /**
+     * Specifies whether the notification center is animated.
+     */
+    public boolean isAnimated() {
+        return animated;
+    }
+
+    public void setAnimated(boolean animated) {
+        this.animated = animated;
+    }
+
+    /**
+     * Specifies whether visible notification should be marked as read when the popup is shown.
+     */
+    public boolean isMarkAsReadOnShow() {
+        return markAsReadOnShow;
+    }
+
+    public void setMarkAsReadOnShow(boolean markAsReadOnShow) {
+        this.markAsReadOnShow = markAsReadOnShow;
+    }
+
+    /**
+     * Specifies whether dismissed notifications should be set as READ.
+     * <p></p>
+     * For this to work use one of the dismiss methods offered by the control.
+     */
+    public boolean isMarkAsReadOnDismiss() {
+        return markAsReadOnDismiss;
+    }
+
+    public void setMarkAsReadOnDismiss(boolean markAsReadOnDismiss) {
+        this.markAsReadOnDismiss = markAsReadOnDismiss;
+    }
+
+    /**
+     * Specifies the action to perform when the bell icon is clicked.
+     */
+    public EventHandler<MouseEvent> getOnIconClicked() {
+        return onIconClicked;
+    }
+
+    public void setOnIconClicked(EventHandler<MouseEvent> onIconClicked) {
+        this.onIconClicked = onIconClicked;
+    }
+
+    //================================================================================
+    // Delegate Methods
+    //================================================================================
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#getCell(int)}.
+     */
+    public MFXNotificationCell getCell(int index) {
+        return virtualFlow.getCell(index);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#getCells()}.
+     */
+    public Map<Integer, MFXNotificationCell> getCells() {
+        return virtualFlow.getCells();
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#scrollBy(double)}.
+     */
+    public void scrollBy(double pixels) {
+        virtualFlow.scrollBy(pixels);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#scrollTo(int)}.
+     */
+    public void scrollTo(int index) {
+        virtualFlow.scrollTo(index);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#scrollToFirst()}.
+     */
+    public void scrollToFirst() {
+        virtualFlow.scrollToFirst();
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#scrollToLast()}.
+     */
+    public void scrollToLast() {
+        virtualFlow.scrollToLast();
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#scrollToPixel(double)}.
+     */
+    public void scrollToPixel(double pixel) {
+        virtualFlow.scrollToPixel(pixel);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#setHSpeed(double, double)}.
+     */
+    public void setHSpeed(double unit, double block) {
+        virtualFlow.setHSpeed(unit, block);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#setVSpeed(double, double)}.
+     */
+    public void setVSpeed(double unit, double block) {
+        virtualFlow.setVSpeed(unit, block);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#getVerticalPosition()}.
+     */
+    public double getVerticalPosition() {
+        return virtualFlow.getVerticalPosition();
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#getHorizontalPosition()}.
+     */
+    public double getHorizontalPosition() {
+        return virtualFlow.getHorizontalPosition();
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#setCellFactory(Function)}.
+     */
+    public void setCellFactory(Function<INotification, MFXNotificationCell> cellFactory) {
+        virtualFlow.setCellFactory(cellFactory);
+    }
+
+    /**
+     * Delegate method for {@link SimpleVirtualFlow#features()}.
+     */
+    public SimpleVirtualFlow<INotification, MFXNotificationCell>.Features features() {
+        return virtualFlow.features();
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXNotificationCenterSkin(this, virtualFlow);
+    }
+}

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

@@ -52,7 +52,6 @@ import javafx.scene.paint.Color;
  * <p></p>
  * Side notes:
  * <p> - the context menu is redefined in the skin since some methods are private in the skin.
- * <p> - see {@link #enableContextMenuTextSelectionFix(boolean)}.
  */
 public class MFXPasswordField extends MFXTextField {
     //================================================================================
@@ -128,7 +127,7 @@ public class MFXPasswordField extends MFXTextField {
      */
     @Override
     protected void defaultContextMenu() {
-        setMFXContextMenu(MFXContextMenu.Builder.build(this).install());
+        setMFXContextMenu(MFXContextMenu.Builder.build(this).installAndGet());
     }
 
     public String getPassword() {

+ 357 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXPopup.java

@@ -0,0 +1,357 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.beans.PopupPositionBean;
+import io.github.palexdev.materialfx.beans.PositionBean;
+import io.github.palexdev.materialfx.effects.Interpolators;
+import io.github.palexdev.materialfx.skins.MFXPopupSkin;
+import io.github.palexdev.materialfx.utils.AnimationUtils.KeyFrames;
+import io.github.palexdev.materialfx.utils.AnimationUtils.TimelineBuilder;
+import javafx.animation.Animation;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.css.PseudoClass;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.geometry.HPos;
+import javafx.geometry.Point2D;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.PopupControl;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.transform.Scale;
+import javafx.stage.Window;
+
+import java.util.function.BiFunction;
+
+/**
+ * Custom and better implementation of a {@link PopupControl}.
+ * <p></p>
+ * Setting the popup's content is now easier, it is animated (can be disabled), the animation
+ * can be changed but the most important features are the hover property and PseudoClass ("popup-hover" in css)
+ * that specifies when the mouse is on the content, and the new show methods that make use of {@link HPos}
+ * and {@link VPos} to compute the position for you, no more x and y computation, leave it to {@code MFXPopup}.
+ * <p>
+ * Of course if needed JavaFX's methods are still available.
+ * <p></p>
+ * Also allows to reposition the popup on demand by calling {@link #reposition()} and also
+ * offers a new {@link EventType}.
+ */
+public class MFXPopup extends PopupControl {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private PopupPositionBean position;
+    private final ObjectProperty<Node> content = new SimpleObjectProperty<>() {
+        @Override
+        public void set(Node newValue) {
+            Node oldValue = get();
+            if (oldValue != null) {
+                oldValue.removeEventFilter(MouseEvent.MOUSE_ENTERED, entered);
+                oldValue.removeEventFilter(MouseEvent.MOUSE_EXITED, exited);
+            }
+            newValue.addEventFilter(MouseEvent.MOUSE_ENTERED, entered);
+            newValue.addEventFilter(MouseEvent.MOUSE_EXITED, exited);
+            super.set(newValue);
+        }
+    };
+    private BiFunction<Node, Scale, Animation> animationProvider;
+    private boolean animated = true;
+
+    private final PseudoClass HOVER_PSEUDO_CLASS = PseudoClass.getPseudoClass("popup-hover");
+    private final BooleanProperty hover = new SimpleBooleanProperty(false);
+    private final EventHandler<MouseEvent> entered = event -> setHover(true);
+    private final EventHandler<MouseEvent> exited = event -> setHover(false);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXPopup() {
+        animationProvider = (node, scale) -> TimelineBuilder.build()
+                .show(150, node)
+                .add(KeyFrames.of(150,
+                        scale.xProperty(), 1, Interpolators.INTERPOLATOR_V2
+                ))
+                .add(KeyFrames.of(200,
+                        scale.yProperty(), 1, Interpolators.INTERPOLATOR_V2
+
+                ))
+                .getAnimation();
+        initialize();
+    }
+
+    public MFXPopup(Node content) {
+        setContent(content);
+        animationProvider = (node, scale) -> TimelineBuilder.build()
+                .show(150, node)
+                .add(KeyFrames.of(150,
+                        scale.xProperty(), 1, Interpolators.INTERPOLATOR_V2
+                ))
+                .add(KeyFrames.of(200,
+                        scale.yProperty(), 1, Interpolators.INTERPOLATOR_V2
+
+                ))
+                .getAnimation();
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        setAutoFix(false);
+        setAutoHide(true);
+        setHideOnEscape(true);
+
+        hover.addListener(invalidated -> pseudoClassStateChanged(HOVER_PSEUDO_CLASS, hover.get()));
+    }
+
+    /**
+     * Shows the popup at the BOTTOM_RIGHT of the specified node.
+     * <p></p>
+     * Calls {@link #show(Node, HPos, VPos, double, double)}.
+     */
+    public void show(Node node) {
+        show(node, HPos.RIGHT, VPos.BOTTOM, 0, 0);
+    }
+
+    /**
+     * Shows the popup at the given positions.
+     * <p></p>
+     * Calls {@link #show(Node, HPos, VPos, double, double)}.
+     */
+    public void show(Node node, HPos hPos, VPos vPos) {
+        show(node, hPos, vPos, 0, 0);
+    }
+
+    /**
+     * Shows the popup at the given positions, then shifts the computed coordinates
+     * by the given offsets.
+     * <p></p>
+     * Once the position is computed, it is stored in a {@link PopupPositionBean} alongside other
+     * useful info. These info are important because before the popup is actually shown, its skin
+     * is created. The {@link MFXPopupSkin} uses these info to properly position and animate the popup.
+     * This is needed because before creating the skin the content is not laid out so its sizes/bounds are 0
+     * and the "real" coordinates cannot be computed. For this reason, the coordinates stored in the
+     * position bean are not reliable, because they do not take into account the adjustments applied
+     * by the skin. (as of now, maybe will be improved in the future)
+     */
+    public void show(Node node, HPos hPos, VPos vPos, double xOffset, double yOffset) {
+        if (node.getScene() == null || node.getScene().getWindow() == null) {
+                throw new IllegalStateException("Cannot show the popup. The node must be attached to a scene/window!");
+            }
+
+        Window window = node.getScene().getWindow();
+        PositionBean position = computePosition(node, window, hPos, vPos);
+        this.position = new PopupPositionBean(node, position, hPos, vPos, xOffset, yOffset);
+        show(window, position.getX(), position.getY());
+    }
+
+    // TODO add show Window
+
+    /**
+     * Repositions the popup by recomputing the position from
+     * the previous stored info.
+     * <p>
+     * This should be called when the owner's position changes.
+     *
+     * @see #show(Node, HPos, VPos, double, double)
+     * @see PopupPositionBean
+     */
+    public void reposition() {
+        if (!isShowing() || position == null) return;
+        position = computeReposition();
+        double containerW = getContent().prefWidth(-1);
+        double containerH = getContent().prefHeight(-1);
+        HPos hPos = position.getHPos();
+        VPos vPos = position.getVPos();
+        double xOffset = position.getXOffset();
+        double yOffset = position.getYOffset();
+
+        double tx = 0;
+        double ty = 0;
+        switch (hPos) {
+            case CENTER: {
+                tx = -(Math.abs(containerW - position.getOwnerWidth()) / 2) + xOffset;
+                break;
+            }
+            case LEFT: {
+                tx = -containerW + xOffset;
+                break;
+            }
+            case RIGHT: {
+                tx = xOffset;
+                break;
+            }
+        }
+        switch (vPos) {
+            case BOTTOM: {
+                ty = yOffset;
+                break;
+            }
+            case CENTER: {
+                ty = -(Math.abs(containerH - position.getOwnerHeight()) / 2) + yOffset;
+                break;
+            }
+            case TOP: {
+                ty = -containerH + yOffset;
+                break;
+            }
+        }
+
+        setX(position.getX() + tx);
+        setY(position.getY() + ty);
+    }
+
+    /**
+     * Computes the initial (x, y) coordinates for the given parameters.
+     * These will be "refined" in the skin.
+     */
+    private PositionBean computePosition(Node node, Window window, HPos hPos, VPos vPos) {
+        Point2D origin = node.localToScene(0, 0);
+
+        double nodeWidth = node.prefWidth(-1);
+        double nodeHeight = node.prefHeight(-1);
+        double x = window.getX() + origin.getX() + node.getScene().getX() + (hPos == HPos.LEFT ? nodeWidth : 0);
+        double y = window.getY() + origin.getY() + node.getScene().getY() + (vPos == VPos.BOTTOM ? nodeHeight : 0);
+        return PositionBean.of(x, y);
+    }
+
+    /**
+     * Used to compute the new position of the popup when repositioning.
+     */
+    private PopupPositionBean computeReposition() {
+        Node node = position.getOwner();
+        Window window = node.getScene().getWindow();
+        HPos hPos = position.getHPos();
+        VPos vPos = position.getVPos();
+        PositionBean positionBean = computePosition(node, window, hPos, vPos);
+        return new PopupPositionBean(node, positionBean, hPos, vPos, position.getXOffset(), position.getYOffset());
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+
+    /**
+     * @return the instance of the {@link PopupPositionBean} computed when showing
+     * or repositioning the popup. Note that it will return null if the popup is not showing.
+     */
+    public PopupPositionBean getPosition() {
+        return position;
+    }
+
+    public Node getContent() {
+        return content.get();
+    }
+
+    /**
+     * Specifies the popup's content.
+     * <p>
+     * As of now changing the content while the popup is shown won't have any effect.
+     * For it to work, the popup must be closed and reopened again.
+     * As of now, there's no plan to improve this because such use case would introduce
+     * an unnecessary layer of complexity and in my opinion it's also a discouraged practice to change
+     * the popup's content while open. It would be better to use a container Pane and change its content.
+     * <p></p>
+     * This is a property because when the content changes it's needed to add the necessary handlers to it
+     * to make the "hover" feature work.
+     * <p></p>
+     * The content <b>cannot</b> be null.
+     */
+    public ObjectProperty<Node> contentProperty() {
+        return content;
+    }
+
+    public void setContent(Node content) {
+        this.content.set(content);
+    }
+
+    /**
+     * @return the function used by the skin to produce the popup's animation
+     */
+    public BiFunction<Node, Scale, Animation> getAnimationProvider() {
+        return animationProvider;
+    }
+
+    /**
+     * Sets the function used by the skin to produce the popup's animation.
+     * <p>
+     * The input parameters are the popup's container (it's a {@link StackPane}, and
+     * the {@link Scale} transform applied to the container.
+     */
+    public void setAnimationProvider(BiFunction<Node, Scale, Animation> animationProvider) {
+        this.animationProvider = animationProvider;
+    }
+
+    /**
+     * Specifies whether tha popup's is animated.
+     */
+    public boolean isAnimated() {
+        return animated;
+    }
+
+    public void setAnimated(boolean animated) {
+        this.animated = animated;
+    }
+
+    public boolean isHover() {
+        return hover.get();
+    }
+
+    /**
+     * Specifies if the mouse is on the popup's content.
+     */
+    public BooleanProperty hoverProperty() {
+        return hover;
+    }
+
+    public void setHover(boolean hover) {
+        this.hover.set(hover);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    /**
+     * {@inheritDoc}
+     * <p></p>
+     * Overridden to set the stored {@link PopupPositionBean} to null.
+     */
+    @Override
+    public void hide() {
+        position = null;
+        super.hide();
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXPopupSkin(this);
+    }
+
+    //================================================================================
+    // Events
+    //================================================================================
+
+    /**
+     * Events class for {@link MFXPopup}s.
+     * <p></p>
+     * Introduces a new event type: "REPOSITION_EVENT". It can be used to inform a popup that it should
+     * reposition. The typical use of this event is in case of Control/Skin or in general MVC pattern.
+     * If you don't have a reference to the popup, fire this event and capture it in the class that has the popup's reference
+     * then call, {@link #reposition()}.
+     */
+    public static class MFXPopupEvent extends Event {
+
+        public static final EventType<MFXPopupEvent> REPOSITION_EVENT = new EventType<>(ANY, "Reposition Event");
+
+        public MFXPopupEvent(EventType<? extends Event> eventType) {
+            super(eventType);
+        }
+    }
+}

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXToggleNode;
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
 import io.github.palexdev.materialfx.skins.MFXRectangleToggleNodeSkin;
 import javafx.beans.property.ObjectProperty;

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

@@ -0,0 +1,147 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.enums.NotificationState;
+import io.github.palexdev.materialfx.notifications.base.INotification;
+import io.github.palexdev.materialfx.utils.StringUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.Region;
+
+import java.time.Instant;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * Simple implementation of {@link INotification}.
+ * <p></p>
+ * By default the {@link INotification#getTimeToStringConverter()} function is set to use {@link StringUtils#timeToHumanReadable(long)}.
+ * By default the {@link INotification#setOnUpdateElapsed(BiConsumer)} function is set to do nothing.
+ * <p></p>
+ * Offers a Builder to build a notification with fluent design.
+ */
+public class MFXSimpleNotification implements INotification {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private Region content;
+
+    private final ObjectProperty<NotificationState> state = new SimpleObjectProperty<>(NotificationState.UNREAD);
+    private final long createdTime;
+    private Function<Long, String> timeToStringConverter = StringUtils::timeToHumanReadable;
+    private BiConsumer<Long, String> onUpdate = (elapsedLong, elapsedString) -> {};
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    protected MFXSimpleNotification() {
+        this(new AnchorPane());
+    }
+
+    public MFXSimpleNotification(Region content) {
+        createdTime = Instant.now().getEpochSecond();
+        if (content == null) {
+            throw new IllegalArgumentException("Content cannot be null!");
+        }
+        this.content = content;
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+
+    @Override
+    public Region getContent() {
+        return content;
+    }
+
+    protected void setContent(Region content) {
+        this.content = content;
+    }
+
+    @Override
+    public NotificationState getState() {
+        return state.get();
+    }
+
+    @Override
+    public ObjectProperty<NotificationState> notificationStateProperty() {
+        return state;
+    }
+
+    @Override
+    public void setNotificationState(NotificationState state) {
+        this.state.set(state);
+    }
+
+    @Override
+    public long getTime() {
+        return createdTime;
+    }
+
+    @Override
+    public long getElapsedTime() {
+        return Instant.now().getEpochSecond() - createdTime;
+    }
+
+    @Override
+    public Function<Long, String> getTimeToStringConverter() {
+        return timeToStringConverter;
+    }
+
+    @Override
+    public void setTimeToStringConverter(Function<Long, String> converter) {
+        this.timeToStringConverter = converter;
+    }
+
+    @Override
+    public void updateElapsed() {
+        long elapsedTime = getElapsedTime();
+        onUpdate.accept(elapsedTime, timeToStringConverter.apply(elapsedTime));
+    }
+
+    @Override
+    public void setOnUpdateElapsed(BiConsumer<Long, String> elapsedConsumer) {
+        this.onUpdate = elapsedConsumer;
+    }
+
+    //================================================================================
+    // Builder
+    //================================================================================
+    public static class Builder {
+        private final MFXSimpleNotification notification = new MFXSimpleNotification();
+
+        protected Builder() {}
+
+        public static Builder build() {
+            return new Builder();
+        }
+
+        public Builder setContent(Region content) {
+            if (content == null) {
+                throw new IllegalArgumentException("Content cannot be null!");
+            }
+            notification.setContent(content);
+            return this;
+        }
+
+        public Builder setState(NotificationState state) {
+            notification.setNotificationState(state);
+            return this;
+        }
+
+        public Builder setTimeToStringConverter(Function<Long, String> converter) {
+            notification.setTimeToStringConverter(converter);
+            return this;
+        }
+
+        public Builder setOnUpdateElapsed(BiConsumer<Long, String> elapsedConsumer) {
+            notification.setOnUpdateElapsed(elapsedConsumer);
+            return this;
+        }
+
+        public MFXSimpleNotification get() {
+            return notification;
+        }
+    }
+}

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

@@ -20,10 +20,10 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.NumberRange;
-import io.github.palexdev.materialfx.controls.enums.SliderEnums.SliderMode;
-import io.github.palexdev.materialfx.controls.enums.SliderEnums.SliderPopupSide;
+import io.github.palexdev.materialfx.enums.SliderEnums.SliderMode;
+import io.github.palexdev.materialfx.enums.SliderEnums.SliderPopupSide;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXSliderSkin;
 import io.github.palexdev.materialfx.utils.NodeUtils;
@@ -264,7 +264,7 @@ public class MFXSlider extends Control {
             rippleGenerator.setMouseTransparent(true);
             rippleGenerator.setRadiusMultiplier(2.5);
             rippleGenerator.setRippleRadius(6);
-            rippleGenerator.setRipplePositionFunction(mouseEvent -> new RipplePosition(stackPane.getWidth() / 2, stackPane.getHeight() / 2));
+            rippleGenerator.setRipplePositionFunction(mouseEvent -> new PositionBean(stackPane.getWidth() / 2, stackPane.getHeight() / 2));
             stackPane.addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);
             stackPane.getChildren().add(rippleGenerator);
 

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

@@ -19,9 +19,9 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.controls.base.AbstractMFXDialog;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.factories.MFXStageDialogFactory;
+import io.github.palexdev.materialfx.enums.DialogType;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.factories.MFXStageDialogFactory;
 import io.github.palexdev.materialfx.effects.MFXScrimEffect;
 import io.github.palexdev.materialfx.utils.AnimationUtils;
 import javafx.animation.KeyFrame;

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.MFXStepperToggle.MFXStepperToggleEvent;
-import io.github.palexdev.materialfx.controls.enums.StepperToggleState;
+import io.github.palexdev.materialfx.enums.StepperToggleState;
 import io.github.palexdev.materialfx.skins.MFXStepperSkin;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;

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

@@ -19,8 +19,8 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.enums.StepperToggleState;
-import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.enums.StepperToggleState;
+import io.github.palexdev.materialfx.enums.TextPosition;
 import io.github.palexdev.materialfx.skins.MFXStepperSkin;
 import io.github.palexdev.materialfx.skins.MFXStepperToggleSkin;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;

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

@@ -19,10 +19,10 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
 import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.css.PseudoClass;
@@ -89,7 +89,7 @@ public class MFXTableRow<T> extends HBox {
         rippleGenerator.setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.RECTANGLE).setOffsetW(10).build(this));
         rippleGenerator.setComputeRadiusMultiplier(true);
         rippleGenerator.setManaged(false);
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> new PositionBean(event.getX(), event.getY()));
         rippleGenerator.setTranslateX(-5);
         rippleGenerator.rippleRadiusProperty().bind(widthProperty().divide(2.0));
         addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);

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

@@ -19,7 +19,7 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.controls.cell.MFXTableColumn;
-import io.github.palexdev.materialfx.controls.enums.SortState;
+import io.github.palexdev.materialfx.enums.SortState;
 import javafx.beans.property.ReadOnlyObjectProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import javafx.collections.ListChangeListener;

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

@@ -19,7 +19,7 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.enums.DialogType;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXTextFieldSkin;
 import io.github.palexdev.materialfx.utils.ColorUtils;
@@ -262,7 +262,7 @@ public class MFXTextField extends TextField implements Validated<MFXDialogValida
                         .addSeparator()
                         .addMenuItem(redo)
                         .addMenuItem(undo)
-                        .install()
+                        .installAndGet()
         );
     }
 

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

@@ -19,8 +19,8 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.beans.MFXLoaderBean;
-import io.github.palexdev.materialfx.controls.enums.LoaderCacheLevel;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.enums.LoaderCacheLevel;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.utils.LoaderUtils;
 import io.github.palexdev.materialfx.utils.ToggleButtonsUtil;
 import javafx.application.Platform;

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

@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2021 Parisi Alessandro
- * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- * MaterialFX is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * MaterialFX is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.controls;
-
-import io.github.palexdev.materialfx.controls.base.AbstractMFXNotificationPane;
-import io.github.palexdev.materialfx.font.MFXFontIcon;
-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;
-
-/**
- * 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);
-        MFXFontIcon xIcon = new MFXFontIcon("mfx-x-circle", 14);
-        closeButton.setGraphic(xIcon);
-        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) {
-            throw new IndexOutOfBoundsException("Could not add button at index:" + index +
-                    ", list size is:" + this.buttonsBox.getChildren().size());
-        }
-    }
-
-    /**
-     * 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 following code,
-     * if the content was still truncated at the end of the transition the method was executed again and again until the isTruncated property
-     * was false. The PositionManager had two extra methods, repositionNotifications and buildRepositionAnimation with the expandValue as parameter,
-     * the reposition method had to be recalled every time too with the same frequency as the expandNotificationMethod but as you can see this class
-     * 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();
-    }
-    */
-}

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

@@ -20,8 +20,8 @@ package io.github.palexdev.materialfx.controls.base;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
 import io.github.palexdev.materialfx.controls.MFXDialog;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.enums.DialogType;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.effects.MFXScrimEffect;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.animation.ParallelTransition;

+ 0 - 88
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXListView.java

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

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

@@ -19,11 +19,11 @@
 package io.github.palexdev.materialfx.controls.cell;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.controls.MFXCheckListView;
 import io.github.palexdev.materialfx.controls.MFXCheckbox;
 import io.github.palexdev.materialfx.controls.cell.base.AbstractMFXListCell;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.beans.binding.Bindings;
 import javafx.scene.Node;
@@ -90,7 +90,7 @@ public class MFXCheckListCell<T> extends AbstractMFXListCell<T> {
      */
     protected void setupRippleGenerator() {
         rippleGenerator.setManaged(false);
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
         rippleGenerator.rippleRadiusProperty().bind(widthProperty().divide(2.0));
         addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
             if (NodeUtils.inHierarchy(event, checkbox)) {

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

@@ -22,7 +22,7 @@ import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.MFXListView;
 import io.github.palexdev.materialfx.controls.cell.base.AbstractMFXListCell;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import javafx.scene.Node;
 import javafx.scene.control.Label;
 import javafx.scene.input.MouseButton;
@@ -83,7 +83,7 @@ public class MFXListCell<T> extends AbstractMFXListCell<T> {
      */
     protected void setupRippleGenerator() {
         rippleGenerator.setManaged(false);
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
         rippleGenerator.rippleRadiusProperty().bind(widthProperty().divide(2.0));
         addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
             if (event.getButton() == MouseButton.PRIMARY) {

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

@@ -0,0 +1,224 @@
+package io.github.palexdev.materialfx.controls.cell;
+
+import io.github.palexdev.materialfx.controls.MFXCheckbox;
+import io.github.palexdev.materialfx.controls.MFXNotificationCenter;
+import io.github.palexdev.materialfx.effects.Interpolators;
+import io.github.palexdev.materialfx.notifications.base.INotification;
+import io.github.palexdev.materialfx.utils.AnimationUtils.KeyFrames;
+import io.github.palexdev.materialfx.utils.AnimationUtils.ParallelBuilder;
+import io.github.palexdev.virtualizedfx.cell.Cell;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.*;
+import javafx.css.PseudoClass;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+
+/**
+ * Implementation of a {@link Cell} for usage with {@link MFXNotificationCenter}.
+ * <p></p>
+ * Includes a checkbox to allow selecting notifications.
+ */
+public class MFXNotificationCell extends HBox implements Cell<INotification> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-notification-cell";
+    private final MFXNotificationCenter notificationCenter;
+    private final ReadOnlyObjectWrapper<INotification> notification = new ReadOnlyObjectWrapper<>();
+    private final ReadOnlyIntegerWrapper index = new ReadOnlyIntegerWrapper();
+    private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper();
+
+    protected final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
+    protected final StackPane container;
+    protected final MFXCheckbox checkbox;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXNotificationCell(MFXNotificationCenter notificationCenter, INotification notification) {
+        this.notificationCenter = notificationCenter;
+        setNotification(notification);
+
+        setPrefHeight(65);
+        setMaxHeight(USE_PREF_SIZE);
+        setAlignment(Pos.CENTER_LEFT);
+
+        checkbox = new MFXCheckbox("");
+        checkbox.setId("check");
+        checkbox.setMarkType("mfx-variant7-mark");
+
+        container = new StackPane(checkbox);
+        container.setMinWidth(USE_PREF_SIZE);
+        container.setPrefWidth(0);
+        container.setMaxWidth(USE_PREF_SIZE);
+
+        Rectangle clip = new Rectangle();
+        clip.widthProperty().bind(container.widthProperty());
+        clip.heightProperty().bind(container.heightProperty());
+        container.setClip(clip);
+
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds the style class, calls {@link #setBehavior()} then {@link #render(INotification)}
+     * for the first time.
+     */
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setBehavior();
+        render(getNotification());
+    }
+
+    /**
+     * Sets the following behaviors:
+     * <p>
+     * - Binds the selected property to the notification center' selection model (checks for index). <p>
+     * - Updates the selected PseudoClass state when selected property changes. <p>
+     * - Adds a listener to the checkbox' selection state to call {@link #updateSelection(boolean)}. <p>
+     * - Adds a listener to the notification center's {@link MFXNotificationCenter#selectionModeProperty()} to call {@link #expand(boolean)}.
+     */
+    protected void setBehavior() {
+        selected.addListener(invalidated -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, selected.get()));
+        selected.bind(Bindings.createBooleanBinding(() -> {
+            boolean contained = notificationCenter.getSelectionModel().getSelection().containsKey(getIndex());
+            checkbox.setSelected(contained);
+            return contained;
+        }, notificationCenter.getSelectionModel().selectionProperty(), index));
+
+        checkbox.selectedProperty().addListener((observable, oldValue, newValue) -> updateSelection(newValue));
+        notificationCenter.selectionModeProperty().addListener((observable, oldValue, newValue) -> expand(newValue));
+    }
+
+    /**
+     * Responsible for rendering the cell's content.
+     */
+    protected void render(INotification notification) {
+        if (notificationCenter.isSelectionMode()) {
+            checkbox.setOpacity(1.0);
+            checkbox.setPrefWidth(45);
+        }
+        getChildren().setAll(container, notification.getContent());
+    }
+
+    /**
+     * Responsible for updating the selection state according to the checkbox' state.
+     * <p>
+     * If checked is true then the cell should be selected, otherwise it is deselected.
+     */
+    protected void updateSelection(boolean checked) {
+        int index = getIndex();
+        if (checked) {
+            notificationCenter.getSelectionModel().selectIndex(index);
+        } else {
+            notificationCenter.getSelectionModel().deselectIndex(index);
+        }
+    }
+
+    /**
+     * Responsible for showing/hiding the checkbox.
+     */
+    protected void expand(boolean selectionMode) {
+        double width = selectionMode ? 45 : 0;
+        double opacity = selectionMode ? 1 : 0;
+        if (notificationCenter.isAnimated()) {
+            ParallelBuilder.build()
+                    .add(
+                            KeyFrames.of(150, checkbox.opacityProperty(), opacity, Interpolators.EASE_OUT),
+                            KeyFrames.of(250, container.prefWidthProperty(), width, Interpolators.EASE_OUT_SINE)
+                    ).getAnimation().play();
+        } else {
+            container.setPrefWidth(width);
+            checkbox.setOpacity(opacity);
+        }
+        if (!selectionMode) {
+            notificationCenter.getSelectionModel().clearSelection();
+        }
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+
+    @Override
+    public Node getNode() {
+        return this;
+    }
+
+    /**
+     * Updates the notification property of the cell, then calls {@link #render(INotification)}.
+     * <p>
+     * This is called after {@link #updateIndex(int)}.
+     */
+    @Override
+    public void updateItem(INotification notification) {
+        setNotification(notification);
+        render(notification);
+    }
+
+    /**
+     * Updates the index property of the cell.
+     * <p>
+     * This is called before {@link #updateItem(INotification)}.
+     */
+    @Override
+    public void updateIndex(int index) {
+        setIndex(index);
+    }
+
+    //================================================================================
+    // Getters/Setters
+    //================================================================================
+
+    public INotification getNotification() {
+        return this.notification.get();
+    }
+
+    /**
+     * Specifies the current shown notification (in other words the cell's content).
+     */
+    public ReadOnlyObjectProperty<INotification> notificationProperty() {
+        return this.notification.getReadOnlyProperty();
+    }
+
+    protected void setNotification(INotification notification) {
+        this.notification.set(notification);
+    }
+
+    public int getIndex() {
+        return this.index.get();
+    }
+
+    /**
+     * Specifies the cell's index.
+     */
+    protected ReadOnlyIntegerProperty indexProperty() {
+        return this.index.getReadOnlyProperty();
+    }
+
+    protected void setIndex(int index) {
+        this.index.set(index);
+    }
+
+    public boolean isSelected() {
+        return this.selected.get();
+    }
+
+    /**
+     * Specifies the selection state of the cell.
+     */
+    public ReadOnlyBooleanProperty selectedProperty() {
+        return this.selected.getReadOnlyProperty();
+    }
+
+    protected void setSelected(boolean selected) {
+        this.selected.set(selected);
+    }
+}

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

@@ -21,7 +21,7 @@ package io.github.palexdev.materialfx.controls.cell;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
 import io.github.palexdev.materialfx.controls.MFXTableView;
-import io.github.palexdev.materialfx.controls.enums.SortState;
+import io.github.palexdev.materialfx.enums.SortState;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXTableColumnSkin;
 import io.github.palexdev.materialfx.skins.MFXTableViewSkin;

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

@@ -166,7 +166,7 @@ public abstract class AbstractMFXListCell<T> extends HBox implements Cell<T> {
     }
 
     /**
-     * Index property of the cell.
+     * Specifies the cell's index.
      */
     public ReadOnlyIntegerProperty indexProperty() {
         return index.getReadOnlyProperty();
@@ -181,7 +181,7 @@ public abstract class AbstractMFXListCell<T> extends HBox implements Cell<T> {
     }
 
     /**
-     * The selection state property of the cell.
+     * Specifies the selection state of the cell.
      */
     public ReadOnlyBooleanProperty selectedProperty() {
         return selected.getReadOnlyProperty();

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

@@ -21,7 +21,7 @@ package io.github.palexdev.materialfx.controls.legacy;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
 import io.github.palexdev.materialfx.controls.MFXComboBox;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.enums.DialogType;
 import io.github.palexdev.materialfx.skins.legacy.MFXLegacyComboBoxSkin;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls.legacy;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.css.*;
 import javafx.geometry.Insets;
@@ -127,7 +127,7 @@ public class MFXLegacyListCell<T> extends ListCell<T> {
 
     protected void setupRippleGenerator() {
         rippleGenerator.setRippleColor(Color.rgb(50, 150, 255));
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
         addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);
     }
 

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

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls.legacy;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.css.*;
 import javafx.geometry.Insets;
@@ -68,7 +68,7 @@ public class MFXLegacyTableRow<T> extends TableRow<T> {
 
     private void setupRippleGenerator() {
         rippleGenerator.setRippleColor(Color.rgb(50, 150, 255));
-        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setRipplePositionFunction(event -> PositionBean.of(event.getX(), event.getY()));
         addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);
     }
 

+ 138 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyTableView.java

@@ -0,0 +1,138 @@
+package io.github.palexdev.materialfx.effects;
+
+import javafx.animation.Interpolator;
+import javafx.animation.Transition;
+import javafx.util.Duration;
+
+import java.util.function.Consumer;
+
+/**
+ * A simple implementation of {@link Transition} that allows to specify
+ * what to do when the {@link #interpolate(double)} method is called by using
+ * a {@link Consumer}.
+ */
+public class ConsumerTransition extends Transition {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private Consumer<Double> interpolateConsumer;
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Sets the transition duration.
+     */
+    public ConsumerTransition setDuration(Duration duration) {
+        this.setCycleDuration(duration);
+        return this;
+    }
+
+    /**
+     * Sets the transition duration in milliseconds.
+     */
+    public ConsumerTransition setDuration(double millis) {
+        this.setCycleDuration(Duration.millis(millis));
+        return this;
+    }
+
+    /**
+     * Sets the consumer used by the {@link #interpolate(double)} method.
+     */
+    public ConsumerTransition setInterpolateConsumer(Consumer<Double> interpolateConsumer) {
+        this.interpolateConsumer = interpolateConsumer;
+        return this;
+    }
+
+    /**
+     * Sets the transition's interpolator.
+     */
+    public ConsumerTransition setInterpolatorFluent(Interpolator interpolator) {
+        this.setInterpolator(interpolator);
+        return this;
+    }
+
+    /**
+     * Sets the transition's delay.
+     */
+    public ConsumerTransition setDelayFluent(Duration duration) {
+        this.setDelay(duration);
+        return this;
+    }
+
+    /**
+     * Calls {@link #setInterpolateConsumer(Consumer)} and then starts the animation.
+     */
+    public void playWithConsumer(Consumer<Double> interpolateConsumer) {
+        setInterpolateConsumer(interpolateConsumer);
+        this.play();
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+
+    /**
+     * {@inheritDoc}
+     * <p></p>
+     * Implementation to make use of a {@link Consumer}.
+     */
+    @Override
+    protected void interpolate(double frac) {
+        this.interpolateConsumer.accept(frac);
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer);
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer and duration.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, Duration duration) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration);
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer and duration in milliseconds.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, double duration) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration);
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer, duration and interpolator.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, Duration duration, Interpolator interpolator) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration).setInterpolatorFluent(interpolator);
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer, duration in milliseconds and interpolator.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, double duration, Interpolator interpolator) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration).setInterpolatorFluent(interpolator);
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer, duration and interpolator.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, Duration duration, Interpolators interpolator) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration).setInterpolatorFluent(interpolator.toInterpolator());
+    }
+
+    /**
+     * Creates a new {@code ConsumerTransition} with the given consumer, duration in milliseconds and interpolator.
+     */
+    public static ConsumerTransition of(Consumer<Double> interpolateConsumer, double duration, Interpolators interpolator) {
+        return (new ConsumerTransition()).setInterpolateConsumer(interpolateConsumer).setDuration(duration).setInterpolatorFluent(interpolator.toInterpolator());
+    }
+}

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

@@ -0,0 +1,101 @@
+package io.github.palexdev.materialfx.effects;
+
+import javafx.animation.Interpolator;
+
+import java.util.function.Function;
+
+/**
+ * Enumerator that offers some new {@link Interpolator}s for JavaFX's animations.
+ */
+public enum Interpolators {
+    INTERPOLATOR_V1(null) {
+        public Interpolator toInterpolator() {
+            return Interpolator.SPLINE(0.25D, 0.1D, 0.25D, 1.0D);
+        }
+    },
+    INTERPOLATOR_V2(null) {
+        public Interpolator toInterpolator() {
+            return Interpolator.SPLINE(0.0825D, 0.3025D, 0.0875D, 0.9975D);
+        }
+    },
+    LINEAR((t) -> t) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_IN((t) -> t * t * t) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_IN_SINE((t) -> 1.0D - Math.cos(t * 3.141592653589793D / 2.0D)) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_OUT((t) -> 1.0D - (1.0D - t) * (1.0D - t) * (1.0D - t)) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_OUT_SINE((t) -> {
+        return Math.sin(t * 3.141592653589793D / 2.0D);
+    }) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_IN_OUT((t) -> t < 0.5D ? 4.0D * t * t * t : 1.0D - Math.pow(-2.0D * t + 2.0D, 3.0D) / 2.0D) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    },
+    EASE_IN_OUT_SINE((t) -> -(Math.cos(3.141592653589793D * t) - 1.0D) / 2.0D) {
+        public Interpolator toInterpolator() {
+            return new Interpolator() {
+                protected double curve(double t) {
+                    return getCurve().apply(t);
+                }
+            };
+        }
+    };
+
+    private final Function<Double, Double> curve;
+
+    Interpolators(Function<Double, Double> curve) {
+        this.curve = curve;
+    }
+
+    /**
+     * Converts a Function<Double, Double> to a JavaFX's {@link Interpolator}.
+     */
+    public abstract Interpolator toInterpolator();
+
+    public Function<Double, Double> getCurve() {
+        return this.curve;
+    }
+}

+ 10 - 9
materialfx/src/main/java/io/github/palexdev/materialfx/effects/MFXDepthManager.java

@@ -18,8 +18,9 @@
 
 package io.github.palexdev.materialfx.effects.ripple;
 
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.beans.PositionBean;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.effects.MFXDepthManager;
 import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator.CircleRipple;
@@ -123,12 +124,12 @@ public class MFXCircleRippleGenerator extends AbstractMFXRippleGenerator<CircleR
         }
         setClip(getClipSupplier().get());
 
-        RipplePosition position = getRipplePositionFunction().apply(event);
+        PositionBean position = getRipplePositionFunction().apply(event);
 
         CircleRipple ripple = getRippleSupplier().get();
-        ripple.setXPosition(position.getXPosition());
-        ripple.centerXProperty().bind(position.xPositionProperty());
-        ripple.centerYProperty().bind(position.yPositionProperty());
+        ripple.setXPosition(position.getX());
+        ripple.centerXProperty().bind(position.xProperty());
+        ripple.centerYProperty().bind(position.yProperty());
         ripple.setFill(getRippleColor());
 
         Animation rippleAnimation = ripple.getAnimation();
@@ -319,16 +320,16 @@ public class MFXCircleRippleGenerator extends AbstractMFXRippleGenerator<CircleR
 
     @Override
     public void defaultPositionFunction() {
-        setRipplePositionFunction(event -> new RipplePosition());
+        setRipplePositionFunction(event -> new PositionBean());
     }
 
     @Override
-    public Function<MouseEvent, RipplePosition> getRipplePositionFunction() {
+    public Function<MouseEvent, PositionBean> getRipplePositionFunction() {
         return positionFunction;
     }
 
     @Override
-    public void setRipplePositionFunction(Function<MouseEvent, RipplePosition> positionFunction) {
+    public void setRipplePositionFunction(Function<MouseEvent, PositionBean> positionFunction) {
         super.positionFunction = positionFunction;
     }
 

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

@@ -18,8 +18,8 @@
 
 package io.github.palexdev.materialfx.effects.ripple;
 
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import javafx.animation.*;
 import javafx.beans.property.ObjectProperty;

+ 0 - 74
materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/RipplePosition.java

@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 Parisi Alessandro
- * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- * MaterialFX is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * MaterialFX is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.effects.ripple;
-
-import io.github.palexdev.materialfx.effects.ripple.base.IRippleGenerator;
-import io.github.palexdev.materialfx.skins.MFXToggleButtonSkin;
-import javafx.beans.property.DoubleProperty;
-import javafx.beans.property.SimpleDoubleProperty;
-
-import java.util.function.Function;
-
-/**
- * Simple bean to wrap the coordinates of generated ripples.
- * <p>
- * This is used by the ripple generator's position function as the return type,
- * {@link IRippleGenerator#setRipplePositionFunction(Function)}.
- * <p>
- * Note that both the positions are JavaFX properties, this allows to change the ripple position during
- * its animation, an example can be seen in the {@link MFXToggleButtonSkin}
- * <p></p>
- * In {@link MFXCircleRippleGenerator} the ripple center properties are already bound to these values.
- */
-public class RipplePosition {
-    private final DoubleProperty xPosition = new SimpleDoubleProperty(0);
-    private final DoubleProperty yPosition = new SimpleDoubleProperty(0);
-
-    public RipplePosition() {
-    }
-
-    public RipplePosition(double xPosition, double yPosition) {
-        setXPosition(xPosition);
-        setYPosition(yPosition);
-    }
-
-    public double getXPosition() {
-        return xPosition.get();
-    }
-
-    public DoubleProperty xPositionProperty() {
-        return xPosition;
-    }
-
-    public void setXPosition(double xPosition) {
-        this.xPosition.set(xPosition);
-    }
-
-    public double getYPosition() {
-        return yPosition.get();
-    }
-
-    public DoubleProperty yPositionProperty() {
-        return yPosition;
-    }
-
-    public void setYPosition(double yPosition) {
-        this.yPosition.set(yPosition);
-    }
-}

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/base/AbstractMFXRippleGenerator.java

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.effects.ripple.base;
 
 import io.github.palexdev.materialfx.collections.ObservableStack;
 import io.github.palexdev.materialfx.effects.DepthLevel;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import io.github.palexdev.materialfx.skins.MFXCheckboxSkin;
 import javafx.animation.Animation;
 import javafx.beans.property.*;
@@ -60,7 +60,7 @@ public abstract class AbstractMFXRippleGenerator<T extends IRipple> extends Regi
     protected final Region region;
     protected Supplier<Shape> clipSupplier;
     protected Supplier<T> rippleSupplier;
-    protected Function<MouseEvent, RipplePosition> positionFunction;
+    protected Function<MouseEvent, PositionBean> positionFunction;
 
     protected final BooleanProperty animateBackground = new SimpleBooleanProperty(true);
     protected final BooleanProperty animateShadow = new SimpleBooleanProperty(false);

+ 5 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/base/IRipple.java

@@ -18,8 +18,8 @@
 
 package io.github.palexdev.materialfx.effects.ripple.base;
 
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
-import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.beans.PositionBean;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.Region;
 import javafx.scene.shape.Shape;
@@ -68,7 +68,7 @@ public interface IRippleGenerator<T extends IRipple> {
     /**
      * @return the current generator's position function
      */
-    Function<MouseEvent, RipplePosition> getRipplePositionFunction();
+    Function<MouseEvent, PositionBean> getRipplePositionFunction();
 
     /**
      * Sets the generator's ripple position function to the specified one.
@@ -76,9 +76,9 @@ public interface IRippleGenerator<T extends IRipple> {
      * This {@link Function} is responsible for computing the ripple's x and y
      * coordinates before the animation is played. The function takes a MouseEvent as the input
      * (since in most controls the coordinates are the x and y coordinates of the mouse event)
-     * and returns a {@link RipplePosition} bean.
+     * and returns a {@link PositionBean} bean.
      */
-    void setRipplePositionFunction(Function<MouseEvent, RipplePosition> positionFunction);
+    void setRipplePositionFunction(Function<MouseEvent, PositionBean> positionFunction);
 
     /**
      * Every ripple generator should have a default ripple supplier.

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/ButtonType.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/ButtonType.java

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 public enum ButtonType {
     FLAT,

+ 22 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/enums/ChainMode.java

@@ -0,0 +1,22 @@
+package io.github.palexdev.materialfx.enums;
+
+/**
+ * Enumeration to specify how two predicates should be chained.
+ * Also specify how a ChainMode enumeration should be represented in UI.
+ */
+public enum ChainMode {
+    AND("&"),
+    OR("or");
+
+    public static boolean useAlternativeAnd = false;
+    private final String text;
+
+    ChainMode(String text) {
+        this.text = text;
+    }
+
+    public String text() {
+        return this == AND && useAlternativeAnd ? "and" : this.text;
+    }
+
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/DialogType.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/DialogType.java

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 public enum DialogType {
     ERROR,

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/LoaderCacheLevel.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/LoaderCacheLevel.java

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 import io.github.palexdev.materialfx.controls.MFXHLoader;
 import io.github.palexdev.materialfx.controls.MFXVLoader;

+ 10 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationCounterStyle.java

@@ -0,0 +1,10 @@
+package io.github.palexdev.materialfx.enums;
+
+import io.github.palexdev.materialfx.controls.MFXNotificationCenter;
+
+/**
+ * Enumeration to specify the style of a {@link MFXNotificationCenter}'s counter.
+ */
+public enum NotificationCounterStyle {
+    DOT, NUMBER
+}

+ 30 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationPos.java

@@ -0,0 +1,30 @@
+package io.github.palexdev.materialfx.enums;
+
+import io.github.palexdev.materialfx.notifications.MFXNotificationCenterSystem;
+import io.github.palexdev.materialfx.notifications.MFXNotificationSystem;
+
+/**
+ * Enumeration to specify where a notification has to be shown.
+ * <p>
+ * Used by {@link MFXNotificationCenterSystem} and {@link MFXNotificationSystem}.
+ */
+public enum NotificationPos {
+    TOP_CENTER,
+    TOP_LEFT,
+    TOP_RIGHT,
+    BOTTOM_CENTER,
+    BOTTOM_LEFT,
+    BOTTOM_RIGHT;
+
+    public boolean isTop() {
+        return this == TOP_LEFT || this == TOP_CENTER || this == TOP_RIGHT;
+    }
+
+    public boolean isCenter() {
+        return this == TOP_CENTER || this == BOTTOM_CENTER;
+    }
+
+    public boolean isRight() {
+        return this == TOP_RIGHT || this == BOTTOM_RIGHT;
+    }
+}

+ 8 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/enums/NotificationState.java

@@ -0,0 +1,8 @@
+package io.github.palexdev.materialfx.enums;
+
+/**
+ * Enumeration to represent the read state of a notification.
+ */
+public enum NotificationState {
+    READ, UNREAD;
+}

+ 12 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SliderEnums.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/SliderEnums.java

@@ -16,16 +16,27 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
+import io.github.palexdev.materialfx.controls.MFXSlider;
+
+/**
+ * Class that contains some enumerators to be used with {@link MFXSlider}.
+ */
 public class SliderEnums {
 
     private SliderEnums() {}
 
+    /**
+     * Enumeration to specify the snap behavior of {@link MFXSlider}.
+     */
     public enum SliderMode {
         DEFAULT, SNAP_TO_TICKS
     }
 
+    /**
+     * Enumeration to specify on which side to show the {@link MFXSlider}'s popup.
+     */
     public enum SliderPopupSide {
         DEFAULT, OTHER_SIDE
     }

+ 4 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SortState.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/SortState.java

@@ -16,8 +16,11 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
+/**
+ * Enumerations to represent sorting.
+ */
 public enum SortState {
     ASCENDING,
     DESCENDING,

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/StepperToggleState.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/StepperToggleState.java

@@ -16,12 +16,12 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 import io.github.palexdev.materialfx.controls.MFXStepperToggle;
 
 /**
- * Enumerator to represent the state of a {@link MFXStepperToggle}
+ * Enumerator to represent the states of a {@link MFXStepperToggle}
  */
 public enum StepperToggleState {
     SELECTED,

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/Styles.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/Styles.java

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 /**
  * This class contains various enumerators used in MaterialFX controls which

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/TextPosition.java → materialfx/src/main/java/io/github/palexdev/materialfx/enums/TextPosition.java

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.enums;
+package io.github.palexdev.materialfx.enums;
 
 public enum TextPosition {
     TOP,

+ 53 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/factories/InsetsFactory.java

@@ -0,0 +1,53 @@
+package io.github.palexdev.materialfx.factories;
+
+import javafx.geometry.Insets;
+
+/**
+ * Convenience class to build {@link Insets} objects.
+ */
+public class InsetsFactory {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    private InsetsFactory() {}
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+    public static Insets all(double topRightBottomLeft) {
+        return new Insets(topRightBottomLeft);
+    }
+
+    public static Insets none() {
+        return Insets.EMPTY;
+    }
+
+    public static Insets top(double top) {
+        return new Insets(top, 0, 0, 0);
+    }
+
+    public static Insets right(double right) {
+        return new Insets(0, right, 0, 0);
+    }
+
+    public static Insets bottom(double bottom) {
+        return new Insets(0, 0, bottom, 0);
+    }
+
+    public static Insets left(double left) {
+        return new Insets(0, 0, 0, left);
+    }
+
+    public static Insets of(double top, double right) {
+        return new Insets(top, right, 0, 0);
+    }
+
+    public static Insets of(double top, double right, double bottom) {
+        return new Insets(top, right, bottom, 0);
+    }
+
+    public static Insets of(double top, double right, double bottom, double left) {
+        return new Insets(top, right, bottom, left);
+    }
+}

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

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.factories;
+package io.github.palexdev.materialfx.factories;
 
 import javafx.animation.Interpolator;
 import javafx.animation.KeyFrame;

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

@@ -16,13 +16,13 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.factories;
+package io.github.palexdev.materialfx.factories;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
 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.enums.ButtonType;
+import io.github.palexdev.materialfx.enums.DialogType;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.geometry.Insets;

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/MFXStageDialogFactory.java → materialfx/src/main/java/io/github/palexdev/materialfx/factories/MFXStageDialogFactory.java

@@ -16,10 +16,10 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.factories;
+package io.github.palexdev.materialfx.factories;
 
 import io.github.palexdev.materialfx.controls.base.AbstractMFXDialog;
-import io.github.palexdev.materialfx.controls.enums.DialogType;
+import io.github.palexdev.materialfx.enums.DialogType;
 import javafx.scene.Scene;
 import javafx.scene.layout.Pane;
 import javafx.scene.paint.Color;

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

@@ -16,7 +16,7 @@
  * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.factories;
+package io.github.palexdev.materialfx.factories;
 
 import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
 import javafx.scene.layout.Region;

+ 51 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/BooleanFilter.java

@@ -0,0 +1,51 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+import javafx.util.converter.BooleanStringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link AbstractFilter} for boolean fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for booleans equality
+ * <p> - "is not": checks for booleans inequality
+ */
+public class BooleanFilter<T> extends AbstractFilter<T, Boolean> {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public BooleanFilter(String name, Function<T, Boolean> extractor) {
+        this(name, extractor, new BooleanStringConverter());
+    }
+
+    public BooleanFilter(String name, Function<T, Boolean> extractor, StringConverter<Boolean> converter) {
+        super(name, extractor, converter);
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<Boolean, Boolean>> defaultPredicates() {
+        return Stream.<BiPredicateBean<Boolean, Boolean>>of(
+                new BiPredicateBean<>("is", Boolean::equals),
+                new BiPredicateBean<>("is not", (aBoolean, aBoolean2) -> !aBoolean.equals(aBoolean2))
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final BooleanFilter<T> extend(BiPredicateBean<Boolean, Boolean>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 59 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/DoubleFilter.java

@@ -0,0 +1,59 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+import javafx.util.converter.DoubleStringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link NumberFilter} for double fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for doubles equality
+ * <p> - "is not": checks for doubles inequality
+ * <p> - "greater than": checks if a double is greater than another double
+ * <p> - "greater or equal to": checks if a double is greater or equal to another double
+ * <p> - "lesser than": checks if a double is lesser than another double
+ * <p> - "lesser or equal to": checks if a double is lesser or equal to another double
+ */
+public class DoubleFilter<T> extends NumberFilter<T, Double> {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public DoubleFilter(String name, Function<T, Double> extractor) {
+        this(name, extractor, new DoubleStringConverter());
+    }
+
+    public DoubleFilter(String name, Function<T, Double> extractor, StringConverter<Double> converter) {
+        super(name, extractor, converter);
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<Double, Double>> defaultPredicates() {
+        return Stream.<BiPredicateBean<Double, Double>>of(
+                new BiPredicateBean<>("is", Double::equals),
+                new BiPredicateBean<>("is not", (aDouble, aDouble2) -> !aDouble.equals(aDouble2)),
+                new BiPredicateBean<>("greater than", (aDouble, aDouble2) -> aDouble > aDouble2),
+                new BiPredicateBean<>("greater or equal to", (aDouble, aDouble2) -> aDouble >= aDouble2),
+                new BiPredicateBean<>("lesser than", (aDouble, aDouble2) -> aDouble < aDouble2),
+                new BiPredicateBean<>("lesser or equal to", (aDouble, aDouble2) -> aDouble <= aDouble2)
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final DoubleFilter<T> extend(BiPredicateBean<Double, Double>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 66 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/EnumFilter.java

@@ -0,0 +1,66 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.utils.EnumStringConverter;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link AbstractFilter} for {@link Enum} fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for enums equality
+ * <p> - "is not": checks for enums inequality
+ * <p></p>
+ * This filter is special because to extract the enumerations of a given E enum, it's
+ * needed to also pass the type to the constructor. This is necessary for the {@link EnumStringConverter}.
+ */
+public class EnumFilter<T, E extends Enum<E>> extends AbstractFilter<T, E> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final Class<E> enumType;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public EnumFilter(String name, Function<T, E> extractor, Class<E> enumType) {
+        this(name, extractor, enumType, new EnumStringConverter<>(enumType));
+    }
+
+    public EnumFilter(String name, Function<T, E> extractor, Class<E> enumType, StringConverter<E> converter) {
+        super(name, extractor, converter);
+        this.enumType = enumType;
+    }
+
+    //================================================================================
+    // Getters
+    //================================================================================
+    public Class<E> getEnumType() {
+        return enumType;
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<E, E>> defaultPredicates() {
+        return Stream.<BiPredicateBean<E, E>>of(
+                new BiPredicateBean<>("is", Enum::equals),
+                new BiPredicateBean<>("is not", (anEnum, anEnum2) -> !anEnum.equals(anEnum2))
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final EnumFilter<T, E> extend(BiPredicateBean<E, E>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 0 - 23
materialfx/src/main/java/io/github/palexdev/materialfx/filter/EvaluationMode.java

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

+ 59 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/FloatFilter.java

@@ -0,0 +1,59 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+import javafx.util.converter.FloatStringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link NumberFilter} for float fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for floats equality
+ * <p> - "is not": checks for floats inequality
+ * <p> - "greater than": checks if a float is greater than another float
+ * <p> - "greater or equal to": checks if a float is greater or equal to another float
+ * <p> - "lesser than": checks if a float is lesser than another float
+ * <p> - "lesser or equal to": checks if a float is lesser or equal to another float
+ */
+public class FloatFilter<T> extends NumberFilter<T, Float> {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public FloatFilter(String name, Function<T, Float> extractor) {
+        this(name, extractor, new FloatStringConverter());
+    }
+
+    public FloatFilter(String name, Function<T, Float> extractor, StringConverter<Float> converter) {
+        super(name, extractor, converter);
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<Float, Float>> defaultPredicates() {
+        return Stream.<BiPredicateBean<Float, Float>>of(
+                new BiPredicateBean<>("is", Float::equals),
+                new BiPredicateBean<>("is not", (aFloat, aFloat2) -> !aFloat.equals(aFloat2)),
+                new BiPredicateBean<>("greater than", (aFloat, aFloat2) -> aFloat > aFloat2),
+                new BiPredicateBean<>("greater or equal to", (aFloat, aFloat2) -> aFloat >= aFloat2),
+                new BiPredicateBean<>("lesser than", (aFloat, aFloat2) -> aFloat < aFloat2),
+                new BiPredicateBean<>("lesser or equal to", (aFloat, aFloat2) -> aFloat <= aFloat2)
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final FloatFilter<T> extend(BiPredicateBean<Float, Float>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 0 - 27
materialfx/src/main/java/io/github/palexdev/materialfx/filter/IFilterable.java

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

+ 59 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/IntegerFilter.java

@@ -0,0 +1,59 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+import javafx.util.converter.IntegerStringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link NumberFilter} for integer fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for integers equality
+ * <p> - "is not": checks for integers inequality
+ * <p> - "greater than": checks if a integer is greater than another integer
+ * <p> - "greater or equal to": checks if a integer is greater or equal to another integer
+ * <p> - "lesser than": checks if a integer is lesser than another integer
+ * <p> - "lesser or equal to": checks if a integer is lesser or equal to another integer
+ */
+public class IntegerFilter<T> extends NumberFilter<T, Integer> {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public IntegerFilter(String name, Function<T, Integer> extractor) {
+        this(name, extractor, new IntegerStringConverter());
+    }
+
+    public IntegerFilter(String name, Function<T, Integer> extractor, StringConverter<Integer> converter) {
+        super(name, extractor, converter);
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<Integer, Integer>> defaultPredicates() {
+        return Stream.<BiPredicateBean<Integer, Integer>>of(
+                new BiPredicateBean<>("is", Integer::equals),
+                new BiPredicateBean<>("is not", (anInteger, anInteger2) -> !anInteger.equals(anInteger2)),
+                new BiPredicateBean<>("greater than", (anInteger, anInteger2) -> anInteger > anInteger2),
+                new BiPredicateBean<>("greater or equal to", (anInteger, anInteger2) -> anInteger >= anInteger2),
+                new BiPredicateBean<>("lesser than", (anInteger, anInteger2) -> anInteger < anInteger2),
+                new BiPredicateBean<>("lesser or equal to", (anInteger, anInteger2) -> anInteger <= anInteger2)
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final IntegerFilter<T> extend(BiPredicateBean<Integer, Integer>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 59 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/LongFilter.java

@@ -0,0 +1,59 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.beans.BiPredicateBean;
+import io.github.palexdev.materialfx.utils.FXCollectors;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+import javafx.util.converter.LongStringConverter;
+
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Extension of {@link NumberFilter} for long fields.
+ * <p></p>
+ * Offers the following default {@link BiPredicateBean}s:
+ * <p> - "is": checks for longs equality
+ * <p> - "is not": checks for longs inequality
+ * <p> - "greater than": checks if a long is greater than another long
+ * <p> - "greater or equal to": checks if a long is greater or equal to another long
+ * <p> - "lesser than": checks if a long is lesser than another long
+ * <p> - "lesser or equal to": checks if a long is lesser or equal to another long
+ */
+public class LongFilter<T> extends NumberFilter<T, Long> {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public LongFilter(String name, Function<T, Long> extractor) {
+        this(name, extractor, new LongStringConverter());
+    }
+
+    public LongFilter(String name, Function<T, Long> extractor, StringConverter<Long> converter) {
+        super(name, extractor, converter);
+    }
+
+    //================================================================================
+    // Overridden Methods
+    //================================================================================
+    @Override
+    protected ObservableList<BiPredicateBean<Long, Long>> defaultPredicates() {
+        return Stream.<BiPredicateBean<Long, Long>>of(
+                new BiPredicateBean<>("is", Long::equals),
+                new BiPredicateBean<>("is not", (aLong, aLong2) -> !aLong.equals(aLong2)),
+                new BiPredicateBean<>("greater than", (aLong, aLong2) -> aLong > aLong2),
+                new BiPredicateBean<>("greater or equal to", (aLong, aLong2) -> aLong >= aLong2),
+                new BiPredicateBean<>("lesser than", (aLong, aLong2) -> aLong < aLong2),
+                new BiPredicateBean<>("lesser or equal to", (aLong, aLong2) -> aLong <= aLong2)
+        ).collect(FXCollectors.toList());
+    }
+
+    @SafeVarargs
+    @Override
+    protected final LongFilter<T> extend(BiPredicateBean<Long, Long>... predicateBeans) {
+        Collections.addAll(super.predicates, predicateBeans);
+        return this;
+    }
+}

+ 0 - 195
materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXEvaluationBox.java

@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2021 Parisi Alessandro
- * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- * MaterialFX is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * MaterialFX is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.filter;
-
-import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.MFXComboBox;
-import io.github.palexdev.materialfx.controls.MFXTextField;
-import io.github.palexdev.materialfx.controls.enums.Styles;
-import io.github.palexdev.materialfx.font.MFXFontIcon;
-import io.github.palexdev.materialfx.utils.StringUtils;
-import javafx.beans.binding.Bindings;
-import javafx.collections.FXCollections;
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.control.Label;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.paint.Color;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.BiPredicate;
-
-/**
- * This little control provides a graphical way of evaluating a condition on
- * a given string with a specific predicate. The computed boolean can also be chained with
- * other conditions as an AND or an OR, specified by {@link EvaluationMode}.
- * <p></p>
- * The control is made of:
- * <p> - An icon that should be used by the control's parent to remove it from the children list
- * <p> - A text field that provides one of the strings to test
- * <p> - A combo box that contains the predicate to apply
- * <p></p>
- * An usage example would be:
- * <p></p>
- * Let's say I have a string {@code s1 = AbcdE} and a string {@code s2 = "cde"} provided by the text field.
- * <p>
- * If the selected predicate is "Contains" then {@link #test(String)} will check if s1 contains s2 and will return false.
- * <p>
- * If the selected predicate is "Contains Ignore Case" then {@link #test(String)} will check if s1 contains s2 ignoring case and will return true.
- *
- * <p></p>
- * <b>N.B: </b> Since "Contains Any" and "Contains All" are advanced functions the field text is cleared when one of those functions is selected
- * in the combo box and a prompt text that shows a small example on how to format the string is set.
- *
- * @see BiPredicate
- */
-public class MFXEvaluationBox extends HBox {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private final String STYLE_CLASS = "mfx/evaluation-box";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/MFXEvaluationBox.css");
-
-    private final EvaluationMode mode;
-    private final Map<String, BiPredicate<String, String>> biPredicates = new LinkedHashMap<>();
-    private final MFXFontIcon removeIcon;
-    private final MFXTextField inputField;
-    private final MFXComboBox<String> predicatesCombo;
-
-    //================================================================================
-    // Constructor
-    //================================================================================
-    public MFXEvaluationBox(EvaluationMode mode) {
-        setPrefWidth(600);
-        setAlignment(Pos.CENTER_LEFT);
-        setSpacing(20);
-        setPadding(new Insets(15, 10, 15, 10));
-
-        this.mode = mode;
-
-        Label modeLabel = new Label(mode.name());
-        modeLabel.setId("modeLabel");
-        modeLabel.setPrefSize(40, 40);
-        modeLabel.setMaxHeight(Double.MAX_VALUE);
-        modeLabel.setPadding(new Insets(3));
-        modeLabel.setAlignment(Pos.CENTER);
-
-        HBox box = new HBox(5);
-        box.setAlignment(Pos.CENTER);
-
-        Label predicateLabel = new Label("Evaluation Predicate:");
-        predicatesCombo = new MFXComboBox<>();
-        predicatesCombo.setComboStyle(Styles.ComboBoxStyles.STYLE2);
-        predicatesCombo.setPrefSize(180, 27);
-        predicatesCombo.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
-
-        inputField = new MFXTextField();
-        inputField.setPromptText("Input String...");
-        inputField.setAnimateLines(false);
-        inputField.setLineColor(Color.web("#4d4d4d"));
-        inputField.setLineStrokeWidth(1);
-        inputField.setMaxWidth(Double.MAX_VALUE);
-        HBox.setHgrow(inputField, Priority.ALWAYS);
-        HBox.setMargin(inputField, new Insets(0, 10, 2, 0));
-        inputField.getStylesheets().add(STYLESHEET);
-
-        removeIcon = new MFXFontIcon("mfx-x-circle", 16, Color.web("#4D4D4D"));
-        removeIcon.colorProperty().bind(Bindings.createObjectBinding(
-                () -> removeIcon.isHover() ? Color.web("#EF6E6B") : Color.web("#4D4D4D"),
-                removeIcon.hoverProperty()
-        ));
-
-        box.getChildren().addAll(predicateLabel, predicatesCombo);
-        getChildren().addAll(removeIcon, modeLabel, box, inputField);
-
-        initialize();
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-
-        biPredicates.put("Contains", String::contains);
-        biPredicates.put("Contains Ignore Case", StringUtils::containsIgnoreCase);
-        biPredicates.put("Contains Any", StringUtils::containsAny);
-        biPredicates.put("Contains All", StringUtils::containsAll);
-        biPredicates.put("Starts With", String::startsWith);
-        biPredicates.put("Start With Ignore Case", StringUtils::startsWithIgnoreCase);
-        biPredicates.put("Ends With", String::endsWith);
-        biPredicates.put("Ends With Ignore Case", StringUtils::endsWithIgnoreCase);
-        biPredicates.put("Equals", String::equals);
-        biPredicates.put("Equals Ignore Case", String::equalsIgnoreCase);
-
-        predicatesCombo.selectedValueProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue.equals("Contains Any") || newValue.equals("Contains All")) {
-                inputField.setPromptText("Eg. \"A, B, C, DEF GHI, E, F...\"");
-                inputField.clear();
-            } else {
-                inputField.setPromptText("");
-            }
-        });
-
-        predicatesCombo.setItems(FXCollections.observableArrayList(biPredicates.keySet()));
-        predicatesCombo.getSelectionModel().selectFirst();
-    }
-
-    /**
-     * Applies the selected predicate (provided by the combo box) to the given
-     * string and the text provided by the text field.
-     */
-    public Boolean test(String testString) {
-        if (getPredicate() != null) {
-            return biPredicates.get(getPredicate()).test(testString, inputField.getText());
-        }
-        return false;
-    }
-
-    /**
-     * @return the evaluation mode of this control
-     */
-    public EvaluationMode getMode() {
-        return mode;
-    }
-
-    /**
-     * @return the currently selected predicate
-     */
-    public String getPredicate() {
-        return predicatesCombo.getSelectedValue();
-    }
-
-    /**
-     * @return the remove icon instance
-     */
-    public MFXFontIcon getRemoveIcon() {
-        return removeIcon;
-    }
-
-    //================================================================================
-    // Override Methods
-    //================================================================================
-    @Override
-    public String getUserAgentStylesheet() {
-        return STYLESHEET;
-    }
-}

+ 23 - 33
materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXFilterDialog.java

@@ -18,35 +18,14 @@
 
 package io.github.palexdev.materialfx.filter;
 
-import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.*;
-import io.github.palexdev.materialfx.controls.cell.MFXListCell;
-import io.github.palexdev.materialfx.controls.enums.Styles;
-import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
-import io.github.palexdev.materialfx.effects.DepthLevel;
-import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
-import io.github.palexdev.materialfx.font.MFXFontIcon;
-import javafx.beans.binding.Bindings;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.layout.StackPane;
-import javafx.scene.paint.Color;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
+// TODO remake?
 /**
  * This dialog provides a graphical way of filtering a given list of T items based
  * on the conditions specified by the added evaluation boxes.
  *
  * @see MFXEvaluationBox
  */
+/*
 public class MFXFilterDialog<T> extends MFXDialog {
     //================================================================================
     // Properties
@@ -154,9 +133,11 @@ public class MFXFilterDialog<T> extends MFXDialog {
         setBehavior();
     }
 
-    /**
+    */
+/**
      * Sets the buttons behavior
-     */
+     *//*
+
     private void setBehavior() {
         addEventFilter(MouseEvent.MOUSE_PRESSED, event -> requestFocus());
 
@@ -165,7 +146,8 @@ public class MFXFilterDialog<T> extends MFXDialog {
         clear.setOnAction(event -> evaluationBoxes.clear());
     }
 
-    /**
+    */
+/**
      * Filters the given list and returns an observable filtered list.
      * <p></p>
      * Calls {@link #filter(String)} on each item for filtering.
@@ -173,7 +155,8 @@ public class MFXFilterDialog<T> extends MFXDialog {
      * <b>N.B:</b> The evaluation is done by calling the item's toString method or, if the item implements {@link IFilterable},
      * by calling {@link IFilterable#toFilterString()}. If the toString method is not overridden
      * or does not contain any useful information for filtering it won't work.
-     */
+     *//*
+
     public ObservableList<T> filter(List<T> list) {
         return list.stream()
                 .filter(item -> {
@@ -187,9 +170,11 @@ public class MFXFilterDialog<T> extends MFXDialog {
                 .collect(Collectors.toCollection(FXCollections::observableArrayList));
     }
 
-    /**
+    */
+/**
      * Tests all the evaluation boxes conditions on the given string.
-     */
+     *//*
+
     private boolean filter(String filterString) {
         Boolean expression = null;
         for (MFXEvaluationBox box : evaluationBoxes) {
@@ -210,9 +195,11 @@ public class MFXFilterDialog<T> extends MFXDialog {
         return expression != null ? expression : false;
     }
 
-    /**
+    */
+/**
      * Adds a new {@link MFXEvaluationBox} with the specified {@link EvaluationMode} to the dialog.
-     */
+     *//*
+
     private void addFilterBox(EvaluationMode mode) {
         MFXEvaluationBox evaluationBox = new MFXEvaluationBox(mode);
         HBox.setHgrow(evaluationBox, Priority.ALWAYS);
@@ -220,9 +207,11 @@ public class MFXFilterDialog<T> extends MFXDialog {
         evaluationBoxes.add(evaluationBox);
     }
 
-    /**
+    */
+/**
      * @return the filter button instance
-     */
+     *//*
+
     public MFXButton getFilterButton() {
         return filterButton;
     }
@@ -245,3 +234,4 @@ public class MFXFilterDialog<T> extends MFXDialog {
         closeIcon.resizeRelocate(ciX, ciY, ciSize, ciSize);
     }
 }
+*/

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio