How to commit when clicking outside an editable TableView cell in JavaFX?

后端 未结 4 830
清歌不尽
清歌不尽 2020-12-29 10:25

I have a table cell factory responsible for creating an editable cell in a JavaFX TableView.

I\'m trying to implement some added functionality to the tableview so th

相关标签:
4条回答
  • 2020-12-29 10:54

    Since I could not find kuaw26's source code (dead link) I developed my own solution for java 8. I found out that the TextField in the code above never receives a keyReleased event for the esc-key, therefore his code does not work.

    Unfortunately I needed to duplicate code from TextFieldTableCell and CellUtils and adapt it, since TextFieldTableCell uses a private TextField and CellUtils is package protected. This is probably not the best OO way.

    Here is my solution:

    // package yourLib;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.event.Event;
    import javafx.scene.control.Label;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableColumn.CellEditEvent;
    import javafx.scene.control.TablePosition;
    import javafx.scene.control.TableView;
    import javafx.scene.control.TextField;
    import javafx.scene.input.KeyCode;
    import javafx.util.Callback;
    import javafx.util.StringConverter;
    import javafx.util.converter.DefaultStringConverter;
    
    /**
     * A class containing a {@link TableCell} implementation that draws a 
     * {@link TextField} node inside the cell. If the TextField is
     * left, the value is commited.
     * 
     */
    
    public class AcceptOnExitTableCell<S,T> extends TableCell<S,T> {
    
    /***************************************************************************
     *                                                                         *
     * Static cell factories                                                   *
     *                                                                         *
     **************************************************************************/
    
    /**
     * Provides a {@link TextField} that allows editing of the cell content when
     * the cell is double-clicked, or when 
     * {@link TableView#edit(int, javafx.scene.control.TableColumn)} is called. 
     * This method will only  work on {@link TableColumn} instances which are of
     * type String.
     * 
     * @return A {@link Callback} that can be inserted into the 
     *      {@link TableColumn#cellFactoryProperty() cell factory property} of a 
     *      TableColumn, that enables textual editing of the content.
     */
    public static <S> Callback<TableColumn<S,String>, TableCell<S,String>> forTableColumn() {
        return forTableColumn(new DefaultStringConverter());
    }
    
    /**
     * Provides a {@link TextField} that allows editing of the cell content when
     * the cell is double-clicked, or when 
     * {@link TableView#edit(int, javafx.scene.control.TableColumn) } is called. 
     * This method will work  on any {@link TableColumn} instance, regardless of 
     * its generic type. However, to enable this, a {@link StringConverter} must 
     * be provided that will convert the given String (from what the user typed 
     * in) into an instance of type T. This item will then be passed along to the 
     * {@link TableColumn#onEditCommitProperty()} callback.
     * 
     * @param converter A {@link StringConverter} that can convert the given String 
     *      (from what the user typed in) into an instance of type T.
     * @return A {@link Callback} that can be inserted into the 
     *      {@link TableColumn#cellFactoryProperty() cell factory property} of a 
     *      TableColumn, that enables textual editing of the content.
     */
    public static <S,T> Callback<TableColumn<S,T>, TableCell<S,T>> forTableColumn(
            final StringConverter<T> converter) {
        return list -> new AcceptOnExitTableCell<S,T>(converter);
    }
    
    
    /***************************************************************************
     *                                                                         *
     * Fields                                                                  *
     *                                                                         *
     **************************************************************************/    
    
    private TextField textField;
    private boolean escapePressed=false;
    private TablePosition<S, ?> tablePos=null;
    
    
    /***************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/
    
    /**
     * Creates a default TextFieldTableCell with a null converter. Without a 
     * {@link StringConverter} specified, this cell will not be able to accept
     * input from the TextField (as it will not know how to convert this back
     * to the domain object). It is therefore strongly encouraged to not use
     * this constructor unless you intend to set the converter separately.
     */
    public AcceptOnExitTableCell() { 
        this(null);
    } 
    
    /**
     * Creates a TextFieldTableCell that provides a {@link TextField} when put 
     * into editing mode that allows editing of the cell content. This method 
     * will work on any TableColumn instance, regardless of its generic type. 
     * However, to enable this, a {@link StringConverter} must be provided that 
     * will convert the given String (from what the user typed in) into an 
     * instance of type T. This item will then be passed along to the 
     * {@link TableColumn#onEditCommitProperty()} callback.
     * 
     * @param converter A {@link StringConverter converter} that can convert 
     *      the given String (from what the user typed in) into an instance of 
     *      type T.
     */
    public AcceptOnExitTableCell(StringConverter<T> converter) {
        this.getStyleClass().add("text-field-table-cell");
        setConverter(converter);
    }
    
    
    
    /***************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/
    
    // --- converter
    private ObjectProperty<StringConverter<T>> converter = 
            new SimpleObjectProperty<StringConverter<T>>(this, "converter");
    
    /**
     * The {@link StringConverter} property.
     */
    public final ObjectProperty<StringConverter<T>> converterProperty() { 
        return converter; 
    }
    
    /** 
     * Sets the {@link StringConverter} to be used in this cell.
     */
    public final void setConverter(StringConverter<T> value) { 
        converterProperty().set(value); 
    }
    
    /**
     * Returns the {@link StringConverter} used in this cell.
     */
    public final StringConverter<T> getConverter() { 
        return converterProperty().get(); 
    }  
    
    
    
    /***************************************************************************
     *                                                                         *
     * Public API                                                              *
     *                                                                         *
     **************************************************************************/
    
    /** {@inheritDoc} */
    @Override public void startEdit() {
        if (! isEditable() 
                || ! getTableView().isEditable() 
                || ! getTableColumn().isEditable()) {
            return;
        }
        super.startEdit();
    
        if (isEditing()) {
            if (textField == null) {
                textField = getTextField(); 
            }
            escapePressed=false;
            startEdit(textField);
            final TableView<S> table = getTableView();
            tablePos=table.getEditingCell();
        }
    }
    
    /** {@inheritDoc} */
    @Override public void commitEdit(T newValue) {
        if (! isEditing()) 
            return;
    
        final TableView<S> table = getTableView();
        if (table != null) {
            // Inform the TableView of the edit being ready to be committed.
            CellEditEvent editEvent = new CellEditEvent(
                table,
                tablePos,
                TableColumn.editCommitEvent(),
                newValue
            );
    
            Event.fireEvent(getTableColumn(), editEvent);
        }
    
        // we need to setEditing(false):
       super.cancelEdit(); // this fires an invalid EditCancelEvent.
    
        // update the item within this cell, so that it represents the new value
        updateItem(newValue, false);
    
        if (table != null) {
            // reset the editing cell on the TableView
            table.edit(-1, null);
    
            // request focus back onto the table, only if the current focus
            // owner has the table as a parent (otherwise the user might have
            // clicked out of the table entirely and given focus to something else.
            // It would be rude of us to request it back again.
           // requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
        }
    }
    
    
    /** {@inheritDoc} */
    @Override public void cancelEdit() {
        if(escapePressed) {
            // this is a cancel event after escape key
            super.cancelEdit();
            setText(getItemText()); // restore the original text in the view
        }
        else {
            // this is not a cancel event after escape key
            // we interpret it as commit.
            String newText=textField.getText(); // get the new text from the view
            this.commitEdit(getConverter().fromString(newText)); // commit the new text to the model
        }
        setGraphic(null); // stop editing with TextField
    
    }
    
    /** {@inheritDoc} */
    @Override public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        updateItem();
    }
    
    /***************************************************************************
     *                                                                         *
     *  // djw code taken and adapted from package protected CellUtils.        *
     *                                                                         *
     **************************************************************************/
    
    private TextField getTextField() {
    
        final TextField textField = new TextField(getItemText());
    
        // Use onAction here rather than onKeyReleased (with check for Enter),
        // as otherwise we encounter RT-34685
        textField.setOnAction(event -> {
            if (converter == null) {
                throw new IllegalStateException(
                        "Attempting to convert text input into Object, but provided "
                                + "StringConverter is null. Be sure to set a StringConverter "
                                + "in your cell factory.");
            }
            this.commitEdit(getConverter().fromString(textField.getText()));
            event.consume();
        });
        textField.setOnKeyPressed(t -> { if (t.getCode() == KeyCode.ESCAPE) escapePressed = true; else escapePressed = false; });
        textField.setOnKeyReleased(t -> {
            if (t.getCode() == KeyCode.ESCAPE) {
                // djw the code may depend on java version / expose incompatibilities: 
                throw new IllegalArgumentException("did not expect esc key releases here.");
            }
        });
        return textField;
    }
    
    private String getItemText() {
        return getConverter() == null ?
                getItem() == null ? "" : getItem().toString() :
                    getConverter().toString(getItem());
    }
    
    private void updateItem() {
        if (isEmpty()) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getItemText());
                }
                setText(null);
                setGraphic(textField);      
            } else {
                setText(getItemText());
                setGraphic(null);
            }
        }
    }
    
    private void startEdit(final TextField textField) {
        if (textField != null) {
            textField.setText(getItemText());
        }
        setText(null);
        setGraphic(textField);
        textField.selectAll();
    
        // requesting focus so that key input can immediately go into the
        // TextField (see RT-28132)
        textField.requestFocus();
     }
    }
    
    0 讨论(0)
  • 2020-12-29 10:59

    You could do it by overriding the method commitEdit as next:

    @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);
    }
    

    This workaround comes from https://gist.github.com/james-d/be5bbd6255a4640a5357#file-editcell-java-L109

    0 讨论(0)
  • 2020-12-29 11:08

    I created my own workaround (but for JavaFX 2). Main idea - transform cancelEdit() to commitEdit(). With possible validation of committed text via validator.

    /** Validator. */
    public interface TextColumnValidator<T> {
        boolean valid(T rowVal, String newVal);
    }
    
    /**
     * Special table text field cell that commit its content on focus lost.
     */
    public class TextFieldTableCellEx<S> extends TextFieldTableCell<S, String> {
        /** */
        private final TextColumnValidator<S> validator;
        /** */
        private boolean cancelling;
        /** */
        private boolean hardCancel;
        /** */
        private String curTxt = "";
    
        /** Create cell factory. */
        public static <S> Callback<TableColumn<S, String>, TableCell<S, String>>
            cellFactory(final TextColumnValidator<S> validator) {
                return new Callback<TableColumn<S, String>, TableCell<S, String>>() {
                    @Override public TableCell<S, String> call(TableColumn<S, String> col) {
                        return new TextFieldTableCellEx<>(validator);
                    }
                };
        }
    
        /**
         * Text field cell constructor.
         *
         * @param validator Input text validator.
         */
        private TextFieldTableCellEx(TextColumnValidator<S> validator) {
            this.validator = validator;
        }
    
        /** {@inheritDoc} */
        @Override public void startEdit() {
            super.startEdit();
    
            curTxt = "";
    
            hardCancel = false;
    
            Node g = getGraphic();
    
            if (g != null) {
                final TextField tf = (TextField)g;
    
                tf.textProperty().addListener(new ChangeListener<String>() {
                    @Override public void changed(ObservableValue<? extends String> val, String oldVal, String newVal) {
                        curTxt = newVal;
                    }
                });
    
                tf.setOnKeyReleased(new EventHandler<KeyEvent>() {
                    @Override public void handle(KeyEvent evt) {
                        if (KeyCode.ENTER == evt.getCode())
                            cancelEdit();
                        else if (KeyCode.ESCAPE == evt.getCode()) {
                            hardCancel = true;
    
                            cancelEdit();
                        }
                    }
                });
    
                // Special hack for editable TextFieldTableCell.
                // Cancel edit when focus lost from text field, but do not cancel if focus lost to VirtualFlow.
                tf.focusedProperty().addListener(new ChangeListener<Boolean>() {
                    @Override public void changed(ObservableValue<? extends Boolean> val, Boolean oldVal, Boolean newVal) {
                        Node fo = getScene().getFocusOwner();
    
                        if (!newVal) {
                            if (fo instanceof VirtualFlow) {
                                if (fo.getParent().getParent() != getTableView())
                                    cancelEdit();
                            }
                            else
                                cancelEdit();
                        }
                    }
                });
    
                Platform.runLater(new Runnable() {
                    @Override public void run() {
                        tf.requestFocus();
                    }
                });
            }
        }
    
        /** {@inheritDoc} */
        @Override public void cancelEdit() {
            if (cancelling)
                super.cancelEdit();
            else
                try {
                    cancelling = true;
    
                    if (hardCancel || curTxt.trim().isEmpty())
                        super.cancelEdit();
                    else if (validator.valid(getTableView().getSelectionModel().getSelectedItem(), curTxt))
                        commitEdit(curTxt);
                    else
                        super.cancelEdit();
                }
                finally {
                    cancelling = false;
                }
        }
    }
    

    Update: this code was written as a part of Apache Ignite Schema Import GUI Utility. See full version of TableCell code: https://github.com/apache/ignite/blob/ignite-1.9/modules/schema-import/src/main/java/org/apache/ignite/schema/ui/Controls.java

    Also you could build this utility (it is a very simple utility with 2 screens) and play with it under Java7/javaFx2 & Java8/JavaFx8.

    I tested - it works under both of them.

    0 讨论(0)
  • 2020-12-29 11:11

    Here's how I did it - I binded the textField's text property with the text property of the cell (bidirectional).

    class EditingCell<S, T> extends TableCell<S, T> {
    
            private final TextField mTextField;
    
            public EditingCell() {
    
                super();
    
                mTextField = new TextField();
    
                mTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
    
                    @Override
                    public void handle(KeyEvent event) {
    
                        if( event.getCode().equals(KeyCode.ENTER) )
                            commitEdit((T)mTextField.getText());
                    }
                });
    
                mTextField.focusedProperty().addListener(new ChangeListener<Boolean>() {
    
                    @Override
                    public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
    
                        if( !newValue )
                            commitEdit((T)mTextField.getText());
                    }
    
                });
    
                mTextField.textProperty().bindBidirectional(textProperty());
            }
    
            @Override
            public void startEdit() {
    
                super.startEdit();
    
                setGraphic(mTextField);
            }
    
            @Override
            public void cancelEdit() {
    
                super.cancelEdit();
    
                setGraphic(null);
            }
    
            @Override
            public void updateItem(final T item, final boolean empty) {
    
                super.updateItem(item, empty);
    
                if( empty ) {
                    setText(null);
                    setGraphic(null);
                }
                else {
                    if( item == null ) {
                        setGraphic(null);
                    }
                    else {
                        if( isEditing() ) {
                            setGraphic(mTextField);
                            setText((String)getItem());
                        }
                        else {
                            setGraphic(null);
                            setText((String)getItem());
                        }
                    }
                }
            }
        }
    
    0 讨论(0)
提交回复
热议问题