Search code examples
javafxtableviewjavafx-8

Issue firing a valid cancel CellEditEvent when editing from one cell to another


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. enter image description here

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