0

I have a TableView where every row has a ContextMenu like in the image below.

enter image description here

When I click on the first MenuItem called ("Contrassegna riga come analizzata"), I want all selected rows of the TableView (in the example above the ones starting with 22002649 and 22016572) to change color. If they are already coloured, I want them to remove it.

I tried with the following code but it obviously works only with the last selected row and not with others

    tableView.setRowFactory(
            new Callback<TableView, TableRow>() {
                @Override
                public TableRow call(TableView tableView0) {
                    final TableRow row = new TableRow<>();
                    final ContextMenu rowMenu = new ContextMenu();
                    
                    final PseudoClass checkedPC = PseudoClass.getPseudoClass("checked");
                    
                    MenuItem doneRiga = new MenuItem("Contrassegna riga come analizzata");
                    doneRiga.setOnAction(j -> {
                        
                        if (!row.getPseudoClassStates().contains(checkedPC))
                            row.pseudoClassStateChanged(checkedPC, true);
                        else
                            row.pseudoClassStateChanged(checkedPC, false);
                    });
                    MenuItem doneArticolo = new MenuItem("Contrassegna articolo come analizzato");
                    
                    rowMenu.getItems().addAll(doneRiga, doneArticolo);
                    
                    return row;
                }
            });

Consequently I obtain the following result

enter image description here

Any suggestions? Thank you

  • 3
    You need a property in the model class for the table items representing whether or not the item should be highlighted. Have the table row implementation observe that property and set the pseudo class as needed. Then in the listener for the menu item just toggle the property for the selected items. There are other questions on this site which address this. Off-topic: don’t use raw types. – James_D Sep 17 '22 at 14:17
  • Similar (but very old) question: https://stackoverflow.com/q/20350099/2189127 – James_D Sep 17 '22 at 14:23

1 Answers1

3

This is really a duplicate of Programmatically change the TableView row appearance, but since that question is quite old, here is a solution using more modern Java idioms.

Typically your model class should contain observable properties for all data that is required to view it. In this case, your table items can be either "analyzed" or "not analyzed", so they would usually have a boolean property to represent that. For example:

public class Item {
    private final StringProperty name = new SimpleStringProperty();
    private final BooleanProperty analyzed = new SimpleBooleanProperty();

    public Item(String name) {
        setName(name);
    }

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

    public StringProperty nameProperty() {
        return name;
    }

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

    public boolean getAnalyzed() {
        return analyzed.get();
    }

    public BooleanProperty analyzedProperty() {
        return analyzed;
    }

    public void setAnalyzed(boolean analyzed) {
        this.analyzed.set(analyzed);
    }
}

Your table row needs to do two things:

  1. Observe the analyzedProperty() of the current item it is displaying, so it updates the state if that property changes. Note this mean if the item changes, it needs to remove a listener from the old item (i.e. stop observing the property in the old item) and add a listener to the new item (start observing the property in the new item).
  2. If the item changes, update the state of the row to reflect the analyzed state of the new item.

A table row implementation that does this looks like:

TableRow<Item> row = new TableRow<>(){
    private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
            updateState(isNowAnalyzed);

    {
        // Make sure we are observing the analyzedProperty on the current item
        itemProperty().addListener((obs, oldItem, newItem) -> {
            if (oldItem != null) {
                oldItem.analyzedProperty().removeListener(analyzedListener);
            }
            if (newItem != null) {
                newItem.analyzedProperty().addListener(analyzedListener);
            }
        });
    }
    @Override
    protected void updateItem(Item item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            updateState(false);
        } else {
            updateState(item.getAnalyzed());
        }
    }

    private void updateState(boolean analyzed) {
        pseudoClassStateChanged(analyzedPC, analyzed);
    }
}; 

Note that in JavaFX 19 you can use the flatMap() API to simplify this code considerably:

TableRow<Item> row = new TableRow<>();
row.itemProperty()
    .flatMap(Item::analyzedProperty)
    .orElse(false)
    .addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
        row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
    });

Now to change the state of the selected items, you just need to iterate through them and toggle the analyzed state:

ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyzed");
analyzedMI.setOnAction(e -> {
    // Toggle analyzed state of selected items
    List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
    for (Item item : selectedItems) {
        item.setAnalyzed(! item.getAnalyzed());
    }
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);

Putting it all together in a complete example:

package org.jamesd.examples.highlightrows;

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.List;

public class HelloApplication extends Application {

    private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
    @Override
    public void start(Stage stage) throws IOException {
        TableView<Item> table = new TableView<>();
        TableColumn<Item, String> column = new TableColumn<>("Item");
        column.setCellValueFactory(data -> data.getValue().nameProperty());
        table.getColumns().add(column);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        table.setRowFactory(tc -> {
            TableRow<Item> row = new TableRow<>();
            row.itemProperty()
                .flatMap(Item::analyzedProperty)
                .orElse(false)
                .addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
                    row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
                });
                
            // Prior to JavaFX 19 you need something like the following (which is probably less robust):
//            TableRow<Item> row = new TableRow<>(){
//                private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
//                        updateState(isNowAnalyzed);
//
//                {
//                    // Make sure we are observing the analyzedProperty on the current item
//                    itemProperty().addListener((obs, oldItem, newItem) -> {
//                        if (oldItem != null) {
//                            oldItem.analyzedProperty().removeListener(analyzedListener);
//                        }
//                        if (newItem != null) {
//                            newItem.analyzedProperty().addListener(analyzedListener);
//                        }
//                    });
//                }
//                @Override
//                protected void updateItem(Item item, boolean empty) {
//                    super.updateItem(item, empty);
//                    if (empty || item == null) {
//                        updateState(false);
//                    } else {
//                        updateState(item.getAnalyzed());
//                    }
//                }
//
//                private void updateState(boolean analyzed) {
//                    pseudoClassStateChanged(analyzedPC, analyzed);
//                }
//            };

            ContextMenu menu = new ContextMenu();
            MenuItem analyzedMI = new MenuItem("Analyzed");
            analyzedMI.setOnAction(e -> {
                // Toggle analyzed state of selected items
                List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
                for (Item item : selectedItems) {
                    item.setAnalyzed(! item.getAnalyzed());
                }
            });
            menu.getItems().add(analyzedMI);
            row.setContextMenu(menu);
            return row;
        });

        for (int i = 1 ; i <= 20 ; i++) {
            table.getItems().add(new Item("Item "+i));
        }

        BorderPane root = new BorderPane(table);
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final BooleanProperty analyzed = new SimpleBooleanProperty();

        public Item(String name) {
            setName(name);
        }

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

        public StringProperty nameProperty() {
            return name;
        }

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

        public boolean getAnalyzed() {
            return analyzed.get();
        }

        public BooleanProperty analyzedProperty() {
            return analyzed;
        }

        public void setAnalyzed(boolean analyzed) {
            this.analyzed.set(analyzed);
        }
    }

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

with the stylesheet style.css (in the same package as the application class):

.table-row-cell:analyzed {
    -fx-control-inner-background: derive(green, 20%);
    -fx-control-inner-background-alt: green;
    -fx-selection-bar: #00b140;
}

If for some reason you cannot change the model class (Item in the code above), you need to track which items are "analyzed" separately in a way that can be observed. An ObservableList could be used for this:

final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();

Now the table row can observe that list, and update the CSS pseudoclass if the list changes:

TableRow<Item> row = new TableRow<>(){
    {
        // If the list of analyzed items changes, make sure the state is correct:
        analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
            updateState(analyzedItems.contains(getItem()));
        });
    }
    @Override
    protected void updateItem(Item item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            updateState(false);
        } else {
            updateState(analyzedItems.contains(item));
        }
    }

    private void updateState(boolean analyzed) {
        pseudoClassStateChanged(analyzedPC, analyzed);
    }
};

and you can toggle the state by adding or removing items from the list of analyzed items accordingly:

ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyze");
analyzedMI.setOnAction(e -> {
    // Toggle analyzed state of selected items
    List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
    for (Item item : selectedItems) {
        if (analyzedItems.contains(item)) {
            analyzedItems.remove(item);
        } else {
            analyzedItems.add(item);
        }
    }
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);

The complete example in this case looks like;

package org.jamesd.examples.highlightrows;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.List;

public class HelloApplication extends Application {

    private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
    @Override
    public void start(Stage stage) throws IOException {
        TableView<Item> table = new TableView<>();
        TableColumn<Item, String> column = new TableColumn<>("Item");
        column.setCellValueFactory(data -> data.getValue().nameProperty());
        table.getColumns().add(column);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();
        table.setRowFactory(tc -> {
            TableRow<Item> row = new TableRow<>(){
                {
                    // If the list of analyzed items changes, make sure the state is correct:
                    analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
                        updateState(analyzedItems.contains(getItem()));
                    });
                }
                @Override
                protected void updateItem(Item item, boolean empty) {
                    super.updateItem(item, empty);
                    if (empty || item == null) {
                        updateState(false);
                    } else {
                        updateState(analyzedItems.contains(item));
                    }
                }

                private void updateState(boolean analyzed) {
                    pseudoClassStateChanged(analyzedPC, analyzed);
                }
            };
            ContextMenu menu = new ContextMenu();
            MenuItem analyzedMI = new MenuItem("Analyze");
            analyzedMI.setOnAction(e -> {
                // Toggle analyzed state of selected items
                List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
                for (Item item : selectedItems) {
                    if (analyzedItems.contains(item)) {
                        analyzedItems.remove(item);
                    } else {
                        analyzedItems.add(item);
                    }
                }
            });
            menu.getItems().add(analyzedMI);
            row.setContextMenu(menu);
            return row;
        });

        for (int i = 1 ; i <= 20 ; i++) {
            table.getItems().add(new Item("Item "+i));
        }

        BorderPane root = new BorderPane(table);
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();

        public Item(String name) {
            setName(name);
        }

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

        public StringProperty nameProperty() {
            return name;
        }

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

    public static void main(String[] args) {
        launch();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322