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