vic
vic

Reputation: 2818

Various Colours In The Same Table Cell, Possible?

I have a request to display a string in various colours in a table cell, that is one portion of a string in one colour and the rest in another colour (either the background or the text). I have found an article on changing the cell background colour, but not a portion of a cell. That is close to the requirement, but don't meet the requirement.

The only possible solution, I can think of, is to use the Text type which can be set with various colours after splitting a string into two parts. But, how to use the Text type data with the TableView setup as the following?

aColumn.setCellValueFactory(p -> new SimpleStringProperty(...) );
...
aTalbeView.setItems(FXcollections.observableArrayList(...));

I am still new to JavaFX. Is it doable? If so, how shall I approach a solution?

A mock up table is attached below.
enter image description here

Upvotes: 1

Views: 132

Answers (2)

jewelsea
jewelsea

Reputation: 159291

The approach used in this answer

  • An additional range parameter is added to the backing model to indicate the highlight range for text in the cell.

  • The cellValueFactory uses a binding statement to allow the cell to respond to updates to either the text in the cell or the highlight range.

  • Labels within an HBox are used for the cell graphic rather than a TextFlow as labels have more styling options (e.g. for text background) than text nodes in TextFlow.

  • Using multiple labels within the cells does change some of the eliding behavior of the cell when not enough room is available in the column to include all text, this could be customized by setting properties on the HBOX or label to configure this behavior how you want.

  • CSS stylesheet for styling is included in the code but could be extracted to a separate stylesheet if desired.

  • I didn't thoroughly test the solution, so there may be logic errors around some of the boundary conditions.

Screenshots

Highlighted a subset of text within a cell in a non-selected row:

enter image description here

Highlighted a subset of text within a cell in a selected row:

enter image description here

Example code

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HighlightedTextTableViewer extends Application {

    private static final String CSS_DATA_URL = "data:text/css,";
    private static final String HIGHLIGHTABLE_LABEL_CSS = CSS_DATA_URL + // language=CSS
            """
            .highlightable {
                -fx-font-family: monospace; 
                -fx-font-weight: bold;
            }
            
            .highlight {
                 -fx-background-color: cornflowerblue;
                 -fx-text-fill: white;
            }
            """;

    private static final String HIGHLIGHTABLE_STYLE_CLASS = "highlightable";
    private static final String HIGHLIGHTED_STYLE_CLASS = "highlight";

    @Override
    public void start(Stage stage) {
        TableView<Field> table = createTable();
        populateTable(table);

        VBox layout = new VBox(
                10,
                table
        );
        layout.setPadding(new Insets(10));
        layout.setPrefHeight(100);

        stage.setScene(new Scene(layout));
        stage.show();
    }

    private TableView<Field> createTable() {
        TableView<Field> table = new TableView<>();

        TableColumn<Field, String> nameColumn = new TableColumn<>("Name");
        nameColumn.setCellValueFactory(
                p -> p.getValue().nameProperty()
        );

        TableColumn<Field, Field> valueColumn = new TableColumn<>("Value");
        valueColumn.setCellValueFactory(
                p -> Bindings.createObjectBinding(
                        p::getValue,
                        p.getValue().valueProperty(), p.getValue().highlightRangeProperty()
                )
        );
        valueColumn.setCellFactory(param -> new HighlightableTextCell());

        //noinspection unchecked
        table.getColumns().setAll(nameColumn, valueColumn);

        return table;
    }

    public static class HighlightableTextCell extends TableCell<Field, Field> {
        protected void updateItem(Field item, boolean empty) {
            super.updateItem(item, empty);

            if (item == null || empty || item.getValue() == null) {
                setGraphic(null);
            } else {
                setGraphic(constructTextBox(item));
            }
        }

        private Node constructTextBox(Field item) {
            HBox textBox = new HBox();
            textBox.getStylesheets().setAll(HIGHLIGHTABLE_LABEL_CSS);
            textBox.getStyleClass().add(HIGHLIGHTABLE_STYLE_CLASS);

            int from = item.getHighlightRange() != null ? item.getHighlightRange().from() : -1;
            int valueLen = item.getValue() != null ? item.getValue().length() : -1;
            int to = item.getHighlightRange() != null ? item.getHighlightRange().to() : -1;

            if (item.highlightRangeProperty() == null
                    || from >= to
                    || from > valueLen
            ) { // no highlight specified or no highlight in range.
                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue()
                        )
                );
            } else {
                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        0,
                                        from
                                )
                        )
                );

                if (from >= valueLen) {
                    return textBox;
                }

                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        from,
                                        Math.min(valueLen, to)
                                ), HIGHLIGHTED_STYLE_CLASS
                        )
                );

                if (to >= valueLen) {
                    return textBox;
                }

                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        to
                                )
                        )
                );
            }

            return textBox;
        }

        private Label createStyledLabel(String value, String... styleClasses) {
            Label label = new Label(value);
            label.getStyleClass().setAll(styleClasses);

            return label;
        }
    }

    private void populateTable(TableView<Field> table) {
        table.getItems().addAll(
                new Field("Dragon", "93 6d 6d da", null),
                new Field("Rainbow", "0c fb ff 1c", new Range(3, 8))
        );
    }

}

class Field {
    private final StringProperty name;
    private final StringProperty value;
    private final ObjectProperty<Range> highlightRange;

    public Field(String name, String value, Range highlightRange) {
        this.name = new SimpleStringProperty(name);
        this.value = new SimpleStringProperty(value);
        this.highlightRange = new SimpleObjectProperty<>(highlightRange);
    }

    public String getName() {
        return name.get();
    }

    public StringProperty nameProperty() {
        return name;
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public String getValue() {
        return value.get();
    }

    public StringProperty valueProperty() {
        return value;
    }

    public void setValue(String value) {
        this.value.set(value);
    }

    public Range getHighlightRange() {
        return highlightRange.get();
    }

    public ObjectProperty<Range> highlightRangeProperty() {
        return highlightRange;
    }

    public void setHighlightRange(Range highlightRange) {
        this.highlightRange.set(highlightRange);
    }
}

record Range(int from, int to) {}

Alternative using TextField

An alternative to the HBox for displaying highlighted text would be to use a TextField (non-editable), which allows a selection to be set (via APIs on the text field), however, I did not attempt a solution with a TextField approach. A TextField may allow a user to drag the mouse to select text (perhaps could be disabled if desired by making the field mouse transparent).

Related Questions (uses TextFlow)

Upvotes: 3

James_D
James_D

Reputation: 209330

The cellValueFactory is used to tell the cell what data to display. To tell the cell how to display its data, use a cellFactory. The two are more or less independent.

So you can do

aColumn.setCellValueFactory(p -> new SimpleStringProperty(...));

and then something like:

aColumn.setCellFactory(tc -> new TableCell<>() {
    private final String[] palette = new String[] {
        "#1B9E77", "#D95F02", "#7570B3", "#E7298A",
        "#66A61E", "#E6AB02", "#A6761D", "#666666" };
    private TextFlow flow = new TextFlow();

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setGraphic(null);
        } else {
            flow.getChildren().clear();
            int i = 0 ;
            for (String word : item.split("\\s")) {
                Text text = new Text(word);
                text.setFill(Color.web(palette[i++ % palette.length]);
                flow.getChildren().add(text);
                flow.getChildren().add(new Text(" "));
            }
            setGraphic(flow);
        }
    }
});

This assumes each cell has multiple words (separated by whitespace) and colors each word a different color. You can implement the different colors any way you like; this shows the basic idea.

Upvotes: 3

Related Questions