Search code examples
javafxcomboboxnullpointerexception

ComboBox value is null if select same item with control key pressed


I have an issue with ComboBox when I select same item with control key pressed . This is what happens :

enter image description here

as you can see in the gif , combobox change values normally , but when control key is pressed , the selected item is null if the new value is the same as the previous one . I am using javafx 23 in openjdk 23.0.1 ( Amazon coretto ) on a linux ubuntu mate 24.04.

This is my minimal reproducible example

public class App extends Application {
public static void main(String[] args) {
    launch();
}

@Override
public void start(Stage stage) throws IOException {
    
    ComboBox<String> comboBox = new ComboBox<>();
    
    comboBox.getItems().setAll("one", "two");
    comboBox.getSelectionModel().selectFirst();
    Scene scene = new Scene(new StackPane(comboBox), 320, 240);
    String title = null;
    scene.setOnKeyPressed(
            keyEvent -> stage.setTitle(keyEvent.isControlDown() ? "Control down" : ""));
    scene.setOnKeyReleased(e -> stage.setTitle(""));
    stage.setTitle(title);
    stage.setScene(scene);
    stage.show();
}
}

Solution

  • I think this is intended behavior: to deselect the selection when ctrl key is pressed.

    You can modify this behavior in two ways.

    • by including an event filter on the cell (as mentioned by @Slaw)
    • by including a custom behavior

    Option 1 (event filter):

    Include a mouse pressed event filter in the custom ListCell constructor, to consume the event when the shortcut is pressed and if the cell is already selected. The only side effect is you lose any mouse pressed events on the cell.

    class MyListCell<T> extends ListCell<T> {
        public MyListCell() {
            addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
                if (e.isShortcutDown() && isSelected()) {
                    e.consume();
                }
            });
        }
    }
    

    Option 2 (custom behavior):

    This is more like specifically tweaking the select functionality rather than blocking all mouse pressed handlers of the cell. The general idea is to create a custom list cell/skin/behavior classes and tweak the doSelect method to ignore the shortcutDown parameter when processing the selection. The behavior code will be something like below:

    you may need to include the below VM argument

    --add-exports javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED

    class MyListCellBehavior<T> extends ListCellBehavior<T> {
    
        public MyListCellBehavior(ListCell<T> control) {
            super(control);
        }
    
        @Override
        protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
            // We always pass the shortcutDown as false to ignore the inputs.
            super.doSelect(x, y, button, clickCount, shiftDown, false);
        }
    }
    

    The full working demo with both approaches is below:

    import com.sun.javafx.scene.control.behavior.ListCellBehavior;
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.Skin;
    import javafx.scene.control.skin.ListCellSkin;
    import javafx.scene.input.MouseButton;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class CustomListCellBehaviour_Demo extends Application {
        public static void main(String[] args) {
            launch();
        }
    
        @Override
        public void start(Stage stage) {
            ComboBox<String> comboBox = new ComboBox<>();
            comboBox.getItems().setAll("one", "two");
            comboBox.getSelectionModel().selectFirst();
            comboBox.setCellFactory(param -> new MyListCell<>());
            Scene scene = new Scene(new StackPane(comboBox), 320, 240);
            String title = null;
            scene.setOnKeyPressed(
                    keyEvent -> stage.setTitle(keyEvent.isControlDown() ? "Control down" : ""));
            scene.setOnKeyReleased(e -> stage.setTitle(""));
            stage.setTitle(title);
            stage.setScene(scene);
            stage.show();
        }
    
        class MyListCell<T> extends ListCell<T> {
            public MyListCell() {
                // UNCOMMENT THE BELOW CODE FOR OPTION 1 APPROACH AND GET RID OF THE SKIN CLASSES
    //            addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
    //                if (e.isShortcutDown() && isSelected()) {
    //                    e.consume();
    //                }
    //            });
            }
    
            @Override
            protected Skin<?> createDefaultSkin() {
                return new MyListCellSkin<T>(this);
            }
    
            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
                if (!empty) {
                    setText(item.toString());
                } else {
                    setText(null);
                }
            }
        }
    
        class MyListCellSkin<T> extends ListCellSkin {
    
            private MyListCellBehavior<T> behavior;
    
            public MyListCellSkin(ListCell<T> control) {
                super(control);
                behavior = new MyListCellBehavior<>(control);
            }
    
            @Override
            public void dispose() {
                super.dispose();
                if (behavior != null) {
                    behavior.dispose();
                }
            }
        }
    
        class MyListCellBehavior<T> extends ListCellBehavior<T> {
    
            public MyListCellBehavior(ListCell<T> control) {
                super(control);
            }
    
            @Override
            protected void doSelect(double x, double y, MouseButton button, int clickCount, boolean shiftDown, boolean shortcutDown) {
                // We always pass the shortcutDown as false to ignore the inputs.
                super.doSelect(x, y, button, clickCount, shiftDown, false);
            }
        }
    }