mercredi 18 novembre 2015

JavaFX: CheckBox inside TableCell breaks the traversal order

Consider the following example:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class TestCheckBoxTab extends Application {
    public void start (Stage stage) {
        HBox root = new HBox();
        root.getChildren().addAll(new TextField(), new TextField(), new CheckBox(), new TextField());

        stage.setScene(new Scene(root));
        stage.show();
    }

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

Here you can easily use the TAB and SHIFT+TAB commands to traverse through the different controls, and it works properly.

But if you have the same controls inside table cells in a TableView, the CheckBox breaks the traversal order. This is demonstrated in the following example:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

public class AlwaysEditableTable extends Application {
    public void start(Stage stage) {

        TableView<ObservableList<StringProperty>> table = new TableView<>();
        table.setEditable(true);
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.setPrefWidth(505);

        // Dummy columns
        ObservableList<String> columns = FXCollections.observableArrayList("Column1", "Column2", "Column3", "Column4",
                "Column5");

        // Dummy data
        ObservableList<StringProperty> row1 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
                new SimpleStringProperty("Cell2"), new SimpleStringProperty("0"), new SimpleStringProperty("Cell4"),
                new SimpleStringProperty("1"));
        ObservableList<ObservableList<StringProperty>> data = FXCollections.observableArrayList();
        data.add(row1);

        for (int i = 0; i < columns.size(); i++) {
            final int j = i;
            TableColumn<ObservableList<StringProperty>, String> col = new TableColumn<>(columns.get(i));
            col.setCellValueFactory(param -> param.getValue().get(j));
            col.setPrefWidth(100);

            if (i == 2 || i == 4) {
                col.setCellFactory(e -> new CheckBoxCell(j));
            } else {
                col.setCellFactory(e -> new AlwaysEditingCell(j));
            }

            table.getColumns().add(col);
        }

        table.setItems(data);

        Scene scene = new Scene(table);
        stage.setScene(scene);
        stage.show();
    }

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

    /**
     * A cell that contains a text field that is always shown. The text of the
     * text field is bound to the underlying data.
     */
    public static class AlwaysEditingCell extends TableCell<ObservableList<StringProperty>, String> {

        private final TextField textField;

        public AlwaysEditingCell(int columnIndex) {

            textField = new TextField();

            this.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
                if (isNowEmpty) {
                    setGraphic(null);
                } else {
                    setGraphic(textField);
                }
            });

            // The index is not changed until tableData is instantiated, so this
            // ensure the we wont get a NullPointerException when we do the
            // binding.
            this.indexProperty().addListener((obs, oldValue, newValue) -> {

                ObservableList<ObservableList<StringProperty>> tableData = getTableView().getItems();
                int oldIndex = oldValue.intValue();
                if (oldIndex >= 0 && oldIndex < tableData.size()) {
                    textField.textProperty().unbindBidirectional(tableData.get(oldIndex).get(columnIndex));
                }
                int newIndex = newValue.intValue();
                if (newIndex >= 0 && newIndex < tableData.size()) {
                    textField.textProperty().bindBidirectional(tableData.get(newIndex).get(columnIndex));
                    setGraphic(textField);
                } else {
                    setGraphic(null);
                }

            });
        }
    }

    /**
     * A cell containing a checkbox. The checkbox represent the underlying value
     * in the cell. If the cell value is 0, the checkbox is unchecked. Checking
     * or unchecking the checkbox will change the underlying value.
     */
    public static class CheckBoxCell extends TableCell<ObservableList<StringProperty>, String> {

        private final CheckBox box;
        private ObservableList<ObservableList<StringProperty>> tableData;

        public CheckBoxCell(int columnIndex) {

            this.box = new CheckBox();

            this.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
                if (isNowEmpty) {
                    setGraphic(null);
                } else {
                    setGraphic(box);
                }
            });

            this.indexProperty().addListener((obs, oldValue, newValue) -> {

                tableData = getTableView().getItems();

                int newIndex = newValue.intValue();
                if (newIndex >= 0 && newIndex < tableData.size()) {
                    // If active value is "1", the check box will be set to
                    // selected.
                    box.setSelected(tableData.get(getIndex()).get(columnIndex).equals("1"));

                    // We add a listener to the selected property. This will
                    // allow us to execute code every time the check box is
                    // selected or deselected.
                    box.selectedProperty().addListener((observable, oldVal, newVal) -> {
                        if (newVal) {
                            // If newValue is true the checkBox is selected, and
                            // we set the corresponding cell value to "1".
                            tableData.get(getIndex()).get(columnIndex).set("1");
                        } else {
                            // Otherwise we set it to "0".
                            tableData.get(getIndex()).get(columnIndex).set("0");
                        }
                    });

                    setGraphic(box);
                } else {
                    setGraphic(null);
                }

            });
        }
    }
}

When pressing TAB while focusing a TextField inside a TableCell, the focus is moved to the next control properly. But if a CheckBox inside a TableCell is focused, the focus is moved to the first Control in the TableView instead of to the next Control. SHIFT+TAB while focusing a CheckBoxwill move the focus to the last control in the TableView. If I add a TextField outside the TableView, SHIFT+TAB while focusing a CheckBox will actually focus that TextField, while TAB behaviour still at least keeps the focus inside the TableView. The CheckBox somehow breaks the traversal order.

This is strange to me since TextField and CheckBox seems to have the same TAB functionality implemented, due to the fact the traversal order is correct in the first example. Something I guess is inherited from the Control class.

Does anybody know anything about this? I tried to look for some kind of EventFilter or listener for the TAB and SHIFT+TAB commands inside the source code of Control, TextField, CheckBox, TextInputControl and even in the Scene source, but I couldn't find it anywhere.

I also tried implementing my own TAB functionality for the CheckBox cell, which ended up raising another issue.




Aucun commentaire:

Enregistrer un commentaire