JavaFX Issue in MOUSE_CLICKED event propagation - javafx

I am encountering an inconsistent behavior with MOUSE_CLICKED event propagation. Below is the scenario in which I am having the issues:
Background:
I have an editable TableView, where some action needs to be performed on click of a TableCell. So I included the below eventHandler in the TableCell constructor.
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
System.out.println("!!! THIS NEEDS TO BE TRIGGERED ON EVERY CLICK !!!");
});
And the underlying implementation for editable TableCell factory is to show text in the Label (set as graphic of cell) and when editing, the Label is replaced with TextField. I have other requirements which I cannot achieve if I use setText() method of the TableCell. So for that reason I am using Label as graphic.
So far everything works well., that when I click on the cell it is turing into edit mode and the MOUSE_CLICKED handler is firing.
Issue:
Instead of clicking on the empty space in the cell, if I directly click on the Label, the edit behaviour works well but the MOUSE_CLICKED handler is not fired. Upon some debugging, I noticed that the cell edit is triggred on MOUSE_RELEASED event and as in the startEdit() the MOUSE_RELEASED target(Label) is removed and replaced with TextField, the MOUSE_CLICKED event is not propagated to cell. This analysis has convinced me why the MOUSE_CLICKED handler is not triggered when I directly click on Label (as the Label doesn't exist in the scenegraph anymore).
But what confuses me is that this behavior is not consistent. Lets say if I click on the Label of Cell-A for the first time, the edit and MOUSE_CLICKED handler are working fine. And from there on, only the edit is triggered and not the MOUSE_CLICKED handler.
Now if I click on the Label of Cell-B for the first time,. it is the same behavior, works for the first click but not for the other consecutive clicks. The confusing part is, if again click on Cell-A label, works for the first time but not the consecutive ones.
Below is the quick demo that demonstrates the issue:
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class EditableTableViewDemo extends Application {
#Override
public void start(final Stage primaryStage) throws Exception {
final ObservableList<TableDataObj> items = FXCollections.observableArrayList();
final int no = 2;
for (int i = 0; i < no; i++) {
final String firstName = "First Name " + i;
final String lastName = "Last Name " + i;
final String city = "City " + i;
items.add(new TableDataObj(firstName, lastName, city));
}
final ColumnConstraints cc = new ColumnConstraints();
cc.setHgrow(Priority.ALWAYS);
cc.setFillWidth(true);
final TableView<TableDataObj> tacticalTable = buildTable();
tacticalTable.setItems(items);
final GridPane pane1 = new GridPane();
pane1.setVgap(5);
pane1.setPadding(new Insets(10));
pane1.addRow(0, new Label());
pane1.addRow(1, tacticalTable);
pane1.getColumnConstraints().add(cc);
HBox.setHgrow(pane1, Priority.ALWAYS);
final VBox vb = new VBox();
vb.setSpacing(10);
vb.setPadding(new Insets(10));
vb.getChildren().addAll(pane1);
VBox.setVgrow(pane1, Priority.ALWAYS);
final Scene sc = new Scene(vb);
primaryStage.setScene(sc);
primaryStage.setTitle("Editable Table Demo");
primaryStage.show();
}
#SuppressWarnings("unchecked")
private TableView<TableDataObj> buildTable() {
final TableView<TableDataObj> tableView = new TableView<>();
tableView.setEditable(true);
final TableColumn<TableDataObj, String> fnCol = new TableColumn<>();
fnCol.setText("First Name");
fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
fnCol.setPrefWidth(150);
final TableColumn<TableDataObj, String> lnCol = new TableColumn<>();
lnCol.setText("Last Name");
lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
lnCol.setPrefWidth(150);
final TableColumn<TableDataObj, String> cityCol = new TableColumn<>();
cityCol.setEditable(true);
cityCol.setText("City");
cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
cityCol.setPrefWidth(150);
cityCol.setCellFactory(param -> {
final EditingCell<TableDataObj, String> cell = new EditingCell<>();
cell.setOnMouseClicked(e -> cell.startEdit());
return cell;
});
tableView.getColumns().addAll(fnCol, lnCol, cityCol);
return tableView;
}
class EditingCell<T, S> extends TableCell<T, S> {
public EditingCell() {
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
System.out.println("MOUSE_PRESSED on cell '" + getItem() + "' with target :: " + e.getTarget().getClass().getSimpleName());
});
/* Doesn't make any difference if I changed the below addEventHandler to addEventFilter */
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
System.out.println("!!! THIS NEEDS TO BE TRIGGERED ON EVERY CLICK !!!");
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
private TextField textField;
private Label label = new Label();
#Override
public void cancelEdit() {
super.cancelEdit();
label.setText(getString());
setGraphic(label);
}
#Override
public void commitEdit(final S newValue) {
super.commitEdit(newValue);
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
#Override
public void updateItem(final S item, final boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(textField);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
} else {
label.setText(getString());
setGraphic(label);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(getWidth() - getGraphicTextGap() * 2);
textField.setOnKeyPressed(keyEvent -> {
if (keyEvent.getCode() == KeyCode.ESCAPE) {
cancelEdit();
keyEvent.consume();
} else if (keyEvent.getCode() == KeyCode.ENTER) {
commitEdit(getItem());
keyEvent.consume();
}
});
/* Cancel edit when loosing focus. */
textField.focusedProperty().addListener((obs, prevFocus, focused) -> {
if (!focused) {
cancelEdit();
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
class TableDataObj {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty city = new SimpleStringProperty();
public TableDataObj(final String fn, final String ln, final String cty) {
setFirstName(fn);
setLastName(ln);
setCity(cty);
}
public StringProperty cityProperty() {
return city;
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setCity(final String city1) {
city.set(city1);
}
public void setFirstName(final String firstName1) {
firstName.set(firstName1);
}
public void setLastName(final String lastName1) {
lastName.set(lastName1);
}
}
}
The output is as below:
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
!!! THIS NEEDS TO BE TRIGGERED ON EVERY CLICK !!!
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
MOUSE_PRESSED on cell 'City 1' with target :: LabeledText
!!! THIS NEEDS TO BE TRIGGERED ON EVERY CLICK !!!
MOUSE_PRESSED on cell 'City 1' with target :: LabeledText
MOUSE_PRESSED on cell 'City 1' with target :: LabeledText
MOUSE_PRESSED on cell 'City 1' with target :: LabeledText
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
!!! THIS NEEDS TO BE TRIGGERED ON EVERY CLICK !!!
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
MOUSE_PRESSED on cell 'City 0' with target :: LabeledText
In the above output, On first click on Label of cell, the edit and MOUSE_CLICKED handler are fired and from the second click onwards only the edit is triggered but not the MOUSE_CLICKED handler.
So my question here is: Is this behaviour an issue in JavaFX or am I missing some crucial info that causes this inconsistency? This behaviour is observed on both JavaFX8 and JavaFX18.

Related

JavaFx TableView get changed rows

I have a problem with JavaFx.
I have a table view, which shows the Data I want.
Now I change the Data in the TableView.
My changeListener works, so far so good.
Now I want the rows of the table with changes in it to appear in a different color.
But I just can't find a way to get the specific row.
I tried to google the solution but all I can find is how to get a selected row.
But there won't be any user input. The data just refreshes.
Can you help me?
Probably I was just to stupid to find the right keywords.
I think of something like:
tableview.getRow(indexOfChangedRow).setStyle
The TableRow maintains a property called Item, which holds the data for the row. You need a RowFactory that binds the background to some value in your Table data. Generally, your Table data is going to have fields composed of ObservableValues, like Properties. So you end up with a Property (the Table data Model), which is composed of Properties. This means that you'll need a ChangeListener to manually reset the Binding on the Background property of the TableRow whenever the Item property is changed.
Here's an example:
public class TableBackground extends Application {
private ObservableList<TableModel> tableData = FXCollections.observableArrayList();
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(createContent()));
primaryStage.show();
}
private Parent createContent() {
BorderPane borderPane = new BorderPane();
borderPane.setCenter(createTable());
borderPane.setBottom(createAddBox());
return borderPane;
}
private Node createTable() {
TableView<TableModel> tableView = new TableView<>();
TableColumn<TableModel, String> firstColumn = new TableColumn<>("Field 1");
firstColumn.setCellValueFactory(p -> p.getValue().field1Property());
tableView.getColumns().add(firstColumn);
TableColumn<TableModel, String> secondColumn = new TableColumn<>("Field 2");
secondColumn.setCellValueFactory(p -> p.getValue().field2Property());
tableView.getColumns().add(secondColumn);
tableView.setItems(tableData);
Background redBackground = new Background(new BackgroundFill(Color.RED, null, null));
Background blueBackground = new Background(new BackgroundFill(Color.CORNFLOWERBLUE, null, null));
tableView.setRowFactory(t -> {
TableRow<TableModel> row = new TableRow<TableModel>();
row.itemProperty().addListener((ob, oldValue, newValue) -> {
row.backgroundProperty().bind(Bindings.createObjectBinding(() -> newValue.isNewRow() ? redBackground : blueBackground,
newValue.newRowProperty()));
});
return row;
});
return tableView;
}
private Node createAddBox() {
TextField textField1 = new TextField();
TextField textField2 = new TextField();
Button button = new Button("Add Row");
button.setOnAction(evt -> {
tableData.forEach(dataItem -> dataItem.setNewRow(false));
tableData.add(new TableModel(textField1.getText(), textField2.getText()));
});
return new HBox(10, textField1, textField2, button);
}
}
And the TableModel would look like this:
public class TableModel {
private StringProperty field1 = new SimpleStringProperty("");
private StringProperty field2 = new SimpleStringProperty("");
private BooleanProperty newRow = new SimpleBooleanProperty(true);
public TableModel(String field1Data, String field2Data) {
field1.set(field1Data);
field2.set(field2Data);
setNewRow(true);
}
public String getField1() {
return field1.get();
}
public StringProperty field1Property() {
return field1;
}
public void setField1(String field1) {
this.field1.set(field1);
}
public String getField2() {
return field2.get();
}
public StringProperty field2Property() {
return field2;
}
public void setField2(String field2) {
this.field2.set(field2);
}
public boolean isNewRow() {
return newRow.get();
}
public BooleanProperty newRowProperty() {
return newRow;
}
public void setNewRow(boolean newRow) {
this.newRow.set(newRow);
}
}
The important part is the RowFactory:
Background redBackground = new Background(new BackgroundFill(Color.RED, null, null));
Background blueBackground = new Background(new BackgroundFill(Color.CORNFLOWERBLUE, null, null));
tableView.setRowFactory(t -> {
TableRow<TableModel> row = new TableRow<TableModel>();
row.itemProperty().addListener((ob, oldValue, newValue) -> {
row.backgroundProperty().bind(Bindings.createObjectBinding(() -> newValue.isNewRow() ? redBackground : blueBackground,
newValue.newRowProperty()));
});
return row;
});
It's a plain vanilla row except that it has a ChangeListener on row.itemProperty() which puts a Binding on row.backgroundProperty() based on value of new items TableModel.newRowProperty(). If it's new, then the background is red, otherwise it's blue.
The Button OnAction event, sets all of the existing rows newRowProperty() to false, and then adds a new row with newRowProperty() set to true. This means that all of the existing rows will turn blue, and the new row will be red.

Updating current row when clicked upon in TableView JavaFX

So I have code that works if the user selects a different row than the one currently selected
table.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue == null) {
updateDetails(oldValue);
return;
}
updateDetails(newValue);
});
}
However, I want this to work if the user clicks on the same value as well - basically, there's a part of the code that modifies an image shown but that image doesn't update itself unless I click on another row then go back to the row I was previously on. I would like to be able to update the row I'm on simply by clicking on it (which would call updateDetails) but can't seem to figure this out...
Create a custom rowFactory and add a mouse listener to it.
Example
This displays the old value property of the last item clicked and the new item clicked as text of the Label.
TableView<Item> tv = new TableView<>(FXCollections.observableArrayList(new Item("foo"), new Item("bar"), new Item("42")));
Label label = new Label();
TableColumn<Item, String> valueColumn = new TableColumn<>("value");
valueColumn.setCellValueFactory(d -> d.getValue().valueProperty());
tv.getColumns().add(valueColumn);
EventHandler<MouseEvent> eventHandler = new EventHandler<MouseEvent>() {
private Item lastItem;
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
TableRow<Item> source = (TableRow<Item>) event.getSource();
if (!source.isEmpty()) {
label.setText(MessageFormat.format("old: {0}; new: {1}", lastItem == null ? null : lastItem.getValue(), (lastItem = source.getItem()).getValue()));
}
}
}
};
tv.setRowFactory(t -> {
TableRow<Item> row = new TableRow();
row.setOnMouseClicked(eventHandler);
return row;
});
public class Item {
public Item() {
}
public Item(String value) {
this.value.set(value);
}
private final StringProperty value = new SimpleStringProperty();
public String getValue() {
return value.get();
}
public void setValue(String val) {
value.set(val);
}
public StringProperty valueProperty() {
return value;
}
}

How to detect double click on a tableview row in javafx while using fxml with scene builder [duplicate]

I need to detect double clicks on a row of a TableView.
How can I listen for double clicks on any part of the row and get all data of this row to print it to the console?
TableView<MyType> table = new TableView<>();
//...
table.setRowFactory( tv -> {
TableRow<MyType> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (! row.isEmpty()) ) {
MyType rowData = row.getItem();
System.out.println(rowData);
}
});
return row ;
});
Here is a complete working example:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewDoubleClickOnRow extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setRowFactory(tv -> {
TableRow<Item> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (! row.isEmpty()) ) {
Item rowData = row.getItem();
System.out.println("Double click on: "+rowData.getName());
}
});
return row ;
});
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Random rng = new Random();
for (int i = 1 ; i <= 50 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(1000)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Example:
table.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown() && event.getClickCount() == 2) {
System.out.println(table.getSelectionModel().getSelectedItem());
}
}
});
If you are using custom selection model, then you can get the row from event, example:
table.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown() && event.getClickCount() == 2) {
Node node = ((Node) event.getTarget()).getParent();
TableRow row;
if (node instanceof TableRow) {
row = (TableRow) node;
} else {
// clicking on text part
row = (TableRow) node.getParent();
}
System.out.println(row.getItem());
}
}
});
This works for me:
table.setOnMouseClicked((MouseEvent event) -> {
if (event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2){
System.out.println(table.getSelectionModel().getSelectedItem());
}
});
}
If you are using SceneBuilder you can set your table's OnMouseClicked to handleRowSelect() method as shown below:
MyType temp;
Date lastClickTime;
#FXML
private void handleRowSelect() {
MyType row = myTableView.getSelectionModel().getSelectedItem();
if (row == null) return;
if(row != temp){
temp = row;
lastClickTime = new Date();
} else if(row == temp) {
Date now = new Date();
long diff = now.getTime() - lastClickTime.getTime();
if (diff < 300){ //another click registered in 300 millis
System.out.println("Edit dialog");
} else {
lastClickTime = new Date();
}
}
}
Extending the previous answer:
The extra check ensures the selected row was double clicked - ignoring double clicks on empty rows or the column header
table.setRowFactory(param -> {
TableRow<MyType> row = new TableRow<>();
row.setOnMouseClicked(event -> Optional.ofNullable(row.getItem()).ifPresent(rowData-> {
if(event.getClickCount() == 2 && rowData.equals(table.getSelectionModel().getSelectedItem())){
System.out.println(rowData);
}
}));
return row;
});
```
This answer has been tested:
table.setOnMouseClicked( event -> {
if( event.getClickCount() == 2 ) {
System.out.println( table.getSelectionModel().getSelectedItem());
}});
table.getSelectionModel().getSelectedItem() can be use since we catch a double-click. One the first click the selection moves, on the second this handler is executed.
I had similar situation not to detect mouse double click event on TableView.
Above all samples worked perfectly. but my application did not detect double click event at all.
But I found that if TableView is on editable, mouse double click event can not be detected !!
check your application if TableView is on editable like this.
tableView.setEditable( true );
if then, double click event only raises on same row selected.

JavaFX editable cell with focus change to different populated cell

I need editable cells for JavaFX TableView. The default TextFieldTableCell requires the user to press enter to commit a change. I think a typical user expects the change to be kept when clicking outside the cell. All the features I want include:
Single-click selects the cell and
Another single-click on a cell, in the selected cell, or enter press, starts editing.
A double-click on a cell starts editing.
Pressing enter commits the changes to the cell
Changing mouse focus anywhere outside the cell commits the changes to the cell
I found a EditCell version in this post
It fulfills the first 4 requirements and partially the 5th, but when the user clicks on another populated cell in the table the edit changes are lost. The focus listener is triggered, but no commit. Clicking on an empty cell or another scene element commits the changes.
There is a supposedly a solution provided in post
However, the solution only contains snippets of code instead of a working example. I was not able to implement it.
Can anyone help put the pieces together and demo a class than extends TableCell that has all the features I listed above?
I'm probably a little late to this party but here goes.
Not being able to commit the value of the changed cell is probably due to the default implementation of the commitEdit method in TableCell, as it treats a loss of focus as a cancel-action by default.
However, user James_D created a nice workaround here
EDIT:
Example class based on James_D's work
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class TableViewCommitOnFocusLoss extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
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")
);
Button showDataButton = new Button("Debug data");
showDataButton.setOnAction(event -> table.getItems().stream()
.map(p -> String.format("%s %s", p.getFirstName(), p.getLastName()))
.forEach(System.out::println));
Scene scene = new Scene(new BorderPane(table, null, null, showDataButton, null), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
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 ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
class EditCell<S, T> extends TableCell<S, T> {
// Text field for editing
// TODO: allow this to be a plugable control.
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 ;
public EditCell(StringConverter<T> converter) {
this.converter = converter ;
itemProperty().addListener((obx, oldItem, newItem) -> {
if (newItem == null) {
setText(null);
} else {
setText(converter.toString(newItem));
}
});
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt -> {
commitEdit(this.converter.fromString(textField.getText()));
});
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (! isNowFocused) {
commitEdit(this.converter.fromString(textField.getText()));
}
});
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
textField.setText(converter.toString(getItem()));
cancelEdit();
event.consume();
} else if (event.getCode() == KeyCode.RIGHT) {
getTableView().getSelectionModel().selectRightCell();
event.consume();
} else if (event.getCode() == KeyCode.LEFT) {
getTableView().getSelectionModel().selectLeftCell();
event.consume();
} else if (event.getCode() == KeyCode.UP) {
getTableView().getSelectionModel().selectAboveCell();
event.consume();
} else if (event.getCode() == KeyCode.DOWN) {
getTableView().getSelectionModel().selectBelowCell();
event.consume();
}
});
}
/**
* 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
*/
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();
textField.setText(converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
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();
TableColumn.CellEditEvent<S, T> event = new TableColumn.CellEditEvent<>(table,
new TablePosition<S,T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}

Type to edit in TableView

Problem
I'd like to switch to edit mode in my TableView as soon as I type. I don't want to doubleclick or press to enter on each and every cell first, that's annoying.
I've come up with the following piece of code. Problem is that it is more or less side-effect programming and I suspect troubles. When you use KEY_RELEASED in order to switch the table into edit mode, the 1st key press gets lost.
So you have to use KEY_PRESSED. It all seems to work fine now, but once in a while you get a race condition and the caret in the TextField cell editor is before the typed text instead of after it. But when you continue typing, then the text gets appended correctly after the existing text.
It appears okay, but from a developing point of view it seems like a mess with race conditions.
Question
Does anyone have a proper way of doing a "type-to-edit" functionality?
Code
Here's the code I've got so far:
public class InlineEditingTableView extends Application {
private final ObservableList<Data> data =
FXCollections.observableArrayList(
new Data(1.,5.),
new Data(2.,6.),
new Data(3.,7.),
new Data(4.,8.)
);
private TableView<Data> table;
#Override
public void start(Stage stage) {
// create edtiable table
table = new TableView<Data>();
table.setEditable(true);
// column 1 contains numbers
TableColumn<Data, Number> number1Col = new TableColumn<>("Number 1");
number1Col.setMinWidth(100);
number1Col.setCellValueFactory( cellData -> cellData.getValue().number1Property());
number1Col.setCellFactory( createNumberCellFactory());
number1Col.setOnEditCommit(new EventHandler<CellEditEvent<Data, Number>>() {
#Override
public void handle(CellEditEvent<Data, Number> t) {
System.out.println( t);
// ((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
}
});
// column 2 contains numbers
TableColumn<Data, Number> number2Col = new TableColumn<>("Number 2");
number2Col.setMinWidth(100);
number2Col.setCellValueFactory( cellData -> cellData.getValue().number2Property());
number2Col.setCellFactory( createNumberCellFactory());
// add columns & data to table
table.setItems(data);
table.getColumns().addAll( number1Col, number2Col);
// switch to edit mode on keypress
// this must be KeyEvent.KEY_PRESSED so that the key gets forwarded to the editing cell; it wouldn't be forwarded on KEY_RELEASED
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
// event.consume(); // don't consume the event or else the values won't be updated;
return;
}
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null) {
if( event.getCode().isLetterKey() || event.getCode().isDigitKey()) {
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
}
}
}
});
table.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
table.getSelectionModel().selectBelowCell();
}
}
});
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
// add nodes to stage
BorderPane root = new BorderPane();
root.setCenter(table);
Scene scene = new Scene( root, 800,600);
stage.setScene(scene);
stage.show();
}
/**
* Number cell factory which converts strings to numbers and vice versa.
* #return
*/
private Callback<TableColumn<Data, Number>, TableCell<Data, Number>> createNumberCellFactory() {
Callback<TableColumn<Data, Number>, TableCell<Data, Number>> factory = TextFieldTableCell.forTableColumn( new StringConverter<Number>() {
#Override
public Number fromString(String string) {
return Double.parseDouble(string);
}
#Override
public String toString(Number object) {
return object.toString();
}
});
return factory;
}
/**
* Table data container
*/
public static class Data {
private final SimpleDoubleProperty number1;
private final SimpleDoubleProperty number2;
private Data( Double number1, Double number2) {
this.number1 = new SimpleDoubleProperty(number1);
this.number2 = new SimpleDoubleProperty(number2);
}
public final DoubleProperty number1Property() {
return this.number1;
}
public final double getNumber1() {
return this.number1Property().get();
}
public final void setNumber1(final double number1) {
this.number1Property().set(number1);
}
public final DoubleProperty number2Property() {
return this.number2;
}
public final double getNumber2() {
return this.number2Property().get();
}
public final void setNumber2(final double number2) {
this.number2Property().set(number2);
}
}
public static void main(String[] args) {
launch(args);
}
}
To edit immediately on clicking a cell, it makes more sense to me to have the TextFields permanently displayed in the table, instead of transitioning to a special "edit mode" and switch from a Label to a TextField. (I would think of this as having all cells always in "edit mode", which I think makes sense with the behavior you want.)
If that kind of UI works for your requirements, you can just render text fields in the cell and bind bidirectionally the text field's textProperty to the appropriate property in your model. The tricky part here is getting hold of that property: you have to go from the cell to the table row, then to the item for the table row, and then to the property you need. At any time, one of those may change (possibly to null), so you have to deal with those possibilities.
Give the usual example:
public class Person {
// ...
public StringProperty firstNameProperty() { ... }
// etc...
}
You can do
TableView<Person> table = new TableView<>();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
textField.textProperty().unbindBidirectional(((Person) oldPerson).firstNameProperty());
}
if (newPerson != null) {
textField.textProperty().bindBidirectional(((Person) newPerson).firstNameProperty());
}
};
cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
if (oldRow != null) {
oldRow.itemProperty().removeListener(rowItemListener);
if (oldRow.getItem() != null) {
textField.textProperty().unbindBidirectional(((Person) oldRow.getItem()).firstNameProperty());
}
}
if (newRow != null) {
newRow.itemProperty().addListener(rowItemListener);
if (newRow.getItem() != null) {
textField.textProperty().bindBidirectional(((Person) newRow.getItem()).firstNameProperty());
}
}
});
return cell ;
});
You can greatly reduce the code complexity here by using the EasyBind framework, which provides (among other things) ways to get "properties of properties" with appropriate handling for null:
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
textField.textProperty().bindBidirectional(
EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> ((Person)p).firstNameProperty()));
return cell ;
});
Here is a complete example, where I factored the cell factory code above into a more general method:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.easybind.EasyBind;
public class LiveTableViewCell extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
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.getColumns().addAll(
createColumn("First Name", Person::firstNameProperty),
createColumn("Last Name", Person::lastNameProperty),
createColumn("Email", Person::emailProperty)
);
Button button = new Button("Debug");
button.setOnAction(e -> table.getItems().stream().map(p -> String.format("%s %s %s", p.getFirstName(), p.getLastName(), p.getEmail())).forEach(System.out::println));
primaryStage.setScene(new Scene(new BorderPane(table, null, null, button, null), 600, 120));
primaryStage.show();
}
private TableColumn<Person, String> createColumn(String title, Function<Person, Property<String>> property) {
TableColumn<Person, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
// Example of maintaining selection behavior when text field gains
// focus. You can also call getSelectedCells().add(...) on the selection
// model if you want to maintain multiple selected cells, etc.
textField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
if (isFocused) {
cell.getTableView().getSelectionModel().select(cell.getIndex(), cell.getTableColumn());
}
});
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
// If not using EasyBind, you need the following commented-out code in place of the next statement:
// ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
// if (oldPerson != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldPerson));
// }
// if (newPerson != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newPerson));
// }
// };
// cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
// if (oldRow != null) {
// oldRow.itemProperty().removeListener(rowItemListener);
// if (oldRow.getItem() != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldRow.getItem()));
// }
// }
// if (newRow != null) {
// newRow.itemProperty().addListener(rowItemListener);
// if (newRow.getItem() != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newRow.getItem()));
// }
// }
// });
textField.textProperty().bindBidirectional(EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> (property.apply((Person)p))));
return cell ;
});
return col ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
(The annoying downcasts here are because TableCell<S,T>.getTableRow() returns a raw TableRow object, instead of a TableRow<S>, for reasons I have never understood.)
I think you can avoid it by implementing custom text field tablecell, where you can put the caret at the end of the item text manually on entering edit mode.
Another approach is to enter edit mode on focus:
table.getFocusModel().focusedCellProperty().addListener(
( ObservableValue<? extends TablePosition> observable, TablePosition oldValue, TablePosition newValue ) ->
{
if ( newValue != null )
{
Platform.runLater( () ->
{
table.edit( newValue.getRow(), newValue.getTableColumn() );
} );
}
}
);
a couple of years late, but I actually found a solution to this (using a Robot).
this.setOnKeyTyped(x -> {
String typed = x.getCharacter();
//can make editing start only when certain keys (e.g. digits) are typed.
if(typed != null && typed.matches("[0-9]")) {
Robot robot = new Robot();
robot.keyPress(KeyCode.ENTER);
}
});

Resources