简体   繁体   English

在JavaFX中单击外部可编辑的TableView单元格时如何提交?

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

I have a table cell factory responsible for creating an editable cell in a JavaFX TableView. 我有一个表格单元工厂负责在JavaFX TableView中创建可编辑单元格。

I'm trying to implement some added functionality to the tableview so that when the user clicks outside the editable cell a commit is made (the edited text is saved, and not discarded as per the default tableview behavior.) 我正在尝试为tableview实现一些附加功能,以便当用户在可编辑单元格外部单击时进行提交(编辑后的文本将被保存,而不会根据默认的tableview行为进行丢弃。)

I added an textField.focusedProperty() event handler, where I commit the text from the text field. 我添加了一个textField.focusedProperty()事件处理程序,我从文本字段提交文本。 However, when one clicks outside the current cell cancelEdit() gets called and calling commitEdit(textField.getText()); 但是,当在当前单元cancelEdit()单击时,将调用cancelEdit()并调用commitEdit(textField.getText()); has no effect. 没有效果。

I have come to realize that once cancelEdit() is called the TableCell.isEditing() returns false and so the commit will never happen. 我已经意识到,一旦调用了cancelEdit()TableCell.isEditing()返回false,因此提交永远不会发生。

How can I make so that when the user clicks outside the editable cell the text is committed? 如何在用户点击可编辑单元格外部时提交文本?

After committing an setOnEditCommit() event handler will take care of the validation and database logic. 提交setOnEditCommit()事件处理程序后,将处理验证和数据库逻辑。 I haven't included it here since it will most likely complicate things even further. 我没有把它包含在这里,因为它很可能会使事情进一步复杂化。

// EditingCell - for editing capability in a TableCell
public static class EditingCell extends TableCell<Person, String> {
private TextField textField;

public EditingCell() {
}

@Override public void startEdit() {
    super.startEdit();

    if (textField == null) {
        createTextField();
    }
    setText(null);
    setGraphic(textField);
    textField.selectAll();
}

@Override public void cancelEdit() {
    super.cancelEdit();
    setText((String) getItem());
    setGraphic(null);
}

@Override public void updateItem(String item, boolean empty) {
    super.updateItem(item, empty);
    if (empty) {
        setText(null);
        setGraphic(null);
    } else {
        if (isEditing()) {
            if (textField != null) {
                textField.setText(getString());
            }
            setText(null);
            setGraphic(textField);
        } else {
            setText(getString());
            setGraphic(null);
        }
    }
}

private void createTextField() {
    textField = new TextField(getString());
    textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
    textField.setOnKeyReleased(new EventHandler<KeyEvent>() {                
        @Override public void handle(KeyEvent t) {
            if (t.getCode() == KeyCode.ENTER) {
                commitEdit(textField.getText());
            } else if (t.getCode() == KeyCode.ESCAPE) {
                cancelEdit();
            }
        }
    });

    textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
         @Override
         public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
             if (!newValue) {
                    commitEdit(textField.getText());
             }
         }
    });
}

private String getString() {
    return getItem() == null ? "" : getItem().toString();
}
} 

You could do it by overriding the method commitEdit as next: 你可以通过覆盖下一个方法commitEdit来做到这一点:

@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 此解决方法来自https://gist.github.com/james-d/be5bbd6255a4640a5357#file-editcell-java-L109

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. 由于我找不到kuaw26的源代码(死链接),我为java 8开发了自己的解决方案。我发现上面代码中的TextField从未收到esc-key的keyReleased事件,因此他的代码不起作用。

Unfortunately I needed to duplicate code from TextFieldTableCell and CellUtils and adapt it, since TextFieldTableCell uses a private TextField and CellUtils is package protected. 不幸的是,我需要从TextFieldTableCell和CellUtils复制代码并对其进行调整,因为TextFieldTableCell使用私有TextField,而CellUtils受包保护。 This is probably not the best OO way. 这可能不是最好的OO方式。

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

Here's how I did it - I binded the textField's text property with the text property of the cell (bidirectional). 这是我如何做到的 - 我将textField的text属性与单元格的text属性(双向)绑定在一起。

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

I created my own workaround (but for JavaFX 2). 我创建了自己的解决方法(但对于JavaFX 2)。 Main idea - transform cancelEdit() to commitEdit(). 主要思想 - 将cancelEdit()转换为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. 更新:此代码是作为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 查看TableCell代码的完整版: 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. 你也可以构建这个实用程序(它是一个非常简单的实用程序,有2个屏幕),并在Java7 / javaFx2和Java8 / JavaFx8下使用它。

I tested - it works under both of them. 我测试过 - 它在两者下运行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM