This discussion is archived
7 Replies Latest reply: Dec 13, 2012 4:19 AM by 867657 RSS

Auto-filter items in an editable ComboBox

867657 Newbie
Currently Being Moderated
Hi everyone,

I have an editable ComboBox<MyItem>. When I type into the textfield of the combobox I want to filter the items displayed accordingly and then select one available item.

Has anyone done this?

I'm adding ChangeListeners to:

.selectionModelProperty().getValue().selectedItemProperty()
.editorProperty().getValue().textProperty()
.valueProperty()

without success.

Cheers,
N
  • 1. Re: Auto-filter items in an editable ComboBox
    James_D Guru
    Currently Being Moderated
    Adding a listener to the textProperty of the editor seemed to work. This might get you started:
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    import javafx.application.Application;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.FXCollections;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.ComboBox;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class FilterComboBox extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        final List<String> items = Arrays.asList(new String[] { "aardvark",
            "apple", "application", "banana", "orangutang", "orange" });
        final BorderPane root = new BorderPane();
        final ComboBox<String> comboBox = new ComboBox<>(
            FXCollections.observableArrayList(items));
        comboBox.setEditable(true);
        comboBox.getEditor().textProperty()
            .addListener(new ChangeListener<String>() {
              @Override
              public void changed(ObservableValue<? extends String> observable,
                  String oldValue, String newValue) {
                filterItems(newValue, comboBox, items);
              }
            });
        comboBox.setOnAction(new EventHandler<ActionEvent>() {
    
          @Override
          public void handle(ActionEvent event) {
            // Reset so all options are available:
            comboBox.setItems(FXCollections.observableArrayList(items));
          }
    
        });
    
        root.setTop(comboBox);
        Scene scene = new Scene(root, 200, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      private <T> void filterItems(String filter, ComboBox<T> comboBox,
          List<T> items) {
        List<T> filteredItems = new ArrayList<>();
        for (T item : items) {
          if (item.toString().toLowerCase().startsWith(filter.toLowerCase())) {
            filteredItems.add(item);
          }
        }
        comboBox.setItems(FXCollections.observableArrayList(filteredItems));
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }
    Edited by: James_D on Dec 5, 2012 9:42 AM (formatted code)
  • 2. Re: Auto-filter items in an editable ComboBox
    867657 Newbie
    Currently Being Moderated
    Thanx James,

    That is very similar to the code I had cooked up. The troubles start when you start traversing the shown list and selecting an item. After that both the filtering and text editing is just not working constantly and correctly. The event firing is not very logical (nulls and extra firing rounds) and I can't pinpoint the problem.

    The functionality I need is just like any other browser ADDRESS BAR functionality.

    I would have thought before hand that the ComboBox was made for stuff like this!

    N
  • 3. Re: Auto-filter items in an editable ComboBox
    James_D Guru
    Currently Being Moderated
    Just had the work Xmas party, so I might not be capable of anything meaningful.

    I think the problem is that we don't know the internal interactions between the combo box's text field and its popup list. Selecting something in the popup likely changes the text in the text field; this of course will fire any listeners registered on the text field's text property. We have no idea whether those are invoked before or after any "internal" listeners associated with that text field. (And of course the real problem is that this can be changed between versions.) It certainly looks like changing the list of items and then letting the selection model's select(...) methods be invoked is bad; the index of the selected item is determined before the list of items changes.

    It may be that the safest option is to roll your own component by defining a text field and a list view in a popup window. That may not actually be too difficult, not going to attempt it now but might take a shot later tomorrow.
  • 4. Re: Auto-filter items in an editable ComboBox
    867657 Newbie
    Currently Being Moderated
    Yes, maybe. I have a custom control based on code from Narayan that does this for me today. Its just not very nice, it's become quite of a hack that is difficult to debug and understand.

    I'm thinking about making some of our stuff open source, and I feel like I have to fix this before I do that. ComboBox should be a perfect candidate for a clean browser address bar filter implementation.

    I might end up just re-writing the custom control like you suggest though.

    Cheers,
    N
  • 5. Re: Auto-filter items in an editable ComboBox
    jsmith Guru
    Currently Being Moderated
    There is a filter control of sorts in the search box in Ensemble . . . it's not a combobox though I think . . . perhaps there's a reason for that . . . and the usability of it on a Mac in a browser is atrocious (keeps popping up the popup over the top of the edit field as you type in it and then when you do finally select an item from the popup nothing happens) . . . perhaps there is a reason for that too . . .

    Weirdly, if you download Ensemble from the Mac App Store, the filter control there doesn't have quite the same awful usability issues of the browser embedded version of Ensemble hosted on the JavaFX samples page.
  • 6. Re: Auto-filter items in an editable ComboBox
    James_D Guru
    Currently Being Moderated
    This is pretty workable, but not perfect. The trick I used here was to avoid filtering if the text in the editor is set due to a selection being made. It won't automatically change the selection (the label at the bottom monitors the current selection); when I tried to do that it seemed to destroy any reasonable notion of what's selected in the editor.

    The commented-out code is an attempt to revert to the full list on a selection being made. It seems to make this work we need to delay the processing until after the default handlers have been invoked; hence the Platform.runLater(...). This definitely falls into the category of "hack", and perhaps isn't particularly desirable from a usability standpoint anyway. Also, scrolling is a bit weird with this enabled; if you type and then use the arrow keys for selection, it won't scroll to the selected item on the first press of the arrow keys.

    A similar thing happens with changing the selected text in the editor: it only seems to work if you reschedule it using Platform.runLater(...). It isn't clear to me why, but not doing this messes up typing a bit.
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.FXCollections;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class FilterComboBox extends Application {
    
      @Override
      public void start(Stage primaryStage) {
    
        final List<String> items = Arrays.asList(new String[] { "Alabama",
            "Alaska", "Arizona", "Arkansas", "California", "Colorado",
            "Connecticut", "Delaware", "Georgia", "Florida", "Hawaii", "Idaho",
            "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana",
            "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota",
            "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada",
            "New Hampshire", "New Jersey", "New Mexico", "New York",
            "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon",
            "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota",
            "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington",
            "West Virginia", "Wisconsin", "Wyoming" });
        final BorderPane root = new BorderPane();
        final ComboBox<String> comboBox = new ComboBox<>(
            FXCollections.observableArrayList(items));
        comboBox.setEditable(true);
    
        comboBox.getEditor().textProperty()
            .addListener(new ChangeListener<String>() {
              @Override
              public void changed(ObservableValue<? extends String> observable,
                  String oldValue, String newValue) {
                final TextField editor = comboBox.getEditor();
                final String selected = comboBox.getSelectionModel()
                    .getSelectedItem();
                if (selected == null || !selected.equals(editor.getText())) {
                  filterItems(newValue, comboBox, items);
                  comboBox.show();
                  if (comboBox.getItems().size() == 1) {
                    final String onlyOption = comboBox.getItems().get(0);
                    final String current = editor.getText();
                    if (onlyOption.length() > current.length()) {
                      editor.setText(onlyOption);
                      // Not quite sure why this only works using
                      // Platform.runLater(...)
                      Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                          editor.selectRange(current.length(), onlyOption.length());
                        }
                      });
                    }
                  }
                }
              }
            });
    //    comboBox.setOnAction(new EventHandler<ActionEvent>() {
    //      @Override
    //      public void handle(ActionEvent event) {
    //        // Reset so all options are available:
    //        Platform.runLater(new Runnable() {
    //          @Override
    //          public void run() {
    //            String selected = comboBox.getSelectionModel().getSelectedItem();
    //            if (comboBox.getItems().size() < items.size()) {
    //              comboBox.setItems(FXCollections.observableArrayList(items));
    //              String newSelected = comboBox.getSelectionModel()
    //                  .getSelectedItem();
    //              if (newSelected == null || !newSelected.equals(selected)) {
    //                comboBox.getSelectionModel().select(selected);
    //              }
    //            }
    //          }
    //        });
    //      }
    //    });
    
        root.setTop(comboBox);
        Label label = new Label();
        label.textProperty().bind(
            comboBox.getSelectionModel().selectedItemProperty());
        root.setBottom(label);
        Scene scene = new Scene(root, 200, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      private <T> void filterItems(String filter, ComboBox<T> comboBox,
          List<T> items) {
        List<T> filteredItems = new ArrayList<>();
        for (T item : items) {
          if (item.toString().toLowerCase().startsWith(filter.toLowerCase())) {
            filteredItems.add(item);
          }
        }
        comboBox.setItems(FXCollections.observableArrayList(filteredItems));
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }
  • 7. Re: Auto-filter items in an editable ComboBox
    867657 Newbie
    Currently Being Moderated
    Thank you James,

    That is a "workable" solution :)

    I'm going to put this on a hold for a couple of months though, it looks as ComboBox is not production ready for this use-case.

    Thanks again.

    Cheers,
    Nuwanda

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points