Background. I'm using the refresh solution from JavaFX 2.1 TableView refresh items as the project I'm on is stuck on Java 8u40. I'm aware of the refresh()
was added in 8u60.
I'm also using custom cell content, and that content has mouse listeners, bizarre customer requirements, don't ask.
The issue is that mouse events made whilst the table is updating are sometimes lost. The event is seen by the TableCell, but not by the custom content.
The example at the end demonstrates this issue if you click repeatedly on the "custom stuff" label. I'd expect to always see a pair of events.
MouseClicked on custom stuff
MouseClicked on TableCell
However, we only see MouseClicked on TableCell
in some cases.
I can only only assume at this point the table is midway through a redraw and the custom label isn't currently part of the scene graph. I don't understand how this could happen as both the mouse events and table update are occuring on the same thread....
I'm after better explanation of what is happening and a possible workaround.
public class Test extends Application {
public static void main(String[] args) {
launch(args);
}
public class Item {
private int x;
public Item(int x) {
this.x = x;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
@Override
public int hashCode() {
return x;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Item) {
return x == ((Item) obj).x;
}
return false;
}
}
private static class CustomCell extends TableCell<Item, Integer> {
private Label custom;
public CustomCell() {
super();
setOnMouseClicked(event -> System.out.println("MouseClicked on TableCell"));
custom = new Label("Custom stuff");
custom.setOnMouseClicked(event -> System.out.println("MouseClicked on custom stuff"));
}
@Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(custom);
}
}
}
@Override
public void start(Stage primaryStage) throws Exception {
List<Item> original = Arrays.asList(new Item(1), new Item(2));
ObservableList<Item> list = FXCollections.observableArrayList(original);
TableView<Item> tableView = new TableView<Item>(list);
TableColumn<Item, Integer> column = new TableColumn<>();
column.setPrefWidth(100);
column.setCellValueFactory(new PropertyValueFactory<>("x"));
column.setCellFactory(col -> new CustomCell());
tableView.getColumns().add(column);
primaryStage.setScene(new Scene(new HBox(tableView)));
primaryStage.show();
Timeline animation = new Timeline();
animation.setCycleCount(Animation.INDEFINITE);
animation.getKeyFrames().add(new KeyFrame(Duration.millis(400), e -> {
// poor-mans refresh
list.removeAll(list);
list.addAll(original);
}));
animation.play();
}
}
Further investigation shows it is the call to setGraphic(null)
that causes the issue, in turn casued by the removeAll()
. During this time before the item is in place the events are lost.
I've developed an alternative refresh mechanism, which misuses the cell value factory and avoids the removeAll() call.
1) Add a JavaFX property to your item model that isn't used for anything else
private IntegerProperty update = new SimpleIntegerProperty();
public final IntegerProperty updateProperty() {
return this.update;
}
public void update() {
updateProperty().set(update.get() + 1);
}
2) Change all the columns that need updating to use this, it doesn't matter as all the columns are deriving their values from multiple fields anyway
column.setCellValueFactory(new PropertyValueFactory<>("update"));
3) Change the poor-mans refresh
// poor-mans refresh
for (Item each : list) {
each.update();
}