Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manually typing in text in JavaFX Spinner is not updating the value (unless user presses ENTER)

It seems that the Spinner control does not update a manually typed-in value until the user explicitly presses enter. So, they could type in a value (not press enter) exit the control, and submit the form, and the value displayed in the spinner is NOT the value of the Spinner, it is the old value.

My idea was to add a listener to the lost focus event, but I can't see a way to gain access to the typed-in value?

spinner.focusedProperty().addListener((observable, oldValue, newValue) -> 
{
    //if focus lost
    if(!newValue)
    {
        //somehow get the text the user typed in?
    }
});

This is odd behavior, it seems to go against the convention of a GUI spinner control.

like image 742
James Wierzba Avatar asked Sep 01 '15 20:09

James Wierzba


2 Answers

Unfortunately, Spinner doesn't behave as expected: in most OS, it should commit the edited value on focus lost. Even more unfortunate, it doesn't provide any configuration option to easily make it behave as expected.

So we have to manually commit the value in a listener to the focusedProperty. On the bright side, Spinner already has code doing so - it's private, though, we have to c&p it

/**
 * c&p from Spinner
 */
private <T> void commitEditorText(Spinner<T> spinner) {
    if (!spinner.isEditable()) return;
    String text = spinner.getEditor().getText();
    SpinnerValueFactory<T> valueFactory = spinner.getValueFactory();
    if (valueFactory != null) {
        StringConverter<T> converter = valueFactory.getConverter();
        if (converter != null) {
            T value = converter.fromString(text);
            valueFactory.setValue(value);
        }
    }
}

// useage in client code
spinner.focusedProperty().addListener((s, ov, nv) -> {
    if (nv) return;
    //intuitive method on textField, has no effect, though
    //spinner.getEditor().commitValue(); 
    commitEditorText(spinner);
});

Note that there's a method

textField.commitValue()

which I would have expected to ... well ... commit the value, which has no effect. It's (final!) implemented to update the value of the textFormatter if available. Doesn't work in the Spinner, even if you use a textFormatter for validation. Might be some internal listener missing or the spinner not yet updated to the relatively new api - didn't dig, though.


Update

While playing around a bit more with TextFormatter I noticed that a formatter guarantees to commit on focusLost:

The value is updated when the control loses its focus or it is commited (TextField only)

Which indeed works as documented such that we could add a listener to the formatter's valueProperty to get notified whenever the value is committed:

TextField field = new TextField();
TextFormatter fieldFormatter = new TextFormatter(
      TextFormatter.IDENTITY_STRING_CONVERTER, "initial");
field.setTextFormatter(fieldFormatter);
fieldFormatter.valueProperty().addListener((s, ov, nv) -> {
    // do stuff that needs to be done on commit
} );

Triggers for a commit:

  • user hits ENTER
  • control looses focus
  • field.setText is called programmatically (this is undocumented behaviour!)

Coming back to the spinner: we can use this commit-on-focusLost behaviour of a formatter's value to force a commit on the spinnerFactory's value. Something like

// normal setup of spinner
SpinnerValueFactory factory = new IntegerSpinnerValueFactory(0, 10000, 0);
spinner.setValueFactory(factory);
spinner.setEditable(true);
// hook in a formatter with the same properties as the factory
TextFormatter formatter = new TextFormatter(factory.getConverter(), factory.getValue());
spinner.getEditor().setTextFormatter(formatter);
// bidi-bind the values
factory.valueProperty().bindBidirectional(formatter.valueProperty());

Note that editing (either typing or programmatically replacing/appending/pasting text) does not trigger a commit - so this cannot be used if commit-on-text-change is needed.

like image 52
kleopatra Avatar answered Nov 10 '22 03:11

kleopatra


@kleopatra headed to a right direction, but the copy-paste solution feels awkward and the TextFormatter-based one did not work for me at all. So here's a shorter one, which forces Spinner to call it's private commitEditorText() as desired:

spinner.focusedProperty().addListener((observable, oldValue, newValue) -> {
  if (!newValue) {
    spinner.increment(0); // won't change value, but will commit editor
  }
});
like image 32
Sergio Avatar answered Nov 10 '22 01:11

Sergio