TableColumn.setStyle() Does not work inside setOnEditCommit(...)

69 Views Asked by At

In method setOnEditCommit() of TableColumn i trty to change the color of cell's text and it does no effect. It is TableView wih only one row. here mycode...What i'm doing wrong ?

columnnumberofsession.setOnEditCommit(event -> {
    try {
        ffnumberofsession.checkValue(event.getNewValue());
    }
    catch(IllegalArgumentException iae) {
        System.err.println("The value for number of session is invalid !");

        event.getTableColumn().setStyle("-fx-text-fill: red;");
        return;
    }

    event.getTableColumn().setStyle("-fx-text-fill: black;");
    System.out.println("Value is correct.");
});

I try the method ouside of <setOnEditCommin()> callback and it's work well. The color of text change but it is a static test. I want the color of cell text become red when i catch an error. I don't see why it doesn't work inside setOnEditCommit().

1

There are 1 best solutions below

1
jewelsea On

What you are doing wrong

I'm not exactly sure, but on the other hand I am not sure why what you are doing would work either.

i try to change the color of cell's text

No you don't, I mean not directly.

You call:

event.getTableColumn().setStyle("-fx-text-fill: red;") 

to change the column's fill style, not the cell's fill style.

The column is a different thing from the cell.

I did try testing this, if you have a TableView and a TableColumn object and you call:

myTableColumn.setStyle("-fx-text-fill: red;");

on the column it will change the style of all the labels in the cells in the column (but not the table header) to red.

This is unrelated to editing and matches what Estienne states when he mentions

I try the method outside of <setOnEditCommin()> callback and it's work well.

I just don't know how and why that actually works though. As far as I can tell the behavior of styling table cells via styling the columns is not documented in the CSS reference guide.

What may be going wrong with your approach

Usually, with editing in the TableView, there are multiple nodes involved in the cell implementation and the nodes switch in and out of view depending on whether the cell is being edited or not. This may be interfering with the undocumented behavior for styling text cell fills by setting styles on the column. That is just my speculation though.

Suggested approach

Usually for these state change styles, instead of setting styles directly in code, it is better practice, more configurable and more robust to apply and remove style classes (or pseudo classes) together with stylesheets with appropriate rules defined.

For example in the updateItem for the cell, check if the session is invalid, and if so ensure the cell has the session-invalid style class, otherwise ensure it does not.

Then define the styling for the session-invalid style class in your CSS stylesheet. You might need an extractor on the observable items list to get the update to fire.

If necessary a separate property can be defined for the items backing the TableView (or a global property if the state of the entire TableView is affected), and that property can indicate the styles to apply when the items are updated. In this case, an additional property is probably not necessary as the validation check can be performed in updateItem.

Example snippets demonstrating the suggested approach

CSS styling (you can define this in an external stylesheet, I just defined it inline to cut down on files):

public static final String INVALID_STYLECLASS = "invalid";
public static final String CSS_DATA = "data:text/css,";
private static final String CSS = CSS_DATA + // language=CSS
           """
           .invalid {
               -fx-text-fill: red;
           }
           """;

Custom text field table cell which applies styling when the cell data is invalid. In this case it will be invalid if the email column is edited not to include an @ symbol in the email.

TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(item -> item.getValue().emailProperty());
emailCol.setCellFactory(param -> new TextFieldTableCell<>(new DefaultStringConverter()) {
   @Override
   public void updateItem(String item, boolean empty) {
      super.updateItem(item, empty);

       if (item != null && !empty && !isValidEmail(item)) {
          if (!getStyleClass().contains(INVALID_STYLECLASS)) {
             getStyleClass().add(INVALID_STYLECLASS);
          }
       } else {
          getStyleClass().remove(INVALID_STYLECLASS);
       }
   }

   private static boolean isValidEmail(String item) {
      return item.contains("@");
   }
});
emailCol.setEditable(true);

The table needs to be made editable too:

table.setEditable(true);

When the items list backing the table is defined, an extractor is set on the email data so that the email cells will be updated when the email changes:

final ObservableList<Person> data = FXCollections.observableArrayList(
        person -> new Observable[] { person.emailProperty()}
);
data.addAll(
        // data added here . . .
);

The outcome will be that, if you double click a cell, edit its value and change it to something invalid (e.g. remove the @ symbol from the email), the cell will be highlighted in red until it is fixed.

Example App

Output after editing one of the emails to make the value invalid:

example

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;

import java.io.IOException;

public class StatedTableApp extends Application {


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

   public static final String INVALID_STYLECLASS = "invalid";
   public static final String CSS_DATA = "data:text/css,";
   private static final String CSS = CSS_DATA + // language=CSS
               """
               .invalid {
                   -fx-text-fill: red;
               }
               """;

   @Override
   public void start(Stage stage) throws IOException {
      final TableView<Person> table = new TableView<>();

      final ObservableList<Person> data = FXCollections.observableArrayList(
              person -> new Observable[] { person.emailProperty()}
      );
      data.addAll(
              new Person("Jacob", "Smith", "[email protected]"),
              new Person("Isabella", "Johnson", "[email protected]"),
              new Person("Ethan", "Williams", "[email protected]"),
              new Person("Emma", "Jones", "[email protected]"),
              new Person("Michael", "Brown", "[email protected]")
      );

      table.getSelectionModel().setCellSelectionEnabled(true);
      table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

      TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
      firstNameCol.setMinWidth(100);
      firstNameCol.setCellValueFactory(item -> item.getValue().firstNameProperty());
      TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
      lastNameCol.setMinWidth(100);
      lastNameCol.setCellValueFactory(item -> item.getValue().lastNameProperty());
      TableColumn<Person, String> emailCol = new TableColumn<>("Email");
      emailCol.setMinWidth(200);
      emailCol.setCellValueFactory(item -> item.getValue().emailProperty());
      emailCol.setCellFactory(param -> new TextFieldTableCell<>(new DefaultStringConverter()) {
         @Override
         public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);

             if (item != null && !empty && !isValidEmail(item)) {
                if (!getStyleClass().contains(INVALID_STYLECLASS)) {
                   getStyleClass().add(INVALID_STYLECLASS);
                }
             } else {
                getStyleClass().remove(INVALID_STYLECLASS);
             }
         }

         private static boolean isValidEmail(String item) {
            return item.contains("@");
         }
      });
      emailCol.setEditable(true);

      table.setEditable(true);
      table.setItems(data);
      //noinspection unchecked
      table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);

      Scene scene = new Scene(table);
      scene.getStylesheets().add(CSS);
      stage.setScene(scene);
      stage.show();
   }

   public static class Person {
      private final StringProperty firstName;
      private final StringProperty lastName;
      private final StringProperty email;

      private Person(String fName, String lName, String email) {
         this.firstName = new SimpleStringProperty(fName);
         this.lastName = new SimpleStringProperty(lName);
         this.email = new SimpleStringProperty(email);
      }

      public String getFirstName() {
         return firstName.get();
      }

      public void setFirstName(String fName) {
         firstName.set(fName);
      }

      public StringProperty firstNameProperty() {
         return firstName;
      }

      public String getLastName() {
         return lastName.get();
      }

      public void setLastName(String fName) {
         lastName.set(fName);
      }

      public StringProperty lastNameProperty() {
         return lastName;
      }

      public String getEmail() {
         return email.get();
      }

      public void setEmail(String fName) {
         email.set(fName);
      }

      public StringProperty emailProperty() {
         return email;
      }
   }
}

This remaining information is supplemental to the core answer above. If the answer is too long and the information below is not relevant to your situation, then ignore it.

FAQ

I would have liked to be able to change the color of the text of the column header and that of the cell.

Changing the header color is an also possible, but I didn't want to include an implementation for that in this answer. It wasn't clear to me if that was part of the original question, plus the implementation for that will be a bit different.

Unless somebody else provides a supplemental answer about header styling to this question, check to see if there is a duplicate of the header question on StackOverflow. If not, then you may wish ask to ask a new question on dynamic header styling.

Additional info on styling columns

As noted by James in comments on this question, there is some inbuilt undocumented logic in the table cells where the cells copy some of the style information from the column styles.

looking at the source code for TableCell, the table cell registers a listener with its table column's style class list, and copies all the column's style classes into its own style class list.

However the style copying logic in the cells from column styles is only partial (and I don’t think it copies styles to the column header fields):

I don't see anywhere where it does anything similar with CSS pseudoclasses. I haven't tested, but it looks like the effect of setting a table column's CSS pseudoclasses on the cells in the column may be different to setting regular CSS classes, which if true would not be ideal.

You could rely on the undocumented logic for copying style information from columns to cells. But it may not work as expected in some situations (such as your edit commit case in the question). If it is not working, I'd discourage trying to create bandaid workarounds to get it to work for your case. Instead directly and explicitly style the individual table cells (like in this answer) and column headers (not covered in detail in this answer).

Supplementary info on styling table column headers

As also noted by James in comments:

TableColumnHeader appears to do the same thing (copying style classes and inline styles from its TableColumn) but iirc -fx-text-fill is not inherited by the label it contains.

This was my finding too. The inheritance mode for various CSS styles is not all that clearly documented. One might think that because a table cell is inside a table column that it would just automatically inherit the column's styles. But that is not the case for non-inherited styles.

By default CSS styles are not inherited:

boolean isInherits() If true, the value of this property is the same as the parent's computed value of this property. Returns: false by default unless inherits is true

And the implementation of the -fx-fill style does not set the inherits property to true, so it does not inherit.

That is why I was confused that setting the fill on the column actually changed the cell style in the case where editing is not involved. It is because of the hidden style copying in the TableView implementation that James refers to in his comment.

However James did outline an approach to styling the header explicitly which should work (I didn't try it):

So setting the style of the text in the table column header via adding a style class to the table column itself would likely need something like

.table-column-header.invalid .label { -fx-text-fill: red; } 

which is pretty ugly, and note there is no space between the first two class selectors but there is before the .label.

Also note, that, unlike the fill color, the -fx-font-* CSS styles do inherit. So, if you set the font size, weight, italics or family on the column, the font changes for both the header labels and all the cells in the column. Which can be somewhat surprising if you don't quite understand what is really going on :-)