I\'d like to create a table with the following features:
I got curious and did some background research.
You are facing the problem of a well-known bug in the JavaFX.
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.
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.
There is a hack shown in a SO thread, which seems to get the job done, although I haven't tried it (yet).
I've run into the same issue and I solved it by combining these two code snippets:
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;
}
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());
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.