I have an InvalidationListener
set on the selectedProperty()
of a custom TableCell
. I use it to change the editability and background colour of rows based on a boolean value in the data model.
I was debugging the mysterious disappearance of row color when I click on a cell and then click on a previous cell in the same row and tracked it down to isSelected()
's value at the point at which row colour is set.
From what I could see, isSelected()
changes from false
to true
while moving forwards in a row, but changes from true
to false
while moving backwards.
Why does it behave like that? Shouldn't it be consistent in the changes it reports?
I tried using selectedProperty.get()
instead of isSelected()
, and a ChangeListener
rather than an InvalidationListener
, but got the same result. And the same thing happens if I navigate with the keyboard rather than the mouse.
Here's a MVCE that demonstrates the issue. It's based on user Slaw's answer here How to set a TableRow's background colour based on whether or not it's selected and/or a value in the data model, in a JavaFX8 TableView? and user kleopatra's answer here TreeTableView : setting a row not editable.
To reproduce the behaviour, click the second or third cell of a row. It will change colour according to the matrix shown below. Then click on a previous cell in the same row. The row colour should disappear and revert to default. The values of isSelected()
and selectedProperty()
will output to the console.
I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and Scene Builder 8.3.
Test45_Listeners.java
package test45_listeners;
import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
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 Test45_Listeners extends Application {
private final ObservableList<TestModel> olTestModel
= FXCollections.observableArrayList(testmodel -> new Observable[] {});
private Parent createContent() {
//Initialise the TableView and data
createDummyData(100);
TableView<TestModel> table = new TableView<>();
TableColumn<TestModel, String> colField1 = new TableColumn<>("field1");
colField1.setCellValueFactory(features -> features.getValue().field1Property());
colField1.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));
TableColumn<TestModel, String> colField2 = new TableColumn<>("field2");
colField2.setCellValueFactory(features -> features.getValue().field2Property());
colField2.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));
TableColumn<TestModel, String> colField3 = new TableColumn<>("field3");
colField3.setCellValueFactory(features -> features.getValue().field3Property());
colField3.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));
table.setItems(olTestModel);
table.getColumns().addAll(Arrays.asList(colField1, colField2, colField3));
table.setEditable(true);
table.getSelectionModel().setCellSelectionEnabled(true);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
//Set a row factory to set the background colour of any LOCKED row to be yellow
table.setRowFactory(tv -> {
TableRow<TestModel> row = new TableRow<TestModel>() {
@Override
public void updateItem(TestModel item, boolean empty) {
super.updateItem(item, empty);
boolean locked = false;
if ( getItem() != null ) {
locked = getItem().lockedProperty().get();
setEditable( ! locked );
}
if ( !isEmpty() && locked ) {
setStyle("-fx-background-color: yellow;");
} else {
setStyle(null);
}
}
};
return row;
});
BorderPane content = new BorderPane(table);
return content;
}
private void createDummyData(int count) {
for ( int i=0; i<count; i++ ) {
boolean locked = Math.random() >= 0.5;
olTestModel.add(new TestModel(locked, (locked ? "row LOCKED" : "row NOT locked"),
Integer.toString(i), "a"+Integer.toString(i)));
}
}
private class TestModel {
private final BooleanProperty locked;
private final StringProperty field1;
private final StringProperty field2;
private final StringProperty field3;
public TestModel(
boolean locked,
String field1,
String field2,
String field3
) {
this.locked = new SimpleBooleanProperty(locked);
this.field1 = new SimpleStringProperty(field1);
this.field2 = new SimpleStringProperty(field2);
this.field3 = new SimpleStringProperty(field3);
}
public boolean getLocked() {return locked.get();}
public void setLocked(boolean locked) {this.locked.set(locked);}
public BooleanProperty lockedProperty() {return locked;}
public String getField1() {return field1.get();}
public void setField1(String field1) {this.field1.set(field1);}
public StringProperty field1Property() {return field1;}
public String getField2() {return field2.get();}
public void setField2(String field2) {this.field2.set(field2);}
public StringProperty field2Property() {return field2;}
public String getField3() {return field3.get();}
public void setField3(String field3) {this.field3.set(field3);}
public StringProperty field3Property() {return field3;}
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setTitle("Test");
stage.setWidth(350);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
TestTextCell.java
package test45_listeners;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
public class TestTextCell<S, T> extends TableCell<S, T> {
public final TextField textField = new TextField();
public final StringConverter<T> converter;
private BooleanProperty isLockedProperty;
private final InvalidationListener strongListener = (Observable observable) -> {
updateStyle();
};
private final WeakInvalidationListener weakListener = new WeakInvalidationListener(strongListener);
/*
public ChangeListener<Boolean> strongListener = (ObservableValue<? extends Boolean> observable, Boolean wasFocused, Boolean isNowFocused) -> {
updateStyle();
};
public final WeakChangeListener<Boolean> weakListener = new WeakChangeListener<Boolean>(strongListener);
*/
//*********************************************************************************************************************
public TestTextCell(StringConverter<T> converter, Function<S, BooleanProperty> methodGetLockedProperty) {
this.converter = converter;
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
itemProperty().addListener((obx, oldItem, newItem) -> {
if (newItem == null) {
setText(null);
} else {
setText(converter.toString(newItem));
if ( methodGetLockedProperty != null ) {
S datamodel = getTableView().getItems().get(getIndex());
isLockedProperty = methodGetLockedProperty.apply(datamodel);
} else {
isLockedProperty = new SimpleBooleanProperty(false);
}
}
});
//Add the invalidation listener
selectedProperty().addListener(strongListener);
}
//*******************************************************************************************************************
public static <S> TestTextCell<S, String> createStringTextCell(Function<S, BooleanProperty> methodGetLockedProperty) {
return new TestTextCell<S, String>(new DefaultStringConverter(), methodGetLockedProperty);
}
//*******************************************************************************************************************
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = (T) getItem();
if (oldItem != null) {
selectedProperty().removeListener(weakListener);
}
super.updateItem(item, empty);
if (item != null) {
selectedProperty().addListener(weakListener);
if ( getTableRow() != null ) {
if (getGraphic() != null) {
getGraphic().disableProperty().bind(
Bindings.not(getTableRow().editableProperty())
);
}
}
}
}
@Override
public void startEdit() {
if ( ! isLockedProperty.get() ) {
super.startEdit();
if (getGraphic() != null) {
textField.setText(converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getGraphic().requestFocus();
}
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
//*******************************************************************************************************************
private void updateStyle() {
System.out.println("in updateStyle(), isLockedProperty = " + isLockedProperty.get()
+ ", isSelected() = " + isSelected() + ", selectedProperty.get() = " + selectedProperty().get());
if ( getTableRow() != null ) {
if ( isLockedProperty.get() && isSelected() ) {
// if ( isLockedProperty.get() && selectedProperty().get() ) {
getTableRow().setStyle("-fx-background-color: pink;");
} else if ( isLockedProperty.get() && ! isSelected()) {
// } else if ( isLockedProperty.get() && ! selectedProperty().get() ) {
getTableRow().setStyle("-fx-background-color: yellow;");
} else if ( ! isLockedProperty.get() && isSelected() ) {
// } else if ( ! isLockedProperty.get() && selectedProperty().get() ) {
getTableRow().setStyle("-fx-background-color: #b6e1fc;");
} else if ( ! isLockedProperty.get() && ! isSelected() ) {
// } else if ( ! isLockedProperty.get() && ! selectedProperty().get() ) {
getTableRow().setStyle(null);
} else {
throw new AssertionError("how did I get here?");
}
}
}
}