Search code examples
javalistviewjavafxcomboboxfxml

Add a button at the end of a ComboBox list view


I need to create a combo box which can have a button at the end of its list view. This list can have items added or removed and displays a scroll bar when the number of items is over 5. Also, this list view does not close itself when an item is selected.

How to add a button "New item" like in the following screen shots ?

enter image description here

Here is the source code:

helloApplication.java

package com.example.demo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

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

HelloController.java

package com.example.demo;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.skin.ComboBoxListViewSkin;

import java.net.URL;
import java.util.ResourceBundle;

public class HelloController implements Initializable {

    @FXML
    private ComboBox<String> frequencyPlanComboBox;

    @Override
    public void initialize(URL pURL, ResourceBundle pResourceBundle) {
        frequencyPlanComboBox.getItems()
                             .addAll("aaa", "bbb", "ccc");
        ComboBoxListViewSkin<String> comboBoxListViewSkin = new ComboBoxListViewSkin<String>(frequencyPlanComboBox);
        comboBoxListViewSkin.setHideOnClick(false);
        frequencyPlanComboBox.setSkin(comboBoxListViewSkin);
    }
}

hello-view.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<VBox alignment="CENTER" spacing="20.0" xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
    </padding>

    <Label fx:id="welcomeText" />
   <ComboBox fx:id="frequencyPlanComboBox" prefWidth="150.0" visibleRowCount="5" />
</VBox>

Solution

  • Edited version:

    As pointed out in the comments, the mock-up in the OP has the button outside the list view, so that it is visible without scrolling to the bottom of the list view. You can accomplish this by creating a custom skin which overrides the getPopupContent() method:

    import javafx.fxml.FXML;
    import javafx.fxml.Initializable;
    import javafx.geometry.Bounds;
    import javafx.geometry.Pos;
    import javafx.scene.Node;
    import javafx.scene.control.Button;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.skin.ComboBoxListViewSkin;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.VBox;
    
    import java.net.URL;
    import java.util.ResourceBundle;
    
    public class HelloController implements Initializable {
    
        @FXML
        private ComboBox<String> frequencyPlanComboBox;
    
        @Override
        public void initialize(URL pURL, ResourceBundle pResourceBundle) {
    
            for (int i = 1 ; i <=20 ;i++) {
                frequencyPlanComboBox.getItems().add("Item "+i);
            }
    
            ComboBoxListViewSkin<String> comboBoxListViewSkin = new ComboBoxListViewSkin<String>(frequencyPlanComboBox) {
                private Button button = new Button("Add New");
    
                private VBox pane = new VBox();
                {
                    pane.setStyle("-fx-background-color: -fx-control-inner-background;");
                }
    
                @Override
                public Node getPopupContent() {
                    Node defaultContent = super.getPopupContent();
                    defaultContent.setManaged(true);
                    pane.getChildren().setAll(defaultContent, button);
                    return pane ;
                }
            };
            comboBoxListViewSkin.setHideOnClick(false);
            frequencyPlanComboBox.setSkin(comboBoxListViewSkin);
    
        }
    }
    

    Here is the original solution, which is probably less desirable:

    Add a null value to the end of the combo box items list, and use a custom list cell which displays the button for that value.

    import javafx.fxml.FXML;
    import javafx.fxml.Initializable;
    import javafx.scene.control.Button;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.skin.ComboBoxListViewSkin;
    
    import java.net.URL;
    import java.util.ResourceBundle;
    
    public class HelloController implements Initializable {
    
        @FXML
        private ComboBox<String> frequencyPlanComboBox;
    
        @Override
        public void initialize(URL pURL, ResourceBundle pResourceBundle) {
            frequencyPlanComboBox.getItems()
                    .addAll("aaa", "bbb", "ccc", null);
            ComboBoxListViewSkin<String> comboBoxListViewSkin = new ComboBoxListViewSkin<String>(frequencyPlanComboBox);
            comboBoxListViewSkin.setHideOnClick(false);
            frequencyPlanComboBox.setSkin(comboBoxListViewSkin);
    
            frequencyPlanComboBox.setCellFactory(lv -> new ListCell<>() {
                private Button button = new Button("Add New...");
                {
                    button.setOnAction(e -> {
                        /*
                        Do something
                         */
                        System.out.println("Button pressed");
                    });
                }
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
                    if (empty) {
                        setText("") ;
                        setGraphic(null);
                    } else if (item == null) {
                        setText("");
                        setGraphic(button);
                    } else {
                        setText(item);
                        setGraphic(null);
                    }
                }
            });
        }
    }
    

    You probably want to experiment a bit with styling that cell, so it doesn't appear selected.