Issue
I am implementing an editable TableView
which relies on the CellEditEvents
for cancel, start & commit events.
In the below example, the city column is editable, and the corresponding events are triggered when:
- Cancel: Pressing escape in text field or when the focus is lost from text field.
- Commit: Pressing enter in text field.
The start and cancel events are triggering properly when I traverse from an editing cell to the RadioButton
. But it is throwing error when traversing from one cell to another.
Please check the below gif(for steps) and the console output.
On City edit start :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
On City edit cancel :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
On City edit start :: TableDataObj{firstName=First Name 1, lastName=Last Name 1, city=City 1}
On City edit cancel :: TableDataObj{firstName=First Name 1, lastName=Last Name 1, city=City 1}
On City edit start :: TableDataObj{firstName=First Name 0, lastName=Last Name 0, city=City 0}
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.TableColumn$CellEditEvent.getTableView(TableColumn.java:772)
at javafx.scene.control.TableColumn$CellEditEvent.getRowValue(TableColumn.java:829)
at com.thales.javafx.tableview.CancelTableEditDemo.lambda$buildTable$7(CancelTableEditDemo.java:84)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.control.TableCell.cancelEdit(TableCell.java:400)
at com.thales.javafx.tableview.CancelTableEditDemo$EditingCell.cancelEdit(CancelTableEditDemo.java:105)
at javafx.scene.control.TableCell.updateEditing(TableCell.java:565)
at javafx.scene.control.TableCell.lambda$new$26(TableCell.java:142)
at javafx.beans.WeakInvalidationListener.invalidated(WeakInvalidationListener.java:83)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:349)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyObjectWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyObjectWrapper.java:176)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:142)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.TableView.setEditingCell(TableView.java:1145)
at javafx.scene.control.TableView.edit(TableView.java:1457)
at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:106)
at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:38)
at com.sun.javafx.scene.control.behavior.CellBehaviorBase.handleClicks(CellBehaviorBase.java:269)
at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.simpleSelect(TableCellBehaviorBase.java:218)
at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.doSelect(TableCellBehaviorBase.java:148)
at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mousePressed(CellBehaviorBase.java:150)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:95)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:89)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3485)
at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:352)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:275)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$355(GlassViewEventHandler.java:388)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:387)
at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
at com.sun.glass.ui.View.notifyMouse(View.java:937)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$149(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
What I am expecting is : When traversing from Cell-0 to Cell-1, it has to fire a valid Cancel event for Cell-0 before start edit of Cell-1.
Can any of you please help me in figuring where/what I am missing?
Below is the full working code of 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.layout.*;
import javafx.stage.Stage;
public class CancelTableEditDemo extends Application {
public static void main(String... a) {
Application.launch(a);
}
@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(i, firstName, lastName, city));
}
final TableView<TableDataObj> table = buildTable();
table.setItems(items);
final VBox root = new VBox(new RadioButton("Use this for focus changing"), table);
root.setSpacing(10);
root.setPadding(new Insets(10));
VBox.setVgrow(table, Priority.ALWAYS);
final Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("Cancel Table Edit Demo");
primaryStage.show();
}
@SuppressWarnings("unchecked")
private TableView<TableDataObj> buildTable() {
final TableView<TableDataObj> tableView = new TableView<>();
tableView.setEditable(true);
final TableColumn<TableDataObj, Integer> idCol = new TableColumn<>();
idCol.setText("Id");
idCol.setCellValueFactory(param -> param.getValue().idProperty().asObject());
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 -> {
tableView.edit(cell.getTableRow().getIndex(), cityCol);
});
return cell;
});
cityCol.setOnEditStart(e -> {
System.out.println("On City edit start :: " + e.getRowValue());
});
cityCol.setOnEditCancel(e -> {
System.out.println("On City edit cancel :: " + e.getRowValue());
});
cityCol.setOnEditCommit(e -> {
System.out.println("On City edit commit :: val : " + e.getNewValue() + " :: " + e.getRowValue());
e.getRowValue().setCity(e.getNewValue());
});
tableView.getColumns().addAll(idCol, fnCol, lnCol, cityCol);
return tableView;
}
/**
* Editing Cell
*/
class EditingCell<T, S> extends TableCell<T, S> {
private TextField textField;
@Override
public void cancelEdit() {
super.cancelEdit();
updateItem(getItem(), getItem() == null);
}
@Override
public void commitEdit(final S newValue) {
super.commitEdit(newValue);
}
@Override
public void startEdit() {
super.startEdit();
updateItem(getItem(), getItem() == null);
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) {
createTextField();
}
textField.setText(getString());
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(item != null ? item.toString() : "");
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
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((S) textField.getText()); // For now casting directly for testing
keyEvent.consume();
}
});
/* Cancel edit when loosing focus. */
textField.focusedProperty().addListener((obs, prevFocus, focused) -> {
if (!focused) {
cancelEdit();
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
/**
* Data object.
*/
class TableDataObj {
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty city = new SimpleStringProperty();
public TableDataObj(final int i, final String fn, final String ln, final String cty) {
setId(i);
setFirstName(fn);
setLastName(ln);
setCity(cty);
}
public StringProperty cityProperty() {
return city;
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getCity() {
return city.get();
}
public String getFirstName() {
return firstName.get();
}
public int getId() {
return id.get();
}
public String getLastName() {
return lastName.get();
}
public IntegerProperty idProperty() {
return id;
}
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 setId(final int idA) {
id.set(idA);
}
public void setLastName(final String lastName1) {
lastName.set(lastName1);
}
@Override
public String toString() {
return "TableDataObj{" +
"firstName=" + firstName.get() +
", lastName=" + lastName.get() +
", city=" + city.get() +
'}';
}
}
}
Solution
Ok.. as I have to look for a workaround till I upgrade to JavaFX 17, below are the changes I came up with (for JavaFX 8):
Firstly, adding a null check for TablePosition in the onCancelEdit event handler to ensure no errors are thrown because of the internal bug.
cityCol.setOnEditCancel(e -> {
if (e.getTablePosition() != null) {
System.out.println("On City edit cancel :: " + e.getRowValue());
}
});
Secondly, to fire the correct cancel event, I am explicitly firing the cancel event when the conditions are not correct.
@Override
public void cancelEdit() {
TablePosition<T, ?> editingCell = getTableView().getEditingCell();
super.cancelEdit();
// If the editingCell is null, then the editCancelEvent fired in super method has no impact. So explicitly firing a valid editCancelEvent.
if (editingCell == null) {
final TablePosition<T, S> pos = new TablePosition<>(getTableView(), getTableRow().getIndex(), getTableColumn());
Event.fireEvent(getTableColumn(), new TableColumn.CellEditEvent<>(getTableView(), pos, TableColumn.editCancelEvent(), null));
}
setText(getItem() != null ? getItem().toString() : "");
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
A full working demo with the changes is below:
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.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class CancelTableEditDemo extends Application {
public static void main(String... a) {
Application.launch(a);
}
@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(i, firstName, lastName, city));
}
final TableView<TableDataObj> table = buildTable();
table.setItems(items);
final VBox root = new VBox(new RadioButton("Use this for focus changing"), table);
root.setSpacing(10);
root.setPadding(new Insets(10));
VBox.setVgrow(table, Priority.ALWAYS);
final Scene sc = new Scene(root);
primaryStage.setScene(sc);
primaryStage.setTitle("Cancel Table Edit Demo");
primaryStage.show();
}
@SuppressWarnings("unchecked")
private TableView<TableDataObj> buildTable() {
final TableView<TableDataObj> tableView = new TableView<>();
tableView.setEditable(true);
final TableColumn<TableDataObj, Integer> idCol = new TableColumn<>();
idCol.setText("Id");
idCol.setCellValueFactory(param -> param.getValue().idProperty().asObject());
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 -> {
tableView.edit(cell.getTableRow().getIndex(), cityCol);
});
return cell;
});
cityCol.setOnEditStart(e -> {
System.out.println("On City edit start :: " + e.getRowValue());
});
cityCol.setOnEditCancel(e -> {
if (e.getTablePosition() != null) {
System.out.println("On City edit cancel :: " + e.getRowValue());
}
});
cityCol.setOnEditCommit(e -> {
System.out.println("On City edit commit :: val : " + e.getNewValue() + " :: " + e.getRowValue());
e.getRowValue().setCity(e.getNewValue());
});
tableView.getColumns().addAll(idCol, fnCol, lnCol, cityCol);
return tableView;
}
/**
* Editing Cell
*/
class EditingCell<T, S> extends TableCell<T, S> {
private TextField textField;
@Override
public void cancelEdit() {
TablePosition<T, ?> editingCell = getTableView().getEditingCell();
super.cancelEdit();
// If the editingCell is null, then the editCancelEvent fired in super method has no impact. So explicitly firing a valid editCancelEvent.
if (editingCell == null) {
final TablePosition<T, S> pos = new TablePosition<>(getTableView(), getTableRow().getIndex(), getTableColumn());
Event.fireEvent(getTableColumn(), new TableColumn.CellEditEvent<>(getTableView(), pos, TableColumn.editCancelEvent(), null));
}
setText(getItem() != null ? getItem().toString() : "");
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
@Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
textField.setText(getString());
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
textField.requestFocus();
}
@Override
public void updateItem(final S item, final boolean empty) {
super.updateItem(item, empty);
setGraphic(null);
if (empty) {
setText(null);
} else {
setText(item != null ? item.toString() : "");
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
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((S) textField.getText()); // For now casting directly for testing
keyEvent.consume();
}
});
/* Cancel edit when loosing focus. */
textField.focusedProperty().addListener((obs, prevFocus, focused) -> {
if (!focused && isEditing()) {
cancelEdit();
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
/**
* Data object.
*/
class TableDataObj {
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty city = new SimpleStringProperty();
public TableDataObj(final int i, final String fn, final String ln, final String cty) {
setId(i);
setFirstName(fn);
setLastName(ln);
setCity(cty);
}
public StringProperty cityProperty() {
return city;
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getCity() {
return city.get();
}
public String getFirstName() {
return firstName.get();
}
public int getId() {
return id.get();
}
public String getLastName() {
return lastName.get();
}
public IntegerProperty idProperty() {
return id;
}
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 setId(final int idA) {
id.set(idA);
}
public void setLastName(final String lastName1) {
lastName.set(lastName1);
}
@Override
public String toString() {
return "TableDataObj{" +
"firstName=" + firstName.get() +
", lastName=" + lastName.get() +
", city=" + city.get() +
'}';
}
}
}
Answered By - Sai Dandem
Answer Checked By - Willingham (JavaFixing Volunteer)