goldarm5
goldarm5

Reputation: 49

Changing Items of ComboBox without changing the ValueProperty

Edit: Im trying to build a Combobox with search function and this is what I came up with:

public class SearchableComboBox<T> extends ComboBox<T> {

private ObservableList<T> filteredItems;
private ObservableList<T> originalItems;
private T selectedItem;
private StringProperty filter = new SimpleStringProperty("");

public SearchableComboBox () {
    this.setTooltip(new Tooltip());
    this.setOnKeyPressed(this::handleOnKeyPressed);
    this.getTooltip().textProperty().bind(filter);

    this.showingProperty().addListener(new ChangeListener<Boolean>() {
        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
            // If user "closes" the ComboBox dropdown list: reset filter, reset list to the full original list, hide tooltip
            if (newValue == false) {
                filter.setValue("");;
                setItems(originalItems);
                getTooltip().hide();
            // If user opens the combobox dropdown list: get a copy of the items and show tooltip   
            } else {
                originalItems = getItems();
                Window stage = getScene().getWindow();
                getTooltip().show(stage);
            }
        }

    });

}

public void handleOnKeyPressed(KeyEvent e) {

    //Only execute if the dropdown list of the combobox is opened
    if (this.showingProperty().getValue() == true) {
        // Get key and add it to the filter string
        String c = e.getText();
        filter.setValue(filter.getValue() + c);
        //Filter out objects that dont contain the filter
        this.filteredItems = this.originalItems.filtered(a -> this.getConverter().toString(a).toLowerCase().contains(filter.getValue().toLowerCase()));
        //Set the items of the combox to the filtered list
        this.setItems(filteredItems);

    }

}

The idea is simple: As Long as the Dropdown list of the Combobox is opened I listen for keypresses and add the characters to a filter. With These filter the list of items of the Combobox is filtered to a list that only contains items, that contain the filter string. Then I use setItems to set the list of items to my filtered list. My Problem is, that the valueProperty of the Combobox changes, but I want the selected object to stay the same until the user selects another from the Dropdown list. I added a ChangeListener to the ValueProperty:

public void changed(ObservableValue<? extends PersonalModel> observable, PersonalModel oldValue,
                PersonalModel newValue) {
            System.out.println("Value changed");
            if (newValue == null) {
                System.out.println(newValue);
            } else {
                System.out.println(personalauswahl.getConverter().toString(newValue));
                labelArbeitszeitAnzeige.setText(String.valueOf(newValue.getArbeitszeit()));
            }
        }

    });

When the value changes the console Looks like this:

Value changed

Andersen, Wiebke (String representation of the object)

or like this:

Value changed

null (object is null)

There are basically 3 cases that are Happening. The first is I open the dropdownlist, dont select an item and type my filter. Then I select an item and my prints will Show me this:

Value changed

Andersen, Wiebke

Value changed

null

Value changed

Andersen, Wiebke

The second case is I open the Dropdown list and select an item. I now proceed to type in a filter and the selected item contains the filter. My prints will Show me this:

Value changed

null

Value changed

Andersen, Wiebke

Every time I press a key and again when I select Andersen, Wiebke again/Close the Dropdown list.

The third case is selecting an item and then proceed to type in a filter that the selected item doesnt contain. As soon as the selected Item doesnt contain the filter anymore the value of the valueProperty gets changed to null. If I select a new item then I get this:

Value changed

Budziszewski, Karin

Value changed

null

Value changed

Budziszewski, Karin

What I want is that the ValueProperty doesnt change until the user selects a new item from the Dropdown list. Also Id really like to know why exactly the valueproperty changes all the time for me. Espacially since I dont really think there is a fundamental difference between my solution and the one Zephyr provided. We both filter the original list with a filter string and then use setItems() to set the list of the Combobox to the newly filtered one. And as mentioned in a comment below I cant even use his solution´, because I cant get setEditable of the Combobox to work:

When I try personalauswahl.setEditable(true); I get Caused by: java.lang.NullPointerException at de.statistik_nord.klr.controller.EingabeController$1.toString(EingabeController.java:93) at de.statistik_nord.klr.controller.EingabeController$1.toString(EingabeController.java:1) which points to this line of code: return object.getName() +", " + object.getVorname();

Upvotes: 0

Views: 2337

Answers (2)

goldarm5
goldarm5

Reputation: 49

The best solution I found is slightly modified Version of this: JavaFX searchable combobox (like js select2)

The things I modified were to make the InputFilter class generic and that the Combobox loses Focus after closing the dropdown-list. Here the code:

import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.ComboBox;

public class InputFilter<T> implements ChangeListener<String> {

private ComboBox<T> box;
private FilteredList<T> items;
private boolean upperCase;
private int maxLength;
private String restriction;
private int count = 0;

/**
 * @param box
 *            The combo box to whose textProperty this listener is
 *            added.
 * @param items
 *            The {@link FilteredList} containing the items in the list.
 */
public InputFilter(ComboBox<T> box, FilteredList<T> items, boolean upperCase, int maxLength,
        String restriction) {
    this.box = box;
    this.items = items;
    this.upperCase = upperCase;
    this.maxLength = maxLength;
    this.restriction = restriction;
    this.box.setItems(items);
    this.box.showingProperty().addListener(new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
            if (newValue == false) {
                items.setPredicate(null);
                box.getParent().requestFocus();
            }

        }

    });
}

public InputFilter(ComboBox<T> box, FilteredList<T> items, boolean upperCase, int maxLength) {
    this(box, items, upperCase, maxLength, null);
}

public InputFilter(ComboBox<T> box, FilteredList<T> items, boolean upperCase) {
    this(box, items, upperCase, -1, null);
}

public InputFilter(ComboBox<T> box, FilteredList<T> items) {
    this(box, items, false);
}

@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
    StringProperty value = new SimpleStringProperty(newValue);
    this.count++;
    System.out.println(this.count);
    System.out.println(oldValue);
    System.out.println(newValue);
    // If any item is selected we save that reference.
    T selected = box.getSelectionModel().getSelectedItem() != null
            ? box.getSelectionModel().getSelectedItem() : null;

    String selectedString = null;
    // We save the String of the selected item.
    if (selected != null) {
        selectedString =  this.box.getConverter().toString(selected);
    }

    if (upperCase) {
        value.set(value.get().toUpperCase());
    }

    if (maxLength >= 0 && value.get().length() > maxLength) {
        value.set(oldValue);
    }

    if (restriction != null) {
        if (!value.get().matches(restriction + "*")) {
            value.set(oldValue);
        }
    }

    // If an item is selected and the value in the editor is the same
    // as the selected item we don't filter the list.
    if (selected != null && value.get().equals(selectedString)) {
        // This will place the caret at the end of the string when
        // something is selected.
        System.out.println(value.get());
        System.out.println(selectedString);
        Platform.runLater(() -> box.getEditor().end());
    } else {
        items.setPredicate(item -> {
            System.out.println("setPredicate");
            System.out.println(value.get());
            T itemString = item;
            if (this.box.getConverter().toString(itemString).toUpperCase().contains(value.get().toUpperCase())) {
                return true;
            } else {
                return false;
            }
        });
    }

    // If the popup isn't already showing we show it.
    if (!box.isShowing()) {
        // If the new value is empty we don't want to show the popup,
        // since
        // this will happen when the combo box gets manually reset.
        if (!newValue.isEmpty() && box.isFocused()) {
            box.show();
        }
    }
    // If it is showing and there's only one item in the popup, which is
    // an
    // exact match to the text, we hide the dropdown.
    else {
        if (items.size() == 1) {
            // We need to get the String differently depending on the
            // nature
            // of the object.
            T item = items.get(0);

            // To get the value we want to compare with the written
            // value, we need to crop the value according to the current
            // selectionCrop.
            T comparableItem = item;

            if (value.get().equals(comparableItem)) {
                Platform.runLater(() -> box.hide());
            }
        }
    }

    box.getEditor().setText(value.get());
}

}

The InputFilter is then added as changeListener for the textField ofthe Combobox:

comboBox.getEditor().textProperty().addListener(new InputFilter<YourCustomClass>(comboBox, new FilteredList<YourCustomClass>(comboBox.getItems())));

Currently Combobox.setEditable(true) has to be done manually outside, but I plan to move that into the InputFilter itself. Also you need to set a String converter for the Combobox. This solution is wrking grea for me so far, the only thing missing is Support for the space key while typing in the search key.

Upvotes: 1

Zephyr
Zephyr

Reputation: 10253

What I would suggest is to create a FilteredList from your original list. Then, use a Predicate to filter out the non-matching results. If you set your ComboBox items to that filtered list, it will always display either all items, or those matching your search terms.

The ValueProperty will only be updated when the user "commits" the change by pressing [enter].

I have a short MCVE application here to demonstrate with comments throughout:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    // Create a list of items
    private final ObservableList<String> items = FXCollections.observableArrayList();

    // Create the ComboBox
    private final ComboBox<String> comboBox = new ComboBox<>();

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

    @Override
    public void start(Stage primaryStage) {

        // Simple Interface
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));

        // Allow manual entry into ComboBox
        comboBox.setEditable(true);

        // Add sample items to our list
        items.addAll("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten");

        createListener();

        root.getChildren().add(comboBox);

        // Show the stage
        primaryStage.setScene(new Scene(root));
        primaryStage.setTitle("Filtered ComboBox");
        primaryStage.show();
    }

    private void createListener() {

        // Create the listener to filter the list as user enters search terms
        FilteredList<String> filteredList = new FilteredList<>(items);

        // Add listener to our ComboBox textfield to filter the list
        comboBox.getEditor().textProperty().addListener((observable, oldValue, newValue) ->
                filteredList.setPredicate(item -> {

                    // If the TextField is empty, return all items in the original list
                    if (newValue == null || newValue.isEmpty()) {
                        return true;
                    }

                    // Check if the search term is contained anywhere in our list
                    if (item.toLowerCase().contains(newValue.toLowerCase().trim())) {
                        return true;
                    }

                    // No matches found
                    return false;
                }));

        // Finally, let's add the filtered list to our ComboBox
        comboBox.setItems(filteredList);

    }
}

You will have a simple, editable ComboBox that filters out the values from the list that do not match.


With this method, you do not need to listen for each keypress, but can provide any filtering instructions within the Predicate itself, as shown above.


RESULTS:

screenshot

screenshot

EDIT:

There are some issues with the editable ComboBox that would need to be worked through, however, as selecting an item from the list throws an IndexOutOfBoundsException.

This could be mitigated by using a separate TextField for the filter instead, but keeping mostly the same code as above. Instead of adding the listener to the comboBox.getEditor(), just change that to textField. This will filter the list without a problem.

Here is a complete MCVE with that method:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    // Create a list of items
    private final ObservableList<String> items = FXCollections.observableArrayList();

    // Create the search field
    TextField textField = new TextField("Filter ...");

    // Create the ComboBox
    private final ComboBox<String> comboBox = new ComboBox<>();

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

    @Override
    public void start(Stage primaryStage) {

        // Simple Interface
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));

        // Add sample items to our list
        items.addAll("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten");

        createListener();

        root.getChildren().addAll(textField, comboBox);

        // Show the stage
        primaryStage.setScene(new Scene(root));
        primaryStage.setTitle("Filtered ComboBox");
        primaryStage.show();
    }

    private void createListener() {

        // Create the listener to filter the list as user enters search terms
        FilteredList<String> filteredList = new FilteredList<>(items);

        // Add listener to our ComboBox textfield to filter the list
        textField.textProperty().addListener((observable, oldValue, newValue) ->
                filteredList.setPredicate(item -> {

                    // If the TextField is empty, return all items in the original list
                    if (newValue == null || newValue.isEmpty()) {
                        return true;
                    }

                    // Check if the search term is contained anywhere in our list
                    if (item.toLowerCase().contains(newValue.toLowerCase().trim())) {
                        return true;
                    }

                    // No matches found
                    return false;
                }));

        // Finally, let's add the filtered list to our ComboBox
        comboBox.setItems(filteredList);

        // Allow the ComboBox to extend in size
        comboBox.setMaxWidth(Double.MAX_VALUE);

    }
}

screenshot

Upvotes: 2

Related Questions