-
Bug
-
Resolution: Unresolved
-
P3
-
9
-
9-ea-104
-
Cause Known
There are two variants, one happens on hiding the popup the other on moving the mouse over the arrow. Below is a minimal example to reproduce both:
Variant A:
- run the example as-is
- open popup
- hide popup and see the NPE
top of stacktrace:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.skin.ComboBoxPopupControl.lambda$createPopup$913(ComboBoxPopupControl.java:464)
at javafx.stage.PopupWindow.doAutoHide(PopupWindow.java:840)
Variant B:
- uncomment the line setting editable to true, compile and run
- move mouse over arrow and see NPE
Top of stacktrace:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.skin.ComboBoxBaseSkin.lambda$new$900(ComboBoxBaseSkin.java:108)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
The technical reason for both is the access to the behavior via getBehavior which is package-private (thus can't be overridden) and defaults to null.
A fix might be tricky: can't widen the method scope because Behavior is private api which must not leak into public api (I assume those are the rules?)
A working hack is twofold:
- create our own popup (c&p core, replacing getBehavior by our own) and reflectively replace super's field value
- make sure we set editability only after the skin is installed
The example:
public class ComboCustomSkinNPE_Report extends Application {
/**
* Minimal custom skin and behavior.
*/
private static class CustomComboSkin<T> extends ComboBoxPopupControl<T> {
private Pane content;
private Label display;
private ComboBoxBaseBehavior<T> behavior;
public CustomComboSkin(ComboBoxBase<T> control) {
super(control);
// most basic behavior
behavior = new CustomComboBehavior<>(control);
getPopupContent().setManaged(false);
getChildren().add(getPopupContent());
getChildren().add(getDisplayNode());
}
//---------- implement abstract methods
@Override
protected Node getPopupContent() {
if (content == null) {
content = new VBox(10);
for (int i = 0; i <5; i++) {
content.getChildren().add(new Label("dummy-item " + i));
}
}
return content;
}
@Override
protected TextField getEditor() {
return null;
}
@Override
protected StringConverter<T> getConverter() {
return null;
}
@Override
public Node getDisplayNode() {
if (display == null) {
display = new Label("nothing real");
}
return display;
}
}
private static class CustomComboBehavior<T> extends ComboBoxBaseBehavior<T> {
public CustomComboBehavior(ComboBoxBase<T> comboBox) {
super(comboBox);
}
}
private Parent getContent() {
ComboBox customFromStart = new ComboBox(FXCollections.observableArrayList("one", "two", "three")) {
@Override
protected Skin createDefaultSkin() {
return new CustomComboSkin<>(this);
}
};
// if editable _before_ installing skin, throws NPE on moving mouse over arrow
// customFromStart.setEditable(true);
Pane coreLane = new HBox(10, new Label("combo core with custom skin"), customFromStart);
return new VBox(10, coreLane);
}
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Variant A:
- run the example as-is
- open popup
- hide popup and see the NPE
top of stacktrace:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.skin.ComboBoxPopupControl.lambda$createPopup$913(ComboBoxPopupControl.java:464)
at javafx.stage.PopupWindow.doAutoHide(PopupWindow.java:840)
Variant B:
- uncomment the line setting editable to true, compile and run
- move mouse over arrow and see NPE
Top of stacktrace:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.skin.ComboBoxBaseSkin.lambda$new$900(ComboBoxBaseSkin.java:108)
at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
The technical reason for both is the access to the behavior via getBehavior which is package-private (thus can't be overridden) and defaults to null.
A fix might be tricky: can't widen the method scope because Behavior is private api which must not leak into public api (I assume those are the rules?)
A working hack is twofold:
- create our own popup (c&p core, replacing getBehavior by our own) and reflectively replace super's field value
- make sure we set editability only after the skin is installed
The example:
public class ComboCustomSkinNPE_Report extends Application {
/**
* Minimal custom skin and behavior.
*/
private static class CustomComboSkin<T> extends ComboBoxPopupControl<T> {
private Pane content;
private Label display;
private ComboBoxBaseBehavior<T> behavior;
public CustomComboSkin(ComboBoxBase<T> control) {
super(control);
// most basic behavior
behavior = new CustomComboBehavior<>(control);
getPopupContent().setManaged(false);
getChildren().add(getPopupContent());
getChildren().add(getDisplayNode());
}
//---------- implement abstract methods
@Override
protected Node getPopupContent() {
if (content == null) {
content = new VBox(10);
for (int i = 0; i <5; i++) {
content.getChildren().add(new Label("dummy-item " + i));
}
}
return content;
}
@Override
protected TextField getEditor() {
return null;
}
@Override
protected StringConverter<T> getConverter() {
return null;
}
@Override
public Node getDisplayNode() {
if (display == null) {
display = new Label("nothing real");
}
return display;
}
}
private static class CustomComboBehavior<T> extends ComboBoxBaseBehavior<T> {
public CustomComboBehavior(ComboBoxBase<T> comboBox) {
super(comboBox);
}
}
private Parent getContent() {
ComboBox customFromStart = new ComboBox(FXCollections.observableArrayList("one", "two", "three")) {
@Override
protected Skin createDefaultSkin() {
return new CustomComboSkin<>(this);
}
};
// if editable _before_ installing skin, throws NPE on moving mouse over arrow
// customFromStart.setEditable(true);
Pane coreLane = new HBox(10, new Label("combo core with custom skin"), customFromStart);
return new VBox(10, coreLane);
}
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}