1

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();
}
Community
  • 1
  • 1
Adam
  • 35,919
  • 9
  • 100
  • 137
  • It's pretty hard to believe the explanation, though I haven't tested your code. In particular, I find it difficult to imagine how an event could be processed in between the graphic being set to null and it being set back to the label, since the entire thing is single threaded. As I'm sure you know, if you use JavaFX properties in the model, manual refreshes will be unnecessary. Isn't it easier to take that approach, even if it results in an additional layer in your model classes (i.e. each entity having a "UI model" with JavaFX properties and a second Java bean model)? – James_D May 17 '17 at 23:59
  • FWIW I did get a chance to test the example (with both JDK 1.8.0_40 and a recent version) you posted and couldn't reproduce the behavior you describe: I always saw both mouse listeners being invoked when clicking on the label. Note that there's a small amount of padding in a table cell, so there's some space where you can click on the table cell but not on the label: are you sure you're not just observing clicks in that small space? – James_D May 18 '17 at 13:31
  • @James_D Thanks for looking into this. What's your platform? I've seen this on both Windows 7 and Redhat 6u2... Yes I'm aware I could use a "view-model", however the requirements are for cells that contain multiple values from different fields, and can be updated from external models in certain circumstances, so there isn't a straight 1:1 mapping with any single field. I'm still interested in identifying the underlying cause. Yes I'm certain about click being in the area.... – Adam May 18 '17 at 14:48
  • @James_D I also printed out the MouseEvent which contains the "pick result" and this was the Text node inside the Label node, so must have been within bounds... – Adam May 18 '17 at 14:49
  • I'm running on Mac OSX 10.12.2. Tested JDK 1.8.0_40 and 1.8.0_121. If I have time later I'll try it on an Ubuntu machine (don't think I have 1.8.0_40 on there though). – James_D May 18 '17 at 14:50

0 Answers0