Browse Source

Minor fixes and improvements

AbstractMFXFlowlessListView:
MFXScrollPane:
:recycle: Moved smooth scrolling to utils class
:recycle: Improved smooth scrolling with trackpad (Fixes issue #53)
:bug: Fixed horizontal scrolling not working if smooth scrolling was added. Now there's not a distinction between horizontal and vertical smooth scrolling, the effect now works in both directions. (Fixes issue #51)

ScrollUtils:
:sparkles: Added new method to animate a scroll pane's scroll bars (fade in/out)

NodeUtils:
:sparkles: Added two new convenience methods to execute an action when a control's scene or skin is not null anymore

ExecutionUtils:
:sparkles: New utility class to help with concurrency and callables

AnimationUtils:
:sparkles: Ported from one of my personal projects. Utility to easily create simple and complex animations with fluent api. Also offers methods to enable and disable nodes, and methods for text transitions
Alessadro Parisi 4 years ago
parent
commit
a02c929402

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

@@ -24,6 +24,7 @@ import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.demo.MFXResourcesLoader;
 import io.github.palexdev.materialfx.demo.MFXResourcesLoader;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
+import io.github.palexdev.materialfx.utils.ScrollUtils;
 import javafx.animation.KeyFrame;
 import javafx.animation.KeyFrame;
 import javafx.animation.KeyValue;
 import javafx.animation.KeyValue;
 import javafx.animation.ParallelTransition;
 import javafx.animation.ParallelTransition;
@@ -157,7 +158,8 @@ public class DemoController implements Initializable {
         vLoader.start();
         vLoader.start();
 
 
         // Others
         // Others
-        MFXScrollPane.smoothVScrolling(scrollPane, 2);
+        ScrollUtils.addSmoothScrolling(scrollPane, 2);
+        ScrollUtils.animateScrollBars(scrollPane, 500, 500);
         primaryStage.sceneProperty().addListener((observable, oldValue, newValue) -> {
         primaryStage.sceneProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue != null) {
             if (newValue != null) {
                 Scene scene = primaryStage.getScene();
                 Scene scene = primaryStage.getScene();

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

@@ -24,6 +24,7 @@ import io.github.palexdev.materialfx.controls.MFXLabel;
 import io.github.palexdev.materialfx.controls.cell.MFXFlowlessListCell;
 import io.github.palexdev.materialfx.controls.cell.MFXFlowlessListCell;
 import io.github.palexdev.materialfx.font.FontResources;
 import io.github.palexdev.materialfx.font.FontResources;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.ScrollUtils;
 import javafx.fxml.FXML;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.fxml.Initializable;
 import javafx.geometry.Insets;
 import javafx.geometry.Insets;
@@ -55,7 +56,7 @@ public class FontResourcesDemoController implements Initializable {
             cell.setFixedCellHeight(48);
             cell.setFixedCellHeight(48);
             return cell;
             return cell;
         });
         });
-        MFXFlowlessListView.setSmoothScrolling(list, 5);
+        ScrollUtils.addSmoothScrolling(list, 5);
         populateList();
         populateList();
         count.setText(list.getItems().size() + " Icons");
         count.setText(list.getItems().size() + " Icons");
     }
     }

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

@@ -21,6 +21,7 @@ package io.github.palexdev.materialfx.demo.controllers;
 
 
 import io.github.palexdev.materialfx.controls.MFXScrollPane;
 import io.github.palexdev.materialfx.controls.MFXScrollPane;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.ScrollUtils;
 import javafx.fxml.FXML;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.fxml.Initializable;
 
 
@@ -37,8 +38,9 @@ public class ScrollPaneDemoController implements Initializable {
 
 
     @Override
     @Override
     public void initialize(URL location, ResourceBundle resources) {
     public void initialize(URL location, ResourceBundle resources) {
-        MFXScrollPane.smoothVScrolling(scrollPaneV);
-        MFXScrollPane.smoothVScrolling(scrollPaneVH);
+        ScrollUtils.addSmoothScrolling(scrollPaneV);
+        ScrollUtils.addSmoothScrolling(scrollPaneVH);
+        ScrollUtils.animateScrollBars(scrollPaneVH);
     }
     }
 
 
     @FXML
     @FXML

+ 8 - 4
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TreeviewsDemoController.java

@@ -18,9 +18,13 @@
 
 
 package io.github.palexdev.materialfx.demo.controllers;
 package io.github.palexdev.materialfx.demo.controllers;
 
 
-import io.github.palexdev.materialfx.controls.*;
+import io.github.palexdev.materialfx.controls.MFXCheckTreeItem;
+import io.github.palexdev.materialfx.controls.MFXCheckTreeView;
+import io.github.palexdev.materialfx.controls.MFXTreeItem;
+import io.github.palexdev.materialfx.controls.MFXTreeView;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.ColorUtils;
+import io.github.palexdev.materialfx.utils.ScrollUtils;
 import javafx.fxml.FXML;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.fxml.Initializable;
 import javafx.geometry.Pos;
 import javafx.geometry.Pos;
@@ -61,9 +65,9 @@ public class TreeviewsDemoController implements Initializable {
 
 
         checkTreeView.setRoot(createCheckRoot());
         checkTreeView.setRoot(createCheckRoot());
 
 
-        MFXScrollPane.smoothVScrolling(treeView);
-        MFXScrollPane.smoothVScrolling(treeViewHide);
-        MFXScrollPane.smoothVScrolling(checkTreeView);
+        ScrollUtils.addSmoothScrolling(treeView);
+        ScrollUtils.addSmoothScrolling(treeViewHide);
+        ScrollUtils.addSmoothScrolling(checkTreeView);
 
 
         treeView.getSelectionModel().getSelectedItems().addListener(
         treeView.getSelectionModel().getSelectedItems().addListener(
                 (observable, oldValue, newValue) -> text1.setText("Selected Items Count: " + treeView.getSelectionModel().getSelectedItems().size()));
                 (observable, oldValue, newValue) -> text1.setText("Selected Items Count: " + treeView.getSelectionModel().getSelectedItems().size()));

+ 61 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/AnimationsData.java

@@ -0,0 +1,61 @@
+package io.github.palexdev.materialfx.beans;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.util.Duration;
+
+/**
+ * Simple bean that has a node reference, a duration for the animation and
+ * an action to perform when the animation ends.
+ */
+public class AnimationsData {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final Node node;
+    private final Duration duration;
+    private final EventHandler<ActionEvent> onFinished;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public AnimationsData(Node node, Duration duration, EventHandler<ActionEvent> onFinished) {
+        this.node = node;
+        this.duration = duration;
+        this.onFinished = onFinished;
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    public Node node() {
+        return node;
+    }
+
+    public Duration duration() {
+        return duration;
+    }
+
+    public EventHandler<ActionEvent> onFinished() {
+        return onFinished;
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+
+    /**
+     * Builds a new AnimationsData object with the given node and duration, the action is set to null.
+     */
+    public static AnimationsData of(Node node, Duration duration) {
+        return of(node, duration, null);
+    }
+
+    /**
+     * Builds a new AnimationsData object with the given node and duration and action.
+     */
+    public static AnimationsData of(Node node, Duration duration, EventHandler<ActionEvent> onFinished) {
+        return new AnimationsData(node, duration, onFinished);
+    }
+}

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

@@ -21,24 +21,13 @@ package io.github.palexdev.materialfx.controls;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.skins.MFXScrollPaneSkin;
 import io.github.palexdev.materialfx.skins.MFXScrollPaneSkin;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.ColorUtils;
-import javafx.animation.Animation;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
-import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
-import javafx.event.EventHandler;
-import javafx.geometry.Bounds;
 import javafx.scene.Node;
 import javafx.scene.Node;
 import javafx.scene.control.ScrollPane;
 import javafx.scene.control.ScrollPane;
 import javafx.scene.control.Skin;
 import javafx.scene.control.Skin;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.input.ScrollEvent;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.paint.Paint;
-import javafx.util.Duration;
-
-import java.util.function.Function;
 
 
 /**
 /**
  * This is the implementation of a scroll pane following Google's material design guidelines in JavaFX.
  * This is the implementation of a scroll pane following Google's material design guidelines in JavaFX.
@@ -162,100 +151,9 @@ public class MFXScrollPane extends ScrollPane {
         setStyle(sb.toString());
         setStyle(sb.toString());
     }
     }
 
 
-    //================================================================================
-    // Static Methods
-    //================================================================================
-    private static void customScrolling(ScrollPane scrollPane, DoubleProperty scrollDirection, Function<Bounds, Double> sizeFunc, int speed) {
-        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
-        final double[] pushes = {speed};
-        final double[] derivatives = new double[frictions.length];
-
-        Timeline timeline = new Timeline();
-        final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
-        final EventHandler<ScrollEvent> scrollHandler = event -> {
-            if (event.getEventType() == ScrollEvent.SCROLL) {
-                int direction = event.getDeltaY() > 0 ? -1 : 1;
-                for (int i = 0; i < pushes.length; i++) {
-                    derivatives[i] += direction * pushes[i];
-                }
-                if (timeline.getStatus() == Animation.Status.STOPPED) {
-                    timeline.play();
-                }
-                event.consume();
-            }
-        };
-        if (scrollPane.getContent().getParent() != null) {
-            scrollPane.getContent().getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
-            scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
-        }
-        scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
-            if (oldValue != null) {
-                oldValue.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
-                oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
-            }
-            if (newValue != null) {
-                newValue.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
-                newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
-            }
-        });
-        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
-            for (int i = 0; i < derivatives.length; i++) {
-                derivatives[i] *= frictions[i];
-            }
-            for (int i = 1; i < derivatives.length; i++) {
-                derivatives[i] += derivatives[i - 1];
-            }
-            double dy = derivatives[derivatives.length - 1];
-            double size = sizeFunc.apply(scrollPane.getContent().getLayoutBounds());
-            scrollDirection.set(Math.min(Math.max(scrollDirection.get() + dy / size, 0), 1));
-            if (Math.abs(dy) < 0.001) {
-                timeline.stop();
-            }
-        }));
-        timeline.setCycleCount(Animation.INDEFINITE);
-    }
-
-    /**
-     * Adds smooth vertical scrolling to the specified scroll pane.
-     * <p>
-     * <b>Note: not recommended for small scroll panes</b>
-     *
-     * @param speed regulates the speed of the scrolling
-     */
-    public static void smoothVScrolling(ScrollPane scrollPane, int speed) {
-        customScrolling(scrollPane, scrollPane.vvalueProperty(), Bounds::getHeight, speed);
-    }
-
-    /**
-     * Adds smooth horizontal scrolling to the specified scroll pane.
-     * <p>
-     * <b>Note: not recommended for small scroll panes</b>
-     *
-     * @param speed regulates the speed of the scrolling
-     */
-    public static void smoothHScrolling(ScrollPane scrollPane, int speed) {
-        customScrolling(scrollPane, scrollPane.hvalueProperty(), Bounds::getWidth, speed);
-    }
-
-    /**
-     * Calls {@link #smoothVScrolling(ScrollPane, int)} with a default speed modifier of 1.
-     */
-    public static void smoothVScrolling(ScrollPane scrollPane) {
-        smoothVScrolling(scrollPane, 1);
-    }
-
-    /**
-     * Calls {@link #smoothHScrolling(ScrollPane, int)} with a default speed modifier of 1.
-     */
-    public static void smoothHScrolling(ScrollPane scrollPane) {
-        smoothHScrolling(scrollPane, 1);
-    }
-
-
     //================================================================================
     //================================================================================
     // Override Methods
     // Override Methods
     //================================================================================
     //================================================================================
-
     @Override
     @Override
     protected Skin<?> createDefaultSkin() {
     protected Skin<?> createDefaultSkin() {
         return new MFXScrollPaneSkin(this);
         return new MFXScrollPaneSkin(this);

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

@@ -374,7 +374,7 @@ public class MFXTableView<T> extends Control {
      */
      */
     public static class MFXTableViewEvent extends Event {
     public static class MFXTableViewEvent extends Event {
 
 
-        public static EventType<MFXTableViewEvent> FORCE_UPDATE_EVENT = new EventType<>(ANY, "FORCE_UPDATE_EVENT");
+        public static final EventType<MFXTableViewEvent> FORCE_UPDATE_EVENT = new EventType<>(ANY, "FORCE_UPDATE_EVENT");
 
 
         public MFXTableViewEvent(EventType<? extends Event> eventType) {
         public MFXTableViewEvent(EventType<? extends Event> eventType) {
             super(eventType);
             super(eventType);

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

@@ -18,29 +18,18 @@
 
 
 package io.github.palexdev.materialfx.controls.base;
 package io.github.palexdev.materialfx.controls.base;
 
 
-import io.github.palexdev.materialfx.controls.flowless.VirtualFlow;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.selection.base.IListSelectionModel;
 import io.github.palexdev.materialfx.selection.base.IListSelectionModel;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.ColorUtils;
-import javafx.animation.Animation;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
 import javafx.collections.FXCollections;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import javafx.collections.ObservableList;
 import javafx.css.*;
 import javafx.css.*;
-import javafx.event.EventHandler;
 import javafx.scene.control.Control;
 import javafx.scene.control.Control;
-import javafx.scene.control.Skin;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.input.ScrollEvent;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.paint.Paint;
 import javafx.util.Duration;
 import javafx.util.Duration;
-import org.reactfx.value.Var;
 
 
 import java.util.List;
 import java.util.List;
 import java.util.function.Function;
 import java.util.function.Function;
@@ -125,81 +114,6 @@ public abstract class AbstractMFXFlowlessListView<T, C extends AbstractMFXFlowle
         setStyle(sb.toString());
         setStyle(sb.toString());
     }
     }
 
 
-    public static void setSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView) {
-        setSmoothScrolling(listView, 1);
-    }
-
-    public static void setSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView, int speed) {
-        if (listView.getScene() != null) {
-            VirtualFlow<?, ?> flow = (VirtualFlow<?, ?>) listView.lookup(".virtual-flow");
-            setSmoothScrolling(flow, flow.estimatedScrollYProperty(), speed);
-        } else {
-            listView.skinProperty().addListener(new ChangeListener<>() {
-                @Override
-                public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
-                    if (newValue != null) {
-                        VirtualFlow<?, ?> flow = (VirtualFlow<?, ?>) listView.lookup(".virtual-flow");
-                        setSmoothScrolling(flow, flow.estimatedScrollYProperty(), speed);
-                        listView.skinProperty().removeListener(this);
-                    }
-                }
-            });
-        }
-    }
-
-    private static void setSmoothScrolling(VirtualFlow<?, ?> flow, Var<Double> scrollDirection, int speed) {
-        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
-        final double[] pushes = {speed};
-        final double[] derivatives = new double[frictions.length];
-
-        Timeline timeline = new Timeline();
-        final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
-
-        final EventHandler<ScrollEvent> scrollHandler = event -> {
-            if (event.getEventType() == ScrollEvent.SCROLL) {
-                int direction = event.getDeltaY() > 0 ? -1 : 1;
-                for (int i = 0; i < pushes.length; i++) {
-                    derivatives[i] += direction * pushes[i];
-                }
-                if (timeline.getStatus() == Animation.Status.STOPPED) {
-                    timeline.play();
-                }
-                event.consume();
-            }
-        };
-
-        if (flow.getParent() != null) {
-            flow.getParent().addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
-        }
-       flow.parentProperty().addListener((observable, oldValue, newValue) -> {
-            if (oldValue != null) {
-                oldValue.removeEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
-            }
-            if (newValue != null) {
-                newValue.addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
-            }
-        });
-        flow.addEventFilter(MouseEvent.DRAG_DETECTED, dragHandler);
-        flow.addEventFilter(ScrollEvent.ANY, scrollHandler);
-
-        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
-            for (int i = 0; i < derivatives.length; i++) {
-                derivatives[i] *= frictions[i];
-            }
-            for (int i = 1; i < derivatives.length; i++) {
-                derivatives[i] += derivatives[i - 1];
-            }
-            double dy = derivatives[derivatives.length - 1];
-
-            scrollDirection.setValue(scrollDirection.getValue() + dy);
-
-            if (Math.abs(dy) < 0.001) {
-                timeline.stop();
-            }
-        }));
-        timeline.setCycleCount(Animation.INDEFINITE);
-    }
-
     //================================================================================
     //================================================================================
     // Override Methods
     // Override Methods
     //================================================================================
     //================================================================================

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

@@ -30,6 +30,7 @@ import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
+import io.github.palexdev.materialfx.utils.ScrollUtils;
 import io.github.palexdev.materialfx.utils.StringUtils;
 import io.github.palexdev.materialfx.utils.StringUtils;
 import javafx.animation.*;
 import javafx.animation.*;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.BooleanProperty;
@@ -628,7 +629,7 @@ public class MFXDatePickerContent extends VBox {
         yearsScroll = new MFXScrollPane(buildYears());
         yearsScroll = new MFXScrollPane(buildYears());
         yearsScroll.getStyleClass().add("years-scrollpane");
         yearsScroll.getStyleClass().add("years-scrollpane");
         yearsScroll.setFitToWidth(true);
         yearsScroll.setFitToWidth(true);
-        MFXScrollPane.smoothVScrolling(yearsScroll);
+        ScrollUtils.addSmoothScrolling(yearsScroll);
 
 
         return yearsScroll;
         return yearsScroll;
     }
     }

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

@@ -187,8 +187,6 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
      * Responsible for building the track and the bars for the progress bar.
      * Responsible for building the track and the bars for the progress bar.
      */
      */
     protected Rectangle buildRectangle(String styleClass) {
     protected Rectangle buildRectangle(String styleClass) {
-        MFXProgressBar progressBar = getSkinnable();
-
         Rectangle rectangle = new Rectangle();
         Rectangle rectangle = new Rectangle();
         rectangle.getStyleClass().setAll(styleClass);
         rectangle.getStyleClass().setAll(styleClass);
         rectangle.setStroke(Color.TRANSPARENT);
         rectangle.setStroke(Color.TRANSPARENT);
@@ -196,8 +194,6 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
         rectangle.setStrokeLineJoin(StrokeLineJoin.ROUND);
         rectangle.setStrokeLineJoin(StrokeLineJoin.ROUND);
         rectangle.setStrokeType(StrokeType.INSIDE);
         rectangle.setStrokeType(StrokeType.INSIDE);
         rectangle.setStrokeWidth(0);
         rectangle.setStrokeWidth(0);
-/*        rectangle.arcHeightProperty().bind(progressBar.bordersRadiusProperty());
-        rectangle.arcWidthProperty().bind(progressBar.bordersRadiusProperty());*/
         return rectangle;
         return rectangle;
     }
     }
 
 

+ 486 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/AnimationUtils.java

@@ -0,0 +1,486 @@
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.beans.AnimationsData;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import javafx.animation.*;
+import javafx.animation.Animation.Status;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Labeled;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+/**
+ * Utility class to easily build animations of any sort. Designed with fluent api.
+ */
+public class AnimationUtils {
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    private AnimationUtils() {
+    }
+
+    //================================================================================
+    // Static Methods
+    //================================================================================
+
+    /**
+     * Temporarily disables the given node for the specified duration.
+     */
+    public static void disableTemporarily(Duration duration, Node node) {
+        node.setDisable(true);
+        AnimationUtils.PauseBuilder.build()
+                .setOnFinished(event -> node.setDisable(false))
+                .setDuration(duration)
+                .getAnimation()
+                .play();
+    }
+
+    /**
+     * Calls {@link #disableTemporarily(Duration, Node)} by converting the given millis value
+     * with {@link Duration#millis(double)}.
+     */
+    public static void disableTemporarily(double millis, Node node) {
+        disableTemporarily(Duration.millis(millis), node);
+    }
+
+    /**
+     * Executes the given onFinished action after the specified duration of time.
+     * (Uses a PauseTransition)
+     */
+    public static void executeLater(Duration duration, EventHandler<ActionEvent> onFinished) {
+        PauseBuilder.build().setDuration(duration).setOnFinished(onFinished).getAnimation().play();
+    }
+
+    /**
+     * Calls {@link #executeLater(Duration, EventHandler)} by converting the given millis value
+     * with {@link Duration#millis(double)}.
+     */
+    public static void executeLater(double millis, EventHandler<ActionEvent> onFinished) {
+        executeLater(Duration.millis(millis), onFinished);
+    }
+
+    /**
+     * Sets the text of the given {@link Labeled} with a fade out/fade in transition.
+     *
+     * @param labeled  the labeled control to change the text to
+     * @param duration the fade in and fade out speed
+     * @param nexText  the new text to set
+     * @return an instance of {@link AbstractBuilder}
+     */
+    public static AbstractBuilder transitionText(Labeled labeled, Duration duration, String nexText) {
+        return SequentialBuilder.build()
+                .hide(AnimationsData.of(labeled, duration, event -> labeled.setText(nexText)))
+                .show(AnimationsData.of(labeled, duration));
+    }
+
+    /**
+     * Calls {@link #transitionText(Labeled, Duration, String)} by converting the given millis value
+     * with {@link Duration#millis(double)}.
+     */
+    public static AbstractBuilder transitionText(Labeled labeled, double millis, String nexText) {
+        return transitionText(labeled, Duration.millis(millis), nexText);
+    }
+
+    /**
+     * Sets the text of the given {@link Text} with a fade out/fade in transition.
+     *
+     * @param text     the text control to change the text to
+     * @param duration the fade in and fade out speed
+     * @param nexText  the new text to set
+     * @return an instance of {@link AbstractBuilder}
+     */
+    public static AbstractBuilder transitionText(Text text, Duration duration, String nexText) {
+        return SequentialBuilder.build()
+                .hide(AnimationsData.of(text, duration, event -> text.setText(nexText)))
+                .show(AnimationsData.of(text, duration));
+    }
+
+    /**
+     * Calls {@link #transitionText(Text, Duration, String)} by converting the given millis value
+     * with {@link Duration#millis(double)}.
+     */
+    public static AbstractBuilder transitionText(Text text, double millis, String nexText) {
+        return transitionText(text, Duration.millis(millis), nexText);
+    }
+
+    /**
+     * @return true if the given animation status is RUNNING, otherwise false
+     */
+    public static boolean isPlaying(Animation animation) {
+        return animation.getStatus() == Status.RUNNING;
+    }
+
+    /**
+     * @return true if the given animation status is PAUSED, otherwise false
+     */
+    public static boolean isPaused(Animation animation) {
+        return animation.getStatus() == Status.PAUSED;
+    }
+
+    //================================================================================
+    // Builders
+    //================================================================================
+
+    /**
+     * Common base class for {@link ParallelBuilder} and {@link SequentialBuilder}.
+     * <p></p>
+     * This builder, designed with fluent api, allows you to create simple and complex animations with just a few lines of code.
+     * <p></p>
+     * The builder keeps the reference of the "main" animation (depending on the subclass can be ParallelTransition or SequentialTransition, in
+     * the AbstractBuilder the type is a generic {@link Animation}), and defines and abstract method that subclasses must implement
+     * to properly add animations to the "main".
+     */
+    public static abstract class AbstractBuilder {
+        //================================================================================
+        // Properties
+        //================================================================================
+        protected Animation animation;
+
+        //================================================================================
+        // Abstract Methods
+        //================================================================================
+
+        /**
+         * Adds the given animation to the "main" animation.
+         */
+        protected abstract void addAnimation(Animation animation);
+
+        /**
+         * @return the "main" animation instance
+         */
+        public abstract Animation getAnimation();
+
+        //================================================================================
+        // Methods
+        //================================================================================
+        protected void init(Animation animation) {
+            this.animation = animation;
+        }
+
+        /**
+         * Adds the given animation to the "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(Animation animation) {
+            addAnimation(animation);
+            return this;
+        }
+
+        /**
+         * Sets the given onFinished action to the given animation and then adds it to the
+         * "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(Animation animation, EventHandler<ActionEvent> onFinished) {
+            animation.setOnFinished(onFinished);
+            addAnimation(animation);
+            return this;
+        }
+
+        /**
+         * For each given node builds and adds an animation that disables the node
+         * after the given duration of time.
+         *
+         * @param duration the time after which the nodes are disabled
+         */
+        public AbstractBuilder disable(Duration duration, Node... nodes) {
+            for (Node node : nodes) {
+                addAnimation(
+                        PauseBuilder.build()
+                                .setDuration(duration)
+                                .setOnFinished(end -> node.setDisable(true))
+                                .getAnimation()
+                );
+            }
+            return this;
+        }
+
+        /**
+         * For each given node builds and adds an animation that enables the node
+         * after the given duration of time.
+         *
+         * @param duration the duration after which the nodes are enabled
+         */
+        public AbstractBuilder enable(Duration duration, Node... nodes) {
+            for (Node node : nodes) {
+                addAnimation(
+                        PauseBuilder.build()
+                                .setDuration(duration)
+                                .setOnFinished(end -> node.setDisable(false))
+                                .getAnimation()
+                );
+            }
+            return this;
+        }
+
+        /**
+         * For each given window builds and adds an animation that hides the window by fading it out.
+         *
+         * @param duration the fade animation speed
+         */
+        public AbstractBuilder hide(Duration duration, Window... windows) {
+            for (Window window : windows) {
+                Timeline timeline = new Timeline(
+                        new KeyFrame(duration, new KeyValue(window.opacityProperty(), 0))
+                );
+                addAnimation(timeline);
+            }
+            return this;
+        }
+
+        /**
+         * Calls {@link #hide(Duration, Window...)} by converting the given millis value
+         * with {@link Duration#millis(double)}.
+         */
+        public AbstractBuilder hide(double millis, Window... windows) {
+            return hide(Duration.millis(millis), windows);
+        }
+
+        /**
+         * For each given node builds and adds an animation that hides the node by fading it out.
+         *
+         * @param duration the fade animation speed
+         */
+        public AbstractBuilder hide(Duration duration, Node... nodes) {
+            for (Node node : nodes) {
+                addAnimation(MFXAnimationFactory.FADE_OUT.build(node, duration.toMillis()));
+            }
+            return this;
+        }
+
+        /**
+         * Calls {@link #hide(Duration, Node...)} by converting the given millis value
+         * with {@link Duration#millis(double)}.
+         */
+        public AbstractBuilder hide(double millis, Node... nodes) {
+            return hide(Duration.millis(millis), nodes);
+        }
+
+        /**
+         * Creates and adds a fade out animation for each given {@link AnimationsData}.
+         */
+        public final AbstractBuilder hide(AnimationsData... data) {
+            for (AnimationsData animData : data) {
+                Animation animation = MFXAnimationFactory.FADE_OUT.build(animData.node(), animData.duration().toMillis());
+                animation.setOnFinished(animData.onFinished());
+                addAnimation(animation);
+            }
+            return this;
+        }
+
+        /**
+         * For each given window builds and adds an animation that shows the window by fading it in.
+         *
+         * @param duration the fade animation speed
+         */
+        public AbstractBuilder show(Duration duration, Window... windows) {
+            for (Window window : windows) {
+                Timeline timeline = new Timeline(
+                        new KeyFrame(duration, new KeyValue(window.opacityProperty(), 1.0))
+                );
+                addAnimation(timeline);
+            }
+            return this;
+        }
+
+        /**
+         * Calls {@link #show(Duration, Window...)} by converting the given millis value
+         * with {@link Duration#millis(double)}.
+         */
+        public AbstractBuilder show(double millis, Window... windows) {
+            return show(Duration.millis(millis), windows);
+        }
+
+        /**
+         * For each given node builds and adds an animation that shows the node by fading it in.
+         *
+         * @param duration the fade animation speed
+         */
+        public AbstractBuilder show(Duration duration, Node... nodes) {
+            for (Node node : nodes) {
+                addAnimation(MFXAnimationFactory.FADE_IN.build(node, duration.toMillis()));
+            }
+            return this;
+        }
+
+        /**
+         * Calls {@link #show(Duration, Node...)} by converting the given millis value
+         * with {@link Duration#millis(double)}.
+         */
+        public AbstractBuilder show(double millis, Node... nodes) {
+            return show(Duration.millis(millis), nodes);
+        }
+
+        /**
+         * Creates and adds a fade in animation for each given {@link AnimationsData}.
+         */
+        public final AbstractBuilder show(AnimationsData... data) {
+            for (AnimationsData animData : data) {
+                Animation animation = MFXAnimationFactory.FADE_IN.build(animData.node(), animData.duration().toMillis());
+                animation.setOnFinished(animData.onFinished());
+                addAnimation(animation);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the action to perform when the "main" animation ends.
+         */
+        public AbstractBuilder setOnFinished(EventHandler<ActionEvent> onFinished) {
+            animation.setOnFinished(onFinished);
+            return this;
+        }
+
+        /**
+         * Sets the "main" animation delay.
+         */
+        public AbstractBuilder setDelay(Duration delay) {
+            animation.setDelay(delay);
+            return this;
+        }
+    }
+
+    /**
+     * Implementation of {@link AbstractBuilder} that uses a {@link SequentialTransition} as "main" animation.
+     */
+    public static class SequentialBuilder extends AbstractBuilder {
+        //================================================================================
+        // Properties
+        //================================================================================
+        private final SequentialTransition sequentialTransition = new SequentialTransition();
+
+        //================================================================================
+        // Constructors
+        //================================================================================
+        public SequentialBuilder() {
+            init(sequentialTransition);
+        }
+
+        //================================================================================
+        // Static Methods
+        //================================================================================
+
+        /**
+         * @return a new SequentialBuilder instance. Equivalent to calling the constructor,
+         * it's just a way to omit the new keyword
+         */
+        public static SequentialBuilder build() {
+            return new SequentialBuilder();
+        }
+
+        //================================================================================
+        // Override Methods
+        //================================================================================
+        @Override
+        protected void addAnimation(Animation animation) {
+            sequentialTransition.getChildren().add(animation);
+        }
+
+        @Override
+        public SequentialTransition getAnimation() {
+            return sequentialTransition;
+        }
+    }
+
+    /**
+     * Implementation of {@link AbstractBuilder} that uses a {@link ParallelTransition} as "main" animation.
+     */
+    public static class ParallelBuilder extends AbstractBuilder {
+        //================================================================================
+        // Properties
+        //================================================================================
+        private final ParallelTransition parallelTransition = new ParallelTransition();
+
+        //================================================================================
+        // Constructors
+        //================================================================================
+        public ParallelBuilder() {
+            init(parallelTransition);
+        }
+
+        //================================================================================
+        // Static Methods
+        //================================================================================
+
+        /**
+         * @return a new ParallelBuilder instance. Equivalent to calling the constructor,
+         * it's just a way to omit the new keyword
+         */
+        public static ParallelBuilder build() {
+            return new ParallelBuilder();
+        }
+
+        //================================================================================
+        // Override Methods
+        //================================================================================
+        @Override
+        protected void addAnimation(Animation animation) {
+            parallelTransition.getChildren().add(animation);
+        }
+
+        @Override
+        public ParallelTransition getAnimation() {
+            return parallelTransition;
+        }
+    }
+
+    /**
+     * Builder class to easily create a {@link PauseTransition} with fluent api.
+     */
+    public static class PauseBuilder {
+        //================================================================================
+        // Properties
+        //================================================================================
+        private final PauseTransition pauseTransition = new PauseTransition();
+
+        //================================================================================
+        // Static Methods
+        //================================================================================
+
+        /**
+         * @return a new PauseBuilder instance. Equivalent to calling the constructor,
+         * it's just a way to omit the new keyword
+         */
+        public static PauseBuilder build() {
+            return new PauseBuilder();
+        }
+
+        //================================================================================
+        // Methods
+        //================================================================================
+
+        /**
+         * Sets the pause transition duration.
+         */
+        public PauseBuilder setDuration(Duration value) {
+            pauseTransition.setDuration(value);
+            return this;
+        }
+
+        /**
+         * Calls {@link #setDuration(Duration)} by converting the given millis value
+         * with {@link Duration#millis(double)}.
+         */
+        public PauseBuilder setDuration(double millis) {
+            pauseTransition.setDuration(Duration.millis(millis));
+            return this;
+        }
+
+        /**
+         * Sets the action to perform when the pause transition ends.
+         */
+        public PauseBuilder setOnFinished(EventHandler<ActionEvent> value) {
+            pauseTransition.setOnFinished(value);
+            return this;
+        }
+
+        /**
+         * @return the instance of the PauseTransition
+         */
+        public PauseTransition getAnimation() {
+            return pauseTransition;
+        }
+    }
+}

+ 101 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ExecutionUtils.java

@@ -0,0 +1,101 @@
+package io.github.palexdev.materialfx.utils;
+
+import javafx.application.Platform;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Utils class to help with concurrency and callables.
+ */
+public class ExecutionUtils {
+
+    private static class ThrowableWrapper {
+        Throwable t;
+    }
+
+    /**
+     * Invokes a Runnable on the JavaFX Application Thread and waits for it to finish.
+     *
+     * @param run The Runnable that has to be called on JFX thread.
+     * @throws InterruptedException f the execution is interrupted.
+     * @throws ExecutionException   If a exception is occurred in the run method of the Runnable
+     */
+    public static void runAndWaitEx(final Runnable run)
+            throws InterruptedException, ExecutionException {
+        if (Platform.isFxApplicationThread()) {
+            try {
+                run.run();
+            } catch (Exception e) {
+                throw new ExecutionException(e);
+            }
+        } else {
+            final Lock lock = new ReentrantLock();
+            final Condition condition = lock.newCondition();
+            final ThrowableWrapper throwableWrapper = new ThrowableWrapper();
+            lock.lock();
+            try {
+                Platform.runLater(() -> {
+                    lock.lock();
+                    try {
+                        run.run();
+                    } catch (Throwable e) {
+                        throwableWrapper.t = e;
+                    } finally {
+                        try {
+                            condition.signal();
+                        } finally {
+                            lock.unlock();
+                        }
+                    }
+                });
+                condition.await();
+                if (throwableWrapper.t != null) {
+                    throw new ExecutionException(throwableWrapper.t);
+                }
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Calls {@link #runAndWaitEx(Runnable)} but consumes/ignores any thrown exception.
+     */
+    public static void runAndWait(final Runnable runnable) {
+        try {
+            runAndWaitEx(runnable);
+        } catch (Exception ignored) {
+        }
+    }
+
+    /**
+     * Tries to execute the given callable and prints the stacktrace in case of exception.
+     *
+     * @return the callable result or null in case of exception
+     */
+    public static <V> V tryCallableAndPrint(Callable<V> callable) {
+        try {
+            return callable.call();
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * Tries to execute the given callable but ignores the exception in case of fail.
+     *
+     * @return the callable result or null in case of exception
+     */
+    public static <V> V tryCallableAndIgnore(Callable<V> callable) {
+        try {
+            return callable.call();
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+}

+ 70 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -18,12 +18,16 @@
 
 
 package io.github.palexdev.materialfx.utils;
 package io.github.palexdev.materialfx.utils;
 
 
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
 import javafx.event.Event;
 import javafx.event.Event;
 import javafx.geometry.*;
 import javafx.geometry.*;
 import javafx.scene.Group;
 import javafx.scene.Group;
 import javafx.scene.Node;
 import javafx.scene.Node;
 import javafx.scene.Parent;
 import javafx.scene.Parent;
 import javafx.scene.Scene;
 import javafx.scene.Scene;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
 import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.*;
 import javafx.scene.layout.*;
@@ -38,6 +42,7 @@ import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
+import java.util.concurrent.Callable;
 
 
 /**
 /**
  * Utility class which provides convenience methods for working with Nodes
  * Utility class which provides convenience methods for working with Nodes
@@ -248,13 +253,77 @@ public class NodeUtils {
         for (Node node : parent.getChildrenUnmodifiable()) {
         for (Node node : parent.getChildrenUnmodifiable()) {
             nodes.add(node);
             nodes.add(node);
             if (node instanceof Parent)
             if (node instanceof Parent)
-                addAllDescendents((Parent)node, nodes);
+                addAllDescendents((Parent) node, nodes);
         }
         }
     }
     }
 
 
+    /**
+     * Convenience method to execute a given action after that the given control
+     * has been laid out and its skin is not null anymore.
+     * <p></p>
+     * Note that if the skin is not null the action will be immediately performed and
+     * the listener won't be added to the property.
+     *
+     * @param control        the control to check for skin initialization
+     * @param action         the action to perform when the skin is not null
+     * @param removeListener to specify if the listener added to the skin property
+     *                       should be removed after it is not null anymore.
+     */
+    public static <V> void waitForSkin(Control control, Callable<V> action, boolean removeListener) {
+        if (control.getSkin() != null) {
+            ExecutionUtils.tryCallableAndPrint(action);
+        } else {
+            control.skinProperty().addListener(new ChangeListener<>() {
+                @Override
+                public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
+                    if (newValue != null) {
+                        ExecutionUtils.tryCallableAndPrint(action);
+                        if (removeListener) {
+                            control.skinProperty().removeListener(this);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Convenience method to execute a given action after that the given control
+     * has been laid out and its scene is not null anymore.
+     * <p></p>
+     * Note that if the scene is not null the action will be immediately performed and
+     * the listener won't be added to the property.
+     *
+     * @param control        the control to check for scene initialization
+     * @param action         the action to perform when the scene is not null
+     * @param removeListener to specify if the listener added to the scene property
+     *                       should be removed after it is not null anymore.
+     */
+    public static <V> void waitForScene(Control control, Callable<V> action, boolean removeListener) {
+        if (control.getScene() != null) {
+            ExecutionUtils.tryCallableAndPrint(action);
+        } else {
+            control.sceneProperty().addListener(new ChangeListener<>() {
+                @Override
+                public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
+                    if (newValue != null) {
+                        ExecutionUtils.tryCallableAndPrint(action);
+                        if (removeListener) {
+                            control.sceneProperty().removeListener(this);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    //================================================================================
+    // JavaFX private methods
+    //================================================================================
     /* The following methods are copied from com.sun.javafx.scene.control.skin.Utils class
     /* The following methods are copied from com.sun.javafx.scene.control.skin.Utils class
      * It's a private module, so to avoid adding exports and opens I copied them
      * It's a private module, so to avoid adding exports and opens I copied them
      */
      */
+
     public static double computeXOffset(double width, double contentWidth, HPos hpos) {
     public static double computeXOffset(double width, double contentWidth, HPos hpos) {
         switch (hpos) {
         switch (hpos) {
             case LEFT:
             case LEFT:

+ 345 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java

@@ -0,0 +1,345 @@
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListView;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.controls.flowless.VirtualFlow;
+import javafx.animation.Animation;
+import javafx.animation.Animation.Status;
+import javafx.animation.KeyFrame;
+import javafx.animation.PauseTransition;
+import javafx.animation.Timeline;
+import javafx.event.EventHandler;
+import javafx.geometry.Bounds;
+import javafx.scene.control.Control;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.util.Duration;
+import org.reactfx.value.Var;
+
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class for ScrollPanes and MFXFlowlessListViews.
+ */
+public class ScrollUtils {
+
+    public enum ScrollDirection {
+        UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
+
+        final int intDirection;
+
+        ScrollDirection(int intDirection) {
+            this.intDirection = intDirection;
+        }
+    }
+
+    private ScrollUtils() {
+    }
+
+    /**
+     * Determines if the given ScrollEvent comes from a trackpad.
+     * <p></p>
+     * Although this method works in most cases, it is not very accurate.
+     * Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
+     * we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
+     * and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
+     * (trackpad event) or greater(mouse event).
+     *
+     * @see ScrollEvent#getDeltaX()
+     * @see ScrollEvent#getDeltaY()
+     */
+    public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
+        switch (scrollDirection) {
+            case UP:
+            case DOWN:
+                return Math.abs(event.getDeltaY()) < 10;
+            case LEFT:
+            case RIGHT:
+                return Math.abs(event.getDeltaX()) < 10;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Determines the scroll direction of the given ScrollEvent.
+     * <p></p>
+     * Although this method works fine, it is not very accurate.
+     * In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
+     * you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
+     * <p></p>
+     * For this method to work we assume that this behavior is not possible.
+     * <p></p>
+     * If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
+     * <p>
+     * Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
+     *
+     * @see ScrollEvent#getDeltaX()
+     * @see ScrollEvent#getDeltaY()
+     */
+    public static ScrollDirection determineScrollDirection(ScrollEvent event) {
+        double deltaX = event.getDeltaX();
+        double deltaY = event.getDeltaY();
+
+        if (deltaY == 0.0) {
+            return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
+        } else {
+            return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
+        }
+    }
+
+    //================================================================================
+    // ScrollPanes
+    //================================================================================
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane,
+     * calls {@link #addSmoothScrolling(ScrollPane, double)} with a
+     * default speed value of 1.
+     */
+    public static void addSmoothScrolling(ScrollPane scrollPane) {
+        addSmoothScrolling(scrollPane, 1);
+    }
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
+     * Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
+     * with a default trackPadAdjustment of 7.
+     */
+    public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
+        addSmoothScrolling(scrollPane, speed, 7);
+    }
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane with the given
+     * scroll speed and the given trackPadAdjustment.
+     * <p></p>
+     * The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
+     * This is kind of a workaround and it's not perfect, but at least it's way better than before.
+     * The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
+     */
+    public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
+        smoothScroll(scrollPane, speed, trackPadAdjustment);
+    }
+
+    private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
+        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
+        final double[] derivatives = new double[frictions.length];
+        AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
+
+        Timeline timeline = new Timeline();
+        AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
+        final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
+        final EventHandler<ScrollEvent> scrollHandler = event -> {
+            if (event.getEventType() == ScrollEvent.SCROLL) {
+                scrollDirection.set(determineScrollDirection(event));
+                if (isTrackPad(event, scrollDirection.get())) {
+                    atomicSpeed.set(speed / trackPadAdjustment);
+                } else {
+                    atomicSpeed.set(speed);
+                }
+                derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
+                if (timeline.getStatus() == Status.STOPPED) {
+                    timeline.play();
+                }
+                event.consume();
+            }
+        };
+        if (scrollPane.getContent().getParent() != null) {
+            scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
+            scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
+        }
+        scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
+                oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
+            }
+            if (newValue != null) {
+                newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
+                newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
+            }
+        });
+
+        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
+            for (int i = 0; i < derivatives.length; i++) {
+                derivatives[i] *= frictions[i];
+            }
+            for (int i = 1; i < derivatives.length; i++) {
+                derivatives[i] += derivatives[i - 1];
+            }
+
+            double dy = derivatives[derivatives.length - 1];
+            Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
+            double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
+            double value;
+            switch (scrollDirection.get()) {
+                case LEFT:
+                case RIGHT:
+                    value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
+                    scrollPane.hvalueProperty().set(value);
+                    break;
+                case UP:
+                case DOWN:
+                    value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
+                    scrollPane.vvalueProperty().set(value);
+                    break;
+            }
+
+            if (Math.abs(dy) < 0.001) {
+                timeline.stop();
+            }
+        }));
+        timeline.setCycleCount(Animation.INDEFINITE);
+    }
+
+    /**
+     * Adds a fade in and out effect to the given scroll pane's scroll bars,
+     * calls {@link #animateScrollBars(ScrollPane, double)} with a
+     * default fadeSpeedMillis value of 500.
+     */
+    public static void animateScrollBars(ScrollPane scrollPane) {
+        animateScrollBars(scrollPane, 500);
+    }
+
+    /**
+     * Adds a fade in and out effect to the given scroll pane's scroll bars,
+     * calls {@link #animateScrollBars(ScrollPane, double, double)} with a
+     * default fadeSpeedMillis value of 500 and a default hideAfterMillis value of 800.
+     */
+    public static void animateScrollBars(ScrollPane scrollPane, double fadeSpeedMillis) {
+        animateScrollBars(scrollPane, fadeSpeedMillis, 800);
+    }
+
+    /**
+     * Adds a fade in and out effect to the given scroll pane's scroll bars
+     * with the given fadeSpeedMillis and hideAfterMillis values.
+     *
+     * @see NodeUtils#waitForSkin(Control, Callable, boolean)
+     */
+    public static void animateScrollBars(ScrollPane scrollPane, double fadeSpeedMillis, double hideAfterMillis) {
+        NodeUtils.waitForSkin(scrollPane, () -> {
+            Set<ScrollBar> scrollBars = scrollPane.lookupAll(".scroll-bar").stream()
+                    .filter(node -> node instanceof ScrollBar)
+                    .map(node -> (ScrollBar) node)
+                    .collect(Collectors.toSet());
+            scrollBars.forEach(scrollBar -> {
+                scrollBar.setOpacity(0.0);
+                scrollPane.hoverProperty().addListener((observable, oldValue, newValue) -> {
+                    if (!scrollBar.isVisible()) {
+                        return;
+                    }
+
+                    if (newValue) {
+                        MFXAnimationFactory.FADE_IN.build(scrollBar, fadeSpeedMillis).play();
+                    } else {
+                        PauseTransition pauseTransition = new PauseTransition(Duration.millis(hideAfterMillis));
+                        pauseTransition.setOnFinished(event -> MFXAnimationFactory.FADE_OUT.build(scrollBar, fadeSpeedMillis).play());
+                        pauseTransition.play();
+                    }
+                });
+            });
+            return null;
+        }, true);
+    }
+
+    //================================================================================
+    // ListViews
+    //================================================================================
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane,
+     * calls {@link #addSmoothScrolling(ScrollPane, double)} with a
+     * default speed value of 1.
+     */
+    public static void addSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView) {
+        addSmoothScrolling(listView, 1);
+    }
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
+     * Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
+     * with a default trackPadAdjustment of 7.
+     */
+    public static void addSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView, double speed) {
+        addSmoothScrolling(listView, speed, 7);
+    }
+
+    /**
+     * Adds a smooth scrolling effect to the given scroll pane with the given
+     * scroll speed and the given trackPadAdjustment.
+     * <p></p>
+     * The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
+     * This is kind of a workaround and it's not perfect, but at least it's way better than before.
+     * The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
+     */
+    public static void addSmoothScrolling(AbstractMFXFlowlessListView<?, ?, ?> listView, double speed, double trackPadAdjustment) {
+        NodeUtils.waitForSkin(listView, () -> {
+            VirtualFlow<?, ?> flow = (VirtualFlow<?, ?>) listView.lookup(".virtual-flow");
+            smoothScroll(flow, speed, trackPadAdjustment);
+            return null;
+        }, true);
+    }
+
+    private static void smoothScroll(VirtualFlow<?, ?> flow, double speed, double trackPadAdjustment) {
+        final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
+        final double[] derivatives = new double[frictions.length];
+        AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
+
+        Timeline timeline = new Timeline();
+        AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
+        final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
+        final EventHandler<ScrollEvent> scrollHandler = event -> {
+            if (event.getEventType() == ScrollEvent.SCROLL) {
+                scrollDirection.set(determineScrollDirection(event));
+                if (isTrackPad(event, scrollDirection.get())) {
+                    atomicSpeed.set(speed / trackPadAdjustment);
+                } else {
+                    atomicSpeed.set(speed);
+                }
+                derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
+                if (timeline.getStatus() == Animation.Status.STOPPED) {
+                    timeline.play();
+                }
+                event.consume();
+            }
+        };
+
+        if (flow.getParent() != null) {
+            flow.getParent().addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
+        }
+        flow.parentProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                oldValue.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
+            }
+            if (newValue != null) {
+                newValue.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
+            }
+        });
+        flow.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
+        flow.addEventFilter(ScrollEvent.ANY, scrollHandler);
+
+        timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
+            for (int i = 0; i < derivatives.length; i++) {
+                derivatives[i] *= frictions[i];
+            }
+            for (int i = 1; i < derivatives.length; i++) {
+                derivatives[i] += derivatives[i - 1];
+            }
+
+            double dy = derivatives[derivatives.length - 1];
+            Var<Double> scrollVar = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? flow.estimatedScrollYProperty() : flow.estimatedScrollXProperty();
+            scrollVar.setValue(scrollVar.getValue() + dy);
+
+            if (Math.abs(dy) < 0.001) {
+                timeline.stop();
+            }
+        }));
+        timeline.setCycleCount(Animation.INDEFINITE);
+    }
+}