JDK-8336065 : NPE at this.undoChange.next if two-way binding at TextInputControl is made manual
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: jfx20,8,jfx21,jfx22,jfx23
  • Priority: P4
  • Status: Closed
  • Resolution: Not an Issue
  • OS: generic
  • CPU: generic
  • Submitted: 2024-07-08
  • Updated: 2024-07-20
  • Resolved: 2024-07-10
Description
A DESCRIPTION OF THE PROBLEM :
Pressing Ctrl+Z in a TextInputControl bound to a property (and the property bound back to TextInputControl.textProperty()) leads to:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot read field "next" because "this.undoChange" is null
        at javafx.controls@22.0.1/javafx.scene.control.TextInputControl.updateUndoRedoState(TextInputControl.java:1250)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Create a TextField. Create a separate SimpleStringProperty. Bind a change of TextField.textProperty() to SimpleStringProperty.set() and vice versa. Start the application. Type something in the field. Press Ctrl+Z. See an NPE:

Java:

        TextField textField = new TextField();
        textField.setPromptText("Type something and then press Ctrl+Z.");
        vbox.getChildren().add(textField);
        SimpleStringProperty textProperty = new SimpleStringProperty();
        textProperty.addListener((_, _, newValue) -> textField.textProperty().set(newValue));
        textField.textProperty().addListener((_, _, newValue) -> textProperty.set(newValue));



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Undo is executed.

---------- BEGIN SOURCE ----------
package org.jabreftest.test.javafxreproducer;

import java.io.IOException;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HelloApplication extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        Dialog<String> alert = new Dialog<>();
        alert.setTitle("Information Dialog");
        alert.setHeaderText("Look, an Information Dialog");
        alert.setContentText("I have a great message for you! With some longer text to show the issue");
        alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
        alert.initOwner(stage.getOwner());
        alert.setResizable(true);

        DialogPane dlgPane = new DialogPane();
        dlgPane.getButtonTypes().addAll(ButtonType.CANCEL);
        VBox vbox = new VBox();
        vbox.setPrefWidth(400);
        vbox.setPrefHeight(200);

        TextField textField = new TextField();
        textField.setPromptText("Type something and then press Ctrl+Z.");
        vbox.getChildren().add(textField);
        SimpleStringProperty textProperty = new SimpleStringProperty();
        textProperty.addListener((_, _, newValue) -> textField.textProperty().set(newValue));
        textField.textProperty().addListener((_, _, newValue) -> textProperty.set(newValue));

        dlgPane.setContent(vbox);

        alert.setDialogPane(dlgPane);
        alert.showAndWait();
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
        textInputControl.addEventFilter(KeyEvent.ANY, e -> {
            if (e.getEventType() == KeyEvent.KEY_PRESSED && e.isShortcutDown()) {
				switch (e.getCode()) {
                    case Y -> {
                        redoAction.execute();
                        e.consume();
                    }
                    case Z -> {
                        undoAction.execute();
                        e.consume();
                    }
                }
            }
        });


FREQUENCY : always



Comments
Thank you for clarification. It sounds like the requirements call for a more complex logic that can be expressed with a simple binding. Nevertheless, the kind of circular listeners described earlier is not the right approach, in my opinion.
12-07-2024

Additional Information from submitter: ==================================== The intention was to reposition the cursor after an update through the StringProperty. On save, the content of the StringProperty is cleaned up. Exmaple: Automatic conversion of HTML encoding to Unicode. The text property is updated. JavaFX then positions the cursor to the beginning. We modified that by a hook. If the StringProperty is modified, we get the cursor position, update the TextField (JavaFX changes cursor position) and position the cursor to the old position. (OK, we do a text diff and adapt the position to really match the relative position in the text) With bi directional binding we could not manage to reposition the cursor after "external" update.
12-07-2024

Please use bidirectional binding in this case. Not an issue.
10-07-2024

I am not sure what the OP tried to do with these listeners, but it is clearly a wrong pattern to use. Instead the code should have a bidirectional binding, designed specifically for such situations: textField.textProperty().bindBidirectional(textProperty); I think this ticket should be resolved as 'not an issue'.
10-07-2024

The issue is reproducible with all JFX versions. We get NPE when typing a character and pressing Ctrl + Z. Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot read field "next" because "this.undoChange" is null at javafx.controls@23-internal/javafx.scene.control.TextInputControl.updateUndoRedoState(TextInputControl.java:1250) at javafx.controls@23-internal/javafx.scene.control.TextInputControl.undo(TextInputControl.java:1201) at javafx.controls@23-internal/com.sun.javafx.scene.control.behavior.TextInputControlBehavior.undo(TextInputControlBehavior.java:539) at javafx.controls@23-internal/com.sun.javafx.scene.control.behavior.TextInputControlBehavior.lambda$new$30(TextInputControlBehavior.java:180) at javafx.controls@23-internal/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274) at javafx.base@23-internal/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247) at javafx.base@23-internal/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80) at javafx.base@23-internal/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) at javafx.base@23-internal/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) at javafx.base@23-internal/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) at javafx.base@23-internal/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) at javafx.base@23-internal/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at javafx.base@23-internal/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at javafx.base@23-internal/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at javafx.base@23-internal/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at javafx.base@23-internal/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at javafx.base@23-internal/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) at javafx.base@23-internal/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54) at javafx.base@23-internal/javafx.event.Event.fireEvent(Event.java:199) at javafx.graphics@23-internal/javafx.scene.Scene.processKeyEvent(Scene.java:2197) at javafx.graphics@23-internal/javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2718) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150) at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:430) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249) at javafx.graphics@23-internal/com.sun.glass.ui.View.handleKeyEvent(View.java:542) at javafx.graphics@23-internal/com.sun.glass.ui.View.notifyKey(View.java:966) at javafx.graphics@23-internal/com.sun.glass.ui.gtk.GtkApplication.enterNestedEventLoopImpl(Native Method) at javafx.graphics@23-internal/com.sun.glass.ui.gtk.GtkApplication._enterNestedEventLoop(GtkApplication.java:334) at javafx.graphics@23-internal/com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:513) at javafx.graphics@23-internal/com.sun.glass.ui.EventLoop.enter(EventLoop.java:107) at javafx.graphics@23-internal/com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:656) at javafx.graphics@23-internal/javafx.stage.Stage.showAndWait(Stage.java:469) at javafx.controls@23-internal/javafx.scene.control.HeavyweightDialog.showAndWait(HeavyweightDialog.java:162) at javafx.controls@23-internal/javafx.scene.control.Dialog.showAndWait(Dialog.java:346) at JavaFXTest/application.Test.start(Test.java:43) at javafx.graphics@23-internal/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:839) at javafx.graphics@23-internal/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:483) at javafx.graphics@23-internal/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:456) at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) at javafx.graphics@23-internal/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:455) at javafx.graphics@23-internal/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95) at javafx.graphics@23-internal/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method) at javafx.graphics@23-internal/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$10(GtkApplication.java:264) at java.base/java.lang.Thread.run(Thread.java:1583)
10-07-2024