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.
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
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:
Case#2: Layout of cells when data is added after showing the stage:
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 aTreeView
.TreeCellSkin
!!).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.
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 nodesisNeedsLayout
is true. If it is true, then it forces to layout by calling thelayout()
method. Once I add the below code after my treeView initialiation, all the issues are fixed.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 :
Below is the complete working code of the example with the new solution (using the same treeview.css):