Pārlūkot izejas kodu

New experimental control.
Entirely redone TreeView, TreeItem, TreeCell. (Documentation will be added later in the future, when the control is/is almost completed)

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

PAlex404 4 gadi atpakaļ
vecāks
revīzija
5f713690bb

+ 204 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/TreeItem.java

@@ -0,0 +1,204 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.controls.base.AbstractTreeItem;
+import io.github.palexdev.materialfx.controls.cell.SimpleTreeCell;
+import io.github.palexdev.materialfx.skins.TreeItemSkin;
+import javafx.beans.property.*;
+import javafx.collections.ListChangeListener;
+import javafx.css.*;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventType;
+import javafx.geometry.Insets;
+import javafx.scene.control.Skin;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TreeItem<T> extends AbstractTreeItem<T> {
+    private static final StyleablePropertyFactory<TreeItem<?>> FACTORY = new StyleablePropertyFactory<>(TreeItem.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-tree-item";
+
+    private final BooleanProperty expanded = new SimpleBooleanProperty(false);
+    private final ReadOnlyBooleanWrapper animationRunning = new ReadOnlyBooleanWrapper(false);
+    private final ReadOnlyDoubleWrapper initialHeight = new ReadOnlyDoubleWrapper(0);
+
+    public TreeItem(T data) {
+        super(data);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        items.addListener((ListChangeListener<? super AbstractTreeItem<T>>) change -> {
+            List<AbstractTreeItem<T>> tmpRemoved = new ArrayList<>();
+            List<AbstractTreeItem<T>> tmpAdded = new ArrayList<>();
+
+            while (change.next()) {
+                tmpRemoved.addAll(change.getRemoved());
+                tmpAdded.addAll(change.getAddedSubList());
+            }
+
+            updateChildrenParent(tmpRemoved, null);
+            updateChildrenParent(tmpAdded, this);
+        });
+
+        defaultCellFactory();
+    }
+
+    @Override
+    protected void updateChildrenParent(List<? extends AbstractTreeItem<T>> treeItems, final AbstractTreeItem<T> newParent) {
+        treeItems.forEach(item -> ((TreeItem<T>) item).setItemParent(newParent));
+    }
+
+    private final StyleableDoubleProperty animationDuration = new SimpleStyleableDoubleProperty(
+            StyleableProperties.DURATION,
+            this,
+            "animationDuration",
+            300.0
+    );
+
+    public double getAnimationDuration() {
+        return animationDuration.get();
+    }
+
+    public StyleableDoubleProperty animationDurationProperty() {
+        return animationDuration;
+    }
+
+    public void setAnimationDuration(double animationDuration) {
+        this.animationDuration.set(animationDuration);
+    }
+
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<TreeItem<?>, Number> DURATION =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-animation-duration",
+                        TreeItem::animationDurationProperty,
+                        300.0
+                );
+
+        static {
+            cssMetaDataList = List.of(DURATION);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    protected void defaultCellFactory() {
+        super.cellFactory.set(item -> new SimpleTreeCell<>(item.getData()));
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new TreeItemSkin<>(this);
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return TreeItem.getControlCssMetaDataList();
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+
+        items.forEach(
+                item -> item.setPadding(new Insets(0, 0, 0, 20))
+        );
+    }
+
+    @Override
+    public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        EventDispatchChain chain = super.buildEventDispatchChain(tail);
+
+        AbstractTreeItem<T> item = getItemParent();
+        while (item != null) {
+            chain.prepend(item.getEventDispatcher());
+            item = item.getItemParent();
+        }
+
+        return chain;
+    }
+
+    @Override
+    public String toString() {
+        String className = getClass().getName();
+        String simpleName = className.substring(className.lastIndexOf('.')+1);
+        StringBuilder sb = new StringBuilder();
+        sb.append("[").append(simpleName);
+        sb.append('@');
+        sb.append(Integer.toHexString(hashCode()));
+        sb.append("]");
+        sb.append("[Data:").append(getData()).append("]");
+        if (getId() != null) {
+            sb.append("[id:").append(getId()).append("]");
+        }
+
+        return sb.toString();
+    }
+
+    public boolean isExpanded() {
+        return expanded.get();
+    }
+
+    public BooleanProperty expandedProperty() {
+        return expanded;
+    }
+
+    public void setExpanded(boolean expanded) {
+        this.expanded.set(expanded);
+    }
+
+    public double getInitialHeight() {
+        return initialHeight.get();
+    }
+
+    public ReadOnlyDoubleProperty initialHeightProperty() {
+        return initialHeight;
+    }
+
+    public void setInitialHeight(double height) {
+        if (initialHeight.get() !=  0) {
+            throw new RuntimeException("Initial Height Property is intended for internal use only.");
+        }
+        initialHeight.set(height);
+    }
+
+    public boolean isAnimationRunning() {
+        return animationRunning.get();
+    }
+
+    public ReadOnlyBooleanWrapper animationRunningProperty() {
+        return animationRunning;
+    }
+
+    public static class TreeItemEvent<T> extends Event {
+        private final WeakReference<AbstractTreeItem<T>> itemRef;
+        private final double value;
+
+        public static final EventType<TreeItemEvent<?>> FORCE_UPDATE = new EventType<>(ANY, "FORCE_UPDATE");
+        public static final EventType<TreeItemEvent<?>> EXPAND_EVENT = new EventType<>(ANY, "EXPAND_EVENT");
+        public static final EventType<TreeItemEvent<?>> COLLAPSE_EVENT = new EventType<>(ANY, "COLLAPSE_EVENT");
+
+        public TreeItemEvent(EventType<? extends Event> eventType, AbstractTreeItem<T> item, double value) {
+            super(eventType);
+            this.itemRef = new WeakReference<>(item);
+            this.value = value;
+        }
+
+        public AbstractTreeItem<T> getItem() {
+            return itemRef.get();
+        }
+
+        public double getValue() {
+            return value;
+        }
+    }
+}

+ 50 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/TreeView.java

@@ -0,0 +1,50 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.base.AbstractTreeItem;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Insets;
+
+public class TreeView<T> extends MFXScrollPane {
+    private final String STYLE_CLASS = "mfx-tree-view";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-treeview.css").toString();
+    private final ObjectProperty<AbstractTreeItem<T>> root = new SimpleObjectProperty<>(null);
+
+
+
+    public TreeView(TreeItem<T> root) {
+        setStyle("-fx-border-color: gold");
+        root.setTreeView(this);
+        setRoot(root);
+        setContent(root);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); // TODO remove
+        setPrefSize(250, 500);
+        setPadding(new Insets(3));
+        MFXScrollPane.smoothVScrolling(this);
+
+        getRoot().prefWidthProperty().bind(widthProperty().subtract(10));
+}
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    public AbstractTreeItem<T> getRoot() {
+        return root.get();
+    }
+
+    public ObjectProperty<AbstractTreeItem<T>> rootProperty() {
+        return root;
+    }
+
+    public void setRoot(AbstractTreeItem<T> root) {
+        this.root.set(root);
+    }
+}

+ 55 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractTreeCell.java

@@ -0,0 +1,55 @@
+package io.github.palexdev.materialfx.controls.base;
+
+import io.github.palexdev.materialfx.controls.TreeItem;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+
+// TODO implement StringConverter
+public abstract class AbstractTreeCell<T> extends HBox {
+    protected final ObjectProperty<? super Node> disclosureNode = new SimpleObjectProperty<>();
+    private final DoubleProperty fixedCellSize = new SimpleDoubleProperty();
+
+    public AbstractTreeCell(T data) {
+        this(data, 27);
+    }
+
+    public AbstractTreeCell(T data, double fixedHeight) {
+        this.fixedCellSize.set(fixedHeight);
+
+        setMinHeight(USE_PREF_SIZE);
+        setMaxHeight(USE_PREF_SIZE);
+        prefHeightProperty().bind(fixedCellSize);
+
+        initialize();
+        render(data);
+    }
+
+    protected void initialize() {
+        setAlignment(Pos.CENTER_LEFT);
+        setSpacing(5);
+    }
+
+    protected abstract void defaultDisclosureNode();
+    public abstract Node getDisclosureNode();
+    public abstract <N extends Node> void setDisclosureNode(N node);
+
+    protected abstract void render(T data);
+    public abstract void updateCell(TreeItem<T> item);
+
+    public double getFixedCellSize() {
+        return fixedCellSize.get();
+    }
+
+    public DoubleProperty fixedCellSizeProperty() {
+        return fixedCellSize;
+    }
+
+    public void setFixedCellSize(double fixedCellSize) {
+        this.fixedCellSize.set(fixedCellSize);
+    }
+}

+ 134 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractTreeItem.java

@@ -0,0 +1,134 @@
+package io.github.palexdev.materialfx.controls.base;
+
+import io.github.palexdev.materialfx.controls.TreeView;
+import io.github.palexdev.materialfx.utils.TreeItemStream;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Control;
+import javafx.util.Callback;
+
+import java.util.List;
+
+public abstract class AbstractTreeItem<T> extends Control {
+    protected final T data;
+    protected final ObservableList<AbstractTreeItem<T>> items = FXCollections.observableArrayList();
+    protected AbstractTreeItem<T> parent;
+
+    private final BooleanProperty startExpanded =  new SimpleBooleanProperty(false);
+
+    protected final ObjectProperty<Callback<AbstractTreeItem<T>, AbstractTreeCell<T>>> cellFactory = new SimpleObjectProperty<>();
+    private TreeView<T> treeView;
+
+    public AbstractTreeItem(T data) {
+        this.data = data;
+    }
+
+    protected abstract void defaultCellFactory();
+    protected abstract void updateChildrenParent(List<? extends AbstractTreeItem<T>> treeItems, final AbstractTreeItem<T> newParent);
+
+    public boolean isRoot() {
+        return this.parent == null;
+    }
+
+    public AbstractTreeItem<T> getRoot() {
+        if (isRoot()) {
+            return this;
+        }
+
+        AbstractTreeItem<T> par = this;
+        while (true) {
+            par = par.getItemParent();
+            if (par.isRoot()) {
+                return par;
+            }
+        }
+    }
+
+    public long getIndex() {
+        if (isRoot()){
+            return 0;
+        }
+
+        return TreeItemStream.flattenTree(getRoot())
+                .takeWhile(item -> !item.equals(this))
+                .count();
+    }
+
+    public long getItemsCount(AbstractTreeItem<T> item) {
+        return TreeItemStream.stream(item).count();
+    }
+
+    public int getLevel() {
+        if (isRoot()) {
+            return 0;
+        }
+
+        int index = 0;
+        AbstractTreeItem<T> par = this;
+        while (true) {
+            par = par.getItemParent();
+            index++;
+            if (par.isRoot()) {
+                return index;
+            }
+        }
+    }
+
+    public TreeView<T> getTreeView() {
+        if (isRoot()) {
+            if (treeView != null) return treeView;
+            throw new NullPointerException("TreeView is not set. Before calling this method set this item as the root of a TreeView");
+        } else {
+            return getRoot().getTreeView();
+        }
+    }
+
+    // TODO warning
+    public void setTreeView(TreeView<T> treeView) {
+        this.treeView = treeView;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public ObservableList<AbstractTreeItem<T>> getItems() {
+        return items;
+    }
+
+    public AbstractTreeItem<T> getItemParent() {
+        return this.parent;
+    }
+
+    protected void setItemParent(AbstractTreeItem<T> parent) {
+        this.parent = parent;
+    }
+
+    public boolean isStartExpanded() {
+        return startExpanded.get();
+    }
+
+    public BooleanProperty startExpandedProperty() {
+        return startExpanded;
+    }
+
+    public void setStartExpanded(boolean startExpanded) {
+        this.startExpanded.set(startExpanded);
+    }
+
+    public Callback<AbstractTreeItem<T>, AbstractTreeCell<T>> getCellFactory() {
+        return cellFactory.get();
+    }
+
+    public ObjectProperty<Callback<AbstractTreeItem<T>, AbstractTreeCell<T>>> cellFactoryProperty() {
+        return cellFactory;
+    }
+
+    public void setCellFactory(Callback<AbstractTreeItem<T>, AbstractTreeCell<T>> cellFactory) {
+        this.cellFactory.set(cellFactory);
+    }
+}

+ 92 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/SimpleTreeCell.java

@@ -0,0 +1,92 @@
+package io.github.palexdev.materialfx.controls.cell;
+
+import io.github.palexdev.materialfx.controls.TreeItem;
+import io.github.palexdev.materialfx.controls.base.AbstractTreeCell;
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+
+public class SimpleTreeCell<T> extends AbstractTreeCell<T> {
+
+    public SimpleTreeCell(T data) {
+        super(data);
+    }
+
+    public SimpleTreeCell(T data, double fixedHeight) {
+        super(data, fixedHeight);
+    }
+
+    @Override
+    protected void initialize() {
+        super.initialize();
+
+        defaultDisclosureNode();
+        getChildren().add(0, getDisclosureNode());
+
+        disclosureNode.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                getChildren().set(0, (Node) newValue);
+            }
+        });
+    }
+
+    @Override
+    protected void defaultDisclosureNode() {
+        StackPane disclosureNode = new StackPane();
+        disclosureNode.getStyleClass().setAll("disclosure-node");
+        disclosureNode.setPrefSize(22, 22);
+        NodeUtils.makeRegionCircular(disclosureNode, 9.5);
+
+        RippleGenerator generator = new RippleGenerator(disclosureNode);
+        disclosureNode.getChildren().add(0, generator);
+        disclosureNode.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            generator.setGeneratorCenterX(disclosureNode.getWidth() / 2);
+            generator.setGeneratorCenterY(disclosureNode.getHeight() / 2);
+            generator.createRipple();
+        });
+        setDisclosureNode(disclosureNode);
+    }
+
+    @Override
+    public StackPane getDisclosureNode() {
+        return (StackPane) disclosureNode.get();
+    }
+
+    @Override
+    public <N extends Node> void setDisclosureNode(N node) {
+        disclosureNode.set(node);
+    }
+
+    @Override
+    protected void render(T data) {
+        if (data instanceof Node) {
+            getChildren().add((Node) data);
+        } else {
+            Label label = new Label(data.toString());
+            label.getStyleClass().add("data-label");
+            getChildren().add(label);
+        }
+    }
+
+    @Override
+    public void updateCell(TreeItem<T> item) {
+        StackPane disclosureNode = getDisclosureNode();
+        RippleGenerator generator = (RippleGenerator) disclosureNode.lookup(".ripple-generator");
+
+        if (!item.getItems().isEmpty()) {
+            MFXFontIcon icon = new MFXFontIcon("mfx-chevron-right", 12.5);
+            icon.getStyleClass().add("disclosure-icon");
+            disclosureNode.getChildren().setAll(generator, icon);
+        } else {
+            getDisclosureNode().getChildren().setAll(generator);
+        }
+
+        if (item.isStartExpanded()) {
+            disclosureNode.setRotate(90);
+        }
+    }
+}

+ 207 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/TreeItemSkin.java

@@ -0,0 +1,207 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.TreeItem;
+import io.github.palexdev.materialfx.controls.base.AbstractTreeCell;
+import io.github.palexdev.materialfx.controls.base.AbstractTreeItem;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.animation.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.scene.Node;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static io.github.palexdev.materialfx.controls.TreeItem.TreeItemEvent;
+
+public class TreeItemSkin<T> extends SkinBase<TreeItem<T>> {
+    private final VBox box;
+    private final AbstractTreeCell<T> cell;
+    private final ListChangeListener<AbstractTreeItem<T>> itemsListener;
+
+    private final Interpolator interpolator;
+    private Timeline animation;
+
+    private boolean forcedUpdate = false;
+
+    @SuppressWarnings("SuspiciousMethodCalls")
+    public TreeItemSkin(TreeItem<T> item) {
+        super(item);
+
+        cell = createCell();
+        box = new VBox(cell);
+        box.setMinHeight(Region.USE_PREF_SIZE);
+        box.setMaxHeight(Region.USE_PREF_SIZE);
+
+        item.setInitialHeight(NodeUtils.getNodeHeight(box));
+        getChildren().add(box);
+        box.setPrefHeight(item.getInitialHeight());
+
+        Rectangle clip = new Rectangle();
+        clip.widthProperty().bind(box.widthProperty());
+        clip.heightProperty().bind(box.heightProperty());
+        box.setClip(clip);
+
+        interpolator = Interpolator.SPLINE(0.0825D, 0.3025D, 0.0875D, 0.9975D);
+
+        itemsListener = change -> {
+            List<AbstractTreeItem<T>> tmpRemoved = new ArrayList<>();
+            List<AbstractTreeItem<T>> tmpAdded = new ArrayList<>();
+
+            while (change.next()) {
+                tmpRemoved.addAll(change.getRemoved());
+                tmpAdded.addAll(change.getAddedSubList());
+            }
+
+            box.getChildren().removeAll(tmpRemoved);
+            if (!tmpAdded.isEmpty() && item.isExpanded()) {
+                box.getChildren().addAll(tmpAdded);
+                FXCollections.sort(box.getChildren(), Comparator.comparingInt(item.getItems()::indexOf));
+            }
+            //cell.updateCell(item);
+        };
+        addListeners();
+
+        if (item.isStartExpanded() && !item.getItems().isEmpty()) {
+            forcedUpdate = true;
+            box.getChildren().addAll(item.getItems());
+            box.applyCss();
+            box.layout();
+            box.setPrefHeight(item.getInitialHeight() + computeExpandCollapse());
+            cell.updateCell(item);
+            item.setExpanded(true);
+            forcedUpdate = false;
+        }
+    }
+
+    protected void checkStartExpanded() {
+        TreeItem<T> item = getSkinnable();
+
+
+    }
+
+    private void addListeners() {
+        TreeItem<T> item = getSkinnable();
+
+        item.getItems().addListener(itemsListener);
+        item.expandedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!forcedUpdate) {
+                updateDisplay();
+            }
+        });
+
+        item.addEventHandler(TreeItemEvent.EXPAND_EVENT, expandEvent -> {
+            buildAnimation((item.getHeight() + expandEvent.getValue()));
+            animation.setOnFinished(event -> System.out.println("Final:" + item.getHeight()));
+            animation.play();
+
+            if (item.getItemParent() == null) {
+                expandEvent.consume();
+            }
+        });
+
+        item.addEventHandler(TreeItemEvent.COLLAPSE_EVENT, collapseEvent -> {
+            buildAnimation((item.getHeight() - collapseEvent.getValue()));
+            if (collapseEvent.getItem() == item) {
+                animation.setOnFinished(event -> box.getChildren().subList(1, box.getChildren().size()).clear());
+            }
+            animation.play();
+        });
+
+        //================================================================================
+        // DEBUG
+        //================================================================================
+        //debugListeners(); // TODO remove
+    }
+
+    protected void updateDisplay() {
+        TreeItem<T> item = getSkinnable();
+
+        if (item.isExpanded()) {
+            box.getChildren().addAll(item.getItems());
+            box.applyCss();
+            box.layout();
+            item.fireEvent(new TreeItemEvent<>(TreeItemEvent.EXPAND_EVENT, item, computeExpandCollapse()));
+        } else {
+            item.fireEvent(new TreeItemEvent<>(TreeItemEvent.COLLAPSE_EVENT, item, computeExpandCollapse()));
+        }
+    }
+
+    protected void buildAnimation(double fHeight) {
+        TreeItem<T> item = getSkinnable();
+
+        KeyValue expCollValue = new KeyValue(box.prefHeightProperty(), fHeight, interpolator);
+        KeyFrame expCollFrame = new KeyFrame(Duration.millis(item.getAnimationDuration()), expCollValue);
+        KeyValue disclosureValue = new KeyValue(cell.getDisclosureNode().rotateProperty(), (item.isExpanded() ? 90 : 0), interpolator);
+        KeyFrame disclosureFrame = new KeyFrame(Duration.millis(250), disclosureValue);
+        animation = new Timeline(expCollFrame, disclosureFrame);
+
+        item.animationRunningProperty().bind(animation.statusProperty().isEqualTo(Animation.Status.RUNNING));
+    }
+
+    private boolean animationIsRunning() {
+        TreeItem<T> item = getSkinnable();
+        List<TreeItem<T>> tmp = new ArrayList<>();
+        while (item != null) {
+            tmp.add(item);
+            item = (TreeItem<T>) item.getItemParent();
+        }
+
+        for (TreeItem<T> i : tmp) {
+            if (i != null && i.isAnimationRunning()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected double computeExpandCollapse() {
+        TreeItem<T> item = getSkinnable();
+        double value = item.getItems().stream().mapToDouble(AbstractTreeItem::getHeight).sum();
+
+        if (item.isRoot() && !forcedUpdate && !item.isExpanded()) {
+            value = item.getHeight() - item.getInitialHeight();
+        }
+        return value;
+    }
+
+    protected AbstractTreeCell<T> createCell() {
+        TreeItem<T> item = getSkinnable();
+
+        AbstractTreeCell<T> cell = item.getCellFactory().call(item);
+        Node disclosureNode = cell.getDisclosureNode();
+        disclosureNode.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            if (animationIsRunning()) {
+                return;
+            }
+
+            item.setExpanded(!item.isExpanded());
+        });
+        cell.updateCell(item);
+
+        return cell;
+    }
+
+    //================================================================================
+    // DEBUG
+    //================================================================================
+    private void debugListeners() {
+        TreeItem<T> item = getSkinnable();
+        item.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            System.out.println("\n-------------------------------------------");
+            System.out.println("DATA:" + item.getData());
+            System.out.println("CELL:" + cell.getHeight());
+            System.out.println("ITEM:" + item.getHeight() + ", " + snapSizeY(item.getHeight()));
+            System.out.println("BOX:" + box.getHeight());
+            System.out.println("-------------------------------------------\n");
+        });
+    }
+}
+

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

@@ -1,6 +1,7 @@
 package io.github.palexdev.materialfx.utils;
 package io.github.palexdev.materialfx.utils;
 
 
 import javafx.geometry.*;
 import javafx.geometry.*;
+import javafx.scene.Group;
 import javafx.scene.Node;
 import javafx.scene.Node;
 import javafx.scene.Scene;
 import javafx.scene.Scene;
 import javafx.scene.layout.*;
 import javafx.scene.layout.*;
@@ -133,6 +134,22 @@ public class NodeUtils {
         }
         }
     }
     }
 
 
+    public static double getNodeWidth(Region region) {
+        Group group = new Group(region);
+        Scene scene = new Scene(group);
+        group.applyCss();
+        group.layout();
+        return region.getWidth();
+    }
+
+    public static double getNodeHeight(Region region) {
+        Group group = new Group(region);
+        Scene scene = new Scene(group);
+        group.applyCss();
+        group.layout();
+        return region.getHeight();
+    }
+
     /* 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
      */
      */

+ 27 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/TreeItemIterator.java

@@ -0,0 +1,27 @@
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.controls.base.AbstractTreeItem;
+
+import java.util.Iterator;
+import java.util.Stack;
+
+public class TreeItemIterator<T> implements Iterator<AbstractTreeItem<T>> {
+    private final Stack<AbstractTreeItem<T>> stack = new Stack<>();
+
+    public TreeItemIterator(AbstractTreeItem<T> root) {
+        stack.push(root);
+    }
+
+    @Override
+    public boolean hasNext() {
+        return !stack.isEmpty();
+    }
+
+    @Override
+    public AbstractTreeItem<T> next() {
+        AbstractTreeItem<T> nextItem = stack.pop();
+        nextItem.getItems().forEach(stack::push);
+
+        return nextItem;
+    }
+}

+ 28 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/TreeItemStream.java

@@ -0,0 +1,28 @@
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.controls.base.AbstractTreeItem;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+public class TreeItemStream {
+    public static <T> Stream<AbstractTreeItem<T>> stream(AbstractTreeItem<T> rootItem) {
+        return asStream(new TreeItemIterator<>(rootItem));
+    }
+
+    private static <T> Stream<AbstractTreeItem<T>> asStream(TreeItemIterator<T> iterator) {
+        Iterable<AbstractTreeItem<T>> iterable = () -> iterator;
+
+        return StreamSupport.stream(
+                iterable.spliterator(),
+                false
+        );
+    }
+
+    public static <T> Stream<AbstractTreeItem<T>> flattenTree(final AbstractTreeItem<T> root) {
+        return Stream.concat(
+                Stream.of(root),
+                root.getItems().stream().flatMap(TreeItemStream::flattenTree)
+        );
+    }
+}

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

@@ -0,0 +1,4 @@
+.mfx-tree-view {
+    -fx-padding: 3;
+    -fx-border-insets: -1;
+}