Преглед изворни кода

:boom: MFXVLoader and MFXHLoader have been removed in favor of MFXLoader

The idea is to make the loading mechanism independent of UI. The previous architecture forced the user to create a VBox/HBox. With the new design the user can decide not only where to put the loaded views and how to manage them, but he can also specify per each view how to convert the loaded bean to a Node, which still makes fairly easy to create nav-bars/dashboards (see new DemoController once published for an example)

Controls Package
:recycle: MFXCheckListView, MFXListView, use constructor for building the virtual flow since latest VirtualizedFX version deprecated the Builder class

Skins Package
:recycle: MFXComboBoxSkin, MFXFilterComboBoxSkin, use constructor for building the virtual flow since latest VirtualizedFX version deprecated the Builder class
:recycle: MFXRectangleToggleNodeSkin, better handle mouse events

Utils Package
:truck: Moved some classes to new specific packages, to keep the project more organized
:fire: NodeUtils, removed all methods related to JavaFX's popups since not needed anymore, plus some API utilized will be deprecated in future Java releases

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>
palexdev пре 3 година
родитељ
комит
cd992995b7
23 измењених фајлова са 531 додато и 1268 уклоњено
  1. 0 128
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXLoaderBean.java
  2. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckListView.java
  3. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXDatePicker.java
  4. 0 353
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXHLoader.java
  5. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java
  6. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java
  7. 0 354
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXVLoader.java
  8. 5 5
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/LoaderCacheLevel.java
  9. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  10. 3 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java
  11. 13 28
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRectangleToggleNodeSkin.java
  12. 15 17
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoaderUtils.java
  13. 7 365
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java
  14. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/DateStringConverter.java
  15. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/DayOfWeekStringConverter.java
  16. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/MonthStringConverter.java
  17. 282 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/loader/MFXLoader.java
  18. 187 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/loader/MFXLoaderBean.java
  19. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/OnChanged.java
  20. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/OnInvalidated.java
  21. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/When.java
  22. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXValidator.java
  23. 3 0
      materialfx/src/main/java/module-info.java

+ 0 - 128
materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXLoaderBean.java

@@ -1,128 +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.beans;
-
-import javafx.scene.Parent;
-import javafx.scene.control.ToggleButton;
-import javafx.util.Callback;
-
-import java.net.URL;
-
-/**
- * Support bean for {@code MFXHLoader} and {@code MFXVLoader}.
- * <p>
- * Basically a wrapper for a {@code Node} which is the root of an fxml file,
- * the controller factory of the fxml file (optional), the toggle button associated with the item
- * which is responsible for the views switching, the {@code URL} of the fxml file,
- * and a flag which specified if this is the view to be shown when loading is finished.
- * <p></p>
- * It is <b>highly recommended</b> to use the {@link Builder} class to create a bean.
- */
-public class MFXLoaderBean {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private Parent root;
-    private final boolean defaultRoot;
-    private final Callback<Class<?>, Object> controllerFactory;
-    private final ToggleButton button;
-    private final URL fxmlURL;
-
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXLoaderBean(ToggleButton button, URL fxmlURL, boolean defaultRoot) {
-        this(button, fxmlURL, defaultRoot, null);
-    }
-
-    public MFXLoaderBean(ToggleButton button, URL fxmlURL, boolean defaultRoot, Callback<Class<?>, Object> controllerFactory) {
-        this.button = button;
-        this.fxmlURL = fxmlURL;
-        this.defaultRoot = defaultRoot;
-        this.controllerFactory = controllerFactory;
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    public Parent getRoot() {
-        return root;
-    }
-
-    public boolean isDefault() {
-        return defaultRoot;
-    }
-
-    public Callback<Class<?>, Object> getControllerFactory() {
-        return controllerFactory;
-    }
-
-    public void setRoot(Parent root) {
-        this.root = root;
-    }
-
-    public ToggleButton getButton() {
-        return button;
-    }
-
-    public URL getFxmlURL() {
-        return fxmlURL;
-    }
-
-    /**
-     * Utils class that facilitates the creation of {@code MFXLoaderBeans} with fluent api.
-     */
-    public static class Builder {
-        private final URL fxmlURL;
-        private final ToggleButton button;
-        private boolean defaultRoot;
-        private Callback<Class<?>, Object> controllerFactory;
-
-        private Builder(ToggleButton button, URL fxmlURL) {
-            this.fxmlURL = fxmlURL;
-            this.button = button;
-        }
-
-        /**
-         * Convenience static method to avoid using the new keyword.
-         *
-         * @return a new {@code Builder} instance with the specified parameters
-         */
-        public static Builder build(ToggleButton button, URL fxmlURL) {
-            return new Builder(button, fxmlURL);
-        }
-
-        public Builder setDefaultRoot(boolean defaultRoot) {
-            this.defaultRoot = defaultRoot;
-            return this;
-        }
-
-        public Builder setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
-            this.controllerFactory = controllerFactory;
-            return this;
-        }
-
-        /**
-         * @return the new {@code MFXLoaderBean} instance
-         */
-        public MFXLoaderBean get() {
-            return new MFXLoaderBean(button, fxmlURL, defaultRoot, controllerFactory);
-        }
-    }
-}

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

@@ -71,7 +71,7 @@ public class MFXCheckListView<T> extends AbstractMFXListView<T, MFXCheckListCell
     // Constructors
     //================================================================================
     public MFXCheckListView() {
-        virtualFlow = SimpleVirtualFlow.Builder.create(
+        virtualFlow = new SimpleVirtualFlow<>(
                 itemsProperty(),
                 null,
                 Orientation.VERTICAL
@@ -81,7 +81,7 @@ public class MFXCheckListView<T> extends AbstractMFXListView<T, MFXCheckListCell
 
     public MFXCheckListView(ObservableList<T> items) {
         super(items);
-        virtualFlow = SimpleVirtualFlow.Builder.create(
+        virtualFlow = new SimpleVirtualFlow<>(
                 itemsProperty(),
                 null,
                 Orientation.VERTICAL

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

@@ -15,10 +15,10 @@ import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.skins.MFXDatePickerSkin;
 import io.github.palexdev.materialfx.utils.DateTimeUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
-import io.github.palexdev.materialfx.utils.others.DateStringConverter;
-import io.github.palexdev.materialfx.utils.others.DayOfWeekStringConverter;
-import io.github.palexdev.materialfx.utils.others.MonthStringConverter;
 import io.github.palexdev.materialfx.utils.others.ReusableScheduledExecutor;
+import io.github.palexdev.materialfx.utils.others.dates.DateStringConverter;
+import io.github.palexdev.materialfx.utils.others.dates.DayOfWeekStringConverter;
+import io.github.palexdev.materialfx.utils.others.dates.MonthStringConverter;
 import javafx.beans.property.*;
 import javafx.css.PseudoClass;
 import javafx.geometry.HPos;

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

@@ -1,353 +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.beans.MFXLoaderBean;
-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;
-import javafx.beans.property.*;
-import javafx.fxml.FXMLLoader;
-import javafx.geometry.Pos;
-import javafx.scene.CacheHint;
-import javafx.scene.Parent;
-import javafx.scene.Scene;
-import javafx.scene.control.ToggleGroup;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Pane;
-import javafx.scene.layout.Region;
-import javafx.scene.layout.StackPane;
-
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-/**
- * Convenience class for creating dashboards, no more hassle on managing multiple views.
- * <p>
- * This control extends {@code HBox} and relies on {@link LoaderUtils} for loading fxml files in the background
- * leaving the UI responsive.
- * <p></p>
- * Once everything is set up and the fxml files have been added with the various {@code addItem} methods
- * to start loading the views invoke the {@link #start()} method. That method then will get all the
- * {@code MFXLoaderBeans} in the views map and for each of them checks if the root has not been loaded yet,
- * creates the load callable by calling {@link #buildLoadCallable(MFXLoaderBean)} and submits it to the executor in
- * {@link LoaderUtils}.
- * <p></p>
- * Once everything is loaded: {@link ToggleButtonsUtil#addAlwaysOneSelectedSupport(ToggleGroup)} is called, the loaded
- * toggles are added to the children list and {@link #setDefault()} is called.
- *
- * <b>NOTE: the cache level must be set before invoking the {@link #start()} method.</b>
- * <p>
- * By default it is set to: {@link LoaderCacheLevel#SCENE_CACHE}
- *
- * @see LoaderUtils
- * @see MFXLoaderBean
- * @see ToggleButtonsUtil
- */
-public class MFXHLoader extends HBox {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private final String STYLE_CLASS = "mfx-hloader";
-    private Pane contentPane;
-    private final ToggleGroup toggleGroup;
-
-    private final BooleanProperty animated = new SimpleBooleanProperty(false);
-    private final DoubleProperty animationMillis = new SimpleDoubleProperty(800);
-    private MFXAnimationFactory animationType = MFXAnimationFactory.FADE_IN;
-
-    private final IntegerProperty loadedItems = new SimpleIntegerProperty(0);
-    private final Map<String, MFXLoaderBean> idViewMap;
-    private Supplier<FXMLLoader> fxmlLoaderSupplier;
-
-    private LoaderCacheLevel cacheLevel = LoaderCacheLevel.SCENE_CACHE;
-
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXHLoader() {
-        this(null);
-    }
-
-    public MFXHLoader(Pane contentPane) {
-        this(contentPane, null);
-    }
-
-    public MFXHLoader(Pane contentPane, Supplier<FXMLLoader> fxmlLoaderSupplier) {
-        this.setPrefSize(Region.USE_COMPUTED_SIZE, 60);
-        this.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
-        this.setSpacing(20);
-        this.setAlignment(Pos.CENTER);
-
-        this.contentPane = contentPane;
-        this.toggleGroup = new ToggleGroup();
-
-        this.idViewMap = new LinkedHashMap<>();
-
-        this.fxmlLoaderSupplier = fxmlLoaderSupplier;
-        initialize();
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-        addListeners();
-    }
-
-    /**
-     * Adds a listener to loadedItems to check when all the views have been loaded.
-     * Then: calls {@link ToggleButtonsUtil#addAlwaysOneSelectedSupport(ToggleGroup)},
-     * adds all the toggles to the children list, calls {@link #setDefault()}
-     */
-    private void addListeners() {
-        loadedItems.addListener((observable, oldValue, newValue) -> {
-            if (newValue.intValue() == idViewMap.size()) {
-                ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);
-                getChildren().setAll(idViewMap.values().stream()
-                        .map(MFXLoaderBean::getButton)
-                        .collect(Collectors.toList())
-                );
-                setDefault();
-            }
-        });
-    }
-
-    /**
-     * Gets the built {@code MFXLoaderBean} from the builder,
-     * adds the toggle to the loader's toggle group and puts
-     * the bean in the {@code idViewMap} with a key generated by
-     * {@link LoaderUtils#generateKey(URL)}.
-     */
-    public void addItem(MFXLoaderBean.Builder builder) {
-        MFXLoaderBean loaderBean = builder.get();
-        LoaderUtils.checkFxmlFile(loaderBean.getFxmlURL());
-        loaderBean.getButton().setToggleGroup(toggleGroup);
-        idViewMap.put(LoaderUtils.generateKey(loaderBean.getFxmlURL()), loaderBean);
-    }
-
-    /**
-     * Gets the built {@code MFXLoaderBean} from the builder,
-     * adds the toggle to the loader's toggle group and puts
-     * the bean in the {@code idViewMap} with the specified key.
-     */
-    public void addItem(String key, MFXLoaderBean.Builder builder) {
-        MFXLoaderBean loaderBean = builder.get();
-        LoaderUtils.checkFxmlFile(loaderBean.getFxmlURL());
-        loaderBean.getButton().setToggleGroup(toggleGroup);
-        idViewMap.put(key, loaderBean);
-    }
-
-    /**
-     * Checks if there is a {@code MFXLoaderBean} which has the defaultRoot flag set to true
-     * and sets the bean's toggle state to selected so that the view is shown, this is handled on
-     * the JavaFX's thread.
-     */
-    private void setDefault() {
-        idViewMap.values().stream()
-                .filter(MFXLoaderBean::isDefault)
-                .findFirst().ifPresent(loaderBean -> Platform.runLater(() -> loaderBean.getButton().setSelected(true)));
-    }
-
-    /**
-     * Starts the loading process.
-     * <p></p>
-     * Retrieves the {@code MFXLoaderBeans} in the idViewMap, for each of them
-     * checks if the node has been already loaded, if not then calls {@link #load(MFXLoaderBean)}
-     *
-     * @see MFXLoaderBean
-     */
-    public void start() {
-        if (contentPane == null) {
-            throw new NullPointerException("Content pane has not been set!");
-        }
-
-        List<MFXLoaderBean> loaderBeans = new ArrayList<>(idViewMap.values());
-        for (MFXLoaderBean loaderBean : loaderBeans) {
-            if (loaderBean.getRoot() == null) {
-                load(loaderBean);
-            }
-        }
-    }
-
-    /**
-     * Loads the root node of a {@link MFXLoaderBean}. The load process depends
-     * on the set cache level. If the level is set to {@link LoaderCacheLevel#NONE}
-     * the load task built by {@link #buildLoadCallable(MFXLoaderBean)} is submitted
-     * to the loader executor {@link LoaderUtils#submit(Callable)}.
-     * In the other cases the task is submitted to the executor but the {@link Future#get()} method
-     * is invoked which causes the thread to wait until the fxml is loaded.
-     *
-     * @see #cacheParent(Parent)
-     */
-    private void load(MFXLoaderBean loaderBean) {
-        if (cacheLevel != LoaderCacheLevel.NONE) {
-            try {
-                Parent loaded = LoaderUtils.submit(buildLoadCallable(loaderBean)).get();
-                cacheParent(loaded);
-                loadedItems.set(loadedItems.get() + 1);
-            } catch (InterruptedException | ExecutionException ex) {
-                loadedItems.set(loadedItems.get() + 1);
-                ex.printStackTrace();
-            }
-        } else {
-            LoaderUtils.submit(buildLoadCallable(loaderBean));
-            loadedItems.set(loadedItems.get() + 1);
-        }
-    }
-
-    /**
-     * Called if the cache level is not set to {@link LoaderCacheLevel#NONE}.
-     *
-     * @see LoaderCacheLevel
-     */
-    private void cacheParent(Parent parent) {
-        if (cacheLevel == LoaderCacheLevel.SCENE_JAVAFX_CACHE) {
-            parent.setCache(true);
-            parent.setCacheHint(CacheHint.SPEED);
-        }
-
-        StackPane pane = new StackPane();
-        pane.getChildren().setAll(parent);
-        Scene scene = new Scene(pane);
-        pane.applyCss();
-        pane.layout();
-    }
-
-    /**
-     * Builds the callable which loads the fxmlFile with {@link LoaderUtils#fxmlLoad(FXMLLoader, MFXLoaderBean)}
-     * or {@link LoaderUtils#fxmlLoad(MFXLoaderBean)} if the FXMLLoader supplier is null.
-     * When the file is loaded adds a listener to the toggle's selected property to handle the view switching.
-     */
-    private Callable<Parent> buildLoadCallable(MFXLoaderBean loaderBean) {
-        return () -> {
-            Parent root;
-            if (fxmlLoaderSupplier != null) {
-                root = LoaderUtils.fxmlLoad(fxmlLoaderSupplier.get(), loaderBean);
-            } else {
-                root = LoaderUtils.fxmlLoad(loaderBean);
-            }
-            loaderBean.setRoot(root);
-
-            loaderBean.getButton().selectedProperty().addListener((observable, oldValue, newValue) -> {
-                if (isAnimated()) {
-                    animationType.build(loaderBean.getRoot(), animationMillis.doubleValue()).play();
-                }
-                if (newValue) {
-                    contentPane.getChildren().setAll(loaderBean.getRoot());
-                }
-            });
-            return root;
-        };
-    }
-
-    /**
-     * @return the {@code MFXLoaderBean} to which the specified key is mapped,
-     * or null if this map contains no mapping for the key.
-     */
-    public MFXLoaderBean getLoadItem(String key) {
-        return this.idViewMap.get(key);
-    }
-
-    /**
-     * @return the pane on which the views are switched.
-     */
-    public Pane getContentPane() {
-        return contentPane;
-    }
-
-    /**
-     * Sets the pane on which the views are switched.
-     */
-    public void setContentPane(Pane contentPane) {
-        this.contentPane = contentPane;
-    }
-
-    /**
-     * Sets the fxmlLoaderSupplier to the given parameter.
-     * <p>
-     * <b>NOTICE: this method won't do anything if the fxmlLoaderSupplier is not null.
-     * This method is intended to be used when the loader is used in SceneBuilder which requires a no-arg constructor.</b>
-     */
-    public void setFxmlLoaderSupplier(Supplier<FXMLLoader> fxmlLoaderSupplier) {
-        if (this.fxmlLoaderSupplier == null) {
-            this.fxmlLoaderSupplier = fxmlLoaderSupplier;
-        }
-    }
-
-    public boolean isAnimated() {
-        return animated.get();
-    }
-
-    /**
-     * Specifies if the view switching is animated.
-     */
-    public BooleanProperty animatedProperty() {
-        return animated;
-    }
-
-    public void setAnimated(boolean animated) {
-        this.animated.set(animated);
-    }
-
-    public double getAnimationMillis() {
-        return animationMillis.get();
-    }
-
-    /**
-     * Specified the switch animation duration.
-     */
-    public DoubleProperty animationMillisProperty() {
-        return animationMillis;
-    }
-
-    public void setAnimationMillis(double animationMillis) {
-        this.animationMillis.set(animationMillis);
-    }
-
-    public MFXAnimationFactory getAnimationType() {
-        return animationType;
-    }
-
-    /**
-     * Sets the switch animation type.
-     */
-    public void setAnimationType(MFXAnimationFactory animationType) {
-        this.animationType = animationType;
-    }
-
-    public LoaderCacheLevel getCacheLevel() {
-        return cacheLevel;
-    }
-
-    public void setCacheLevel(LoaderCacheLevel cacheLevel) {
-        this.cacheLevel = cacheLevel;
-    }
-}

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

@@ -71,7 +71,7 @@ public class MFXListView<T> extends AbstractMFXListView<T, MFXListCell<T>> {
     // Constructors
     //================================================================================
     public MFXListView() {
-        virtualFlow = SimpleVirtualFlow.Builder.create(
+        virtualFlow = new SimpleVirtualFlow<>(
                 itemsProperty(),
                 null,
                 Orientation.VERTICAL
@@ -81,7 +81,7 @@ public class MFXListView<T> extends AbstractMFXListView<T, MFXListCell<T>> {
 
     public MFXListView(ObservableList<T> items) {
         super(items);
-        virtualFlow = SimpleVirtualFlow.Builder.create(
+        virtualFlow = new SimpleVirtualFlow<>(
                 itemsProperty(),
                 null,
                 Orientation.VERTICAL

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

@@ -10,7 +10,7 @@ import io.github.palexdev.materialfx.selection.MultipleSelectionModel;
 import io.github.palexdev.materialfx.selection.base.IMultipleSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXTableViewSkin;
 import io.github.palexdev.materialfx.utils.ListChangeProcessor;
-import io.github.palexdev.materialfx.utils.others.When;
+import io.github.palexdev.materialfx.utils.others.observables.When;
 import io.github.palexdev.virtualizedfx.beans.NumberRange;
 import io.github.palexdev.virtualizedfx.flow.simple.SimpleVirtualFlow;
 import io.github.palexdev.virtualizedfx.utils.ListChangeHelper;

+ 0 - 354
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXVLoader.java

@@ -1,354 +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.beans.MFXLoaderBean;
-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;
-import javafx.beans.property.*;
-import javafx.fxml.FXMLLoader;
-import javafx.geometry.Pos;
-import javafx.scene.CacheHint;
-import javafx.scene.Parent;
-import javafx.scene.Scene;
-import javafx.scene.control.ToggleGroup;
-import javafx.scene.layout.Pane;
-import javafx.scene.layout.Region;
-import javafx.scene.layout.StackPane;
-import javafx.scene.layout.VBox;
-
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-/**
- * Convenience class for creating dashboards, no more hassle on managing multiple views.
- * <p>
- * This control extends {@code VBox} and relies on {@link LoaderUtils} for loading fxml files in the background
- * leaving the UI responsive.
- * <p></p>
- * Once everything is set up and the fxml files have been added with the various {@code addItem} methods
- * to start loading the views invoke the {@link #start()} method. That method then will get all the
- * {@code MFXLoaderBean} in the views map and for each of them checks if the root has not been loaded yet,
- * creates the load callable by calling {@link #buildLoadCallable(MFXLoaderBean)} and submits it to the executor in
- * {@link LoaderUtils}.
- * <p></p>
- * Once everything is loaded: {@link ToggleButtonsUtil#addAlwaysOneSelectedSupport(ToggleGroup)} is called, the loaded
- * toggles are added to the children list and {@link #setDefault()} is called.
- * <p></p>
- *
- * <b>NOTE: the cache level must be set before invoking the {@link #start()} method.</b>
- * <p>
- * By default it is set to: {@link LoaderCacheLevel#SCENE_CACHE}
- *
- * @see LoaderUtils
- * @see MFXLoaderBean
- * @see ToggleButtonsUtil
- */
-public class MFXVLoader extends VBox {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private final String STYLE_CLASS = "mfx-vloader";
-    private Pane contentPane;
-    private final ToggleGroup toggleGroup;
-
-    private final BooleanProperty animated = new SimpleBooleanProperty(false);
-    private final DoubleProperty animationMillis = new SimpleDoubleProperty(800);
-    private MFXAnimationFactory animationType = MFXAnimationFactory.FADE_IN;
-
-    private final IntegerProperty loadedItems = new SimpleIntegerProperty(0);
-    private final Map<String, MFXLoaderBean> idViewMap;
-    private Supplier<FXMLLoader> fxmlLoaderSupplier;
-
-    private LoaderCacheLevel cacheLevel = LoaderCacheLevel.SCENE_CACHE;
-
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXVLoader() {
-        this(null);
-    }
-
-    public MFXVLoader(Pane contentPane) {
-        this(contentPane, null);
-    }
-
-    public MFXVLoader(Pane contentPane, Supplier<FXMLLoader> fxmlLoaderSupplier) {
-        this.setPrefSize(Region.USE_COMPUTED_SIZE, 60);
-        this.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
-        this.setSpacing(10);
-        this.setAlignment(Pos.CENTER);
-
-        this.contentPane = contentPane;
-        this.toggleGroup = new ToggleGroup();
-
-        this.idViewMap = new LinkedHashMap<>();
-
-        this.fxmlLoaderSupplier = fxmlLoaderSupplier;
-        initialize();
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-        addListeners();
-    }
-
-    /**
-     * Adds a listener to loadedItems to check when all the views have been loaded.
-     * Then: calls {@link ToggleButtonsUtil#addAlwaysOneSelectedSupport(ToggleGroup)},
-     * adds all the toggles to the children list, calls {@link #setDefault()}
-     */
-    private void addListeners() {
-        loadedItems.addListener((observable, oldValue, newValue) -> {
-            if (newValue.intValue() == idViewMap.size()) {
-                ToggleButtonsUtil.addAlwaysOneSelectedSupport(toggleGroup);
-                getChildren().setAll(idViewMap.values().stream()
-                        .map(MFXLoaderBean::getButton)
-                        .collect(Collectors.toList())
-                );
-                setDefault();
-            }
-        });
-    }
-
-    /**
-     * Gets the built {@code MFXLoaderBean} from the builder,
-     * adds the toggle to the loader's toggle group and puts
-     * the bean in the {@code idViewMap} with a key generated by
-     * {@link LoaderUtils#generateKey(URL)}.
-     */
-    public void addItem(MFXLoaderBean.Builder builder) {
-        MFXLoaderBean loaderBean = builder.get();
-        LoaderUtils.checkFxmlFile(loaderBean.getFxmlURL());
-        loaderBean.getButton().setToggleGroup(toggleGroup);
-        idViewMap.put(LoaderUtils.generateKey(loaderBean.getFxmlURL()), loaderBean);
-    }
-
-    /**
-     * Gets the built {@code MFXLoaderBean} from the builder,
-     * adds the toggle to the loader's toggle group and puts
-     * the bean in the {@code idViewMap} with the specified key.
-     */
-    public void addItem(String key, MFXLoaderBean.Builder builder) {
-        MFXLoaderBean loaderBean = builder.get();
-        LoaderUtils.checkFxmlFile(loaderBean.getFxmlURL());
-        loaderBean.getButton().setToggleGroup(toggleGroup);
-        idViewMap.put(key, loaderBean);
-    }
-
-    /**
-     * Checks if there is a {@code MFXLoaderBean} which has the defaultRoot flag set to true
-     * and sets the bean's toggle state to selected so that the view is shown, this is handled on
-     * the JavaFX's thread.
-     */
-    private void setDefault() {
-        idViewMap.values().stream()
-                .filter(MFXLoaderBean::isDefault)
-                .findFirst().ifPresent(loaderBean -> Platform.runLater(() -> loaderBean.getButton().setSelected(true)));
-    }
-
-    /**
-     * Starts the loading process.
-     * <p></p>
-     * Retrieves the {@code MFXLoaderBeans} in the idViewMap, for each of them
-     * checks if the node has been already loaded, if not then calls {@link #load(MFXLoaderBean)}
-     *
-     * @see MFXLoaderBean
-     */
-    public void start() {
-        if (contentPane == null) {
-            throw new NullPointerException("Content pane has not been set!");
-        }
-
-        List<MFXLoaderBean> loaderBeans = new ArrayList<>(idViewMap.values());
-        for (MFXLoaderBean loaderBean : loaderBeans) {
-            if (loaderBean.getRoot() == null) {
-                load(loaderBean);
-            }
-        }
-    }
-
-    /**
-     * Loads the root node of a {@link MFXLoaderBean}. The load process depends
-     * on the set cache level. If the level is set to {@link LoaderCacheLevel#NONE}
-     * the load task built by {@link #buildLoadCallable(MFXLoaderBean)} is submitted
-     * to the loader executor {@link LoaderUtils#submit(Callable)}.
-     * In the other cases the task is submitted to the executor but the {@link Future#get()} method
-     * is invoked which causes the thread to wait until the fxml is loaded.
-     *
-     * @see #cacheParent(Parent)
-     */
-    private void load(MFXLoaderBean loaderBean) {
-        if (cacheLevel != LoaderCacheLevel.NONE) {
-            try {
-                Parent loaded = LoaderUtils.submit(buildLoadCallable(loaderBean)).get();
-                cacheParent(loaded);
-                loadedItems.set(loadedItems.get() + 1);
-            } catch (InterruptedException | ExecutionException ex) {
-                loadedItems.set(loadedItems.get() + 1);
-                ex.printStackTrace();
-            }
-        } else {
-            LoaderUtils.submit(buildLoadCallable(loaderBean));
-            loadedItems.set(loadedItems.get() + 1);
-        }
-    }
-
-    /**
-     * Called if the cache level is not set to {@link LoaderCacheLevel#NONE}.
-     *
-     * @see LoaderCacheLevel
-     */
-    private void cacheParent(Parent parent) {
-        if (cacheLevel == LoaderCacheLevel.SCENE_JAVAFX_CACHE) {
-            parent.setCache(true);
-            parent.setCacheHint(CacheHint.SPEED);
-        }
-
-        StackPane pane = new StackPane();
-        pane.getChildren().setAll(parent);
-        Scene scene = new Scene(pane);
-        pane.applyCss();
-        pane.layout();
-    }
-
-    /**
-     * Builds the callable which loads the fxmlFile with {@link LoaderUtils#fxmlLoad(FXMLLoader, MFXLoaderBean)}
-     * or {@link LoaderUtils#fxmlLoad(MFXLoaderBean)} if the FXMLLoader supplier is null.
-     * When the file is loaded adds a listener to the toggle's selected property to handle the view switching.
-     */
-    private Callable<Parent> buildLoadCallable(MFXLoaderBean loaderBean) {
-        return () -> {
-            Parent root;
-            if (fxmlLoaderSupplier != null) {
-                root = LoaderUtils.fxmlLoad(fxmlLoaderSupplier.get(), loaderBean);
-            } else {
-                root = LoaderUtils.fxmlLoad(loaderBean);
-            }
-            loaderBean.setRoot(root);
-
-            loaderBean.getButton().selectedProperty().addListener((observable, oldValue, newValue) -> {
-                if (isAnimated()) {
-                    animationType.build(loaderBean.getRoot(), animationMillis.doubleValue()).play();
-                }
-                if (newValue) {
-                        contentPane.getChildren().setAll(loaderBean.getRoot());
-                }
-            });
-            return root;
-        };
-    }
-
-    /**
-     * @return the {@code MFXLoaderBean} to which the specified key is mapped,
-     * or null if this map contains no mapping for the key.
-     */
-    public MFXLoaderBean getLoadItem(String key) {
-        return this.idViewMap.get(key);
-    }
-
-    /**
-     * @return the pane on which the views are switched.
-     */
-    public Pane getContentPane() {
-        return contentPane;
-    }
-
-    /**
-     * Sets the pane on which the views are switched.
-     */
-    public void setContentPane(Pane contentPane) {
-        this.contentPane = contentPane;
-    }
-
-    /**
-     * Sets the fxmlLoaderSupplier to the given parameter.
-     * <p>
-     * <b>NOTICE: this method won't do anything if the fxmlLoaderSupplier is not null.
-     * This method is intended to be used when the loader is used in SceneBuilder which requires a no-arg constructor.</b>
-     */
-    public void setFxmlLoaderSupplier(Supplier<FXMLLoader> fxmlLoaderSupplier) {
-        if (this.fxmlLoaderSupplier == null) {
-            this.fxmlLoaderSupplier = fxmlLoaderSupplier;
-        }
-    }
-
-    public boolean isAnimated() {
-        return animated.get();
-    }
-
-    /**
-     * Specifies if the view switching is animated.
-     */
-    public BooleanProperty animatedProperty() {
-        return animated;
-    }
-
-    public void setAnimated(boolean animated) {
-        this.animated.set(animated);
-    }
-
-    public double getAnimationMillis() {
-        return animationMillis.get();
-    }
-
-    /**
-     * Specified the switch animation duration.
-     */
-    public DoubleProperty animationMillisProperty() {
-        return animationMillis;
-    }
-
-    public void setAnimationMillis(double animationMillis) {
-        this.animationMillis.set(animationMillis);
-    }
-
-    public MFXAnimationFactory getAnimationType() {
-        return animationType;
-    }
-
-    /**
-     * Sets the switch animation type.
-     */
-    public void setAnimationType(MFXAnimationFactory animationType) {
-        this.animationType = animationType;
-    }
-
-    public LoaderCacheLevel getCacheLevel() {
-        return cacheLevel;
-    }
-
-    public void setCacheLevel(LoaderCacheLevel cacheLevel) {
-        this.cacheLevel = cacheLevel;
-    }
-}

+ 5 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/enums/LoaderCacheLevel.java

@@ -18,14 +18,14 @@
 
 package io.github.palexdev.materialfx.enums;
 
-import io.github.palexdev.materialfx.controls.MFXHLoader;
-import io.github.palexdev.materialfx.controls.MFXVLoader;
+import io.github.palexdev.materialfx.utils.others.loader.MFXLoader;
 import javafx.scene.Parent;
 import javafx.scene.Scene;
 
 /**
- * Enumerator to define the level of caching used by {@link MFXHLoader}
- * and {@link MFXVLoader}. By enabling cache the switch performance vastly improves
+ * Enumerator to define the level of caching used by {@link MFXLoader}.
+ * <p>
+ * By enabling cache the switch performance vastly improves
  * but the views cannot be loaded in parallel (quite acceptable loss in some cases).
  */
 public enum LoaderCacheLevel {
@@ -46,7 +46,7 @@ public enum LoaderCacheLevel {
     /**
      * Does what SCENE_CACHE does, plus sets the JavaFX's properties cache to true
      * and the cache hint to SPEED on the loaded root node.
-     * (To be honest I don't know if this truly improves performance since I didn't notice any effective improvement)
+     * (To be honest I don't know if this truly improves performance since I didn't notice anything notable)
      */
     SCENE_JAVAFX_CACHE
 }

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

@@ -212,7 +212,7 @@ public class MFXComboBoxSkin<T> extends MFXTextFieldSkin {
 	protected Node createPopupContent() {
 		MFXComboBox<T> comboBox = getComboBox();
 		if (virtualFlow == null) {
-			virtualFlow = SimpleVirtualFlow.Builder.create(
+			virtualFlow = new SimpleVirtualFlow<>(
 					comboBox.itemsProperty(),
 					comboBox.getCellFactory(),
 					Orientation.VERTICAL

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

@@ -69,7 +69,8 @@ public class MFXFilterComboBoxSkin<T> extends MFXComboBoxSkin<T> {
 	// Overridden Methods
 	//================================================================================
 	@Override
-	protected void initialize() {}
+	protected void initialize() {
+	}
 
 	/**
 	 * {@inheritDoc}
@@ -93,7 +94,7 @@ public class MFXFilterComboBoxSkin<T> extends MFXComboBoxSkin<T> {
 		searchField.textProperty().bindBidirectional(comboBox.searchTextProperty());
 		searchField.setMaxWidth(Double.MAX_VALUE);
 
-		SimpleVirtualFlow<T, Cell<T>> virtualFlow = SimpleVirtualFlow.Builder.create(
+		SimpleVirtualFlow<T, Cell<T>> virtualFlow = new SimpleVirtualFlow<>(
 				filterList,
 				t -> new MFXFilterComboBoxCell<>(comboBox, filterList, t),
 				Orientation.VERTICAL

+ 13 - 28
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRectangleToggleNodeSkin.java

@@ -25,7 +25,6 @@ import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
 import io.github.palexdev.materialfx.utils.LabelUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.beans.binding.Bindings;
-import javafx.event.Event;
 import javafx.scene.Node;
 import javafx.scene.control.SkinBase;
 import javafx.scene.input.MouseEvent;
@@ -68,6 +67,7 @@ public class MFXRectangleToggleNodeSkin extends SkinBase<MFXRectangleToggleNode>
 	    label.setEditable(false);
 	    label.setSelectable(false);
 	    label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+	    label.setMouseTransparent(true);
 
 	    container = new StackPane();
 	    container.alignmentProperty().bind(toggleNode.alignmentProperty());
@@ -125,33 +125,18 @@ public class MFXRectangleToggleNodeSkin extends SkinBase<MFXRectangleToggleNode>
         toggleNode.labelTrailingIconProperty().addListener((observable, oldValue, newValue) -> handleGraphics());
 
         container.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
-            Node leadingIcon = label.getLeadingIcon();
-            Node trailingIcon = label.getTrailingIcon();
-
-            if (leadingIcon != null && NodeUtils.inHierarchy(event, leadingIcon)) {
-                return;
-            }
-            if (trailingIcon != null && NodeUtils.inHierarchy(event, trailingIcon)) {
-                return;
-            }
-
-            toggleNode.setSelected(!toggleNode.isSelected());
-            rippleGenerator.generateRipple(event);
-        });
-
-        label.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
-            Node leadingIcon = label.getLeadingIcon();
-            Node trailingIcon = label.getTrailingIcon();
-
-
-            if (leadingIcon != null && NodeUtils.inHierarchy(event, leadingIcon)) {
-                return;
-            }
-            if (trailingIcon != null && NodeUtils.inHierarchy(event, trailingIcon)) {
-                return;
-            }
-
-            Event.fireEvent(toggleNode, event);
+	        Node leadingIcon = label.getLeadingIcon();
+	        Node trailingIcon = label.getTrailingIcon();
+
+	        if (leadingIcon != null && NodeUtils.inHierarchy(event, leadingIcon)) {
+		        return;
+	        }
+	        if (trailingIcon != null && NodeUtils.inHierarchy(event, trailingIcon)) {
+		        return;
+	        }
+
+	        toggleNode.fire();
+	        rippleGenerator.generateRipple(event);
         });
     }
 

+ 15 - 17
materialfx/src/main/java/io/github/palexdev/materialfx/utils/LoaderUtils.java

@@ -18,9 +18,7 @@
 
 package io.github.palexdev.materialfx.utils;
 
-import io.github.palexdev.materialfx.beans.MFXLoaderBean;
-import io.github.palexdev.materialfx.controls.MFXHLoader;
-import io.github.palexdev.materialfx.controls.MFXVLoader;
+import io.github.palexdev.materialfx.utils.others.loader.MFXLoaderBean;
 import javafx.fxml.FXMLLoader;
 import javafx.scene.Parent;
 import javafx.util.Callback;
@@ -30,7 +28,7 @@ import java.net.URL;
 import java.util.concurrent.*;
 
 /**
- * Utils class which defines the core methods used by {@link MFXHLoader} and {@link MFXVLoader}.
+ * Utils class to load FXML views.
  */
 public class LoaderUtils {
     private static final ThreadPoolExecutor executor;
@@ -82,38 +80,38 @@ public class LoaderUtils {
     }
 
     /**
-     * Creates a new FXMLLoader with location {@link MFXLoaderBean#getFxmlURL()} and
+     * Creates a new FXMLLoader with location {@link MFXLoaderBean#getFxmlFile()} and
      * controller {@link MFXLoaderBean#getControllerFactory()} (if not null) and loads the fxml file.
      *
-     * @return  the loaded object hierarchy from the fxml
-     * @see     #fxmlLoad(FXMLLoader, URL)
-     * @see     #fxmlLoad(FXMLLoader, URL, Callback)
+     * @return the loaded object hierarchy from the fxml
+     * @see #fxmlLoad(FXMLLoader, URL)
+     * @see #fxmlLoad(FXMLLoader, URL, Callback)
      */
     public static Parent fxmlLoad(MFXLoaderBean loaderBean) throws IOException {
         FXMLLoader fxmlLoader = new FXMLLoader();
         if (loaderBean.getControllerFactory() != null) {
-            return fxmlLoad(fxmlLoader, loaderBean.getFxmlURL(), loaderBean.getControllerFactory());
+            return fxmlLoad(fxmlLoader, loaderBean.getFxmlFile(), loaderBean.getControllerFactory());
         }
-        return fxmlLoad(fxmlLoader, loaderBean.getFxmlURL());
+        return fxmlLoad(fxmlLoader, loaderBean.getFxmlFile());
     }
 
     /**
      * Sets the location and the controller factory (if not null) for the given
-     * fxmlLoader with {@link MFXLoaderBean#getFxmlURL()} and {@link MFXLoaderBean#getControllerFactory()},
+     * fxmlLoader with {@link MFXLoaderBean#getFxmlFile()} and {@link MFXLoaderBean#getControllerFactory()},
      * and loads the fxml file.
      * <p></p>
      * This method is useful for example when using a DI framework with JavaFX.
      *
-     * @param   fxmlLoader the FXMLLoader instance to use
-     * @return  the loaded object hierarchy from the fxml
-     * @see     #fxmlLoad(FXMLLoader, URL)
-     * @see     #fxmlLoad(FXMLLoader, URL, Callback)
+     * @param fxmlLoader the FXMLLoader instance to use
+     * @return the loaded object hierarchy from the fxml
+     * @see #fxmlLoad(FXMLLoader, URL)
+     * @see #fxmlLoad(FXMLLoader, URL, Callback)
      */
     public static Parent fxmlLoad(FXMLLoader fxmlLoader, MFXLoaderBean loaderBean) throws IOException {
         if (loaderBean.getControllerFactory() != null) {
-            return fxmlLoad(fxmlLoader, loaderBean.getFxmlURL(), loaderBean.getControllerFactory());
+            return fxmlLoad(fxmlLoader, loaderBean.getFxmlFile(), loaderBean.getControllerFactory());
         }
-        return fxmlLoad(fxmlLoader, loaderBean.getFxmlURL());
+        return fxmlLoad(fxmlLoader, loaderBean.getFxmlFile());
     }
 
     /**

+ 7 - 365
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -23,7 +23,10 @@ import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.css.PseudoClass;
 import javafx.event.Event;
-import javafx.geometry.*;
+import javafx.geometry.Bounds;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.Rectangle2D;
 import javafx.scene.Group;
 import javafx.scene.Node;
 import javafx.scene.Parent;
@@ -37,12 +40,7 @@ import javafx.scene.layout.*;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Circle;
 import javafx.stage.Screen;
-import javafx.stage.Stage;
-import javafx.stage.Window;
-import javafx.util.FXPermission;
 
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -50,7 +48,6 @@ import java.util.List;
  * Utility class which provides convenience methods for working with Nodes
  */
 public class NodeUtils {
-    public static final FXPermission ACCESS_WINDOW_LIST_PERMISSION = new FXPermission("accessWindowList");
 
     private NodeUtils() {
     }
@@ -436,42 +433,13 @@ public class NodeUtils {
         }
     }
 
+    /**
+     * Checks if the given {@link PseudoClass} is currently active on the given {@link Control}.
+     */
     public static boolean isPseudoClassActive(Control control, PseudoClass pseudoClass) {
         return control.getPseudoClassStates().contains(pseudoClass);
     }
 
-    //================================================================================
-    // JavaFX private methods
-    //================================================================================
-    /* The following methods are copied from com.sun.javafx.scene.control.skin.Utils class
-     * It's a private module, so to avoid adding exports and opens I copied them
-     */
-
-    public static double computeXOffset(double width, double contentWidth, HPos hpos) {
-        switch (hpos) {
-            case LEFT:
-                return 0;
-            case CENTER:
-                return (width - contentWidth) / 2;
-            case RIGHT:
-                return width - contentWidth;
-        }
-        return 0;
-    }
-
-    public static double computeYOffset(double height, double contentHeight, VPos vpos) {
-
-        switch (vpos) {
-            case CENTER:
-                return (height - contentHeight) / 2;
-            case BOTTOM:
-                return height - contentHeight;
-            case TOP:
-            default:
-                return 0;
-        }
-    }
-
     /**
      * Attempts to get the {@link Screen} instance on which
      * the given {@link Node} is shown.
@@ -485,330 +453,4 @@ public class NodeUtils {
                 .findFirst()
                 .orElse(null);
     }
-
-    // TODO replace all popups with MFXPopup
-    public static Point2D pointRelativeTo(Node parent, Node node, HPos hpos,
-                                          VPos vpos, double dx, double dy, boolean reposition) {
-        final double nodeWidth = node.getLayoutBounds().getWidth();
-        final double nodeHeight = node.getLayoutBounds().getHeight();
-        return pointRelativeTo(parent, nodeWidth, nodeHeight, hpos, vpos, dx, dy, reposition);
-    }
-
-    public static Point2D pointRelativeTo(Node parent, double anchorWidth,
-                                          double anchorHeight, HPos hpos, VPos vpos, double dx, double dy,
-                                          boolean reposition) {
-        final Bounds parentBounds = getBounds(parent);
-        Scene scene = parent.getScene();
-        NodeOrientation orientation = parent.getEffectiveNodeOrientation();
-
-        if (orientation == NodeOrientation.RIGHT_TO_LEFT) {
-            if (hpos == HPos.LEFT) {
-                hpos = HPos.RIGHT;
-            } else if (hpos == HPos.RIGHT) {
-                hpos = HPos.LEFT;
-            }
-            dx *= -1;
-        }
-
-        double layoutX = positionX(parentBounds, anchorWidth, hpos) + dx;
-        final double layoutY = positionY(parentBounds, anchorHeight, vpos) + dy;
-
-        if (orientation == NodeOrientation.RIGHT_TO_LEFT && hpos == HPos.CENTER) {
-            if (scene.getWindow() instanceof Stage) {
-                layoutX = layoutX + parentBounds.getWidth() - anchorWidth;
-            } else {
-                layoutX = layoutX - parentBounds.getWidth() - anchorWidth;
-            }
-        }
-
-        if (reposition) {
-            return pointRelativeTo(parent, anchorWidth, anchorHeight, layoutX, layoutY, hpos, vpos);
-        } else {
-            return new Point2D(layoutX, layoutY);
-        }
-    }
-
-    /**
-     * This is the fallthrough function that most other functions fall into. It takes
-     * care specifically of the repositioning of the item such that it remains onscreen
-     * as best it can, given it's unique qualities.
-     * <p>
-     * As will all other functions, this one returns a Point2D that represents an x,y
-     * location that should safely position the item onscreen as best as possible.
-     * <p>
-     * Note that <code>width</code> and <height> refer to the width and height of the
-     * node/popup that is needing to be repositioned, not of the parent.
-     * <p>
-     * Don't use the BASELINE vpos, it doesn't make sense and would produce wrong result.
-     */
-    public static Point2D pointRelativeTo(Object parent, double width,
-                                          double height, double screenX, double screenY, HPos hpos, VPos vpos) {
-        double finalScreenX = screenX;
-        double finalScreenY = screenY;
-        final Bounds parentBounds = getBounds(parent);
-
-        // ...and then we get the bounds of this screen
-        final Screen currentScreen = getScreen(parent);
-        final Rectangle2D screenBounds =
-                hasFullScreenStage(currentScreen)
-                        ? currentScreen.getBounds()
-                        : currentScreen.getVisualBounds();
-
-        // test if this layout will force the node to appear outside
-        // of the screens bounds. If so, we must reposition the item to a better position.
-        // We firstly try to do this intelligently, so as to not overlap the parent if
-        // at all possible.
-        if (hpos != null) {
-            // Firstly we consider going off the right hand side
-            if ((finalScreenX + width) > screenBounds.getMaxX()) {
-                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
-            }
-
-            // don't let the node go off to the left of the current screen
-            if (finalScreenX < screenBounds.getMinX()) {
-                finalScreenX = positionX(parentBounds, width, getHPosOpposite(hpos, vpos));
-            }
-        }
-
-        if (vpos != null) {
-            // don't let the node go off the bottom of the current screen
-            if ((finalScreenY + height) > screenBounds.getMaxY()) {
-                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos, vpos));
-            }
-
-            // don't let the node out of the top of the current screen
-            if (finalScreenY < screenBounds.getMinY()) {
-                finalScreenY = positionY(parentBounds, height, getVPosOpposite(hpos, vpos));
-            }
-        }
-
-        // --- after all the moving around, we do one last check / rearrange.
-        // Unlike the check above, this time we are just fully committed to keeping
-        // the item on screen at all costs, regardless of whether or not that results
-        /// in overlapping the parent object.
-        if ((finalScreenX + width) > screenBounds.getMaxX()) {
-            finalScreenX -= (finalScreenX + width - screenBounds.getMaxX());
-        }
-        if (finalScreenX < screenBounds.getMinX()) {
-            finalScreenX = screenBounds.getMinX();
-        }
-        if ((finalScreenY + height) > screenBounds.getMaxY()) {
-            finalScreenY -= (finalScreenY + height - screenBounds.getMaxY());
-        }
-        if (finalScreenY < screenBounds.getMinY()) {
-            finalScreenY = screenBounds.getMinY();
-        }
-
-        return new Point2D(finalScreenX, finalScreenY);
-    }
-
-    private static double positionX(Bounds parentBounds, double width, HPos hpos) {
-        if (hpos == HPos.CENTER) {
-            // this isn't right, but it is needed for root menus to show properly
-            return parentBounds.getMinX();
-        } else if (hpos == HPos.RIGHT) {
-            return parentBounds.getMaxX();
-        } else if (hpos == HPos.LEFT) {
-            return parentBounds.getMinX() - width;
-        } else {
-            return 0;
-        }
-    }
-
-    /**
-     * Utility function that returns the y-axis position that an object should be positioned at,
-     * given the parents screen bounds, the height of the object, and
-     * the required VPos.
-     * <p>
-     * The BASELINE vpos doesn't make sense here, 0 is returned for it.
-     */
-    private static double positionY(Bounds parentBounds, double height, VPos vpos) {
-        if (vpos == VPos.BOTTOM) {
-            return parentBounds.getMaxY();
-        } else if (vpos == VPos.CENTER) {
-            return parentBounds.getMinY();
-        } else if (vpos == VPos.TOP) {
-            return parentBounds.getMinY() - height;
-        } else {
-            return 0;
-        }
-    }
-
-    /**
-     * To facilitate multiple types of parent object, we unfortunately must allow for
-     * Objects to be passed in. This method handles determining the bounds of the
-     * given Object. If the Object type is not supported, a default Bounds will be returned.
-     */
-    private static Bounds getBounds(Object obj) {
-        if (obj instanceof Node) {
-            final Node n = (Node) obj;
-            Bounds b = n.localToScreen(n.getLayoutBounds());
-            return b != null ? b : new BoundingBox(0, 0, 0, 0);
-        } else if (obj instanceof Window) {
-            final Window window = (Window) obj;
-            return new BoundingBox(window.getX(), window.getY(), window.getWidth(), window.getHeight());
-        } else {
-            return new BoundingBox(0, 0, 0, 0);
-        }
-    }
-
-    /*
-     * Simple utility function to return the 'opposite' value of a given HPos, taking
-     * into account the current VPos value. This is used to try and avoid overlapping.
-     */
-    private static HPos getHPosOpposite(HPos hpos, VPos vpos) {
-        if (vpos == VPos.CENTER) {
-            if (hpos == HPos.LEFT) {
-                return HPos.RIGHT;
-            } else if (hpos == HPos.RIGHT) {
-                return HPos.LEFT;
-            } else if (hpos == HPos.CENTER) {
-                return HPos.CENTER;
-            } else {
-                // by default center for now
-                return HPos.CENTER;
-            }
-        } else {
-            return HPos.CENTER;
-        }
-    }
-
-    /*
-     * Simple utitilty function to return the 'opposite' value of a given VPos, taking
-     * into account the current HPos value. This is used to try and avoid overlapping.
-     */
-    private static VPos getVPosOpposite(HPos hpos, VPos vpos) {
-        if (hpos == HPos.CENTER) {
-            if (vpos == VPos.BASELINE) {
-                return VPos.BASELINE;
-            } else if (vpos == VPos.BOTTOM) {
-                return VPos.TOP;
-            } else if (vpos == VPos.CENTER) {
-                return VPos.CENTER;
-            } else if (vpos == VPos.TOP) {
-                return VPos.BOTTOM;
-            } else {
-                // by default center for now
-                return VPos.CENTER;
-            }
-        } else {
-            return VPos.CENTER;
-        }
-    }
-
-    @Deprecated(forRemoval = true, since = "11.13.0")
-    public static boolean hasFullScreenStage(final Screen screen) {
-        final List<Window> allWindows = AccessController.doPrivileged(
-                (PrivilegedAction<List<Window>>) Window::getWindows,
-                null,
-                ACCESS_WINDOW_LIST_PERMISSION);
-
-        for (final Window window : allWindows) {
-            if (window instanceof Stage) {
-                final Stage stage = (Stage) window;
-                if (stage.isFullScreen() && (getScreen(stage) == screen)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    public static Screen getScreen(Object obj) {
-        final Bounds parentBounds = getBounds(obj);
-
-        final Rectangle2D rect = new Rectangle2D(
-                parentBounds.getMinX(),
-                parentBounds.getMinY(),
-                parentBounds.getWidth(),
-                parentBounds.getHeight());
-
-        return getScreenForRectangle(rect);
-    }
-
-    public static Screen getScreenForRectangle(final Rectangle2D rect) {
-        final List<Screen> screens = Screen.getScreens();
-
-        final double rectX0 = rect.getMinX();
-        final double rectX1 = rect.getMaxX();
-        final double rectY0 = rect.getMinY();
-        final double rectY1 = rect.getMaxY();
-
-        Screen selectedScreen;
-
-        selectedScreen = null;
-        double maxIntersection = 0;
-        for (final Screen screen : screens) {
-            final Rectangle2D screenBounds = screen.getBounds();
-            final double intersection =
-                    getIntersectionLength(rectX0, rectX1,
-                            screenBounds.getMinX(),
-                            screenBounds.getMaxX())
-                            * getIntersectionLength(rectY0, rectY1,
-                            screenBounds.getMinY(),
-                            screenBounds.getMaxY());
-
-            if (maxIntersection < intersection) {
-                maxIntersection = intersection;
-                selectedScreen = screen;
-            }
-        }
-
-        if (selectedScreen != null) {
-            return selectedScreen;
-        }
-
-        selectedScreen = Screen.getPrimary();
-        double minDistance = Double.MAX_VALUE;
-        for (final Screen screen : screens) {
-            final Rectangle2D screenBounds = screen.getBounds();
-            final double dx = getOuterDistance(rectX0, rectX1,
-                    screenBounds.getMinX(),
-                    screenBounds.getMaxX());
-            final double dy = getOuterDistance(rectY0, rectY1,
-                    screenBounds.getMinY(),
-                    screenBounds.getMaxY());
-            final double distance = dx * dx + dy * dy;
-
-            if (minDistance > distance) {
-                minDistance = distance;
-                selectedScreen = screen;
-            }
-        }
-
-        return selectedScreen;
-    }
-
-    private static double getIntersectionLength(
-            final double a0, final double a1,
-            final double b0, final double b1) {
-        // (a0 <= a1) && (b0 <= b1)
-        return (a0 <= b0) ? getIntersectionLengthImpl(b0, b1, a1)
-                : getIntersectionLengthImpl(a0, a1, b1);
-    }
-
-    private static double getIntersectionLengthImpl(
-            final double v0, final double v1, final double v) {
-        // (v0 <= v1)
-        if (v <= v0) {
-            return 0;
-        }
-
-        return (v <= v1) ? v - v0 : v1 - v0;
-    }
-
-    private static double getOuterDistance(
-            final double a0, final double a1,
-            final double b0, final double b1) {
-        // (a0 <= a1) && (b0 <= b1)
-        if (a1 <= b0) {
-            return b0 - a1;
-        }
-
-        if (b1 <= a0) {
-            return b1 - a0;
-        }
-
-        return 0;
-    }
 }

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/DateStringConverter.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/DateStringConverter.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.dates;
 
 import javafx.util.StringConverter;
 

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/DayOfWeekStringConverter.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/DayOfWeekStringConverter.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.dates;
 
 import io.github.palexdev.materialfx.utils.EnumUtils;
 import javafx.util.StringConverter;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/MonthStringConverter.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/dates/MonthStringConverter.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.dates;
 
 import io.github.palexdev.materialfx.utils.EnumUtils;
 import javafx.util.StringConverter;

+ 282 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/loader/MFXLoader.java

@@ -0,0 +1,282 @@
+package io.github.palexdev.materialfx.utils.others.loader;
+
+import io.github.palexdev.materialfx.beans.properties.functional.SupplierProperty;
+import io.github.palexdev.materialfx.enums.LoaderCacheLevel;
+import io.github.palexdev.materialfx.utils.LoaderUtils;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.CacheHint;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.layout.StackPane;
+import javafx.util.Callback;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Convenience class for creating dashboards, no more hassle on managing multiple views.
+ * <p>
+ * This control makes use of {@link LoaderUtils} and it's capable of loading multiple FXML views with {@link ExecutorService}s.
+ * <p>
+ * The informations about a view are stored in bean classes, {@link MFXLoaderBean}. Those can be added to this loader
+ * by using the various "addView(...)" methods, and will be stored in a {@code Map}, every bean is associated to a {@code String},
+ * which should be the identifier of the view, you can also use {@link LoaderUtils#generateKey(URL)} to automatically generate a key.
+ * <p></p>
+ * Once every view has been added you can start the loader with either {@link #start()} or {@link #startWith(ExecutorService)}.
+ * <p>
+ * After all views have been loaded the {@link #onLoaded(List)} method is called, see also {@link #setOnLoadedAction(Consumer)}.
+ * <p></p>
+ * This loader has two other notable features:
+ * <p> 1) To load FXML files it uses a {@link FXMLLoader} of course. But, in some cases (like DI framework) you want to use
+ * a specific {@link FXMLLoader}, so the loader creates them with a {@link Supplier}, see {@link #fxmlLoaderSupplierProperty()}.
+ * The default supplier is just "FXMLLoader::new", but you can easily change that if you need to.
+ * <p> 2) To make the views ready for switching the loader can "preload" them (compute both CSS and Layout), you can
+ * manage this behavior by setting the "cache level", see {@link #setCacheLevel(LoaderCacheLevel)} and {@link LoaderCacheLevel}
+ * <p>
+ * <b>NOTE: the cache level must be set before invoking the {@link #start()} method.</b>
+ * <p>
+ * By default it is set to: {@link LoaderCacheLevel#SCENE_CACHE}
+ *
+ * @see LoaderUtils
+ * @see MFXLoaderBean
+ */
+public class MFXLoader {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private final Map<String, MFXLoaderBean> viewMap = new LinkedHashMap<>();
+	private final SupplierProperty<FXMLLoader> fxmlLoaderSupplier = new SupplierProperty<>(FXMLLoader::new) {
+		@Override
+		public void set(Supplier<FXMLLoader> newValue) {
+			if (newValue == null) {
+				super.set(FXMLLoader::new);
+				return;
+			}
+			super.set(newValue);
+		}
+	};
+	private final AtomicInteger loadedCount = new AtomicInteger(0);
+	private Consumer<List<MFXLoaderBean>> onLoadedAction;
+	private LoaderCacheLevel cacheLevel = LoaderCacheLevel.SCENE_CACHE;
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXLoader() {
+		this(null);
+	}
+
+	public MFXLoader(Consumer<List<MFXLoaderBean>> onLoadedAction) {
+		this.onLoadedAction = onLoadedAction;
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * Starts the loading process by collecting all the views from the map
+	 * that are still not loaded. Then for each {@link MFXLoaderBean} builds
+	 * the {@link Callable} used to load the FXML root, see {@link #buildTask(MFXLoaderBean)},
+	 * and sends it to the {@link LoaderUtils}'s executor, to load the view uses
+	 * {@link Future#get()}.
+	 * Once the fxml has been loaded invokes {@link #cacheParent(Parent)} then finally
+	 * increments the number that keeps track of how many views have been loaded.
+	 * <p>
+	 * At the end of the loop calls {@link #onLoaded(List)}, the list
+	 * is given by {@link Map#values()} (wrapped in an ArrayList).
+	 */
+	public void start() {
+		List<MFXLoaderBean> toLoad = viewMap.values().stream()
+				.filter(bean -> !bean.isLoaded())
+				.collect(Collectors.toList());
+		for (MFXLoaderBean bean : toLoad) {
+			try {
+				Callable<Parent> task = buildTask(bean);
+				Parent loaded = LoaderUtils.submit(task).get();
+				cacheParent(loaded);
+				bean.setLoaded(true);
+				loadedCount.addAndGet(1);
+			} catch (InterruptedException | ExecutionException ex) {
+				ex.printStackTrace();
+			} finally {
+				loadedCount.addAndGet(1);
+			}
+		}
+		onLoaded(new ArrayList<>(viewMap.values()));
+	}
+
+	/**
+	 * Same as {@link #start()} but the load tasks are submitted to the given
+	 * {@link ExecutorService}.
+	 */
+	public void startWith(ExecutorService executorService) {
+		List<MFXLoaderBean> toLoad = viewMap.values().stream()
+				.filter(bean -> !bean.isLoaded())
+				.collect(Collectors.toList());
+		for (MFXLoaderBean bean : toLoad) {
+			try {
+				Callable<Parent> task = buildTask(bean);
+				Parent loaded = executorService.submit(task).get();
+				cacheParent(loaded);
+				bean.setLoaded(true);
+				loadedCount.addAndGet(1);
+			} catch (InterruptedException | ExecutionException ex) {
+				ex.printStackTrace();
+			} finally {
+				loadedCount.addAndGet(1);
+			}
+		}
+		onLoaded(new ArrayList<>(viewMap.values()));
+	}
+
+	/**
+	 * Adds the given view to the views map.
+	 */
+	public MFXLoader addView(MFXLoaderBean bean) {
+		LoaderUtils.checkFxmlFile(bean.getFxmlFile());
+		viewMap.put(bean.getViewName(), bean);
+		return this;
+	}
+
+	/**
+	 * Builds a new {@link MFXLoaderBean} with the given identifier and FXML file,
+	 * then adds it to the views map.
+	 */
+	public MFXLoader addView(String viewName, URL fxmlFile) {
+		LoaderUtils.checkFxmlFile(fxmlFile);
+		viewMap.put(viewName, new MFXLoaderBean(viewName, fxmlFile));
+		return this;
+	}
+
+	/**
+	 * Builds a new {@link MFXLoaderBean} with the given identifier, FXML file and
+	 * controller factory, then adds it to the views map.
+	 */
+	public MFXLoader addView(String viewName, URL fxmlFile, Callback<Class<?>, Object> controllerFactory) {
+		LoaderUtils.checkFxmlFile(fxmlFile);
+		viewMap.put(viewName, new MFXLoaderBean(viewName, fxmlFile, controllerFactory, false, null));
+		return this;
+	}
+
+	/**
+	 * @return a view for the given identifier, or null if no view is found
+	 */
+	public MFXLoaderBean getView(String viewName) {
+		return viewMap.getOrDefault(viewName, null);
+	}
+
+	/**
+	 * This method is called once all the views have been loaded by {@link #start()} or
+	 * {@link #startWith(ExecutorService)}.
+	 * <p>
+	 * This simple methods is just responsible for executing the action specified by the user, {@link #setOnLoadedAction(Consumer)},
+	 * if not null.
+	 */
+	protected void onLoaded(List<MFXLoaderBean> beans) {
+		if (onLoadedAction != null) {
+			onLoadedAction.accept(beans);
+		}
+	}
+
+	/**
+	 * Responsible for building the {@link Callable} which will load the given view.
+	 * <p>
+	 * First a {@link FXMLLoader} is created by using the specified supplier, {@link #fxmlLoaderSupplierProperty()},
+	 * then the location and controller factory are set on it.
+	 * <p>
+	 * {@link FXMLLoader#load()} is invoked and then the loaded {@link Parent} is set in the bean.
+	 */
+	private Callable<Parent> buildTask(MFXLoaderBean bean) {
+		List<Callable<Parent>> tasks = new ArrayList<>();
+		return () -> {
+			FXMLLoader loader = getFxmlLoaderSupplier().get();
+			URL fxmlFile = bean.getFxmlFile();
+			Callback<Class<?>, Object> controllerFactory = bean.getControllerFactory();
+			loader.setLocation(fxmlFile);
+			loader.setControllerFactory(controllerFactory);
+
+			Parent root = loader.load();
+			bean.setRoot(root);
+			return root;
+		};
+	}
+
+	/**
+	 * This method is responsible for caching/preloading the loaded views to make them
+	 * ready for switching.
+	 * For a description of the various cache levels, see {@link LoaderCacheLevel}.
+	 * <p>
+	 * If the cache level is set to {@link LoaderCacheLevel#NONE} exits immediately.
+	 */
+	private void cacheParent(Parent parent) {
+		if (cacheLevel == LoaderCacheLevel.NONE) return;
+
+		if (cacheLevel == LoaderCacheLevel.SCENE_JAVAFX_CACHE) {
+			parent.setCache(true);
+			parent.setCacheHint(CacheHint.SPEED);
+		}
+
+		StackPane pane = new StackPane();
+		pane.getChildren().setAll(parent);
+		Scene scene = new Scene(pane);
+		pane.applyCss();
+		pane.layout();
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+	public Supplier<FXMLLoader> getFxmlLoaderSupplier() {
+		return fxmlLoaderSupplier.get();
+	}
+
+	/**
+	 * Specifies the {@link Supplier} used to build a new {@link FXMLLoader} each time a view has to be loaded.
+	 */
+	public SupplierProperty<FXMLLoader> fxmlLoaderSupplierProperty() {
+		return fxmlLoaderSupplier;
+	}
+
+	public MFXLoader setFxmlLoaderSupplier(Supplier<FXMLLoader> fxmlLoaderSupplier) {
+		this.fxmlLoaderSupplier.set(fxmlLoaderSupplier);
+		return this;
+	}
+
+	public Consumer<List<MFXLoaderBean>> getOnLoadedAction() {
+		return onLoadedAction;
+	}
+
+	/**
+	 * Sets the action to perform once all the views have been loaded.
+	 * <p>
+	 * The action is a {@link Consumer} which carries the list fo loaded views.
+	 */
+	public MFXLoader setOnLoadedAction(Consumer<List<MFXLoaderBean>> onLoadedAction) {
+		this.onLoadedAction = onLoadedAction;
+		return this;
+	}
+
+	public LoaderCacheLevel getCacheLevel() {
+		return cacheLevel;
+	}
+
+	/**
+	 * Sets the {@link LoaderCacheLevel} for this loader.
+	 */
+	public MFXLoader setCacheLevel(LoaderCacheLevel cacheLevel) {
+		this.cacheLevel = cacheLevel;
+		return this;
+	}
+}

+ 187 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/loader/MFXLoaderBean.java

@@ -0,0 +1,187 @@
+package io.github.palexdev.materialfx.utils.others.loader;
+
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.util.Callback;
+
+import java.net.URL;
+import java.util.function.Supplier;
+
+/**
+ * Support bean for {@link MFXLoader} to define the properties of a view such as:
+ * <p> - An identifier for the view
+ * <p> - The FXML file of the view
+ * <p> - The root node of the FXML (managed by the loader, set once the view is loaded)
+ * <p> - The controller factory in case the controller uses parameterized constructors (optional)
+ * <p> - A flag to indicate whether this view should be considered the default one (useful for nav-bars/dashboards)
+ * <p> - A flag to indicate whether the view has been loaded (managed by the loader, set to true once the view is loaded)
+ * <p> - A {@link Supplier} function to convert the bean to a {@code Node}. This is useful for example in case you want
+ * to implement a view switcher, you could produce a {@code Button} or any node you want that will handle the view switching,
+ * for a concrete example you could see the MaterialFX's DemoController (in the demo module).
+ * <p></p>
+ * The bean also offers a {@link Builder} with fluent api, it's suggested to use that instead of constructors.
+ * <p>
+ * You can also access the builder with the provided static method {@link #of(String, URL)}.
+ */
+public class MFXLoaderBean {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private final String viewName;
+	private final URL fxmlFile;
+	private Parent root;
+	private Callback<Class<?>, Object> controllerFactory;
+	private boolean defaultView = false;
+	private boolean loaded = false;
+	private Supplier<Node> beanToNodeMapper;
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXLoaderBean(String viewName, URL fxmlFile) {
+		this.viewName = viewName;
+		this.fxmlFile = fxmlFile;
+	}
+
+	public MFXLoaderBean(String viewName, URL fxmlFile, Callback<Class<?>, Object> controllerFactory, boolean defaultView, Supplier<Node> beanToNodeMapper) {
+		this.viewName = viewName;
+		this.fxmlFile = fxmlFile;
+		this.controllerFactory = controllerFactory;
+		this.defaultView = defaultView;
+		this.beanToNodeMapper = beanToNodeMapper;
+	}
+
+	//================================================================================
+	// Static Methods
+	//================================================================================
+	public static Builder of(String viewName, URL fxmlFile) {
+		return new Builder(viewName, fxmlFile);
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+
+	/**
+	 * @return the view's identifier
+	 */
+	public String getViewName() {
+		return viewName;
+	}
+
+	/**
+	 * @return the view's FXML file
+	 */
+	public URL getFxmlFile() {
+		return fxmlFile;
+	}
+
+	/**
+	 * @return the FXML file's root node
+	 */
+	public Parent getRoot() {
+		return root;
+	}
+
+	/**
+	 * Sets the view's root node.
+	 * <p>
+	 * Package private, handled by the loader.
+	 */
+	MFXLoaderBean setRoot(Parent root) {
+		this.root = root;
+		return this;
+	}
+
+	/**
+	 * @return the callback used to produce the view's controller
+	 */
+	public Callback<Class<?>, Object> getControllerFactory() {
+		return controllerFactory;
+	}
+
+	/**
+	 * Sets the callback used to produce the view's controller.
+	 */
+	public MFXLoaderBean setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
+		this.controllerFactory = controllerFactory;
+		return this;
+	}
+
+	/**
+	 * @return whether this view should be considered the default view
+	 */
+	public boolean isDefaultView() {
+		return defaultView;
+	}
+
+	/**
+	 * Sets whether this view should be considered the default view.
+	 */
+	public MFXLoaderBean setDefaultView(boolean defaultView) {
+		this.defaultView = defaultView;
+		return this;
+	}
+
+	/**
+	 * @return whether the view has been loaded by the loader
+	 */
+	public boolean isLoaded() {
+		return loaded;
+	}
+
+	/**
+	 * Sets whether the view has been loaded by the loader.
+	 * <p>
+	 * Package private, handled by the loader.
+	 */
+	MFXLoaderBean setLoaded(boolean loaded) {
+		this.loaded = loaded;
+		return this;
+	}
+
+	/**
+	 * @return the supplier used to convert this view into a {@code Node}
+	 */
+	public Supplier<Node> getBeanToNodeMapper() {
+		return beanToNodeMapper;
+	}
+
+	/**
+	 * Sets the supplier used to convert this view into a {@code Node}.
+	 */
+	public MFXLoaderBean setBeanToNodeMapper(Supplier<Node> beanToNodeMapper) {
+		this.beanToNodeMapper = beanToNodeMapper;
+		return this;
+	}
+
+	//================================================================================
+	// Builder
+	//================================================================================
+	public static class Builder {
+		private final MFXLoaderBean bean;
+
+		public Builder(String viewName, URL fxmlFile) {
+			this.bean = new MFXLoaderBean(viewName, fxmlFile);
+		}
+
+		public Builder setControllerFactory(Callback<Class<?>, Object> controllerFactory) {
+			bean.setControllerFactory(controllerFactory);
+			return this;
+		}
+
+		public Builder setDefaultRoot(boolean defaultRoot) {
+			bean.setDefaultView(defaultRoot);
+			return this;
+		}
+
+		public Builder setBeanToNodeMapper(Supplier<Node> beanToNodeMapper) {
+			bean.setBeanToNodeMapper(beanToNodeMapper);
+			return this;
+		}
+
+		public MFXLoaderBean get() {
+			return bean;
+		}
+	}
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/OnChanged.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/OnChanged.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.observables;
 
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/OnInvalidated.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/OnInvalidated.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.observables;
 
 import javafx.beans.InvalidationListener;
 import javafx.beans.value.ObservableValue;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/When.java → materialfx/src/main/java/io/github/palexdev/materialfx/utils/others/observables/When.java

@@ -1,4 +1,4 @@
-package io.github.palexdev.materialfx.utils.others;
+package io.github.palexdev.materialfx.utils.others.observables;
 
 import javafx.beans.value.ObservableValue;
 

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/validation/MFXValidator.java

@@ -1,6 +1,6 @@
 package io.github.palexdev.materialfx.validation;
 
-import io.github.palexdev.materialfx.utils.others.When;
+import io.github.palexdev.materialfx.utils.others.observables.When;
 import javafx.beans.InvalidationListener;
 import javafx.beans.binding.BooleanExpression;
 import javafx.beans.property.ReadOnlyBooleanProperty;

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

@@ -70,6 +70,9 @@ module MaterialFX {
 	// Utils Package
 	exports io.github.palexdev.materialfx.utils;
 	exports io.github.palexdev.materialfx.utils.others;
+	exports io.github.palexdev.materialfx.utils.others.dates;
+	exports io.github.palexdev.materialfx.utils.others.loader;
+	exports io.github.palexdev.materialfx.utils.others.observables;
 
 	// Validation Package
 	exports io.github.palexdev.materialfx.validation;