TableView doesn't commit values on focus lost event

后端 未结 4 1224
不思量自难忘°
不思量自难忘° 2020-11-29 09:41

I\'d like to create a table with the following features:

  • Edit on key press
  • Enter key = next row
  • Tab key = next column
  • Escape key = c
相关标签:
4条回答
  • 2020-11-29 10:01

    I got curious and did some background research.

    You are facing the problem of a well-known bug in the JavaFX.

    Background

    When you call commitEdit(textField.getText()), the first thing it does is to check the value of isEditing() and returns if the value is false, without committing.

    public void commitEdit(T newValue) {
        if (! isEditing()) return;
    
        ... // Rest of the things
    }
    

    Why does it return false?

    As you have probably found out, as soon as you press TAB or ENTER to change your selection, cancelEdit() is called which sets the TableCell.isEditing() to false. By the time the commitEdit() inside textField's focus property listener is called, isEditing() is already returning false.

    Solutions / Hacks

    There have been on going discussion on the Topic in JavaFX community. People in there have posted hacks, which you are most welcome to look at.

    • TableView, TreeView, ListView - Clicking outside of the edited cell, node, or entry should commit the value
    • TableCell - commit on focus lost not possible in every case

    There is a hack shown in a SO thread, which seems to get the job done, although I haven't tried it (yet).

    0 讨论(0)
  • 2020-11-29 10:09

    I've run into the same issue and I solved it by combining these two code snippets:

    • https://gist.github.com/james-d/be5bbd6255a4640a5357
    • https://gist.github.com/abhinayagarwal/9383881

    Custom TableCell implementation

    public class EditCell<S, T> extends TableCell<S, T> {
        private final TextField textField = new TextField();
    
        // Converter for converting the text in the text field to the user type, and vice-versa:
        private final StringConverter<T> converter;
    
        /**
         * Creates and initializes an edit cell object.
         * 
         * @param converter
         *            the converter to convert from and to strings
         */
        public EditCell(StringConverter<T> converter) {
            this.converter = converter;
    
            itemProperty().addListener((obx, oldItem, newItem) -> {
                setText(newItem != null ? this.converter.toString(newItem) : null);
            });
    
            setGraphic(this.textField);
            setContentDisplay(ContentDisplay.TEXT_ONLY);
    
            this.textField.setOnAction(evt -> {
                commitEdit(this.converter.fromString(this.textField.getText()));
            });
            this.textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
                if (!isNowFocused) {
                    commitEdit(this.converter.fromString(this.textField.getText()));
                }
            });
            this.textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                if (event.getCode() == KeyCode.ESCAPE) {
                    this.textField.setText(this.converter.toString(getItem()));
                    cancelEdit();
                    event.consume();
                } else if (event.getCode() == KeyCode.TAB) {
                    commitEdit(this.converter.fromString(this.textField.getText()));
                    TableColumn<S, ?> nextColumn = getNextColumn(!event.isShiftDown());
                    if (nextColumn != null) {
                        getTableView().getSelectionModel().clearAndSelect(getTableRow().getIndex(), nextColumn);
                        getTableView().edit(getTableRow().getIndex(), nextColumn);
                    }
                }
            });
        }
    
        /**
         * Convenience converter that does nothing (converts Strings to themselves and vice-versa...).
         */
        public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>() {
    
            @Override
            public String toString(String object) {
                return object;
            }
    
            @Override
            public String fromString(String string) {
                return string;
            }
    
        };
    
        /**
         * Convenience method for creating an EditCell for a String value.
         * 
         * @return the edit cell
         */
        public static <S> EditCell<S, String> createStringEditCell() {
            return new EditCell<S, String>(IDENTITY_CONVERTER);
        }
    
        // set the text of the text field and display the graphic
        @Override
        public void startEdit() {
            super.startEdit();
            this.textField.setText(this.converter.toString(getItem()));
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            this.textField.requestFocus();
        }
    
        // revert to text display
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
    
        // commits the edit. Update property if possible and revert to text display
        @Override
        public void commitEdit(T item) {
            // This block is necessary to support commit on losing focus, because the baked-in mechanism
            // sets our editing state to false before we can intercept the loss of focus.
            // The default commitEdit(...) method simply bails if we are not editing...
            if (!isEditing() && !item.equals(getItem())) {
                TableView<S> table = getTableView();
                if (table != null) {
                    TableColumn<S, T> column = getTableColumn();
                    CellEditEvent<S, T> event = new CellEditEvent<>(table,
                            new TablePosition<S, T>(table, getIndex(), column),
                            TableColumn.editCommitEvent(), item);
                    Event.fireEvent(column, event);
                }
            }
    
            super.commitEdit(item);
    
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
    
        /**
         * Finds and returns the next editable column.
         * 
         * @param forward
         *            indicates whether to search forward or backward from the current column
         * @return the next editable column or {@code null} if there is no next column available
         */
        private TableColumn<S, ?> getNextColumn(boolean forward) {
            List<TableColumn<S, ?>> columns = new ArrayList<>();
            for (TableColumn<S, ?> column : getTableView().getColumns()) {
                columns.addAll(getEditableColumns(column));
            }
            // There is no other column that supports editing.
            if (columns.size() < 2) { return null; }
            int currentIndex = columns.indexOf(getTableColumn());
            int nextIndex = currentIndex;
            if (forward) {
                nextIndex++;
                if (nextIndex > columns.size() - 1) {
                    nextIndex = 0;
                }
            } else {
                nextIndex--;
                if (nextIndex < 0) {
                    nextIndex = columns.size() - 1;
                }
            }
            return columns.get(nextIndex);
        }
    
        /**
         * Returns all editable columns of a table column (supports nested columns).
         * 
         * @param root
         *            the table column to check for editable columns
         * @return a list of table columns which are editable
         */
        private List<TableColumn<S, ?>> getEditableColumns(TableColumn<S, ?> root) {
            List<TableColumn<S, ?>> columns = new ArrayList<>();
            if (root.getColumns().isEmpty()) {
                // We only want the leaves that are editable.
                if (root.isEditable()) {
                    columns.add(root);
                }
                return columns;
            } else {
                for (TableColumn<S, ?> column : root.getColumns()) {
                    columns.addAll(getEditableColumns(column));
                }
                return columns;
            }
        }
    }
    

    Controller

        @FXML
        private void initialize() {
            table.getSelectionModel().setCellSelectionEnabled(true);
            table.setEditable(true);
    
            table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
            table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
            table.getColumns().add(createColumn("Email", Person::emailProperty));
    
            table.getItems().addAll(
                    new Person("Jacob", "Smith", "jacob.smith@example.com"),
                    new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                    new Person("Ethan", "Williams", "ethan.williams@example.com"),
                    new Person("Emma", "Jones", "emma.jones@example.com"),
                    new Person("Michael", "Brown", "michael.brown@example.com")
            );
    
            table.setOnKeyPressed(event -> {
                TablePosition<Person, ?> pos = table.getFocusModel().getFocusedCell() ;
                if (pos != null && event.getCode().isLetterKey()) {
                    table.edit(pos.getRow(), pos.getTableColumn());
                }
            });
        }
    
        private <T> TableColumn<T, String> createColumn(String title, Function<T, StringProperty> property) {
            TableColumn<T, String> col = new TableColumn<>(title);
            col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
    
            col.setCellFactory(column -> EditCell.createStringEditCell());
            return col;
        }
    
    0 讨论(0)
  • 2020-11-29 10:15

    My proposal to solve this atrocity is the following (sorry for missing JavaDoc).

    This is a cancel-to-commit redirection solution. I tested it under LINUX with Java 1.8.0-121. Here, the only way how to discard a cell editor is to press ESCAPE.

    import javafx.beans.binding.Bindings;
    import javafx.scene.Node;
    import javafx.scene.control.ContentDisplay;
    import javafx.scene.control.TableCell;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    
    public abstract class AutoCommitTableCell<S,T> extends TableCell<S,T>
    {
        private Node field;
        private boolean startEditing;
        private T defaultValue;
    
    
        /** @return a newly created input field. */
        protected abstract Node newInputField();
    
        /** @return the current value of the input field. */
        protected abstract T getInputValue();
    
        /** Sets given value to the input field. */
        protected abstract void setInputValue(T value);
    
        /** @return the default in case item is null, must be never null, else cell will not be editable. */
        protected abstract T getDefaultValue();
    
        /** @return converts the given value to a string, being the cell-renderer representation. */
        protected abstract String inputValueToText(T value);
    
    
        @Override
        public void startEdit() {
            try {
                startEditing = true;
    
                super.startEdit();  // updateItem() will be called
    
                setInputValue(getItem());
            }
            finally {
                startEditing = false;
            }
        }
    
        /** Redirects to commitEdit(). Leaving the cell should commit, just ESCAPE should cancel. */
        @Override
        public void cancelEdit() {
            // avoid JavaFX NullPointerException when calling commitEdit()
            getTableView().edit(getIndex(), getTableColumn());
    
            commitEdit(getInputValue());
        }
    
        private void cancelOnEscape() {
            if (defaultValue != null)    {   // canceling default means writing null
                setItem(defaultValue = null);
                setText(null);
                setInputValue(null);
            }
            super.cancelEdit();
        }
    
        @Override
        protected void updateItem(T newValue, boolean empty) {
            if (startEditing && newValue == null)
                newValue = (defaultValue = getDefaultValue());
    
            super.updateItem(newValue, empty);
    
            if (empty || newValue == null) {
                setText(null);
                setGraphic(null);
            }
            else {
                setText(inputValueToText(newValue));
                setGraphic(startEditing || isEditing() ? getInputField() : null);
            }
        }
    
        protected final Node getInputField()    {
            if (field == null)    {
                field = newInputField();
    
                // a cell-editor won't be committed or canceled automatically by JFX
                field.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                    if (event.getCode() == KeyCode.ENTER || event.getCode() == KeyCode.TAB)
                        commitEdit(getInputValue());
                    else if (event.getCode() == KeyCode.ESCAPE)
                        cancelOnEscape();
                });
    
                contentDisplayProperty().bind(
                        Bindings.when(editingProperty())
                            .then(ContentDisplay.GRAPHIC_ONLY)
                            .otherwise(ContentDisplay.TEXT_ONLY)
                    );
            }
            return field;
        }
    }
    

    You can extend this class to support any data type.

    Example for a String field is (Person is an example bean):

    import javafx.scene.Node;
    import javafx.scene.control.TextField;
    import jfx.examples.tablebinding.PersonsModel.Person;
    
    public class StringTableCell extends AutoCommitTableCell<Person,String>
    {
        @Override
        protected String getInputValue() {
            return ((TextField) getInputField()).getText();
        }
    
        @Override
        protected void setInputValue(String value) {
            ((TextField) getInputField()).setText(value);
        }
    
        @Override
        protected String getDefaultValue() {
            return "";
        }
    
        @Override
        protected Node newInputField() {
            return new TextField();
        }
    
       @Override
        protected String inputValueToText(String newValue) {
            return newValue;
        }
    }
    

    To be applied in this way:

    final TableColumn<Person,String> nameColumn = new TableColumn<Person,String>("Name");
    nameColumn.setCellValueFactory(
            cellDataFeatures -> cellDataFeatures.getValue().nameProperty());
    nameColumn.setCellFactory(
            cellDataFeatures -> new StringTableCell());
    
    0 讨论(0)
  • 2020-11-29 10:17

    I had found a simple solution which works in my case for TableCells. The idea is to forget about commitEdit at focus lost. Let javafx do its work, and then just update the value of the previously edited cell.

    abstract class EditingTextCell<T, V> extends TableCell<T, V> {
        protected TextField textField;
        private T editedItem;
    
        @Override
        public void startEdit() {
            ...
            textField.focusedProperty().addListener((t, oldval, newval) -> {
                if (!newval) {
                    setItemValue(editedItem, textField.getText());
                }
            });
    
            editedItem = (T) getTableRow().getItem();
        }
        public abstract void setItemValue(T item, String text);
        ...
    }
    

    so, the only trick is to implement the setItemValue() in such a way that it updates the correct part of the item.

    0 讨论(0)
提交回复
热议问题