Search code examples
javafxdatepickertableviewjavafx-11

Why does DatePicker become unresponsive after a startEdit() in a custom JavaFX 11 TableCell?


I have a TableCell that renders a date as a DatePicker when editing and as plain text when not editing.

When the TableCell is edited, the DatePicker should receive focus, its date should be selected and the cursor should be in the editor at the end of the date. The DatePicker's editor should then be ready to accept user input.

However, at the moment, when the cell is edited, the DatePicker gets focus but it (or its editor?) becomes unresponsive. The date isn't selected and the cursor is nowhere in sight. I can't click into the editor nor type anything into the editor. The only thing that works is the button that shows the calendar popup.

Further, this only happens the first time a DatePicker cell is edited in a given row. The next time the same cell in the same row is edited, it works fine.

It also works fine if I change setContentDisplay() to GRAPHIC_ONLY in updateItem() but I don't want to do that.

Below is a MVCE that demonstrates the issue.

To reproduce the behaviour, either double-click on a date cell or use the arrow keys to navigate to it and press F2. The DatePicker will get focus but its date will not be selected and you won't be able to click into the DatePicker's editor.

Now click anywhere off the date cell and then go back to the same cell in the same row and edit it a second time. The date should then be selected and you should be able click or type into the editor.

What am I doing wrong?

I've Googled and found examples of DatePickers in TableViews but all set content display to GRAPHIC_ONLY in updateItem(). I've not been able to find any that use TEXT_ONLY except while editing.

I'm using the 11.0.2 versions of OpenJDK and OpenJFX running in Netbeans 11.0 on Windows 7.

package test016;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
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.ContentDisplay;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;

public class Test016 extends Application {

    private final ObservableList<DataModel> ol = FXCollections.observableArrayList();
    private final TableView<DataModel> tv = new TableView();

    private Parent createContent() {

        loadDummyData();

        createTableViewColumns();

        tv.getSelectionModel().setCellSelectionEnabled(true);
        tv.setEditable(true);
        tv.setItems(ol);
        tv.getSelectionModel().selectFirst();

        BorderPane content = new BorderPane();
        content.setCenter(tv);

        return content;
    }

    private void createTableViewColumns() {

        TableColumn<DataModel,String> col1 = new TableColumn<>("field1");
        TableColumn<DataModel,LocalDate> col2 = new TableColumn<>("field2");
        TableColumn<DataModel,String> col3 = new TableColumn<>("field3");

        col1.setCellValueFactory(cellData -> cellData.getValue().field1Property());
        col1.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        col2.setCellValueFactory(cellData -> cellData.getValue().field2Property());

        //**************************************************************
        //DatePicker TableCell begins here

        col2.setCellFactory(dp -> {

            DatePicker datePicker = new DatePicker();

            StringConverter<LocalDate> dateConverter = new StringConverter<LocalDate>() {
                @Override
                public String toString(LocalDate object) {
                    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("d/MM/yyyy");
                    return ( object == null ? "" : dateFormatter.format(object) );
                }

                @Override
                public LocalDate fromString(String string) {
                    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("d/MM/yyyy");
                    return string.isEmpty() ? null : LocalDate.parse(string, dateFormatter);
                }

            };

            datePicker.setConverter(dateConverter);
            datePicker.setEditable(true);
            datePicker.setDisable(false);

            TableCell<DataModel, LocalDate> cell = new TableCell<>() {

                @Override
                protected void updateItem(LocalDate item, boolean empty) {

                    super.updateItem(item, empty);

                    if (empty) {
                        setText(null);
                        setGraphic(null);
                    } else {
                        //It works I set content display to GRAPHIC_ONLY
                        setContentDisplay(ContentDisplay.TEXT_ONLY);
                        //setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

                        setText(dateConverter.toString(item));
                        datePicker.setValue((LocalDate) item);
                        setGraphic(datePicker);
                    }

                }

                @Override
                public void startEdit() {

                    super.startEdit();

                    if ( getGraphic() != null && getGraphic() instanceof DatePicker ) {
                        //The problem only occurs when I set content display to TEXT_ONLY in updateItem().
                        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

                        //If I requestFocus() on the graphic, the DatePicker gets the focus but I cannot edit the date
                        //by clicking in the editor and typing something in.

                        //If I do NOT requestFocus() on the graphic, the cell containing the DatePicker gets focus but
                        //I can then click in the DatePicker's editor and type something in.  However, I cannot just
                        //start typing as the DatePicker doesn't have focus.

                        //This happens irrespective of whether I double-click on a cell to start the edit or
                        //navigate to the cell via the keyboard and then hit F2.

                        //The behaviour only appears the first time I edit a DatePicker in a given row.
                        //The second and subsequent edits for the same row work fine.

                        getGraphic().requestFocus();

                        //Requesting focus on the DatePicker's editor doesn't appear to have any effect, either with or 
                        //without first requesting focus on the graphic.
                        ( (DatePicker) getGraphic() ).getEditor().requestFocus();
                        ( (DatePicker) getGraphic() ).getEditor().selectAll();
                    }

                }

                @Override
                public void cancelEdit() {
                    super.cancelEdit();
                    setContentDisplay(ContentDisplay.TEXT_ONLY);
                }
            };

            return cell;

        });
        //**************************************************************

        col2.setPrefWidth(120);

        col3.setCellValueFactory(cellData -> cellData.getValue().field3Property());
        col3.setCellFactory(TextFieldTableCell.<DataModel, String>forTableColumn(new DefaultStringConverter()));

        tv.getColumns().addAll(Arrays.asList(col1, col2, col3));

    }

    private void loadDummyData() {

        ol.add(new DataModel("1", LocalDate.parse("2001-01-01"), "x"));
        ol.add(new DataModel("2", LocalDate.parse("2001-01-01"), "x"));
        ol.add(new DataModel("3", LocalDate.parse("2001-01-01"), "x"));
        ol.add(new DataModel("4", LocalDate.parse("2001-01-01"), "x"));
        ol.add(new DataModel("5", LocalDate.parse("2001-01-01"), "x"));

    }

    private class DataModel {

        private final StringProperty field1;
        private final ObjectProperty<LocalDate> field2;
        private final StringProperty field3;

        public DataModel(
            String field1,
            LocalDate field2,
            String field3
        ) {
            this.field1 = new SimpleStringProperty(field1);
            this.field2 = new SimpleObjectProperty<>(field2);
            this.field3 = new SimpleStringProperty(field3);
        }

        public String getField1() {return field1.get().trim();}
        public void setField1(String field1) {this.field1.set(field1);}
        public StringProperty field1Property() {return field1;}

        public LocalDate getField2() {return field2.get();}
        public void setField2(LocalDate field2) {this.field2.set(field2);}
        public ObjectProperty<LocalDate> field2Property() {return field2;}

        public String getField3() {return field3.get().trim();}
        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("OpenJFX11 - DatePicker in TableView");
        stage.setWidth(600D);
        stage.setHeight(600D);
        stage.show();
    }

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

}

Solution

  • Thanks to Slaw's suggestion, a workaround for the unresponsiveness issue is to call applyCss() on the DatePicker after it's set as the graphic but before the requestFocus() and selectAll().

    While the selectAll() will still only take effect from the second edit of the same cell in the same row, the DatePicker will at least now respond to edits.

    @Override
    public void startEdit() {
    
        super.startEdit();
    
        if ( getGraphic() != null && getGraphic() instanceof DatePicker ) {
    
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    
            ( (DatePicker) getGraphic() ).applyCss(); //Workaround for the unresponsiveness issue.
    
            getGraphic().requestFocus();
    
            ( (DatePicker) getGraphic() ).getEditor().requestFocus();
            ( (DatePicker) getGraphic() ).getEditor().selectAll();
        }
    
    }