1

I have a TableView with a TableColumn called "descriptionCol". To do text wrapping I used this cell factory:

descriptionCol.setCellValueFactory((new PropertyValueFactory<Task, String>("description")));

descriptionCol.setCellFactory(new Callback<TableColumn<Task, String>, TableCell<Task, String>>() {
            @Override
            public TableCell<Task, String> call(TableColumn<Task, String> param) {
                final TableCell<Task, String> cell = new TableCell<Task, String>() {
                    private Text text;
                    @Override
                    public void updateItem(String item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!isEmpty()) {
                            text = new Text(item.toString());
                            text.setWrappingWidth(descriptionCol.getWidth() * 2); // Setting the wrapping width to the Text
                            setGraphic(text);
                        }
                    }
                };
                return cell;
            }
        });

which works for wrapping the text in the column but as a side-effect the text isn't styled like the rest of the text in the program.

descriptionCol is defined in fxml as:

<TableColumn fx:id="descriptionCol" prefWidth="75.0" text="Description" />

and is in the <columns> tag of the TableView.

The styled text should be

* {
    -fx-text-fill: -fx-txt;
    -fx-font-size: 12px;
    
    /* Everything in regular CSS format is only there for compatibility reasons */
    color: -fx-txt;
    font-size: 12px;
}

where -fx-txt is in .root as "#FFFFFF"

I've tried doing text.setStyle() in the cell factory, creating a CSS rule as .text {} and inserting the above CSS into the rule in my stylesheet, setting an id to the text object and creating an #id {} rule in the stylesheet, and applying the css to the .table.column {} rule in the stylesheet as well. So far none of these solutions have worked to style the object and I'm all out of ideas now. Also confused why the * {} rule above isn't just being automatically applied to the Text object in the table column but I think that's something to ask in a separate question.

If anymore of the code is needed to get a proper gauge on what's going on please let me know and I can provide.

MRE:

  • Using VS Code with JavaFX 13 and Java 11

Main.java

package com.example;

import java.io.IOException;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application
{
    protected static Scene mainScene;
    protected static ObservableList<Task> list = FXCollections.observableArrayList();

    @Override
    public void start(Stage primStage) throws IOException
    {
        primStage.setTitle("Example");
        mainScene = new Scene(loadFXML("Table"), 640, 480);
        mainScene.getStylesheets().add(getClass().getResource("Main.css").toExternalForm());
        primStage.setScene(mainScene);
        primStage.setMaximized(true);
        primStage.show();
    }

    static void setRoot(String fxml) throws IOException {
        mainScene.setRoot(loadFXML(fxml));
    }

    private static Parent loadFXML(String fxml) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource(fxml + ".fxml"));
        return fxmlLoader.load();
    }

    public static void main(String[] args) {
        launch();
    }
}

Controller.java

package com.example;

import java.util.ResourceBundle;
import javafx.fxml.FXML;
import java.net.URL;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Text;
import javafx.fxml.Initializable;
import javafx.util.Callback;
import javafx.scene.control.TableCell;


public class Controller extends Main implements Initializable
{
    @FXML private BorderPane bPane;
    @FXML private TableView<Task> table;
    @FXML private TableColumn<Task, String> descriptionCol;
    
    @Override
    public void initialize(URL url, ResourceBundle resources)
    {
        // Sample data
        list.add(new Task(100.0, 300.0, MouseButton.PRIMARY));
        list.get(0).setName("Click Corner");
        list.get(0).setDescription("Clicks to top left corner of the screen");

        list.add(new Task("A"));
        list.get(1).setName("Type a");
        list.get(1).setDescription("Types the letter 'a' once");

        // Adds data to table
        table.getItems().setAll(list);
        
        descriptionCol.setCellValueFactory((new PropertyValueFactory<Task, String>("description")));
        descriptionCol.setCellFactory(new Callback<TableColumn<Task, String>, TableCell<Task, String>>() {
            @Override
            public TableCell<Task, String> call(TableColumn<Task, String> param) {
                final TableCell<Task, String> cell = new TableCell<Task, String>() {
                    private Text text;
                    @Override
                    public void updateItem(String item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!isEmpty()) {
                            text = new Text(item.toString());
                            text.setWrappingWidth(descriptionCol.getWidth() * 2); // Setting the wrapping width to the Text
                            setGraphic(text);
                        }
                    }
                };
                return cell;
            }
        });
    }
}

Task.java

package com.example;

import javafx.scene.input.MouseButton;

public class Task
{
    private double x, y;
    private MouseButton button;
    private String description, name, keyCode;

    
    public Task(double xPos, double yPos, MouseButton inButton)
    {
        this.x = xPos;
        this.y = yPos;
        this.button = inButton;
        this.description = null;
        this.name = null;
    }

    public Task(String code)
    {
        this.x = 0;
        this.y = 0;
        this.button = MouseButton.NONE;
        this.description = null;
        this.name = null;
        this.keyCode = code;
    }

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

    public void setDescription(String s)
    {
        this.description = s;
    }

    public String getDescription()
    {
        return this.description;
    }
}

Table.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane fx:id="bPane" prefHeight="1080.0" prefWidth="1920.0" fx:controller="com.example.Controller" xmlns="http://javafx.com/javafx/13" xmlns:fx="http://javafx.com/fxml/1">
    <center>
       <TableView fx:id="table" prefHeight="352.0" prefWidth="543.0">
         <columns>
          <TableColumn fx:id="descriptionCol" prefWidth="75.0" text="Description" />
         </columns>
          <columnResizePolicy>
             <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
          </columnResizePolicy>
       </TableView>
    </center>
</BorderPane>

Main.css

.root {
    -fx-bgc: #121212; /* Dark gray recommended dark theme background color - Material Design */
    -fx-bgc-alt: #242424;
    -fx-pc: #7C4DFF; /* Deep Purple 50 A200 - Material Design */
    -fx-sc: #448AFF; /* Blue 50 A200 - Material Design */
    -fx-sc-uf: #588be0;
    -fx-txt: #FFFFFF;
}

* {
    -fx-background-color: -fx-bgc;
    -fx-text-fill: -fx-txt;
    -fx-font-size: 12px;
    
    /* Everything in regular CSS format is only there for compatibility reasons */
    background-color: -fx-bgc;
    color: -fx-txt;
    font-size: 12px;
}

.table-row-cell:focused, .table-row-cell:focused:even,
.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:even:focused:selected {
    -fx-background-color: -fx-pc, -fx-pc, -fx-pc;
    -fx-background-insets: 0, 1, 2;

    background-color: -fx-pc, -fx-pc, -fx-pc;
}

.table-row-cell:focused:odd,
.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:odd:focused:selected {
    -fx-background-color: -fx-sc, -fx-sc, -fx-sc;
    -fx-background-insets: 0, 1, 2;

    background-color: -fx-sc, -fx-sc, -fx-sc;
}

.table-column {
    -fx-alignment: CENTER;
}

.table-row-cell {
    -fx-table-cell-border-color: -fx-txt;
}

.table-view .column-header-background, .table-view .column-header-background .filler, .table-view .corner {
    -fx-background-color: -fx-bgc;

    background-color: -fx-bgc;
}

.table-row-cell:filled:even .table-column:filled,
.table-row-cell:filled:even .table-column,
.table-row-cell:even .table-column:filled {
    -fx-background-color: -fx-bgc;
    -fx-selection-bar: -fx-pc;
    -fx-selection-bar-non-focused: -fx-pc;

    background-color: -fx-bgc;
}

.table-row-cell:filled:odd .table-column:filled,
.table-row-cell:filled:odd .table-column,
.table-row-cell:odd .table-column:filled {
    -fx-background-color: -fx-bgc-alt;
    -fx-selection-bar: -fx-sc;
    -fx-selection-bar-non-focused: -fx-sc;

    background-color: -fx-bgc-alt;
}

File structure and how I created the MRE:

File structure

  • In VSCode open command palette and create a new JavaFX project provided by Maven.
    • Only thing I changed in the prompts was making the artifact id be "example" instead of "demo" but I don't think that should matter.
  • Under src/main/java/com/example/ include Controller.java, Main.java, and Task.java provided above
  • Under src/main/java/ add requires transitive javafx.graphics; to file module-info.java
    • I did this under requires javafx.fxml; as line 4.
  • Under src/main/resources/com/example/ include Table.fxml and Main.css also provided above.
    • I left in all of the table CSS rules that I did so that you can see exactly what I see
  • I did not change anything in the auto-generated pom.xml and I removed all other .java and .fxml files that were made on creation.
  • 1
    [mcve] please .. btw: do _not_ create new nodes in every call of updateItem – kleopatra Mar 20 '23 at 05:03
  • @kleopatra just added the MRE to the bottom of the question. When you say to not create new nodes in every call I'm assuming you're referring to the Text nodes right? If so I could probably move the private Text text line to be a class variable of the overall class and then do something like check if its null and if it is, create a new Text object, and if not just call setText(item.toString()) right? – Dylan Gresham Mar 20 '23 at 06:30
  • I didn’t try it, but if you simply remove your custom cell factory to use the standard label in a cell and have `-fx-wrap-text: true;` applied to the label via css, does that do what you want? – jewelsea Mar 20 '23 at 08:21
  • Unrelated: [don’t use PropertyValueFactory](https://stackoverflow.com/questions/72437983/why-should-i-avoid-using-propertyvaluefactory-in-javafx). – jewelsea Mar 20 '23 at 08:30
  • hmm .. but you still re-create Text whenever updateItem is called on a not-empty cell (which is .. _often_!) Not near my IDE so can't say whether or not that's part of the problem (the other part is a notorious inability of coping with cells that dynamically change vertical size requirements when updating their content ..), will try later. – kleopatra Mar 20 '23 at 10:12
  • the text's textfill isn't taken at all, even with the default style (f.i. still black if cell is selected - in which case the plain tableCell has white text). Might be a bug. – kleopatra Mar 20 '23 at 12:38
  • in your shoes I would further strip it down, both code (no need for any special data item, simple long string is good enough) and styling (leave out all the noise for the complete table/row, simply try to style the text color when the graphic is a Text) and dig whether it's a bug (if so, consider reporting it :) or expected behavior, in both cases you are done if you succeed and can answer your own question :) I'm off now, RL is calling .. – kleopatra Mar 20 '23 at 12:51
  • 3
    `Text` is a subclass of `Shape`, not `Labeled`, so it has `fill` and `stroke` properties, but no `textFill` property. So you need `-fx-fill`, not `-fx-text-fill` (or use a `Label` instead of a `Text`). – James_D Mar 20 '23 at 13:05
  • I found that some CSS did take for cells unless I called ```applyCss()``` on the parent (i.e. the TableCell) of the node I wanted to apply CSS to in the ```updateItem()``` method. – swpalmer Mar 20 '23 at 13:58
  • Example is still incomplete (no `Task` class provided) – James_D Mar 20 '23 at 15:06
  • @James_D argghh .. you are right, totally missed the Text != Control .. – kleopatra Mar 20 '23 at 15:07
  • @jewelsea the CSS `-fx-wrap-text: true;` unfortunately didn't work for this but I think that might work for something else in my project. – Dylan Gresham Mar 20 '23 at 16:55
  • @James_D thanks for pointing out I left out the Task class, I added that in to the question in case someone wants to do their own testing. Your answer below does solve the problem though, much appreciated! – Dylan Gresham Mar 20 '23 at 16:57
  • 1
    @DylanGresham yes, I tried to wrap text on the cell too, and unfortunately, it did not work out of the box how I would like. The label is just elided rather than wrapped unless you explicitly set the preferred or minimum height of the cell, so the default layout algorithm for the TableView cell isn't really calculating the right height for the complete display of wrapped text and there is no easy way I know of to change this. Hence the hacked cell implementation using Text that you have (with the application in James's answer), is probably a reasonable workaround. – jewelsea Mar 20 '23 at 22:01

1 Answers1

2

Text is a subclass of Shape (not of Labeled) so it inherits Shape properties (both Java properties and CSS properties). Shape (and consequently Text) has a -fx-fill property which is used to fill the glyph. There is no -fx-text-fill property in Text.

You should either use a -fx-fill property in your CSS, or change the Text to a Label. Here is a fix using the first approach (though I'd probably recommend the second, generally; I left it as a Text in case there were pressing reasons to do so in your actual project).

I also fixed the cell implementation. You should not instantiate node subclasses in the updateItem() method, and the updateItem() method should handle all possibilities, including empty cells.

Here's the modified cell factory, which assigns a style class to the Text instance:

    descriptionCol.setCellFactory(col -> {
        final TableCell<Task, String> cell = new TableCell<Task, String>() {
            private Text text;

            {
                text = new Text();
                text.getStyleClass().add("cell-text");
            }
            @Override
            public void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) {
                    setGraphic(null);
                } else {
                    text.setText(item.toString());
                    text.setWrappingWidth(descriptionCol.getWidth() * 2); // Setting the wrapping width to the Text
                    setGraphic(text);
                }
            }
        };
        return cell;
    });

and the corresponding CSS:

.root {
    -fx-bgc: #121212; /* Dark gray recommended dark theme background color - Material Design */
    -fx-bgc-alt: #242424;
    -fx-pc: #7C4DFF; /* Deep Purple 50 A200 - Material Design */
    -fx-sc: #448AFF; /* Blue 50 A200 - Material Design */
    -fx-sc-uf: #588be0;
    -fx-txt: #FFFFFF;
}

* {
    -fx-background-color: -fx-bgc;
    -fx-text-fill: -fx-txt;
    -fx-font-size: 12px;

    /* Everything in regular CSS format is only there for compatibility reasons */
    background-color: -fx-bgc;
    color: -fx-txt;
    font-size: 12px;
}

.table-cell .cell-text {
    -fx-fill: -fx-txt;
}

.table-row-cell:focused, .table-row-cell:focused:even,
.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:even:focused:selected {
    -fx-background-color: -fx-pc, -fx-pc, -fx-pc;
    -fx-background-insets: 0, 1, 2;

    background-color: -fx-pc, -fx-pc, -fx-pc;
}

.table-row-cell:focused:odd,
.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:odd:focused:selected {
    -fx-background-color: -fx-sc, -fx-sc, -fx-sc;
    -fx-background-insets: 0, 1, 2;

    background-color: -fx-sc, -fx-sc, -fx-sc;
}

.table-column {
    -fx-alignment: CENTER;
}

.table-row-cell {
    -fx-table-cell-border-color: -fx-txt;
}

.table-view .column-header-background, .table-view .column-header-background .filler, .table-view .corner {
    -fx-background-color: -fx-bgc;

    background-color: -fx-bgc;
}

.table-row-cell:filled:even .table-column:filled,
.table-row-cell:filled:even .table-column,
.table-row-cell:even .table-column:filled {
    -fx-background-color: -fx-bgc;
    -fx-selection-bar: -fx-pc;
    -fx-selection-bar-non-focused: -fx-pc;

    background-color: -fx-bgc;
}

.table-row-cell:filled:odd .table-column:filled,
.table-row-cell:filled:odd .table-column,
.table-row-cell:odd .table-column:filled {
    -fx-background-color: -fx-bgc-alt;
    -fx-selection-bar: -fx-sc;
    -fx-selection-bar-non-focused: -fx-sc;

    background-color: -fx-bgc-alt;
}

Depending on your actual requirements, it may be enough just to put -fx-fill: -fx-txt; in the "global" style with selector *, but bear in mind this would apply to all Shapes. You can also use the selector Text or table-cell Text, depending on the specificity you need. All of these options would circumvent the need for a custom CSS class to be applied in the cell factory.

James_D
  • 201,275
  • 16
  • 291
  • 322