如何为TableView / TreeTableView的focusLost启用提交?

时间:2014-07-11 09:36:04

标签: java focus tableview javafx-8 treetableview

是否有任何简单的方法让TreeTableView(或TableView)尝试在焦点丢失时提交值?

不幸的是,我没有成功使用javafx TableCellFactories的任何默认实现,这就是为什么我尝试了自己的TreeTableCell实现以及一些不同的tableCell实现,比如来自Graham Smith的实现,这似乎是最直接的转发,因为它已经实现了一个丢失焦点的钩子,但是仍然没有提交值,并且用户更改被重置为原始值。

我的猜测是,每当焦点丢失时,受影响的Cell的editingProperty总是为false,这导致Cell永远不会在focusLost上提交值。这里是原始(oracle-)TreeTableCell实现(8u20ea)的相关部分,它导致我的方法失败:

 @Override public void commitEdit(T newValue) {
        if (! isEditing()) return; // <-- here my approaches are blocked, because on focus lost its not editing anymore.

        final TreeTableView<S> table = getTreeTableView();
        if (table != null) {
            @SuppressWarnings("unchecked")
            TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();

            // Inform the TableView of the edit being ready to be committed.
            CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
                table,
                editingCell,
                TreeTableColumn.<S,T>editCommitEvent(),
                newValue
            );

            Event.fireEvent(getTableColumn(), editEvent);
        }

        // inform parent classes of the commit, so that they can switch us
        // out of the editing state.
        // This MUST come before the updateItem call below, otherwise it will
        // call cancelEdit(), resulting in both commit and cancel events being
        // fired (as identified in RT-29650)
        super.commitEdit(newValue);

        // 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.
            ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
        }
    }

我成功地覆盖了这种方法并且手工提交了价值&#34;&#34;在调用原始的commitEdit()方法之前,这会导致像key这样的键提交两次提交值(在key + on focus lost上)。此外,我根本不喜欢我的方法,所以我想知道,如果其他人在一个更好的解决方案中解决了这个问题。方式是什么?

6 个答案:

答案 0 :(得分:12)

经过一番挖掘,结果证明罪魁祸首(又名:在textField失去焦点之前取消编辑的合作者)是在处理mousePressed时的TableCellBehaviour / Base:

  • mousePressed calls simpleSelect(..)
  • 检测到单击时会调用edit(-1, null)
  • 在TableView上调用相同的方法
  • 将其editingCell属性设置为null
  • tableCell侦听该属性并通过取消自己的编辑来做出反应

不幸的是,hackaround需要3个合作者

  • 带有额外api的TableView以终止编辑
  • 带有覆盖simpleSelect(...)的TableCellBehaviour,在调用super之前调用额外的api(而不是edit(-1 ..))
  • 使用扩展行为配置的TableCell知道表的扩展属性

部分代码段(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);
}

注意:TableCellBehaviour的实现在jdk8u5和jdk8u20之间发生了巨大的变化(黑客的喜悦 - 不适合生产使用;-) - 在后者中覆盖的方法是handleClicks(..)

BTW:JDK-8089514的大量投票(旧jira中的RT-18492)可能加速核心修复。不幸的是,至少需要作者角色来对新跟踪器中的错误进行投票/评论。

答案 1 :(得分:4)

我也需要这个功能并做了一些研究。我在上面提到的XTableView黑客攻击时遇到了一些稳定性问题。

因为问题似乎是commitEdit()在焦点丢失时不会生效,为什么你不只是从TableCell调用你自己的提交回调,如下所示:

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

在单元工厂中,您只需将已提交的值存储到对象或执行任何必要的操作以使其永久化:

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

到目前为止,我还未能找到此解决方法的任何问题。另一方面,我还没有对此进行过广泛的测试。

编辑:嗯,经过一些测试注意到,解决方法在tableview中的大数据工作得很好,但是在焦点丢失后,只有再次双击它时,空的tableview单元格才会更新。有很多方法可以刷新表格视图,但对我来说太过黑了......

EDIT2:添加了setItem(getTextArea()。getText());在呼叫回叫之前 - &gt;也适用于空的tableview。

答案 2 :(得分:1)

保留这是一个愚蠢的建议。似乎太容易了。但是,为什么不覆盖TableCell#cancelEdit()并在调用时手动保存值?当单元格失去焦点时,始终会调用cancelEdit()来取消编辑。

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

我不知道。也许我错过了什么。但它似乎对我有用。

更新:添加了取消修改功能。现在,您可以在聚焦文本字段时按下escape取消编辑。还添加了以便您可以在聚焦文本字段时按Enter键来保存编辑。

答案 3 :(得分:1)

我更喜欢在现有代码上尽可能多地构建,并且由于这种行为仍然没有得到Java 10的修复,这里有一个基于J. Duke's solution的更一般的方法来自bug:JDK-8089311。< / p>

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

答案 4 :(得分:0)

由于TextFieldTableCell遭受了计划在Java 9中修复的功能的重大损失(正如计算在https://bugs.openjdk.java.net/browse/JDK-8089514中),我决定使用替代解决方案。如果这是偏离目标,请接受我的道歉,但现在是:

主要思想是忘记TextFieldTableCell并使用带有TextField的自定义TableCell类。

自定义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 );
        }
    }
}

UI显示可能与TextFieldTableCell不同,但至少,它允许更好的可用性: UI Display

答案 5 :(得分:0)

我找到了一个简单的解决方案,只需要将提交函数提供给特定于数据类型的列:

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