In my experience/opinion, the trick to anything like this is properly representing the data you need in the model. In this case, it looks like you need some (or perhaps all) of the properties in the table model to carry an associated attribute. I would create a specific class for those properties that carry the attribute. For example, if you are wanting the user to be able to verify values in the table, you basically want each cell in the table to show both the value and the current "verification state" of that value. Hence you need a class that encapsulates a value and a verification state, which should be observable. So you would do something like:
public class VerifiableValue<T> {
public enum VerificationState { UNVERIFIED, VERIFIED, ERROR }
private final T value ;
private final ObjectProperty<VerificationState> verificationState = new SimpleObjectProperty<>(VerificationState.UNVERIFIED) ;
public VerifiableValue(T value) {
this.value = value ;
}
public VerifiableValue(T value, VerificationState verificationState) {
this(value);
setVerificationState(verificationState);
}
public T getValue() {
return value ;
}
public final ObjectProperty<VerificationState> verificationStateProperty() {
return this.verificationState;
}
public final VerifiableValue.VerificationState getVerificationState() {
return this.verificationStateProperty().get();
}
public final void setVerificationState(
final VerifiableValue.VerificationState verificationState) {
this.verificationStateProperty().set(verificationState);
}
}
Now create table cells that observe the verification state of the current item in the table. So for example, given a TableColumn<Product, VerifiableValue<Double>> weightColumn
, you might do:
weightColumn.setCellFactory(tc -> {
TextFieldTableCell<Product, VerifiableValue<Double>> cell = new TextFieldTableCell<>();
cell.setConverter(new StringConverter<VerifiableValue<Double>>() {
@Override
public String toString(VerifiableValue<Double> item) {
return item == null ? "" : String.format("%.3f Kg", item.getValue());
}
@Override
public VerifiableValue<T> fromString(String string) {
T value = new Double(string.replace("Kg","").trim());
VerifiableValue.VerificationState verificationState = cell.getItem() == null ? VerifiableValue.VerificationState.UNVERIFIED : cell.getItem().getVerificationState() ;
return new VerifiableValue<>(value, verificationState);
}
});
ChangeListener<VerifiableValue.VerificationState> verifiedListener = (obs, oldState, newState) -> {
if (newState == null || newState == VerifiableValue.VerificationState.UNVERIFIED) {
cell.setStyle("");
} else if (newState == VerifiableValue.VerificationState.VERIFIED) {
cell.setStyle("-fx-background-color: yellow ;");
} else if (newState == VerifiableValue.VerificationState.ERROR) {
cell.setStyle("-fx-background-color: red ;");
}
};
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.verificationStateProperty().removeListener(verifiedListener);
}
if (newItem == null) {
cell.setStyle("");
} else {
if (newItem.getVerificationState() == null || newItem.getVerificationState() == VerifiableValue.VerificationState.UNVERIFIED) {
cell.setStyle("");
} else if (newItem.getVerificationState() == VerifiableValue.VerificationState.VERIFIED) {
cell.setStyle("-fx-background-color: yellow ;");
} else if (newItem.getVerificationState() == VerifiableValue.VerificationState.ERROR) {
cell.setStyle("-fx-background-color: red ;");
}
newItem.verificationStateProperty().addListener(verifiedListener);
}
});
return cell ;
});
Here's a SSCCE. I moved the most important parts to the top of the code (so things are in an unusual order), and move the creation of the table cell to a method to reduce repetitive code. In real life I would probably roll my own table cell for this, so the labels display "Kg" but they don't appear in the text field, and use a text formatter on the text field to prevent invalid input. I would also move the style out of the cell implementation code, and use CSS PseudoClasses to represent the "view state" of the cell, and an external style sheet to actually map those states to colors.
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class CellHighlightingTable extends Application {
public static class VerifiableValue<T> {
public enum VerificationState { UNVERIFIED, VERIFIED, ERROR }
private final T value ;
private final ObjectProperty<VerificationState> verificationState = new SimpleObjectProperty<>(VerificationState.UNVERIFIED) ;
public VerifiableValue(T value) {
this.value = value ;
}
public VerifiableValue(T value, VerificationState verificationState) {
this(value);
setVerificationState(verificationState);
}
public T getValue() {
return value ;
}
public final ObjectProperty<VerificationState> verificationStateProperty() {
return this.verificationState;
}
public final CellHighlightingTable.VerifiableValue.VerificationState getVerificationState() {
return this.verificationStateProperty().get();
}
public final void setVerificationState(
final CellHighlightingTable.VerifiableValue.VerificationState verificationState) {
this.verificationStateProperty().set(verificationState);
}
}
private <T> TableCell<Product, VerifiableValue<T>> createTableCell(String format, Function<String, T> supplier) {
TextFieldTableCell<Product, VerifiableValue<T>> cell = new TextFieldTableCell<>();
cell.setConverter(new StringConverter<VerifiableValue<T>>() {
@Override
public String toString(VerifiableValue<T> item) {
return item == null ? "" : String.format(format, item.getValue());
}
@Override
public VerifiableValue<T> fromString(String string) {
T value = supplier.apply(string);
VerifiableValue.VerificationState verificationState = cell.getItem() == null ? VerifiableValue.VerificationState.UNVERIFIED : cell.getItem().getVerificationState() ;
return new VerifiableValue<>(value, verificationState);
}
});
ChangeListener<VerifiableValue.VerificationState> verifiedListener = (obs, oldState, newState) -> {
if (newState == null || newState == VerifiableValue.VerificationState.UNVERIFIED) {
cell.setStyle("");
} else if (newState == VerifiableValue.VerificationState.VERIFIED) {
cell.setStyle("-fx-background-color: yellow ;");
} else if (newState == VerifiableValue.VerificationState.ERROR) {
cell.setStyle("-fx-background-color: red ;");
}
};
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.verificationStateProperty().removeListener(verifiedListener);
}
if (newItem == null) {
cell.setStyle("");
} else {
if (newItem.getVerificationState() == null || newItem.getVerificationState() == VerifiableValue.VerificationState.UNVERIFIED) {
cell.setStyle("");
} else if (newItem.getVerificationState() == VerifiableValue.VerificationState.VERIFIED) {
cell.setStyle("-fx-background-color: yellow ;");
} else if (newItem.getVerificationState() == VerifiableValue.VerificationState.ERROR) {
cell.setStyle("-fx-background-color: red ;");
}
newItem.verificationStateProperty().addListener(verifiedListener);
}
});
return cell ;
}
@Override
public void start(Stage primaryStage) {
TableView<Product> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.setEditable(true);
TableColumn<Product, String> productCol = new TableColumn<>("Product");
productCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
TableColumn<Product, VerifiableValue<Integer>> quantityColumn = new TableColumn<>("Quantity");
quantityColumn.setCellValueFactory(cellData -> cellData.getValue().quantityProperty());
quantityColumn.setCellFactory(tc -> createTableCell("%,d", Integer::new));
TableColumn<Product, VerifiableValue<Double>> weightColumn = new TableColumn<>("Weight");
weightColumn.setCellValueFactory(cellData -> cellData.getValue().weightProperty());
weightColumn.setCellFactory(tc -> createTableCell("%.3f Kg", Double::new));
table.getColumns().add(productCol);
table.getColumns().add(quantityColumn);
table.getColumns().add(weightColumn);
Button verifySelected = new Button("Verify Selected");
verifySelected.setOnAction(e -> {
for (TablePosition<?, ?> pos : table.getSelectionModel().getSelectedCells()) {
if (pos.getTableColumn() == quantityColumn) {
Product product = table.getItems().get(pos.getRow());
product.getQuantity().setVerificationState(VerifiableValue.VerificationState.VERIFIED);
} else if (pos.getTableColumn() == weightColumn) {
Product product = table.getItems().get(pos.getRow());
product.getWeight().setVerificationState(VerifiableValue.VerificationState.VERIFIED);
}
}
});
verifySelected.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedCells()));
Button errorSelected = new Button("Mark all selected as error");
errorSelected.setOnAction(e -> {
for (TablePosition<?, ?> pos : table.g