Issue
UPDATE - Added minimal reproduceable example at end
I am working to migrate a JavaFX 8 application to OpenJFX / JDK 16.
Everything works pretty well, except the application has a TreeTableView control with custom cell factory. The cells are all being laid out left-aligned to the TreeTableView bounds. My expectation is that each would be laid out with the proper indentation for its level in the tree.
Here is the code for the custom TreeTableCell that the cell factory calls.
package com.mycorp.myapp.features.saleshierarchy;
import com.mycorp.myapp.domain.SalesEntity;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.TextAlignment;
public class SalesTreeCell extends TreeTableCell<SalesEntity, SalesEntity> {
private Label codeLabel = new Label();
private Label descriptionLabel = new Label();
private HBox group = new HBox(codeLabel, descriptionLabel);
public SalesTreeCell() {
}
@Override
protected void updateItem(SalesEntity item, boolean empty) {
super.updateItem(item, empty);
if ( empty || item == null ) {
setGraphic(null);
} else {
final Color textColor;
final Color backgroundColor;
textColor = Color.DARKGOLDENROD;
backgroundColor = Color.TRANSPARENT;
codeLabel.setText(item.getDisplayName());
codeLabel.setFont(Font.font("Arial", 12));
codeLabel.setTextFill(textColor);
codeLabel.setBackground(new Background(new BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY)));
descriptionLabel.setText(item.getDisplayDescription());
descriptionLabel.setFont(Font.font("Arial", FontPosture.ITALIC, 10));
descriptionLabel.setTextFill(canControlNode ? Color.DARKSLATEGRAY : Color.GRAY);
group.setSpacing(10);
setGraphic(group);
}
}
}
How can I make OpenJFX 16 align my custom TreeTableCell correctly? I tried various alignments and tried reviewing the Javadocs but nothing has worked so far. I am thinking maybe there is something I could do with a CSS style?
JDK is Amazon Corretto 16, if it matters. (Note: version 16 is forced on us for external reasons).
Minimal reproduceable example:
package com.subaru.SAM.sandbox;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.TreeTableColumn.CellDataFeatures;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.stage.Stage;
public class TestApp extends javafx.application.Application {
// Class for holding tree table data
public static class MyTreeItem {
public String name;
public String description;
public final List<TestApp.MyTreeItem> children = new ArrayList<>();
public MyTreeItem( final String name, final String description, final MyTreeItem parent ) {
this.name = name;
this.description = description;
if ( parent != null ) {
parent.children.add(this); // Bad form to allow this to escape constructor, but this is quick and dirty and we're single threaded anyway.
}
}
@Override
public String toString() {
return this.name + " ... " + this.description;
}
}
// Class for cell factory (to create TreeTableCell objects for TreeTableView)
public static class MyTreeCell extends TreeTableCell<TestApp.MyTreeItem, TestApp.MyTreeItem> {
private Label nameLabel = new Label();
private Label descriptionLabel = new Label();
private HBox group = new HBox(nameLabel, descriptionLabel);
public MyTreeCell() {
}
@Override
protected void updateItem(MyTreeItem item, boolean empty) {
super.updateItem(item, empty);
if ( empty || item == null ) {
setGraphic(null);
} else {
final Color textColor;
final Color backgroundColor;
textColor = Color.DARKGOLDENROD;
backgroundColor = Color.TRANSPARENT;
nameLabel.setText(item.name);
nameLabel.setFont(Font.font("Arial", 12)); // FIXME Make this a preference
nameLabel.setTextFill(textColor);
nameLabel.setBackground(new Background(new BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY)));
descriptionLabel.setText(item.description);
descriptionLabel.setFont(Font.font("Arial", FontPosture.ITALIC, 10));
group.setSpacing(10);
setGraphic(group);
}
}
}
@Override
public void start(Stage stage) {
// Make a tree table view
final TreeTableView<MyTreeItem> treeTableView = new TreeTableView<>();
// Set data for tree table
final MyTreeItem root = new MyTreeItem("Root Node","This is the root node", null);
final TreeItem<MyTreeItem> rootTreeItem = new TreeItem<>(root);
for ( int i = 0; i < 5; i++) {
final MyTreeItem nodeI = new MyTreeItem("Node " + i,"This is node " + i, root);
final TreeItem<MyTreeItem> treeItemI = new TreeItem<>(nodeI);
rootTreeItem.getChildren().add(treeItemI);
for ( int j = 0; j < 3; j++ ) {
final MyTreeItem nodeJ = new MyTreeItem("Node " + i + "." + j,"This is node " + i + "." + j, nodeI);
final TreeItem<MyTreeItem> treeItemJ = new TreeItem<>(nodeJ);
treeItemI.getChildren().add(treeItemJ);
for ( int k = 0; k < 10; k++ ) {
final MyTreeItem nodeK = new MyTreeItem("Node " + i + "." + j + "." + k,"This is node " + i + "." + j + "." + k, nodeJ);
final TreeItem<MyTreeItem> treeItemK = new TreeItem<>(nodeK);
treeItemJ.getChildren().add(treeItemK);
}
}
}
rootTreeItem.setExpanded(true);
// Create a column
final TreeTableColumn<MyTreeItem, MyTreeItem> ttc = new TreeTableColumn<>();
// ttc.setMinWidth(600);
ttc.setCellValueFactory((CellDataFeatures<MyTreeItem, MyTreeItem> p) -> {
final TreeItem<MyTreeItem> treeItem = p.getValue();
return new ReadOnlyObjectWrapper<MyTreeItem>(treeItem == null ? null : treeItem.getValue());
});
ttc.setCellFactory(param -> new MyTreeCell());
treeTableView.getColumns().add(ttc);
treeTableView.setRoot(rootTreeItem);
StackPane mainPane = new StackPane(treeTableView);
Scene scene = new Scene(new StackPane(mainPane), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Results:
If I comment out this line:
ttc.setCellFactory(param -> new MyTreeCell());
... the problem does not occur. It seems limited to using a custom TreeTableCell class.
Solution
I'm not sure how this worked with Java 8, but the API for updateItem()
seems to require invoking setText()
for non-empty cells, even if the text is not visible. As @VGR suggests, use a zero-width space:
setText("\u2060");
Alternatively, display the description
as usual and set the name
as a graphic, as shown below.
As @kleopatra verifies, this is a bug, fixed in fx18. See also JDK-8278904—Cell: misleading doc of updateItem.
As tested:
import java.util.ArrayList;
import java.util.List;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.TreeTableColumn.CellDataFeatures;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/** https://stackoverflow.com/q/70369081/230513 */
public class TreeTableApp extends javafx.application.Application {
// Class for holding tree table data
public static class MyTreeItem {
public String name;
public String description;
public final List<MyTreeItem> children = new ArrayList<>();
public MyTreeItem(final String name, final String description, final MyTreeItem parent) {
this.name = name;
this.description = description;
if (parent != null) {
parent.children.add(this);
}
}
@Override
public String toString() {
return this.name + "—" + this.description;
}
}
// Class for cell factory (to create TreeTableCell objects for TreeTableView)
private static class MyTreeCell extends TreeTableCell<MyTreeItem, MyTreeItem> {
private final Label nameLabel = new Label();
@Override
protected void updateItem(MyTreeItem item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
nameLabel.setText(item.name);
nameLabel.setTextFill(Color.DARKGOLDENROD);
setGraphicTextGap(8);
setGraphic(nameLabel);
setText(item.description);
setTextFill(Color.BLACK);
}
}
}
@Override
public void start(Stage stage) {
// Make a tree table view
final TreeTableView<MyTreeItem> treeTableView = new TreeTableView<>();
// Set data for tree table
final MyTreeItem root = new MyTreeItem("Root Node", "This is the root node", null);
final TreeItem<MyTreeItem> rootTreeItem = new TreeItem<>(root);
for (int i = 0; i < 5; i++) {
final MyTreeItem nodeI = new MyTreeItem("Node " + i, "This is node " + i, root);
final TreeItem<MyTreeItem> treeItemI = new TreeItem<>(nodeI);
rootTreeItem.getChildren().add(treeItemI);
for (int j = 0; j < 3; j++) {
final MyTreeItem nodeJ = new MyTreeItem("Node " + i + "." + j, "This is node " + i + "." + j, nodeI);
final TreeItem<MyTreeItem> treeItemJ = new TreeItem<>(nodeJ);
treeItemI.getChildren().add(treeItemJ);
for (int k = 0; k < 10; k++) {
final MyTreeItem nodeK = new MyTreeItem("Node " + i + "." + j + "." + k, "This is node " + i + "." + j + "." + k, nodeJ);
final TreeItem<MyTreeItem> treeItemK = new TreeItem<>(nodeK);
treeItemJ.getChildren().add(treeItemK);
}
}
}
rootTreeItem.setExpanded(true);
// Create a column
final TreeTableColumn<MyTreeItem, MyTreeItem> ttc = new TreeTableColumn<>();
ttc.setCellValueFactory((CellDataFeatures<MyTreeItem, MyTreeItem> p) -> {
final TreeItem<MyTreeItem> treeItem = p.getValue();
return new ReadOnlyObjectWrapper<>(treeItem == null ? null : treeItem.getValue());
});
ttc.setCellFactory(param -> new MyTreeCell());
ttc.setPrefWidth(320);
treeTableView.getColumns().add(ttc);
treeTableView.setRoot(rootTreeItem);
StackPane mainPane = new StackPane(treeTableView);
Scene scene = new Scene(new StackPane(mainPane), 320, 240);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Answered By - trashgod
Answer Checked By - Timothy Miller (JavaFixing Admin)