How to enable commit on focusLost for TableView/TreeTableView?

泄露秘密 提交于 2019-11-26 16:40:53

After some digging, turned out that the culprit (aka: the collaborator that cancels the edit before the textField looses focus) is the TableCellBehaviour/Base in its processing of a mousePressed:

  • mousePressed calls simpleSelect(..)
  • on detecting a single click it calls edit(-1, null)
  • which calls the same method on TableView
  • which sets its editingCell property to null
  • a tableCell listens to that property and reacts by canceling its own edit

Unfortunately, a hackaround requires 3 collaborators

  • a TableView with additional api to terminate an edit
  • a TableCellBehaviour with overridden simpleSelect(...) that calls the additional api (instead of edit(-1..)) before calling super
  • a TableCell that is configured with the extended behaviour and is aware of table's extended properties

Some code snippets (full code) :

// on XTableView:
public void terminateEdit() {
    if (!isEditing()) return;
    // terminatingCell is a property that supporting TableCells can listen to
    setTerminatingCell(getEditingCell());
    if (isEditing()) throw new IllegalStateException(
          "expected editing to be terminated but was " + getEditingCell());
    setTerminatingCell(null);
}

// on XTableCellBehaviour: override simpleSelect
@Override
protected void simpleSelect(MouseEvent e) {
    TableCell<S, T> cell = getControl();
    TableView<S> table = cell.getTableColumn().getTableView();
    if (table instanceof XTableView) {
        ((XTableView<S>) table).terminateEdit();
    }
    super.simpleSelect(e);
}

// on XTextFieldTableCell - this method is called from listener
// to table's terminatingCell property
protected void terminateEdit(TablePosition<S, ?> newPosition) {
    if (!isEditing() || !match(newPosition)) return;
    commitEdit();
}

protected void commitEdit() {
    T edited = getConverter().fromString(myTextField.getText());
    commitEdit(edited);
}

/**
 * Implemented to create XTableCellSkin which supports terminating edits.
 */
@Override
protected Skin<?> createDefaultSkin() {
    return new XTableCellSkin<S, T>(this);
}

Note: the implementation of TableCellBehaviour changed massively between jdk8u5 and jdk8u20 (joys of hacking - not fit for production use ;-) - the method to override in the latter is handleClicks(..)

BTW: massive votingfor JDK-8089514 (was RT-18492 in old jira) might speed up a core fix. Unfortunately, at least the author role is needed to vote/comment bugs in the new tracker.

I also needed this functionality and did some study. I faced some stability issues with XTableView hacking mentioned above.

As problem seems to be commitEdit() won't take effect when focus is lost, why you don't just call your own commit callback from TableCell as follows:

public class SimpleEditingTextTableCell extends TableCell {
    private TextArea textArea;
    Callback commitChange;

    public SimpleEditingTextTableCell(Callback commitChange) {
        this.commitChange = commitChange;
    }

    @Override
    public void startEdit() {
         ...

        getTextArea().focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
                if (!arg2) {
                    //commitEdit is replaced with own callback
                    //commitEdit(getTextArea().getText());

                    //Update item now since otherwise, it won't get refreshed
                    setItem(getTextArea().getText());
                    //Example, provide TableRow and index to get Object of TableView in callback implementation
                    commitChange.call(new TableCellChangeInfo(getTableRow(), getTableRow().getIndex(), getTextArea().getText()));
                }
            }
        });
       ...
    }
    ...
}

In cell factory, you just store committed value to the object or do whatever necessary to make it permanent:

col.setCellFactory(new Callback<TableColumn<Object, String>, TableCell<Object, String>>() {
            @Override
            public TableCell<Object, String> call(TableColumn<Object, String> p) {
                return new SimpleEditingTextTableCell(cellChange -> {
                            TableCellChangeInfo changeInfo = (TableCellChangeInfo)cellChange;
                            Object obj = myTableView.getItems().get(changeInfo.getRowIndex());
                            //Save committed value to the object in tableview (and maybe to DB)
                            obj.field = changeInfo.getChangedObj().toString();
                            return true;
                        });
            }
        });

So far, I have not been able to find any problems with this workaround. On the other hand, I haven't been yet done extensive testing on this either.

EDIT: Well, after some testing noticed, the workaround was working well with big data in tableview but with empty tableview cell was not getting updated after focus lost, only when double clicking it again. There would be ways to refresh table view but that too much hacking for me...

EDIT2: Added setItem(getTextArea().getText()); before calling callback -> works with empty tableview as well.

With reservation of this being a dumb suggestion. Seems too easy. But why don't you just override TableCell#cancelEdit() and save the values manually when it is invoked? When the cell loses focus, cancelEdit() is always invoked to cancel the edit.

class EditableCell extends TableCell<ObservableList<StringProperty>, String> {

    private TextField textfield = new TextField();
    private int colIndex;
    private String originalValue = null;

    public EditableCell(int colIndex) {
        this.colIndex = colIndex;
        textfield.prefHeightProperty().bind(heightProperty().subtract(2.0d));
        this.setPadding(new Insets(0));
        this.setAlignment(Pos.CENTER);

        textfield.setOnAction(e -> {
            cancelEdit();
        });

        textfield.setOnKeyPressed(e -> {
            if (e.getCode().equals(KeyCode.ESCAPE)) {
                textfield.setText(originalValue);
            }
        });
    }

    @Override
    public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (isEmpty()) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                textfield.setText(item);
                setGraphic(textfield);
                setText(null);
            } else {
                setText(item);
                setGraphic(null);
            }
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        originalValue = getItem();
        textfield.setText(getItem());
        setGraphic(textfield);
        setText(null);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setGraphic(null);
        setText(textfield.getText());
        ObservableList<StringProperty> row = getTableView().getItems().get(getIndex());
        row.get(colIndex).set(getText());
    }
}

I don't know. Maybe I'm missing something. But it seems to work for me.

Update: Added cancel edit functionality. You can now cancel the edit by pressing escape while focusing the text field. Also added so that you can save the edit by pressing enter while focusing the textfield.

I prefer building as much as possible on the existing code, and since this behaviour is still not fixed w/ Java 10, here's a more general approach based on J. Duke's solution from bug: JDK-8089311.

public class TextFieldTableCellAutoCmt<S, T> extends TextFieldTableCell<S, T> {

    protected TextField txtFldRef;
    protected boolean isEdit;

    public TextFieldTableCellAutoCmt() {
        this(null);
    }

    public TextFieldTableCellAutoCmt(final StringConverter<T> conv) {
        super(conv);
    }

    public static <S> Callback<TableColumn<S, String>, TableCell<S, String>> forTableColumn() {
        return forTableColumn(new DefaultStringConverter());
    }

    public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> conv) {
        return list -> new TextFieldTableCellAutoCmt<S, T>(conv);
    }

    @Override
    public void startEdit() {
        super.startEdit();
        isEdit = true;
        if (updTxtFldRef()) {
            txtFldRef.focusedProperty().addListener(this::onFocusChg);
            txtFldRef.setOnKeyPressed(this::onKeyPrs);
        }
    }

    /**
     * @return whether {@link #txtFldRef} has been changed
     */
    protected boolean updTxtFldRef() {
        final Node g = getGraphic();
        final boolean isUpd = g != null && txtFldRef != g;
        if (isUpd) {
            txtFldRef = g instanceof TextField ? (TextField) g : null;
        }
        return isUpd;
    }

    @Override
    public void commitEdit(final T valNew) {
        if (isEditing()) {
            super.commitEdit(valNew);
        } else {
            final TableView<S> tbl = getTableView();
            if (tbl != null) {
                final TablePosition<S, T> pos = new TablePosition<>(tbl, getTableRow().getIndex(), getTableColumn()); // instead of tbl.getEditingCell()
                final CellEditEvent<S, T> ev  = new CellEditEvent<>(tbl, pos, TableColumn.editCommitEvent(), valNew);
                Event.fireEvent(getTableColumn(), ev);
            }
            updateItem(valNew, false);
            if (tbl != null) {
                tbl.edit(-1, null);
            }
            // TODO ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(tbl);
        }
    }

    public void onFocusChg(final ObservableValue<? extends Boolean> obs, final boolean v0, final boolean v1) {
        if (isEdit && !v1) {
            commitEdit(getConverter().fromString(txtFldRef.getText()));
        }
    }

    protected void onKeyPrs(final KeyEvent e) {
        switch (e.getCode()) {
        case ESCAPE:
            isEdit = false;
            cancelEdit(); // see CellUtils#createTextField(...)
            e.consume();
            break;
        case TAB:
            if (e.isShiftDown()) {
                getTableView().getSelectionModel().selectPrevious();
            } else {
                getTableView().getSelectionModel().selectNext();
            }
            e.consume();
            break;
        case UP:
            getTableView().getSelectionModel().selectAboveCell();
            e.consume();
            break;
        case DOWN:
            getTableView().getSelectionModel().selectBelowCell();
            e.consume();
            break;
        default:
            break;
        }
    }
}

Since TextFieldTableCell suffers from a major loss of function (as reckoned in https://bugs.openjdk.java.net/browse/JDK-8089514) which is planned for fixing in Java 9, I decided to go with an alternative solution. Please accept my apologies if this is off-target but here it is:

The main idea is to forget TextFieldTableCell and to use a custom TableCell class with a TextField in it.

The custom TableCell:

public class CommentCell extends TableCell<ListItem, String> {

    private final TextField comment = new TextField();

    public CommentCell() {
        this.comment.setMaxWidth( Integer.MAX_VALUE );
        this.comment.setDisable( true );
        this.comment.focusedProperty().addListener( new ChangeListener<Boolean>() {
            @Override
            public void changed( ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue,
                    Boolean newPropertyValue ) {
                if ( !newPropertyValue ) {
                    // Binding the TextField text to the model
                    MainController.getInstance().setComment( getTableRow().getIndex(), comment.getText() );
                }
            }
        } );
        this.setGraphic( this.comment );
    }

    @Override
    protected void updateItem( String s, boolean empty ) {
        // Checking if the TextField should be editable (based on model condition)
        if ( MainController.getInstance().isDependency( getTableRow().getIndex() ) ) {
            this.comment.setDisable( false );
            this.comment.setEditable( true );
        }
        // Setting the model value as the text for the TextField
        if ( s != null && !s.isEmpty() ) {
            this.comment.setText( s );
        }
    }
}

The UI display might differ from a TextFieldTableCell but at least, it allows for better usability: UI Display

I found a simple solution to this, one just needs to provide the commit function to column specific to data type:

TableColumn msgstr = new TableColumn("msgstr");
msgstr.setMinWidth(100);
msgstr.prefWidthProperty().bind(widthProperty().divide(3));
msgstr.setCellValueFactory(
        new PropertyValueFactory<>("msgstr")
);
msgstr.setOnEditCommit(new EventHandler<CellEditEvent<PoEntry, String>>() {
@Override
public void handle(CellEditEvent<PoEntry, String> t) {
    ((PoEntry)t.getTableView().getItems().get(t.getTablePosition().getRow())).setMsgstr(t.getNewValue());
}
});
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!