Нередактируемый ComboBox JavaFX 11 не отображает значения вне списка элементов со списком должным образом

У меня проблемы с JaxaFX 11 ComboBox (похоже, что в JavaFX 8 все работает нормально).

Для нередактируемого комбо, то есть отображения выбранного значения в ячейке кнопки (не в редактируемом текстовом поле), значение не отображается (ячейка кнопки, вероятно, считается "пустой"), если новое значение не включено в список элементов комбо, за одним исключением:

Если предыдущее значение равно null (например, отменить выбор предыдущего ненулевого значения с помощью клавиатуры во всплывающем списке), новое ненулевое значение отображается правильно.

Посмотрите простой код, чтобы воспроизвести проблему. Изначально значение комбинации равно null. Нажмите кнопку, чтобы установить значение вне списка элементов. Отображается ОК. Затем выберите какое-либо значение из всплывающего окна. Попробуйте еще раз нажать кнопку. Теперь комбо остается пустым, хотя значение комбо было изменено.

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ComboTest extends Application {
    private ComboBox<String> testCombo;

    @Override public void start(Stage primaryStage) {
        Button btn = new Button("Set test value outside list");
        btn.setOnAction(e -> {
            testCombo.setValue("test value outside list");
        });

        testCombo = new ComboBox<>(FXCollections.observableArrayList(
                "Option 1", "Option 2", "Option 3"
        ));
        testCombo.setPromptText("null now!");

        TextField valueTextField = new TextField();
        testCombo.valueProperty().addListener((ob, ov, nv) -> {
            valueTextField.setText("combo value: " + nv);
        });

        VBox root = new VBox(5);
        root.setPadding(new Insets(5));
        root.getChildren().addAll(btn, testCombo, valueTextField);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("Test Combo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Я что-то пропустил? Я не смог найти никакого обходного пути. Я попытался отладить, но не смог найти ответ. Вроде сначала ставится правильный текст, а потом опять стирается.

(JDK 11.0.2, JavaFX 11.0.2, Netbeans 10)


person Tom    schedule 27.03.2019    source источник
comment
хм... звучит как старая ошибка, которую, как я думал, давно исправили...   -  person kleopatra    schedule 27.03.2019
comment
@kleopatra Я пытался отладить updateItem(int oldIndex) в ListCell, где, как я думал, может возникнуть emtpy=true, но я не могу понять логику...   -  person Tom    schedule 27.03.2019
comment
это что-то странное: я вижу неправильное поведение, но все тесты проходят, должно быть что-то упущено (старая ошибка была bugs.openjdk.java.net/browse/JDK-8127575 - к сожалению, исправлено до того, как стало обычным добавлять ссылку на фиксацию.. )   -  person kleopatra    schedule 27.03.2019
comment
@kleopatra Для упрощения отладки я использовал пользовательскую кнопку ButtonCell (расширяющую ListCell). Я вижу, что: а) переход от значения в списке к другому значению в списке: updateItem (элемент T, логическое значение пусто) выполняется 3 раза с теми же значениями; б) изменение значения в списке на значение вне списка: выполняется только 2 раза с одинаковыми значениями (пусто = верно). Может ли причина быть чем-то позади этого?   -  person Tom    schedule 27.03.2019
comment
может быть - только что узнал, что мои тесты проходят, потому что они вызывают skin.getDisplayNode: при этом обновляется узел (== содержимое buttonCell). Так что действительно похоже на проблему в скине, afaics, отсутствует вызов updateDisplayNode, когда значение установлено на что-то не содержащееся...   -  person kleopatra    schedule 27.03.2019


Ответы (2)


Для меня это выглядит как ошибка: по какой-то причине displayNode (то есть содержимое buttonCell) не обновляется при установке не содержащегося значения, когда выбрано содержащееся значение. Простой доступ к displayNode через его общедоступный API на ComboBoxBaseSkin вызывает правильную настройку.

Чтобы увидеть его обновление, добавьте следующие строки в свой пример и нажмите эту кнопку после того, как несодержимое значение не отображается:

Button display = new Button("getDisplayNode");
display.setOnAction(e -> {
    ((ComboBoxBaseSkin) testCombo.getSkin()).getDisplayNode();
});

Чтобы решить эту проблему, мы можем расширить скин комбо и принудительно обновлять его на каждом проходе макета:

public static class MyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {

    public MyComboBoxSkin(ComboBox<T> control) {
        super(control);
    }

    @Override
    protected void layoutChildren(double x, double y, double w, double h) {
        super.layoutChildren(x, y, w, h);
        // must be wrapped inside a runlater, either before or after calling super
        Platform.runLater(this::getDisplayNode);
    }

}

Применение:

testCombo = new ComboBox<>(FXCollections.observableArrayList("Option 1", "Option 2", "Option 3")) {
    @Override
    protected Skin<?> createDefaultSkin() {
        return new MyComboBoxSkin<>(this);
    }
};

Примечание: реализация скина активно использует несколько логических грязных флагов, которые, кажется, взаимодействуют деструктивно в этом конкретном случае (к сожалению, не понимаю, как именно). Задержка доступа с помощью Platform.runlater, похоже, работает.


Обновить

после некоторых дальнейших раскопок это похоже на регрессию, введенную ленивый-грязный (не моя формулировка, хотя нравится :) исправить. реализация пользовательской ячейки, предоставленная Томом, прекрасно работает.

person kleopatra    schedule 27.03.2019
comment
Большое спасибо. Я нашел еще один возможный обходной путь. Смотрите мой собственный ответ. - person Tom; 28.03.2019

Отвечая на мой собственный вопрос. Я нашел еще один возможный обходной путь. Кажется, это работает, хотя я не уверен, что это «безопасно» (неужели ячейка кнопки никогда не должна быть пустой?).

Суть в следующем: используя настраиваемую ячейку-кнопку, переопределить updateItem(T item, boolean empty) и (в отличие от стандартных реализаций ячеек) ничего не делать (возврат) для empty = true, т.е. не стирать ячейку - № 3_.

Что позади? Похоже, проблема не в том, что текст ячейки кнопки не установлен должным образом, а в том, что он позже стирается какой-то «логикой пустой ячейки»...

Вы можете добавить этот код в мой образец:

testCombo.setButtonCell(new ListCell<>() {
    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {return;} // this is the solution: DO NOT ERASE ON empty=true!
        // further logic copied from the solution in the default skin: 
        // see ComboBoxListViewSkin.updateDisplayText
        // (default testing for "item instanceof Node" omitted for brevity)
        final StringConverter<String> c = testCombo.getConverter();
        final String promptText = testCombo.getPromptText();
        String s = item == null && promptText != null ? promptText
                : c == null ? (item == null ? null : item.toString()) : c.toString(item);
        setText(s);
        setGraphic(null);
    }
});
person Tom    schedule 27.03.2019