1

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.

floxbr
  • 144
  • 1
  • 9
  • [mcve] please .. – kleopatra Apr 18 '22 at 10:18
  • Added a version of the ChoiceBox that can be added be used in any JavaFX program. Should I provide an Application-class as well or is it sufficient as is? – floxbr Apr 18 '22 at 10:35
  • @floxbr The example is not complete. You are forcing us to invest extra work to wrap it with something of our own just in order to make this runnable. This burden should be on your side and not ours. – mipa Apr 18 '22 at 11:06
  • I also added an Application class so you can run the to example classes together. I don't think you learn anything that helps answering the question from running it, though... – floxbr Apr 18 '22 at 11:21
  • 2
    hmm .. don't quite understand what you want to achieve: how is the _change language_ related to update (the visual representation of) all items? Does {A, B, C} stand for the languages? If so why not use languages and an appropriate converter to translate all from the selected (currently chosen) language? Please edit the example to clarify. – kleopatra Apr 18 '22 at 12:24
  • Is [_Change application language on the run_](https://stackoverflow.com/q/32464974/230513) relevant? – trashgod Apr 18 '22 at 12:28
  • @kleopatra If you want it closer to the use case, just imagine a "translated" instead of "selected" and .getValue() to produce the appropriate translated String. Ultimately this does not matter for the question, though. In general I simply have a ChoiceBox where the Strings in the popup depend on the currently selected value and am looking for a way to update said Strings in the popup when I select another value. – floxbr Apr 18 '22 at 13:07
  • @trashgod I think it relates to my "another approach" paragraph: I actually do have StringProperty-objects that are updated when the language changes and with them also StringBindings as suggested in the linked question. However, I don't see a good way to bind them to the popup entries. – floxbr Apr 18 '22 at 13:10
  • please have a look at my answer - will clean up or delete later (have to run now), depending on whether or not my assumptions match your requirement or not :) Whatever it is: don't extend a control (or any class) if you can achieve what you want by configuration! Also note, that a StringConverter is assumed to be immutable. – kleopatra Apr 18 '22 at 13:20
  • Thanks for your help! I think in the current state, what I want to do can indeed be achieved by configuration and does not require extending `ChoiceBox` anymore. – floxbr Apr 18 '22 at 13:44

2 Answers2

1

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);
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Thank you very much for your suggestion! Essentially this is part of what I have done: where you call `currentConverter#set` in `LanguageController#invalidated`, I call my `updateString` method. Instead of setting a new converter, I set it to null and apply the old one again (which probably is a bit more overhead, so I might change that). However, you just omit the potentially problematic part where I set the value to null and then a second time completely. This means that the selection in the `toggleGroup` is lost (i.e., there is no checkmark at the selected option in the popup). – floxbr Apr 18 '22 at 13:29
  • mine is working as expected, your's is not Or in other words, you are doing it the wrong way. Or what do I miss? Why is there any special handling of a null needed? Simply don't do it the way you do it, the converter _must not be mutable_ – kleopatra Apr 18 '22 at 13:31
  • ahh ... paddling back a bit: the check mark is indeed missing - might be a bug, though: see it after clicking again into the selected item. Hmm .. – kleopatra Apr 18 '22 at 13:38
  • Thanks! My converter is just as (im)mutable as yours in the sense that it can change the output if currentLanguage or the return value of its toString method changes. But that does not really matter: as I just said, creating a new one is probably better that setting `null` and then the old one again. As to the "works as expected": updating the converter will reset the `toggleGroup`. Thinking about it a bit more, that could really be considered a bug (or at least an oversight) in the `ChoiceBoxSkin` implementation. – floxbr Apr 18 '22 at 13:41
  • 2
    This looks like a bug. The [skin implementation](https://github.com/openjdk/jfx/blob/master/modules/javafx.controls/src/main/java/javafx/scene/control/skin/ChoiceBoxSkin.java) rebuilds the menu items (creating `RadioMenuItem`s by default) that are used in the choice box popup when the converter is changed. However it doesn't set the selected state of the `RadioMenuItem` for the item that represents the current value. – James_D Apr 18 '22 at 15:35
  • 2
    Indeed. It looks like it's almost the 10th anniversary of when this [bug](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8088507) was first submitted... – James_D Apr 18 '22 at 18:18
  • 1
    arrgghh .. [JDK-8088507](https://bugs.openjdk.java.net/browse/JDK-8088507) found by @James_D seems to be a can of worms: the obvious fix in choiceBoxSkin (updateSelection on converter change), does add the checkmark and shows the blueish background as expected but still misses the offset. This missing offset also happens on initial selection > 0 (converter or not doesn't matter) and seems to be related to something fishy in ContextMenuContent (not yet quite "ready" at the showing?) – kleopatra Apr 19 '22 at 11:18
  • This may be way too naïve, but wouldn't `item.setSelected(o == selectionModel.getSelectedItem())` (with an appropriate null check on `selectionModel`) in `addPopupItem(final T o, int i)` solve this issue? (As a fix to the underlying bug, I mean, not as a workaround.) – James_D Apr 19 '22 at 12:09
  • @James_D tried it, but didn't fix the missing offset – kleopatra Apr 19 '22 at 12:11
1

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);
    }
}
VGR
  • 40,506
  • 4
  • 48
  • 63