0

I have looked days on any ready solution for the subject of having TOGETHER in javafx (pure) :

  • Combobox
  • Multiselect of items through Checkboxes
  • Filter items by the "editable" part of the Combobox

I have had no luck finding what I was looking for so I have now a working solution taken from different separate solution... Thank you to all for this !

Now I would like to know if what I have done follows the best practices or not... It's working... but is it "ugly" solution ? Or would that be a sort of base anyone could use ?

I tied to comment as much as I could, and also kept the basic comment of the sources :

Thank you for your opinions, and suggestions...

Here is the working example :

package application;

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;

@SuppressWarnings ("restriction") // Only applies for PROTECTD library : com.sun.javafx.scene.control.skin.ComboBoxListViewSkin
public class MultiSelectFiltered2 extends Application {
    // These 2 next fields are used in order to keep the FILTERED TEXT entered by user.
    private String  aFilterText         = "";
    private boolean isUserChangeText    = true;
    
    public void start(Stage stage) {
        Text    txt     = new Text(); // A place where to expose the result of checked items.
        HBox    vbxRoot = new HBox(); // A basic root to order the GUI
        
        ComboBox<ChbxItems> cb = new ComboBox<ChbxItems>() {
            // This part is needed in order to NOT have the list hided when an item is selected... 
            // TODO --> Seems a little ugly to me since this part is the PROTECTED part !
            protected javafx.scene.control.Skin<?> createDefaultSkin() {
                return new ComboBoxListViewSkin<ChbxItems>(this) {
                    @Override
                    protected boolean isHideOnClickEnabled() {
                        return false;
                    }
                };
            }
        };
        cb.setEditable(true);
        
        // Create a list with some dummy values.
        ObservableList<ChbxItems> items = FXCollections.observableArrayList();
        items.add(new ChbxItems("One"));
        items.add(new ChbxItems("Two"));
        items.add(new ChbxItems("Three"));
        items.add(new ChbxItems("Four"));
        items.add(new ChbxItems("Five"));
        items.add(new ChbxItems("Six"));
        items.add(new ChbxItems("Seven"));
        items.add(new ChbxItems("Eight"));
        items.add(new ChbxItems("Nine"));
        items.add(new ChbxItems("Ten"));
        
        // Create a FilteredList wrapping the ObservableList.
        FilteredList<ChbxItems> filteredItems = new FilteredList<ChbxItems>(items, p -> true);
        
        // Add a listener to the textProperty of the combo box editor. The
        // listener will simply filter the list every time the input is changed
        // as long as the user hasn't selected an item in the list.
        cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
            // This needs to run on the GUI thread to avoid the error described here:
            // https://bugs.openjdk.java.net/browse/JDK-8081700.
            Platform.runLater(() -> {
                if (isUserChangeText) {
                    aFilterText = cb.getEditor().getText();
                }
                // If the no item in the list is selected or the selected item
                // isn't equal to the current input, we re-filter the list.
                filteredItems.setPredicate(item -> {
                    boolean isPartOfFilter = true;
                    
                    // We return true for any items that starts with the
                    // same letters as the input. We use toUpperCase to
                    // avoid case sensitivity.
                    if (!item.getText().toUpperCase().startsWith(newValue.toUpperCase())) {
                        isPartOfFilter = false;
                    }
                    return isPartOfFilter;
                });
                isUserChangeText = true;
            });
        });
        
        cb.setCellFactory(new Callback<ListView<ChbxItems>, ListCell<ChbxItems>>() {
            @Override
            public ListCell<ChbxItems> call(ListView<ChbxItems> param) {
                return new ListCell<ChbxItems>() {
                    private CheckBox chbx = new CheckBox();
                    // This 'just open bracket' opens the newly CheckBox Class specifics
                    {
                        chbx.setOnAction(new EventHandler<ActionEvent>() {
                            // This VERY IMPORTANT part will effectively set the ChbxItems item
                            // The argument is never used, thus left as 'arg0'
                            @Override
                            public void handle(ActionEvent arg0) {
                                // This is where the usual update of the check box refreshes the editor' text of the parent combo box... we want to avoid this ;-)
                                isUserChangeText = false;
                                // The one line without which your check boxes are going to be checked depending on the position in the list... which changes when the list gets filtered.
                                getListView().getSelectionModel().select(getItem());
                                // Updating the exposed text from the list of checked items... This is added here to have a 'live' update.
                                txt.setText(updateListOfValuesChosen(items));
                            }
                        });
                    }
                    
                    private BooleanProperty booleanProperty; //Will be used for binding... explained bellow.
                    
                    @Override
                    protected void updateItem(ChbxItems item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!empty) {
                            // Binding is used in order to link the checking (selecting) of the item, with the actual 'isSelected' field of the ChbxItems object. 
                            if (booleanProperty != null) {
                                chbx.selectedProperty().unbindBidirectional(booleanProperty);
                            }
                            booleanProperty = item.isSelectedProperty();
                            chbx.selectedProperty().bindBidirectional(booleanProperty);
                            // This is the usual part for the look of the cell
                            setGraphic(chbx);
                            setText(item.getText() + "");
                        } else {
                            // Look of the cell, which has to be "reseted" if no item is attached (empty is true).
                            setGraphic(null);
                            setText("");
                        }
                        // Setting the 'editable' part of the combo box to what the USER wanted
                        // --> When 'onAction' of the check box, the 'behind the scene' update will refresh the combo box editor with the selected object reference otherwise.
                        cb.getEditor().setText(aFilterText);
                        cb.getEditor().positionCaret(aFilterText.length());
                    }
                };
            }
        });
        
        // Yes, it's the filtered items we want to show in the combo box...
        // ...but we want to run through the original items to find out if they are checked or not.
        cb.setItems(filteredItems);
        
        // Some basic cosmetics
        vbxRoot.setSpacing(15);
        vbxRoot.setPadding(new Insets(25));
        vbxRoot.setAlignment(Pos.TOP_LEFT);
        
        // Adding the visual children to root VBOX
        vbxRoot.getChildren().addAll(txt, cb);

        // Ordinary Scene & Stage settings and initialization
        Scene scene = new Scene(vbxRoot);
        stage.setScene(scene);
        stage.show();
    }
    
    // Just a method to expose the list of items checked...
    // This is the result that will be probably the input for following code.
    // -->
    // If the class ChbxItems had a custom object rather than 'text' field,
    // the resulting checked items from here could be a list of these custom objects --> VERY USEFUL
    private String updateListOfValuesChosen(ObservableList<ChbxItems> items) {
        StringBuilder sb = new StringBuilder();
        items.stream().filter(ChbxItems::getIsSelected).forEach(cbitem -> {
            sb.append(cbitem.getText()).append("\n");
        });
        return sb.toString();       
    }
    
    // The CHECKBOX object, with 2 fields :
    // - The boolean part (checked ot not)
    // - The text part which is shown --> Could be a custom object with 'toString()' overridden ;-)
    class ChbxItems {
        private SimpleStringProperty    text        = new SimpleStringProperty();
        private BooleanProperty         isSelected  = new SimpleBooleanProperty();
        
        public ChbxItems(String sText) {
            setText(sText);
        }
        
        public void setText(String text) {
            this.text.set(text);
        }
        
        public String getText() {
            return text.get();
        }
        
        public SimpleStringProperty textProperty() {
            return text;
        }
        
        public void setIsSelected(boolean isSelected) {
            this.isSelected.set(isSelected);
        }
        
        public boolean getIsSelected() {
            return isSelected.get();
        }
        
        public BooleanProperty isSelectedProperty() {
            return isSelected;
        }
    }
    
    public static void main(String[] args) {
        launch();
    }
}
Daric
  • 83
  • 9
  • just a quick question (didn't read thoroughly ;) - assuming you are aware of controlsfx, why don't you use their check-XX controls? – kleopatra Dec 13 '20 at 13:48
  • Hello, thank you for the question ! Controlsfx is a black box (sort of, I know source is available), and is missing the FILTERING part... So in this way I don't depend on any other library, and am learning how to do it in the same time... thus being able to adapt it later on to include it in my TreeTableView (which is the aim). – Daric Dec 14 '20 at 14:14
  • thanks for providing context :) – kleopatra Dec 14 '20 at 14:28

0 Answers0