1

I want to update a row to be strikethrough in a tableview when the user deletes it. I'm somewhat new to javafx and have been searching with no luck.

        donationsTable.setRowFactory(tv -> {
            TableRow<Donation> row = new TableRow<Donation>() {
                // to force updateItem called
                @Override
                protected boolean isItemChanged(Donation d,
                        Donation d2) {
                    return true;
                }
                
                @Override
                public void updateItem(Donation d, boolean empty) {
                    super.updateItem(d, empty) ;
                    if (d == null) {
                        setStyle("");
                    } else if (d.getAction().equals(Donation.DELETE_DONATION)) {
                        setStyle("delete-row");
                    } else if (d.getAction().equals(Donation.NEW_DONATION)) {
                        setStyle("-fx-font-weight: bold;");
                    } else {
                        setStyle("");
                    }           
                }
            };
            row.setOnMouseClicked(event -> {
                    deleteDonation.setDisable(false);
            });
            return row;
        });

The bold works for new donations, but I can't get the strikethrough to work. I did see that it needs to be set on the text, not the row so my css is:

.delete-row .text {
    -fx-strikethrough: true;
}

However, I'm getting a warning: WARNING CSS Error parsing '*{delete-row}: Expected COLON at [1,12] I only have a very basic understanding of css. This is what I have seen in other answers, but I don't understand why it is not working for me.

Any help is much appreciated.

Based on James_D's suggestion, I changed updateItem:


        public void updateItem(Donation d, boolean empty) {
                    super.updateItem(d, empty) ;
                    
                    PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
                    pseudoClassStateChanged(delete, d != null && d.getAction().equals(Donation.DELETE_DONATION));

                    PseudoClass add = PseudoClass.getPseudoClass("add-row");
                    pseudoClassStateChanged(add, d != null && d.getAction().equals(Donation.NEW_DONATION));

                }

css has

.table-row-cell:delete-row .text {
    -fx-strikethrough: true;
}

.table-row-cell:add-row {
    -fx-font-weight: bold;
}

strikethrough still not working and bold stopped working.

1 Answers1

2

The setStyle method will set an inline style on a Node; this style is in the form of a CSS rule. This is what you do with the bold case:

if (d.getAction().equals(Donation.NEW_DONATION)) {
    setStyle("-fx-font-weight: bold;");
}

To add a CSS class to the list of classes for a node, get the list of the node's CSS classes with getStyleClass(), and manipulate it.

You have to be a little careful here, as the list can contain multiple copies of the same value, and additionally you have no control over how many times updateItem() is called and with which Donations as a parameter. The best option is to remove all instances of the class delete-row and add one back in under the correct conditions:

@Override
public void updateItem(Donation d, boolean empty) {
    super.updateItem(d, empty) ;

    getStyleClass().removeAll(Collections.singleton("delete-row"));

    if (d == null) {
        setStyle("");
    } else if (d.getAction().equals(Donation.DELETE_DONATION)) {
        setStyle("");
        getStyleClass().add("delete-row");
    } else if (d.getAction().equals(Donation.NEW_DONATION)) {
        setStyle("-fx-font-weight: bold;");
    } else {
        setStyle("");
    }           
}

Another option is to use a CSS pseudoclass instead:

@Override
public void updateItem(Donation d, boolean empty) {
    super.updateItem(d, empty) ;

    PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
    pseudoClassStateChanged(delete, d != null && d.getAction().equals(Donation.DELETE_DONATION));

    if (d != null && d.getAction().equals(Donation.NEW_DONATION)) {
        setStyle("-fx-font-weight: bold;");
    } else {
        setStyle("");
    }           
}

with

.table-row-cell:delete-row .text {
    -fx-strikethrough: true;
}

I would probably refactor the NEW_DONATION style as a pseudoclass as well in this scenario, for consistency.

Here's a complete example using pseudoclasses. Note that I changed the CSS for bold (as I understand it, using font-weight depends on the system having a bold font for the currently-selected font; using something generic (sans-serif) with a -fx-font rule is more robust.)

Donation.java

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Donation {

    public enum Action { NEW_DONATION, DELETE_DONATION, NO_ACTION }
    
    private final StringProperty name = new SimpleStringProperty() ;
    private final ObjectProperty<Action> action = new SimpleObjectProperty<>() ;
    
    public Donation(String name, Action action) {
        setName(name);
        setAction(action);
    }

    public final StringProperty nameProperty() {
        return this.name;
    }
    

    public final String getName() {
        return this.nameProperty().get();
    }
    

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

    public final ObjectProperty<Action> actionProperty() {
        return this.action;
    }
    

    public final Action getAction() {
        return this.actionProperty().get();
    }
    

    public final void setAction(final Action action) {
        this.actionProperty().set(action);
    }
    
    
    
}

App.java

import java.util.Random;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class App extends Application {

    @Override
    public void start(Stage stage) {
        TableView<Donation> table = new TableView<>();
        
        table.setRowFactory(tv -> {
            TableRow<Donation> row = new TableRow<>() {
                @Override
                protected void updateItem(Donation donation, boolean empty) {
                    super.updateItem(donation, empty);
                    PseudoClass add = PseudoClass.getPseudoClass("add-row");
                    pseudoClassStateChanged(add, 
                        donation != null && donation.getAction() == Donation.Action.NEW_DONATION);

                    PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
                    pseudoClassStateChanged(delete, 
                        donation != null && donation.getAction() == Donation.Action.DELETE_DONATION);
                }
            };
            return row ;
        });
        
        Random rng = new Random();
        for (int i = 1 ; i <= 40 ; i++) {
            table.getItems().add(new Donation("Donation "+i, Donation.Action.values()[rng.nextInt(3)]));
        }
        
        table.getColumns().add(column("Donation", Donation::nameProperty));
        table.getColumns().add(column("Action", Donation::actionProperty));
        
        BorderPane root = new BorderPane(table);
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }
    
    private static <S,T> TableColumn<S,T> column(String name, Function<S, Property<T>> prop) {
        TableColumn<S,T> col = new TableColumn<>(name);
        col.setCellValueFactory(data -> prop.apply(data.getValue()));
        return col ;
    }

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

}

style.css:

.table-row-cell:delete-row .text {
    -fx-strikethrough: true;
}

.table-row-cell:add-row {
    /* -fx-font-weight: bold; */
    -fx-font: bold 1em sans-serif ;
}

enter image description here


Update:

If the property determining the style of the table row is not being observed by one of the columns (e.g. in the above example, the "action" column is not present), you need to arrange for the row to observe that property itself. This is a little tricky, as the row is reused for different table items, so you need to add and remove the listener from the correct property when that happens. This looks like:

    table.setRowFactory(tv -> {
        TableRow<Donation> row = new TableRow<>() {

            // Listener that updates style when the actionProperty() changes
            private final ChangeListener<Donation.Action> listener = 
                (obs, oldAction, newAction) -> updateStyle();
            
            {
                // make sure listener above is registered 
                // with the correct actionProperty()
                itemProperty().addListener((obs, oldDonation, newDonation) -> {
                    if (oldDonation != null) {
                        oldDonation.actionProperty().removeListener(listener);
                    }
                    if (newDonation != null) {
                        newDonation.actionProperty().addListener(listener);
                    }
                });
            }
            
            @Override
            protected void updateItem(Donation donation, boolean empty) {
                super.updateItem(donation, empty);
                updateStyle();
            }

            private void updateStyle() {
                Donation donation = getItem();
                PseudoClass add = PseudoClass.getPseudoClass("add-row");
                pseudoClassStateChanged(add, donation != null && donation.getAction() == Donation.Action.NEW_DONATION);
                PseudoClass delete = PseudoClass.getPseudoClass("delete-row");
                pseudoClassStateChanged(delete, donation != null && donation.getAction() == Donation.Action.DELETE_DONATION);
            }
        };
        return row ;
    });
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Tried both suggestions. removing and adding didn't work. I like the pseudoclass option, the code is cleaner, but still not working. I posted updated code in my question. – Chris Whitcomb Oct 12 '20 at 12:17
  • @ChrisWhitcomb Works for me (with slight modification). See update. – James_D Oct 12 '20 at 12:44
  • That's exactly what I want. The pane the table is on is a dialog. Does the css set in the app.java start function get passed down to all the panes or do I need to set it on the table? I'm thinking that the dialog pane or table may not be seeing the css file. – Chris Whitcomb Oct 12 '20 at 13:26
  • I was not setting the css on the new scene for the dialog. Once I did that it is working great! Thanks for your help. – Chris Whitcomb Oct 12 '20 at 13:37
  • I want to remove the action column and still update the row with strikethrough, but now the updateItem is not called. I see this post https://stackoverflow.com/questions/32119277/colouring-table-row-in-javafx where you say to add an extractor. Unfortunately, I don't see the working answer to the question. – Chris Whitcomb Oct 12 '20 at 16:00
  • @ChrisWhitcomb Hmm. Still works for me if I take the column out. Under what circumstances does it fail? (Initially? On adding new items? Deleting items? Updating items?) – James_D Oct 12 '20 at 16:18
  • Updating items. The 'delete' updates the item in the list by changing the action to DELETE_DONATION. With the action column, things work, but removing the column, the row does not get updated until I add an item. Presumably because updateItem is called. – Chris Whitcomb Oct 12 '20 at 17:00
  • 1
    @ChrisWhitcomb Hmm, yes, apparently the table row's `updateItem()` method is not invoked on update events from the items list (those are fired by the extractor). See update to answer for an alternative solution. – James_D Oct 12 '20 at 17:25
  • Thank you! That's perfect. – Chris Whitcomb Oct 12 '20 at 17:28