JDK-8089950 : Severe ordering of TextInputControl events
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 7u6
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • Submitted: 2012-09-13
  • Updated: 2018-09-05
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
Relates :  
Relates :  
Description
When writing helper objects for textfields that help tidy up user input, optimal ordering of the events coming from TextInputControl is crucial. The bugs I have found are complex, so let me explain by an example. I am to write a class which helps all users write phone numbers the same way. In our app Danish phone numbers should always start with the country code +45 and have a space for every two numbers. So as users are typing a Danish phone number +4512345678 with no spaces, we listen in on the updates to the text field and change the number to the conventional and much more readable +45 12 34 56 78. I have had several attempts at writing this code, and the reason I have not succeeded is because of the way TextField orders the events occurring.

My first attempt was to listen to the textProperty of TextField using a ChangeListener. As the changes arrived I would insert spaces using textField.insertText(...) and make sure to ignore the additional change events stemming from these additional inserts. I have done something quite similar in Swing, but here in JavaFX this fails, because TextField does not update the caret position until *after* my ChangeListener is invoked, leading to this ordering of things:

1. The user types a number in the TextField
2. Before updating the caretPosition TextField invokes our ChangeListener attached to the textProperty
3. We then change the text ourselves, adding spaces to the phone number by invoking textField.insertText()... (and deliberately ignoring the additional change events coming from that)
4. TextField now updates the caretPosition to the position which would have been correct *before* our insertion of spaces, consequently ignoring the caret updates coming from insertText.

I also tried listening for key typed events, but here the problem is our listener is invoked even before the text of the text field is updated.

I think it is crucial that this is solved. Tidying up user input is a well-known, real world problem, and JavaFX has to support that. I do not know all the inner workings of TextField, but one solution to this issue could be to update the caret position before the ChangeListeners of the textProperty are invoked, so additional caret updates are not overwritten.

Comments
It has been a month, Leif. Have you had a chance to look at this?
22-10-2012

Boom. Done. And it works wonders. Attached you will find the classes demonstrating autocorrrection of text field values. I recommend you run the AutocorrectionDemo and then take a look at the inner class ITextField which is a simplification of our extended TextField class which supports autocorrection. Then please see the UsPhoneNumberCorrector and TimeCorrector for examples of how easy one can write autocorrection logic once the Caret class is in place. Looking forward to hearing what you think of this, and if it is something which could be useful for JavaFX.
16-09-2012

The first thing I will try monday is to create a helper class which can take care of moving the caret around when carrying out the inserts and deletes. I think I will call it Caret. So instead of all the if-delete-affects-caret-move-caret logic I can just do getCaret().deleteText(spaceIndex, spaceIndex + 1); and it will automatically take care of moving the caret if needed. I think that will reduce the code above significantly.
15-09-2012

Sure, feel free to attach the code to the issue for review. I think we should look into making replaceSelection() call replaceText(), so that it will be enough to just override the latter using calls to super.replaceText(). This would mean there's no need to have a flag in order to avoid recursion.
14-09-2012

This is awesome. I wrote the test case below, and it actually proves that the approach suggested actually *does* work. I just needed to be very, very careful with the caret updates. So this proves that yes, indeed, it is possible to write autocorrection code which, for instance, ensures a phone number is of a certain format. Now, I am going to turn this into a real design in my subclass of TextField, and add support for adding an Autocorrector which will be responsible for carrying out the formatting. I cannot help asking if you would be interested in reviewing the code, once I have it running ��� it might be something you could build in for the benefit of everyone. I would love to have helped make JavaFX more powerful. Here is the test case I wrote: public class AutocorrectionTest extends Application { private TextField textField = new TextField() { private boolean autocorrecting = false; @Override public void replaceText(int start, int end, String text) { super.replaceText(start, end, text); autocorrect(); } @Override public void replaceSelection(String string) { super.replaceSelection(string); autocorrect(); } protected void autocorrect() { if(!autocorrecting) { try { int caretPosition; autocorrecting = true; int spaceIndex = getText().indexOf(" "); while(spaceIndex != -1) { caretPosition = getCaretPosition(); deleteText(spaceIndex, spaceIndex + 1); if(spaceIndex < caretPosition) caretPosition--; positionCaret(caretPosition); spaceIndex = getText().indexOf(" "); } String text = getText(); if(!text.startsWith("+45")) { insertText(0, "+45"); caretPosition = getCaretPosition() + "+45".length(); positionCaret(caretPosition); } int start = getText().length() - 1; for(int i = start; i >= 3; i--) { if((i - 1) % 2 == 0) { caretPosition = getCaretPosition(); insertText(i, " "); if(i < caretPosition) { caretPosition += 1; positionCaret(caretPosition); } } } text = getText(); final int maxLength = "+45 12 34 56 78".length(); if(text.length() > maxLength) { //too long caretPosition = getCaretPosition(); deleteText(maxLength, text.length()); if(caretPosition > maxLength) positionCaret(maxLength); } } finally { autocorrecting = false; } } } }; @Override public void start(Stage stage) throws Exception { BorderPane borderPane = new BorderPane(); borderPane.setCenter(textField); stage.setWidth(300); stage.setScene(new Scene(borderPane)); stage.show(); } public static void main(String[] args) { Application.launch(args); } }
14-09-2012

OK. I will write a test case then and get back to you.
14-09-2012

Actually, since the caret is moved synchronously by the call to super.replaceText(), I don't see why setting the caret pos afterwards (without using runLater) shouldn't work.
14-09-2012

For now I will just stick to my work-around of only evaluating my Orthograph when the user tabs out of the field. But realtime autocorrection is a very useful feature and I hope this issue will remain open until a viable solution is found.
14-09-2012

I think you are right. Still, it does add more complexity ��� for instance, what happens if the user types after my replaceText super call but before runLater is evaluated? As I have reported in another issue, I have seen runLater take everything from 20 to 2000 milliseconds depending on system load (which is why I have requested a guaranteed Platform.runNext() method).
14-09-2012

I think the easiest workaround is to override replaceText() and replaceSelection() to first call super with the corrected string and indices, and then use Platform.runLater() to make the call to set the caret position.
14-09-2012

Leif, the more I work with this, the more I am convinced there is a general problem with how the caret is controlled. My claim is that if an application invokes any methods that move the caret from inside a listener on the properties of TextField the invokation will not have an effect, because the TextField gets the last say *after* the application's listener's caret change. I just encountered another example of this ��� while listening for focus events I did this: new ChangeListener<Boolean>() { public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { if(newValue && phoneNumberField.getText().isEmpty()) { phoneNumberField.setText("+45"); phoneNumberField.end(); } } } ... Here, the invokation of the end() method should clearly move the caret to the end of the field, however if this listener is invoked when the text field gains focus, the text field moves the caret to whatever position the user clicked with his mouse *after* I call end(), thereby making my end() call have no effect. In short: I think one needs to take a thorough look at the ordering of the events fired and the actions taken by TextField. Ideally the TextField should work in this order: 1. receive input 2. update *all* internal state including the text property and the caret position, so everything is done before any listeners are called 3. *then* call anyregistered listeners (an applications focus listener of a field for instance) 4. do *not* do anything else as that may neutralize what the registered listeners did to update the TextField.
14-09-2012

Thanks again for the tip Leif, but the insertText(int, int) approach turns out to have the exact same problem. The reason why Richards approach works in his blog example is he just hinders insertion ��� he never adds anything to the text. In my case, an example use case could be 1. the user enters +45 ��� text field now contains [+45|] (where | shows the cursor position 2. the user enters an additional 1 ��� text field now contains [+451|] 3. in the insertText(int, int) method I conclude that a space is missing between 5 and 1, so I raise a flag (ignoreNextInsertsCauseTheyAreMyOwn == true) and insert the space using insertText and then lower the flag (ignoreNextInsertsCauseTheyAreMyOwn == false) to be ready for more user edits. 4. surprisingly enough, the end result is now [+45| 1] and not [+45 1|] as expected. Now, to work around this I have tried to set the caret explicitly to the proper position right after my insert, but much to my surprise, that has *no* effect whatsoever ��� it works as if TextField still gets the last say and sets the caret position from the user input (2.) right after I set the caretPosition myself. I believe the other issue as this issue shows that the current text input restriction features are too limited. What I am trying to do here is actually to expand TextField to support textual restriction in general with this interface I have created myself and which I attempt to call every time the user makes changes public interface Orthograph { void orthograph(TextInputControl control); } So what I am actually doing in my code looks like this ExtendedTextField phoneNumberField = new ExtendedTextField(); phoneNumberField.setOrthograph(DanishPhoneNumberOrthograph.getInstance()); I have different implementations of this, one being the one which adds spaces to Danish phone numbers. I have got this working currently, but only because I wait until the user tabs out of the field, at which point the caret position is irrelevant. Perhaps JavaFX could support something similar out of the box?
13-09-2012

Thanks for the tip Leif. I will look into that.
13-09-2012

Instead of listening to events, have you tried overriding replaceText() as described in RT-19402?
13-09-2012