JDK-8151129 : Editable ComboBox value update on focus lost is too late
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 8u74
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • Submitted: 2016-03-03
  • Updated: 2017-12-07
  • Resolved: 2016-06-16
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.
JDK 9
9Resolved
Related Reports
Duplicate :  
Relates :  
Relates :  
Relates :  
Description
Since 8u72 an edited value is also set on focus lost. It looks like a focus listener is internally used to commit the value. However, if a user installs a focus listener to process some value action this listener is called before the value is set (through the focus listener of the control/skin). Means #getValue() fails in this case and returns the old value. The behavior is similar to releases before 8u72 when focus is lost without committing a value by pressing the Enter key.

One would argue to observe the valueProperty instead. However, there are scenarios where it's required to commit the value before all listeners get notified - i. e. if a user requires value validation triggered on focus lost. 

Comments
I know technically this isn't a duplicate of JDK-8150946, but the fix for both Spinner and ComboBox is basically the same, so I'm closing this issue as a duplicate of JDK-8150946, and progressing with posting a webrev for review over there.
16-06-2016

@Jonathan thanks for pointing out why TextField with formatter is working as expected, wasn't aware that the TextInputControl registers its own focusListener for handling the commit. That being the case, I think that all text-like controls (editable Spinner, Picker ..) must do something similar: just as TextInputControl, they should only handle the commit semantics, not the visuals (which are still handled in the skin). Which implies that the text-like controls get commit/cancel semantics. And with it maybe API for a TextFormatter (which in itself then could somehow be aligned with the StringConverter). As an intermediate work-around, we can use a custom combo that installs a focusListener which triggers a manual commit, something like: ComboBox<T> comboBox = new ComboBox<T>(items) { { focusedProperty().addListener((src, ov, nv) -> { if (!nv && isEditable()) { // null guard missing ... setValue(getConverter().fromString(getEditor().getText())); } }); } };
22-03-2016

As as vendor of third party controls Installing the listener after the scene is shown is not an option. JavaFX controls have to be reliable which also means they have to provide a consistent feel otherwise it's pretty hard to use and recommend for real business applications. I would really appreciate if such things would be finally fixed in Java 9.
22-03-2016

I've slightly tweaked the test code above, simply to demonstrate the issue is an order-of-listeners issue, and if the listener is installed after the scene is shown (and hence after the skin is created), the issue goes away. I understand that this is not a solution unto itself, but, to me, it speaks to the need for a way of giving precedence to the installed listeners. In this case, the skin listeners (in particular, the ComboBoxPopupControl listener on the ComboBox focused property) needs to be fired before any 'end user' listeners. If we could somehow support listener precedence, then assuming the 'end user' doesn't alter the precedence of their listener, things would work as expected. Outside of this, I'm not sure there is a way to handle this in a satisfactory way. The reason why the TextField works as expected is because its focus listener is installed inside the TextInputControl itself, so it is properly instantiated before the scene is created (and before users have installed their own listeners). Doing something similar with ComboBox (and Spinner, etc) is conceivable, but it is much more murky, as the ComboBox focus listener is more focused on dealing with the visuals, which are not present in the model. The code is below: import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; import javafx.scene.layout.VBox; import javafx.stage.Stage; import static javafx.scene.control.TextFormatter.IDENTITY_STRING_CONVERTER; public class CommittedOnFocusLost extends Application { private TextField textField; private ComboBox<String> comboBox; private Parent getContent() { // TextField with TextFormatter: behaves as expected: textField = new TextField(); TextFormatter<String> formatter = new TextFormatter<>(IDENTITY_STRING_CONVERTER, "initial"); textField.setTextFormatter(formatter); textField.focusedProperty().addListener((src, ov, nv) -> { if (!nv) { System.out.println("textfield committed: " + textField.getText() + "=" + formatter.getValue()); } }); // compare to combo ObservableList<String> items = FXCollections.observableArrayList("One", "Two", "All"); comboBox = new ComboBox<>(items); comboBox.setEditable(true); comboBox.setValue(items.get(0)); comboBox.focusedProperty().addListener((src, ov, nv) -> { if (!nv) { System.out.println("[Before Skin Installed] combo committed: " + comboBox.getEditor().getText() + "=" + comboBox.getValue()); } }); VBox box = new VBox(10, textField, comboBox); return box; } @Override public void start(Stage primaryStage) throws Exception { primaryStage.setScene(new Scene(getContent())); primaryStage.show(); comboBox.focusedProperty().addListener((src, ov, nv) -> { if (!nv) { System.out.println("[After Skin Installed] combo committed: " + comboBox.getEditor().getText() + "=" + comboBox.getValue()); } }); } public static void main(String[] args) { launch(args); } }
21-03-2016

Spinner might have similar error - but then, it might not be introduced when fixing JDK-8150946 :-)
04-03-2016

As an additional argument for being a bug is that combo's behaviour is inconsistent with textField behavior. Below is an example that demonstrates the difference: run and - edit textField and move focus to next control - expected and actual: value of formatter == text of textField, that is the text is committed - edit combo and move focus to next control - expected: value of combo == text of editor (that is text committed) - actual: value of combo still at old value public class CommittedOnFocusLost extends Application { private Parent getContent() { // TextField with TextFormatter: behaves as expected: TextField textField = new TextField(); TextFormatter<String> formatter = new TextFormatter<>(IDENTITY_STRING_CONVERTER, "initial"); textField.setTextFormatter(formatter); textField.focusedProperty().addListener((src, ov, nv) -> { if (!nv) { System.out.println("textfield committed: " + textField.getText() + "=" + formatter.getValue()); } }); // compare to combo ObservableList<String> items = FXCollections.observableArrayList("One", "Two", "All"); ComboBox<String> comboBox = new ComboBox<>(items); comboBox.setEditable(true); comboBox.setValue(items.get(0)); comboBox.focusedProperty().addListener((src, ov, nv) -> { if (!nv) { System.out.println("combo committed: " + comboBox.getEditor().getText() + "=" + comboBox.getValue()); } }); VBox box = new VBox(10, textField, comboBox); return box; } @Override public void start(Stage primaryStage) throws Exception { primaryStage.setScene(new Scene(getContent())); primaryStage.setTitle(FXUtils.version()); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
03-03-2016