javajavafxcelltablecolumntruncated

How to set tooltip ONLY on truncated cell dynamically


My goal is to display a tooltip on cells where the text is truncated at launch, but also during runtime, which means that if a column is resized, some tool tip may appear or dissapear.

In my cell factory, I thought there will be a property that I could bind or listen to, and simply link a tooltip to this, but I found none.

Here's my code that set a tool tip on every cell in the cell factory:

public void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);

    if (empty || item == null) {
        setText(null);
        setGraphic(null);
    } else {
        setText(adapter.getCellContentParser().toString(item));
        setTooltip(new Tooltip(this.getText());
    }
}

Please note that I look for every possible solution on the site, like these ones : https://stackoverflow.com/questions/15683907/javafx-tableviews-cellvalue-is-not-enough-to-display-in-columns-it-will-be-cl#=

How to show tooltip only on those table cells which text is ellipsized (...)?

And they are perfectly fine when you launch your program, but don't work when you resizing your columns.

Is there something I'm missing ? I find it really strange that there is not an observableproperty for this kind of thing.


Solution

  • This is a bit of a hack, but you can retrieve the child Text node in the cell by searching the scene graph below the cell after layout is performed, and then create a binding for the tooltip that checks if the cell's text is the same as the text's text. Note that we ignore any graphic set on the cell here, so this only searches for the Text node displaying the cell's text property:

    setCellFactory(tc -> new TableCell<>() {
        private Text text ;
        private Tooltip tooltip = new Tooltip() ;
    
        {
            tooltip.textProperty().bind(textProperty());
        }
    
        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
    
            if (item == null || empty) {
                setText("");
            } else {
                setText(item);
            }
        }
    
        @Override
        protected void layoutChildren() {
            super.layoutChildren();
            if (text == null) {
                text = findTextNode(this);
                bindTooltip();
            }               
        }
    
        private void bindTooltip() {
            tooltipProperty().bind(Bindings.createObjectBinding(() -> {
                if (getItem()==null) return null ;
                if (getText().equals(text.getText())) return null ;
                return tooltip ;
            }, text.textProperty(), itemProperty(), textProperty()));
        }
    
        // recursively search through child node graph to find the Text node
        private Text findTextNode(Node root) {
            // Ignore the graphic and all its child nodes:
            if (root == getGraphic()) {
                return null ;
            }
            if (root instanceof Text) {
                return (Text) root ;
            }
            if (root instanceof Parent) {
                for (Node node : ((Parent)root).getChildrenUnmodifiable()) {
                    Text text = findTextNode(node);
                    if (text != null) return text ;
                }
            }
            return null ;
        }
    });
    

    Here's a complete working example:

    import java.util.Random;
    import java.util.function.Function;
    
    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.Property;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.scene.Node;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.Tooltip;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    
    /**
     * JavaFX App
     */
    public class App extends Application {
    
        @Override
        public void start(Stage stage) {
    
            var table = new TableView<Item>();
            var itemCol = column("Item", Item::nameProperty);
            var valueCol = column("Value", Item::valueProperty);
            table.getColumns().add(itemCol);
            table.getColumns().add(valueCol);
    
            itemCol.setCellFactory(tc -> new TableCell<>() {
                private Text text ;
                private Tooltip tooltip = new Tooltip() ;
    
                {
                    tooltip.textProperty().bind(textProperty());
                }
    
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
    
                    if (item == null || empty) {
                        setText("");
                    } else {
                        setText(item);
                    }
                }
    
                @Override
                protected void layoutChildren() {
                    super.layoutChildren();
                    if (text == null) {
                        text = findTextNode(this);
                        bindTooltip();
                    }               
                }
    
                private void bindTooltip() {
                    tooltipProperty().bind(Bindings.createObjectBinding(() -> {
                        if (getItem()==null) return null ;
                        if (getText().equals(text.getText())) return null ;
                        return tooltip ;
                    }, text.textProperty(), itemProperty(), textProperty()));
                }
    
                // recursively search through child node graph to find the Text node
                private Text findTextNode(Node root) {
                    // Ignore the graphic and all its child nodes:
                    if (root == getGraphic()) {
                        return null ;
                    }
                    if (root instanceof Text) {
                        return (Text) root ;
                    }
                    if (root instanceof Parent) {
                        for (Node node : ((Parent)root).getChildrenUnmodifiable()) {
                            Text text = findTextNode(node);
                            if (text != null) return text ;
                        }
                    }
                    return null ;
                }
            });
    
            var rng = new Random();
            for (int i = 1 ; i <= 40 ; i++) {
                table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
            }
    
            var scene = new Scene(new BorderPane(table));
            stage.setScene(scene);
            stage.show();
        }
    
        public static <S,T>  TableColumn<S,T> column(String title, Function<S, Property<T>> prop) {
            TableColumn<S,T> col = new TableColumn<>(title);
            col.setCellValueFactory(cellData -> prop.apply(cellData.getValue()));
            return col ;
        }
    
        public static class Item {
            private final StringProperty name = new SimpleStringProperty();
            private final IntegerProperty value = new SimpleIntegerProperty();
            public Item(String name, int value) {
                this.name.set(name);
                this.value.set(value);
            }
    
            public final StringProperty nameProperty() {
                return name ;
            }
    
            public final IntegerProperty valueProperty() {
                return value ;
            }
        }
    
        public static void main(String[] args) {
            launch();
        }
    
    }