I'd like to create a table with the following features:
- Edit on key press
- Enter key = next row
- Tab key = next column
- Escape key = cancel edit
Below is a code which implements these features. The values should be committed on focus lost. Problem: They aren't committed. The focus change event is fired, the values would be correct according to the console output, but in the end the values in the table cells are the old ones.
Does anyone know how to prevent this and how do you get the current EditingCell object so that I can invoke commit manually? After all there should be some kind of verifier invoked which prevents changing the focus if the values aren't correct.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
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.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TableViewInlineEditDemo extends Application {
private final TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "[email protected]"),
new Person("Isabella", "Johnson", "[email protected]"),
new Person("Ethan", "Williams", "[email protected]"),
new Person("Emma", "Jones", "[email protected]"),
new Person("Michael", "Brown", "[email protected]"));
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setWidth(450);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
Callback<TableColumn<Person, String>, TableCell<Person, String>> cellFactory = (TableColumn<Person, String> p) -> new EditingCell();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
firstNameCol.setCellFactory(cellFactory);
firstNameCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
});
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
lastNameCol.setCellFactory(cellFactory);
lastNameCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setLastName(t.getNewValue());
});
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));
emailCol.setCellFactory(cellFactory);
emailCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setEmail(t.getNewValue());
});
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
// edit mode on keypress
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent e) {
if( e.getCode() == KeyCode.TAB) { // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectNext();
e.consume();
return;
}
else if( e.getCode() == KeyCode.ENTER) { // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectBelowCell();
e.consume();
return;
}
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null) {
if( e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
}
}
}
});
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
final VBox vbox = new VBox();
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus(); // must be before selectAll() or the caret would be in wrong position
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);
// commit on focus lost
textField.focusedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
if( oldValue = true && newValue == false) {
System.out.println( "Focus lost, current value: " + textField.getText());
commitEdit();
}
});
// cancel edit on ESC
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if( e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private boolean commitEdit() {
super.commitEdit(textField.getText());
return true; // TODO: add verifier and check if commit was possible
}
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
Thank you very much!
Edit: I've narrowed it down. It seems the problem is that the JavaFX code cancels the edit mode when the focus changes. That's bad.
public Cell() {
setText(null); // default to null text, to match the null item
// focusTraversable is styleable through css. Calling setFocusTraversable
// makes it look to css like the user set the value and css will not
// override. Initializing focusTraversable by calling set on the
// CssMetaData ensures that css will be able to override the value.
((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
getStyleClass().addAll(DEFAULT_STYLE_CLASS);
/**
* Indicates whether or not this cell has focus. For example, a
* ListView defines zero or one cell as being the "focused" cell. This cell
* would have focused set to true.
*/
super.focusedProperty().addListener(new InvalidationListener() {
@Override public void invalidated(Observable property) {
pseudoClassStateChanged(PSEUDO_CLASS_FOCUSED, isFocused()); // TODO is this necessary??
// The user has shifted focus, so we should cancel the editing on this cell
if (!isFocused() && isEditing()) {
cancelEdit();
}
}
});
// initialize default pseudo-class state
pseudoClassStateChanged(PSEUDO_CLASS_EMPTY, true);
}