5

Recently I updated my application from JavaFX 8 to JavaFX 18. After the migration I found some weird issues related to layout of TreeView. If I understand correctly, by the end of a scene pulse, all the nodes (in fact parents) are rendered completely and the isNeedsLayout is turned to false, till the next change occurs.

This is working as expected in JavaFX 8. Whereas in JavaFX 18, for some nodes isNeedsLayout flag is still true even after the pulse is completed. Is this a bug? Or is it deliberately implemented like that?

In the below demo, I tried to print all the nodes (children of TreeView) state after the pulse is completed. And I can clearly see the difference in output between the two JavaFX versions.

Can anyone tell me, how can I ensure that all the nodes are rendered/laid-out correctly.

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TreeViewLayoutIssue extends Application {
    int k = 1;

    @Override
    public void start(Stage primaryStage) throws Exception {
        final TreeView<String> fxTree = new TreeView<>();
        fxTree.setCellFactory(t -> new TreeCell<String>() {

            Label lbl = new Label();

            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                setText(null);
                if (item != null) {
                    lbl.setText(item);
                    setGraphic(lbl);
                } else {
                    setGraphic(null);
                }
            }

            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                if (getItem() != null) {
                    System.out.println("Layouting ::> " + getItem());
                }
            }
        });
        fxTree.setShowRoot(false);

        StackPane root = new StackPane(fxTree);
        root.setPadding(new Insets(15));
        final Scene scene = new Scene(root, 250, 250);
        scene.getStylesheets().add(this.getClass().getResource("treeview.css").toExternalForm());
        primaryStage.setTitle("TreeView FX18");
        primaryStage.setScene(scene);
        primaryStage.show();
        addData(fxTree);
        
        final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(2000), e -> {
            System.out.println("\nIteration #" + k++);
            printNeedsLayout(fxTree);
            System.out.println("-----------------------------------------------------------------------------");
        }));
        timeline.setCycleCount(3);
        timeline.play();
    }

    private void printNeedsLayout(final Parent parent) {
        System.out.println("  " + parent + " isNeedsLayout: " + parent.isNeedsLayout());
        for (final Node n : parent.getChildrenUnmodifiable()) {
            if (n instanceof Parent) {
                printNeedsLayout((Parent) n);
            }
        }
    }

    private void addData(TreeView<String> fxTree) {
        final TreeItem<String> rootNode = new TreeItem<>("");
        fxTree.setRoot(rootNode);
        final TreeItem<String> grp1Node = new TreeItem<>("Group 1");
        final TreeItem<String> grp2Node = new TreeItem<>("Group 2");
        rootNode.getChildren().addAll(grp1Node, grp2Node);

        final TreeItem<String> subNode = new TreeItem<>("Team");
        grp1Node.getChildren().addAll(subNode);

        final List<TreeItem<String>> groups = Stream.of("Red", "Green", "Yellow", "Blue").map(TreeItem::new).collect(Collectors.toList());
        groups.forEach(itm -> subNode.getChildren().add(itm));

        grp1Node.setExpanded(true);
        grp2Node.setExpanded(true);
        subNode.setExpanded(true);
    }
}

treeview.css

.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node > .arrow {
  -fx-background-color: #77797a;
}

.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node {
  -fx-padding: 5px 6px 3px 8px; /* default is 4px 6px 4px 8px */
}

.virtual-flow .clipped-container .sheet .tree-cell:expanded > .tree-disclosure-node {
  -fx-padding: 7px 6px 1px 8px; /* default is 4px 6px 4px 8px */
}

.virtual-flow .clipped-container .sheet .tree-cell:selected > .tree-disclosure-node > .arrow {
  -fx-background-color: #f7f7f7;
}

And the output is as follows:

When selecting the node for the first time, a slight shift in text is observed in FX18. Whereas there is no issue with FX8.

enter image description here

Output (JavaFX 8): All the node's isNeedsLayout is false.

  TreeView@7abc5bb4[styleClass=tree-view] isNeedsLayout: false
  VirtualFlow[id=virtual-flow, styleClass=virtual-flow] isNeedsLayout: false
  VirtualFlow$ClippedContainer@187e8c80[styleClass=clipped-container] isNeedsLayout: false
  Group@57d7468f[styleClass=sheet] isNeedsLayout: false
  TreeViewLayoutIssue$1@11a5c9a9[styleClass=cell indexed-cell tree-cell]'Group 1' isNeedsLayout: false
  StackPane@7c0429[styleClass=tree-disclosure-node] isNeedsLayout: false
  StackPane@731322a7[styleClass=arrow] isNeedsLayout: false
  TreeViewLayoutIssue$1@198e401a[styleClass=cell indexed-cell tree-cell]'Team' isNeedsLayout: false
  StackPane@e2e9b3[styleClass=tree-disclosure-node] isNeedsLayout: false
  StackPane@18615368[styleClass=arrow] isNeedsLayout: false
  TreeViewLayoutIssue$1@1632f423[styleClass=cell indexed-cell tree-cell]'Red' isNeedsLayout: false
  TreeViewLayoutIssue$1@1f706faa[styleClass=cell indexed-cell tree-cell]'Green' isNeedsLayout: false
  TreeViewLayoutIssue$1@2fc143cc[styleClass=cell indexed-cell tree-cell]'Yellow' isNeedsLayout: false
  TreeViewLayoutIssue$1@6cd95e88[styleClass=cell indexed-cell tree-cell]'Blue' isNeedsLayout: false
  TreeViewLayoutIssue$1@1d2b9016[styleClass=cell indexed-cell tree-cell]'Group 2' isNeedsLayout: false
  TreeViewLayoutIssue$1@7ed38c68[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  TreeViewLayoutIssue$1@4a73e93a[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  TreeViewLayoutIssue$1@2a65b6dd[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  Group@2c481877 isNeedsLayout: false
  TreeViewLayoutIssue$1@1621d92f[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  VirtualScrollBar@9a98f02[styleClass=scroll-bar] isNeedsLayout: false
  StackPane@343dbe88[styleClass=track-background] isNeedsLayout: false
  ScrollBarSkin$2@4727ac31[styleClass=increment-button] isNeedsLayout: false
  Region@6d0e770[styleClass=increment-arrow] isNeedsLayout: false
  ScrollBarSkin$3@201819d7[styleClass=decrement-button] isNeedsLayout: false
  Region@38d26811[styleClass=decrement-arrow] isNeedsLayout: false
  StackPane@2e0a5131[styleClass=track] isNeedsLayout: false
  ScrollBarSkin$1@771a6386[styleClass=thumb] isNeedsLayout: false
  VirtualScrollBar@3177bd8a[styleClass=scroll-bar] isNeedsLayout: false
  StackPane@39ae89a6[styleClass=track-background] isNeedsLayout: false
  ScrollBarSkin$2@3393f75c[styleClass=increment-button] isNeedsLayout: false
  Region@20002045[styleClass=increment-arrow] isNeedsLayout: false
  ScrollBarSkin$3@1230eb63[styleClass=decrement-button] isNeedsLayout: false
  Region@5dcf6e46[styleClass=decrement-arrow] isNeedsLayout: false
  StackPane@342b0a3d[styleClass=track] isNeedsLayout: false
  ScrollBarSkin$1@798546a7[styleClass=thumb] isNeedsLayout: false
  StackPane@7094a28f[styleClass=corner] isNeedsLayout: false

Output (JavaFX 18): Some node's isNeedsLayout is still true.

  TreeView@6d663ddb[styleClass=tree-view] isNeedsLayout: true
  VirtualFlow[id=virtual-flow, styleClass=virtual-flow] isNeedsLayout: false
  VirtualFlow$ClippedContainer@25755334[styleClass=clipped-container] isNeedsLayout: false
  Group@5ec62b29[styleClass=sheet] isNeedsLayout: true
  TreeViewLayoutIssue$1@61b62840[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  TreeViewLayoutIssue$1@1995039d[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  TreeViewLayoutIssue$1@5401ae6f[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
  TreeViewLayoutIssue$1@2d51271e[styleClass=cell indexed-cell tree-cell]'Group 2' isNeedsLayout: true
  TreeViewLayoutIssue$1@500b9275[styleClass=cell indexed-cell tree-cell]'Blue' isNeedsLayout: true
  TreeViewLayoutIssue$1@342e3899[styleClass=cell indexed-cell tree-cell]'Yellow' isNeedsLayout: true
  TreeViewLayoutIssue$1@68c87749[styleClass=cell indexed-cell tree-cell]'Green' isNeedsLayout: true
  TreeViewLayoutIssue$1@18ed5cce[styleClass=cell indexed-cell tree-cell]'Red' isNeedsLayout: true
  TreeViewLayoutIssue$1@2f10a091[styleClass=cell indexed-cell tree-cell]'Team' isNeedsLayout: true
  StackPane@16cb362[styleClass=tree-disclosure-node] isNeedsLayout: true
  StackPane@7b6a2594[styleClass=arrow] isNeedsLayout: false
  TreeViewLayoutIssue$1@70c30d4[styleClass=cell indexed-cell tree-cell]'Group 1' isNeedsLayout: true
  StackPane@698dbc70[styleClass=tree-disclosure-node] isNeedsLayout: true
  StackPane@45cafbcd[styleClass=arrow] isNeedsLayout: false
  Group@461a2cd9 isNeedsLayout: false
  VirtualScrollBar@13f48448[styleClass=scroll-bar] isNeedsLayout: false
  StackPane@1dea4632[styleClass=track-background] isNeedsLayout: false
  ScrollBarSkin$2@6acd8e11[styleClass=increment-button] isNeedsLayout: false
  Region@5ab4b98c[styleClass=increment-arrow] isNeedsLayout: false
  ScrollBarSkin$3@36602364[styleClass=decrement-button] isNeedsLayout: false
  Region@178b3dbd[styleClass=decrement-arrow] isNeedsLayout: false
  StackPane@d25f45e[styleClass=track] isNeedsLayout: false
  ScrollBarSkin$1@4231e81e[styleClass=thumb] isNeedsLayout: false
  VirtualScrollBar@6662e7e5[styleClass=scroll-bar] isNeedsLayout: false
  StackPane@63f40cf6[styleClass=track-background] isNeedsLayout: false
  ScrollBarSkin$2@36c6ae74[styleClass=increment-button] isNeedsLayout: false
  Region@261de839[styleClass=increment-arrow] isNeedsLayout: false
  ScrollBarSkin$3@7f53c4b5[styleClass=decrement-button] isNeedsLayout: false
  Region@6c2c3d1f[styleClass=decrement-arrow] isNeedsLayout: false
  StackPane@721aca85[styleClass=track] isNeedsLayout: false
  ScrollBarSkin$1@314afb8c[styleClass=thumb] isNeedsLayout: false
  StackPane@5183e9a5[styleClass=corner] isNeedsLayout: false
Sai Dandem
  • 8,229
  • 11
  • 26
  • 2
    interesting.. there had been several changes in the layout code of virtualFlow and there are still open issues with treecell layout.. this might be a hint to the/part of a reason. – kleopatra Jun 15 '22 at 05:54
  • do you have an example of the broken layout you are experiencing? – kleopatra Jun 15 '22 at 05:56
  • 1
    hmm .. can reproduce in fx11 and current, so not added by any recent change (in both, the exact pattern depends on when the data is added, either before or after showing). Wondering though if that could be important: what we see here is a false "true" - that is the layout happens more often, shouldn't break anything. Would expect only a false "false" to break layout (because nothing happens when it should). – kleopatra Jun 15 '22 at 09:21
  • 1
    @kleopatra I mainly rely on our custom framework which picks/inspects nodes that are fully rendered. And this inconsistent state of nodes is now breaking the framework. Having said that, I updated the code to include some css changes. You can see in the gif, that the layout is readjusted correctly only after I select a node. (a slight shift in text is observed in FX18 version, whereas FX8 has no issues.) – Sai Dandem Jun 15 '22 at 23:53
  • okay, I see the slight offset movement - thanks for the example :) You might consider filing an issue, so we don't forget to look into it. – kleopatra Jun 16 '22 at 08:57
  • actually, I see it without the custom css – kleopatra Jun 16 '22 at 09:05
  • one last observation: I don't see the movement in fx11, even though the needsLayout flag is true initially ;) Weird .. – kleopatra Jun 16 '22 at 09:33

2 Answers2

4

One observation regarding the shift in text:

If I add the data to treeView after showing the stage, the layout of the cells is in opposite direction to the layout of the cells when adding data before showing the stage.

Case#1: Layout of cells when data is added before showing the stage:

Layouting ::> Group 1
Layouting ::> Team
Layouting ::> Red
Layouting ::> Green
Layouting ::> Yellow
Layouting ::> Blue
Layouting ::> Group 2

Case#2: Layout of cells when data is added after showing the stage:

Layouting ::> Group 2
Layouting ::> Blue
Layouting ::> Yellow
Layouting ::> Green
Layouting ::> Red
Layouting ::> Team
Layouting ::> Group 1

I can see that this is another issue in TreeView. To breifly explain the issue:

  • TreeCellSkin internally maintains a static map(maxDisclosureWidthMap) to keep track of the widest disclosure node width in a TreeView.
  • Lets say I customized the disclosure node width (via css) to have width greater than 18px (18px is the hardcoded value in TreeCellSkin !!).
  • If the nodes are laid-out from bottom to top (case#2), it uses the default 18px disclosure node width for all the cells till it finds the first arrow (here it is 'Team').
  • Once it finds a width greater than the default width, it updates the map. But there is no code to recompute the already computed cells !!.
  • When the next layout request comes, now all the cells are computed with the updated disclosure-node width. And that is the reason for the shift in text.

To prove this, if I update my css to have a larger left-padding to the disclosure node (say 50px). In the below gif, you can see that, the cells above "Team" are laid-out correctly. And when I select the treeView(which forces a layout request), then the other cells use the correct disclosure node width and shifts the text.

.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node {
  -fx-padding: 5px 6px 3px 50px; /* default is 4px 6px 4px 8px */
}

.virtual-flow .clipped-container .sheet .tree-cell:expanded > .tree-disclosure-node {
  -fx-padding: 7px 6px 1px 50px; /* default is 4px 6px 4px 8px */
}

enter image description here

My final solution: [Outdated][see another solution below]

Considering all the above issues (inconsistent isNeedsLayout, text shift.. etc), I came up with the below solution.

The idea is to start an AnimationTimer to keep checking if any of of the child nodes isNeedsLayout is true. If it is true, then it forces to layout by calling the layout() method. Once I add the below code after my treeView initialiation, all the issues are fixed.

// Code to add after initializing TreeView
new AnimationTimer() {
    @Override
    public void handle(final long now) {
        forceLayout(fxTree);
    }
}.start();

private void forceLayout(final Parent parent) {
    for (final Node n : parent.getChildrenUnmodifiable()) {
        if (n instanceof final Parent p) {
            forceLayout(p);
        }
    }
    if (parent.isNeedsLayout()) {
        parent.layout();
    }
}

Now my next biggest fear is: Will it degrade the performance ??

[Update] Alternate Solution:

Though the answer provided by @kleopatra fixes the issue, I can still see a quick layout jump when the window is opened. It is quite obvious behaviour, as we are requesting layout in some unknown time in future(Platform.runLater).

To get rid of this effect, I need to ensure that the layout is corrected with in the same pulse. So I came with a solution to forceLayout (my previous solution) at the end of the pulse whenever there is a change in disclosure node width(@kleopatra's solution)

The new solution is :

  • Whenever there is a change in disclosure node width, we register a listener in scene's postLayoutPulseListener to force layout the treeView .
  • After the layoutPass in the pulse, this postLayoutPulseListener will force the layout of all children in the treeView.
  • Once the force layout is done, we remove this listener to ensure that this will not trigger in next pulses.

Below is the complete working code of the example with the new solution (using the same treeview.css):

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.skin.TreeCellSkin;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TreeViewLayoutIssue extends Application {
    int k = 1;

    /**
     * Utility class to hack around JDK-8288665: broken layout of
     * nested TreeCells.
     */
    public class DisclosureNodeHack {

        /**
         * Key for max disclosure node width
         */
        public static final String DISCLOSURE_NODE_WIDTH = "disclosureNodeWidth";


        public static class HackedTreeCell<String> extends TreeCell<String> {

            Label lbl = new Label();

            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                setText(null);
                if (item != null) {
                    lbl.setText((java.lang.String) item);
                    setGraphic(lbl);
                } else {
                    setGraphic(null);
                }
            }

            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                if (getItem() != null) {
                    System.out.println("Laid-out TreeCell ::> " + getItem());
                }
            }

            @Override
            protected Skin<?> createDefaultSkin() {
                return new DisclosureNodeHack.HackedTreeCellSkin<>(this);
            }

            public final HackedTreeView<String> getHackedTreeView() {
                return (HackedTreeView<String>) getTreeView();
            }
        }

        /**
         * Custom skin that puts the width of the disclosure node in the
         * Tree's properties.
         */
        public static class HackedTreeCellSkin<T> extends TreeCellSkin<T> {

            HackedTreeCell<T> cell;

            public HackedTreeCellSkin(HackedTreeCell<T> control) {
                super(control);
                cell = control;
            }

            @Override
            protected void layoutChildren(double x, double y, double w, double h) {
                super.layoutChildren(x, y, w, h);
                if (getSkinnable().getTreeItem() == null || getSkinnable().getTreeView() == null) return;
                Node disclosure = getSkinnable().lookup(".tree-disclosure-node");
                if (disclosure instanceof Region) {
                    double width = ((Region) disclosure).getWidth();
                    Object prevWidth = getSkinnable().getTreeView().getProperties().get(DISCLOSURE_NODE_WIDTH);
                    getSkinnable().getTreeView().getProperties().put(DISCLOSURE_NODE_WIDTH, width);
                    if (prevWidth == null || ((Double) prevWidth).doubleValue() != width) {
                        cell.getHackedTreeView().installListener();
                    }
                }
            }
        }

        public static class HackedTreeView<T> extends TreeView<T> {
            private Runnable listener = new Runnable() {
                @Override
                public void run() {
                    System.out.println("------ Forcing Layout ------");
                    forceLayout(HackedTreeView.this);
                    getScene().removePostLayoutPulseListener(this);
                }
            };

            public HackedTreeView() {
                setCellFactory(t -> new DisclosureNodeHack.HackedTreeCell());
            }

            private void forceLayout(final Parent parent) {
                for (final Node n : parent.getChildrenUnmodifiable()) {
                    if (n instanceof final Parent p) {
                        forceLayout(p);
                    }
                }
                if (parent.isNeedsLayout()) {
                    parent.layout();
                }
            }

            public final void installListener() {
                getScene().removePostLayoutPulseListener(listener);
                getScene().addPostLayoutPulseListener(listener);
            }
        }

        private DisclosureNodeHack() {
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        final DisclosureNodeHack.HackedTreeView<String> fxTree = new DisclosureNodeHack.HackedTreeView<>();
        fxTree.setShowRoot(false);

        StackPane root = new StackPane(fxTree);
        root.setPadding(new Insets(15));
        final Scene scene = new Scene(root, 250, 250);
        scene.getStylesheets().add(this.getClass().getResource("treeview.css").toExternalForm());
        primaryStage.setTitle("TreeView FX18");
        primaryStage.setScene(scene);
        primaryStage.show();
        addData(fxTree);

        final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(2000), e -> {
            System.out.println("\nIteration #" + k++);
            printNeedsLayout(fxTree);
            System.out.println("-----------------------------------------------------------------------------");
        }));
        timeline.setCycleCount(1);
        timeline.play();
    }

    private void printNeedsLayout(final Parent parent) {
        System.out.println("  " + parent + " isNeedsLayout: " + parent.isNeedsLayout());
        for (final Node n : parent.getChildrenUnmodifiable()) {
            if (n instanceof Parent) {
                printNeedsLayout((Parent) n);
            }
        }
    }

    private void addData(TreeView<String> fxTree) {
        final TreeItem<String> rootNode = new TreeItem<>("");
        fxTree.setRoot(rootNode);
        final TreeItem<String> grp1Node = new TreeItem<>("Group 1");
        final TreeItem<String> grp2Node = new TreeItem<>("Group 2");
        rootNode.getChildren().addAll(grp1Node, grp2Node);

        final TreeItem<String> subNode = new TreeItem<>("Team");
        grp1Node.getChildren().addAll(subNode);

        final List<TreeItem<String>> groups = Stream.of("Red", "Green", "Yellow", "Blue").map(TreeItem::new).collect(Collectors.toList());
        groups.forEach(itm -> subNode.getChildren().add(itm));

        grp1Node.setExpanded(true);
        grp2Node.setExpanded(true);
        subNode.setExpanded(true);
    }
}
Sai Dandem
  • 8,229
  • 11
  • 26
  • 2
    nice trackdown :) and really time for a bug report... *cough, will do later today. as to the AnimationTimer: would be extremely weary to have it in production code – kleopatra Jun 17 '22 at 08:07
  • 1
    the issue: https://bugs.openjdk.org/browse/JDK-8288665 – kleopatra Jun 17 '22 at 12:26
  • 1
    my learn item of the day, pre/postLayoutPulseListener - didn't even know such a beast existed :) – kleopatra Jun 23 '22 at 08:38
3

As already noted in my comment: it's a bug. The underlying reason seems to be sub-optimal cell layout, in particular the layout of the disclosure node (see Sai's self-answer for details). This bug bubbled up after some optimization of layout in VirtualFlow - which still is correct, IMO, but exposed the mis-behavior of cell layout.

An alternative to constantly checking the layout tree in an AnimationTimer is to implement a collaboration between cell and tree:

  • a custom cell skin sets the disclosure node width in the tree's properties
  • the tree listens to the property change and forces a layout of the cell's parent

Below is a utility class providing support for both. It relies on the style tree which is not fully specified for TreeView (but is similar to ListView which is specified) and Node lookup, otherwise uses only public api. Choosing the one or other hack is a matter of personal preferences.

To use the utility:

// in application code
DisclosureNodeHack.installListener(tree);

// set the custom treeCell skin as default via css
.tree-cell {
    -fx-skin: "mypackage.DisclosureNodeHack$HackedTreeCellSkin"; 
}
// for visualization when debugging
.tree-cell > .tree-disclosure-node {
    -fx-padding: 4 6 4 50;
    -fx-background-color: yellow;
}

The utility class (beware: not formally tested, and most probably with much leeway to improve):

/**
 * Utility class to hack around JDK-8288665: broken layout of
 * nested TreeCells.
 */
public class DisclosureNodeHack {

    /** Key for max disclosure node width */
    public static final String DISCLOSURE_NODE_WIDTH = "disclosureNodeWidth";

    /**
     * Custom skin that puts the width of the disclosure node in the
     * Tree's properties.
     */
    public static class HackedTreeCellSkin<T> extends TreeCellSkin<T> {

        public HackedTreeCellSkin(TreeCell<T> control) {
            super(control);
        }

        @Override
        protected void layoutChildren(double x, double y, double w, double h) {
            super.layoutChildren(x, y, w, h);
            if (getSkinnable().getTreeItem() == null || getSkinnable().getTreeView() == null) return;
            Node disclosure = getSkinnable().lookup(".tree-disclosure-node");
            if (disclosure instanceof Region) {
                double width = ((Region) disclosure).getWidth();
                getSkinnable().getTreeView().getProperties().put(DISCLOSURE_NODE_WIDTH, width);
            }
        }
    }

    /**
     * Utility method to register a listener to the tree's properties and
     * forces a re-layout of the flow's sheet if the disclosure width changes.
     *
     * Note: experiments seem to indicate that layout must be done
     * - after the current layout run is ready, that is in Platform.runlater
     * - on the parent of the cell (== sheet), not on the tree
     */
    public static <T> void installListener(TreeView<T> tree) {
        tree.getProperties().addListener((MapChangeListener<Object,Object>)c -> {
            if (DISCLOSURE_NODE_WIDTH.equals(c.getKey())) {
                Platform.runLater(() -> {
                    Node sheet = tree.lookup(".sheet");
                    if (sheet instanceof Parent)
                        ((Parent) sheet).requestLayout();
                });
            }
        });

    };

    private DisclosureNodeHack() {};
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • 1
    thanks for providing the alternate solution :). Indeed this fixes the issue, but with a small glitch. As we are trying to requestLayout in Platform.runLater, I can see a quick flash/jump in the layout when the window is opened. As this effect can be visually seen, I need to still address this issue. So I came up with another solution : Your solution + my solution + postLayoutPulseListener. I updated my answer with the details. – Sai Dandem Jun 23 '22 at 01:01
  • As your answer has guided to my final solution, I am accepting your answer as answered :) – Sai Dandem Jun 23 '22 at 01:03
  • 1
    you are right - runLater may have undesirable side-effects, a more deterministic approach like your's definitely better. Please accept your own answer, as this is only a step into direction of a solution - future readers will want the complete solution :) – kleopatra Jun 23 '22 at 08:35