floxbr
floxbr

Reputation: 154

JavaFX ChoiceBox - How can you update the text of the popup items?

I have a ChoiceBox where I can select the language for my program. When I select another language, the label gets translated as desired (because it is recomputed using ChoiceBoxSkin#getDisplayText and my StringConverter takes the language into account), but the elements in the popup list stay the same.

Now, I could do something like

public void updateStrings() {
    var converter = getConverter();
    setConverter(null);
    setConverter(converter);
    var selected = valueProperty().getValue();
    valueProperty().setValue(null);
    valueProperty().setValue(selected);
}

in my ChoiceBox-subclass. This will re-populate the popup list with the correctly translated texts. Setting the value again is necessary beacause ChoiceBoxSkin#updatePopupItems (which is triggered when changing the converter) also resets the toggleGroup. That means that the selected item would no longer be marked as selected in the popup list.

Despite being kind of ugly, this actually works for my current use case. However, it breaks if any listener of the valueProperty does something problematic on either setting it to null or selecting the desired item a second time.

Am I missing a cleaner or just all-around better way to achieve this?

Another approach might be to use a custom ChoiceBoxSkin. Extending that, I'd have access to ChoiceBoxSkin#getChoiceBoxPopup (although that is commented with "Test only purpose") and could actually bind the text properties of the RadioMenuItems to the corresponding translated StringProperty. But that breaks as soon as ChoiceBoxSkin#updatePopupItems is triggered from anywhere else...

A MRP should be:

import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;

public class LabelChangeChoiceBox extends ChoiceBox<String> {
    private boolean duringUpdate = false;

    public LabelChangeChoiceBox() {
        getItems().addAll("A", "B", "C");
        setConverter(new StringConverter<>() {
            @Override
            public String toString(String item) {
                return item + " selected:" + valueProperty().getValue();
            }

            @Override
            public String fromString(String unused) {
                throw new UnsupportedOperationException();
            }
        });
        valueProperty().addListener((observable, oldValue, newValue) -> {
            if(duringUpdate) {
                return;
            }
            duringUpdate = true;
            updateStrings();
            duringUpdate = false;
        });
    }

    public void updateStrings() {
        var converter = getConverter();
        setConverter(null);
        setConverter(converter);
        var selected = valueProperty().getValue();
        valueProperty().setValue(null);
        valueProperty().setValue(selected);
    }
}

And an Application-class like

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import ui.LabelChangeChoiceBox;

public class Launcher extends Application {
    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new LabelChangeChoiceBox());
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

This works but needs the duringUpdate variable and can break if there is another change listener.

Upvotes: 1

Views: 617

Answers (2)

kleopatra
kleopatra

Reputation: 51535

Not entirely certain whether or not I understand your requirement correctly, my assumptions:

  • there's a ChoiceBox which contains the "language" for your ui, including the itself: lets say it contains the items Locale.ENGLISH and Locale.GERMAN, the visual representation of its items should be "English", "German" if its value is Locale.ENGLISH and "Englisch", "Deutsch" if its value is Locale.GERMAN
  • the visual representation is done by a StringConverter configurable with the value

If so, the solution is in separating out concerns - actually, it's not: the problem described (and hacked!) in the question is JDK-8088507: setting the converter doesn't update the selection of the menu items in the drop down. One hack is as bad or good as another, my personal preferenced would go for a custom skin which

  • adds a change listener to the converter property
  • reflectively calls updateSelection

Something like:

public static class MyChoiceBoxSkin<T> extends ChoiceBoxSkin<T> {

    public MyChoiceBoxSkin(ChoiceBox<T> control) {
        super(control);
        registerChangeListener(control.converterProperty(), e -> {
            // my local reflection helper, use your own
            FXUtils.invokeMethod(ChoiceBoxSkin.class, this, "updateSelection");
        });
    }

}

Note: the hacks - this nor the OP's solution - do not solve the missing offset of the popup on first opening (initially or after selecting an item in the popup).


Not a solution to the question, just one way to have a value-dependent converter ;)

  • have a StringConverter with a fixed value (for simplicity) for conversion
  • have a converter controller having that a property with that value and a second property with a converter configured with the value: make sure the converter is replaced on change of the value
  • bind the controller's value to the box' value and the box' converter to the controller's converter

In (very raw) code:

public static class LanguageConverter<T> extends StringConverter<T> {

    private T currentLanguage;

    public LanguageConverter(T language) {
        currentLanguage = language;
    }

    @Override
    public String toString(T object) {
        Object value = currentLanguage;
        return "" + object + (value != null ? value : "");
    }

    @Override
    public T fromString(String string) {
        return null;
    }

}

public static class LanguageController<T> {

    private ObjectProperty<StringConverter<T>> currentConverter = new SimpleObjectProperty<>();
    private ObjectProperty<T> currentValue = new SimpleObjectProperty<>() {

        @Override
        protected void invalidated() {
            currentConverter.set(new LanguageConverter<>(get()));
        }

    };

}

Usage:

ChoiceBox<String> box = new ChoiceBox<>();
box.getItems().addAll("A", "B", "C");
box.getSelectionModel().selectFirst();
LanguageController<String> controller = new LanguageController<>();
controller.currentValue.bind(box.valueProperty());
box.converterProperty().bind(controller.currentConverter);

Upvotes: 1

VGR
VGR

Reputation: 44404

I’m not sure if this meets your needs, as your description of the problem is unclear in a few places.

Here’s a ChoiceBox which updates its converter using its own chosen language, and also retains its value when that change occurs:

import java.util.Locale;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;

public class FXLocaleSelector
extends Application {
    @Override
    public void start(Stage stage) {
        ChoiceBox<Locale> choiceBox = new ChoiceBox<>();
        choiceBox.getItems().addAll(
            Locale.ENGLISH,
            Locale.FRENCH,
            Locale.GERMAN,
            Locale.ITALIAN,
            Locale.CHINESE,
            Locale.JAPANESE,
            Locale.KOREAN
        );

        choiceBox.converterProperty().bind(
            Bindings.createObjectBinding(
                () -> createConverter(choiceBox.getValue()),
                choiceBox.valueProperty()));

        BorderPane pane = new BorderPane(choiceBox);
        pane.setPadding(new Insets(40));

        stage.setScene(new Scene(pane));
        stage.setTitle("Locale Selector");
        stage.show();
    }

    private StringConverter<Locale> createConverter(Locale locale) {
        Locale conversionLocale =
            (locale != null ? locale : Locale.getDefault());

        return new StringConverter<Locale>() {
            @Override
            public String toString(Locale value) {
                if (value != null) {
                    return value.getDisplayName(conversionLocale);
                } else {
                    return "";
                }
            }

            @Override
            public Locale fromString(String s) {
                return null;
            }
        };
    }

    public static void main(String[] args) {
        launch(FXLocaleSelector.class, args);
    }
}

Upvotes: 1

Related Questions