Strikethough on table row in javafx

542 views Asked by At

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

There are 1 answers

9
James_D On BEST ANSWER

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 ;
    });