combobox jump to typed char
Asked Answered
R

4

9

I stumbled on (in my eyes) a silly problem. However I don't find a solution for this (maybe because of not using the right search keywords, or by making it too difficult when it can be easy..) Scenario:

I have a combobox with 500 customers. I have to select a single costumer.

In Swing, when the list was down and you started typing, it automatically jumps to the typed letter. E.g.:

Items:

  • Adam
  • Dirk
  • Freddy
  • ...
  • Roger
  • Steven
  • Z person

When the combobox list is open, I just type 'R' and, in swing, it jumped to the first customer starting with 'R'. In javafx 2 it seems it does not have that behaviour... Is there some option that I have to enable or should I do something like using an editable combobox instead and make a filter() method that is fired on every keypress?

Edit: sollution based on Bhupendra's answer:

public class FilterComboBox<T> extends ComboBox<T> {
private final FilterComboBox<T> fcbo = this;

//private FilterComboBox fcbo = this;
private ObservableList<T> items;
private ObservableList<T> filter;
private String s;
private Object selection;

private class KeyHandler implements EventHandler< KeyEvent> {

    private SingleSelectionModel<T> sm;

    public KeyHandler() {
        sm = getSelectionModel();
        s = "";
    }

    @Override
    public void handle(KeyEvent event) {
        filter.clear();
        // handle non alphanumeric keys like backspace, delete etc
        if (event.getCode() == KeyCode.BACK_SPACE && s.length() > 0) {
            s = s.substring(0, s.length() - 1);
        } else {
            s += event.getText();
        }

        if (s.length() == 0) {
            fcbo.setItems(items);
            sm.selectFirst();
            return;
        }
        //System.out.println(s);
        if (event.getCode().isLetterKey()) {
            for (T item : items) {
                if (item.toString().toUpperCase().startsWith(s.toUpperCase())) {

                    filter.add(item);
                    //System.out.println(item);

                    fcbo.setItems(filter);

                    //sm.clearSelection();
                    //sm.select(item);

                }
            }
            sm.select(0);
        }

    }
}

public FilterComboBox(final ObservableList<T> items) {
    super(items);
    this.items = items;
    this.filter = FXCollections.observableArrayList();

    setOnKeyReleased(new KeyHandler());

    this.focusedProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            if (newValue == false) {
                s = "";
                fcbo.setItems(items);
                fcbo.getSelectionModel().select((T)selection);
            }

        }
    });

    this.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            if (newValue != null) {
                selection = (Object) newValue;
            }

        }
    });
}

}

Rawhide answered 13/11, 2012 at 14:35 Comment(2)
possible duplicate of how to make an autocomplete combobox in javafx 2.x using modern .fxml and controller.javaMolotov
Yeah I have been looking at the SearchBox but it seems, correct me if I'm wrong, you cannot see the whole list of items when clicking itRawhide
S
7

The simplest form of a filter combo box would be as the code below. But it would need more work to refine it. Also, if the list is huge, as in your case, there might be a performance issues as we are looping thru' the entire collection on each key press.

public class FilterComboBox extends ComboBox< String > {
    private ObservableList< String >    items;

    private class KeyHandler implements EventHandler< KeyEvent > {

        private SingleSelectionModel< String >  sm;
        private String                          s;

        public KeyHandler() {
            sm = getSelectionModel();
            s = "";
        }

        @Override
        public void handle( KeyEvent event ) {
            // handle non alphanumeric keys like backspace, delete etc
            if( event.getCode() == KeyCode.BACK_SPACE && s.length()>0)
                s = s.substring( 0, s.length() - 1 );
            else s += event.getText();

            if( s.length() == 0 ) {
                sm.selectFirst();
                return;
            }
            System.out.println( s );
            for( String item: items ) {
                if( item.startsWith( s ) ) sm.select( item );
            }
        }

    }

    public FilterComboBox( ObservableList< String > items ) {
        super( items );
        this.items = items;

        setOnKeyReleased( new KeyHandler() );
    }
}
Shrapnel answered 14/11, 2012 at 2:47 Comment(1)
Thanks for the code Bhupendra, I had to modify it thou to fit my needs. But it did the trick! I was afraid for performance issues but I have to say I don't notice any problems at all :)Rawhide
O
3

Wouldn't code like this be sufficient?

    comboBox.setOnKeyReleased(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {
            String s = jumpTo(event.getText(), comboBox.getValue(), comboBox.getItems());
            if (s != null) {
                comboBox.setValue(s);
            }
        }
    });

...

static String jumpTo(String keyPressed, String currentlySelected, List<String> items) {
    String key = keyPressed.toUpperCase();
    if (key.matches("^[A-Z]$")) {
        // Only act on letters so that navigating with cursor keys does not
        // try to jump somewhere.
        boolean letterFound = false;
        boolean foundCurrent = currentlySelected == null;
        for (String s : items) {
            if (s.toUpperCase().startsWith(key)) {
                letterFound = true;
                if (foundCurrent) {
                    return s;
                }
                foundCurrent = s.equals(currentlySelected);
            }
        }
        if (letterFound) {
            return jumpTo(keyPressed, null, items);
        }
    }
    return null;
}

This will jump to the first item when you press a letter. If you press that letter again, it jumps to the next item starting with that letter, wrapping back to the first if there are no more items starting with that letter.

Overmeasure answered 12/1, 2014 at 21:17 Comment(0)
R
2

Here is another option how to do it.
An example from the site https://tech.chitgoks.com was taken as the basis.

It features an elegant solution, which, if desired, can be used in the previous examples too.
When typing, the list automatically scrolls, it is very convenient.

    import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.ListView;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;

    import java.time.Duration;
    import java.time.Instant;
    import java.util.Collection;

    class SearchComboBox<T> extends ComboBox<T> {

        private static final int IDLE_INTERVAL_MILLIS = 1000;

        private Instant instant = Instant.now();
        private StringBuilder sb = new StringBuilder();

        public SearchComboBox(Collection<T> choices) {
            this(FXCollections.observableArrayList(choices));
        }

        public SearchComboBox(final ObservableList<T> items) {
            this();
            setItems(items);
            getSelectionModel().selectFirst();
        }

        public SearchComboBox() {
            super();

            this.addEventFilter(KeyEvent.KEY_RELEASED, event -> {
                if (event.getCode() == KeyCode.ESCAPE && sb.length() > 0) {
                    resetSearch();
                }
            });

            this.setOnKeyReleased(event -> {

                        if (Duration.between(instant, Instant.now()).toMillis() > IDLE_INTERVAL_MILLIS) {
                            resetSearch();
                        }

                        instant = Instant.now();

                        if (event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.UP || event.getCode() == KeyCode.TAB) {
                            return;
                        } else if (event.getCode() == KeyCode.BACK_SPACE && sb.length() > 0) {
                            sb.deleteCharAt(sb.length() - 1);
                        } else {
                            sb.append(event.getText().toLowerCase());
                        }

                        if (sb.length() == 0) {
                            return;
                        }

                        boolean found = false;
                        for (int i = 0; i < getItems().size(); i++) {
                            if (event.getCode() != KeyCode.BACK_SPACE && getItems().get(i).toString().toLowerCase().startsWith(sb.toString())) {
                                ListView listView = getListView();
                                listView.getSelectionModel().clearAndSelect(i);
                                scroll();
                                found = true;
                                break;
                            }
                        }

                        if (!found && sb.length() > 0)
                            sb.deleteCharAt(sb.length() - 1);
                    }
            );

            // add a focus listener such that if not in focus, reset the search process
            this.focusedProperty().addListener((observable, oldValue, newValue) -> {
                if (!newValue) {
                    resetSearch();
                } else {
                    scroll();
                }
            });
        }

        private void resetSearch() {
            sb.setLength(0);
            instant = Instant.now();
        }

        private void scroll() {
            ListView listView = getListView();
            int selectedIndex = listView.getSelectionModel().getSelectedIndex();
            listView.scrollTo(selectedIndex == 0 ? selectedIndex : selectedIndex - 1);
        }

        private ListView getListView() {
            return ((ComboBoxListViewSkin) this.getSkin()).getListView();
        }
    }

I improved this example in two ways.

  1. All code is encapsulated in a class - nothing needs to be connected from the outside.
  2. If the user doesn't show activity for some time, then the search string is reset. An alternative solution is how to reset the search: press the Backspace key, or make the ComboBox lose focus.
Romansh answered 15/11, 2019 at 11:34 Comment(0)
U
1

I could not really get Perneel's solution to suit my needs. Bhupendra's was nice but there was one detail : it selects the last matching item. If you have numbers from 0 to 20 (as String), it would return 19 instead of 1 if "1" is typed...

The code below adds the line requiered to solve this.

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

// TODO: Auto-generated Javadoc
/**
 * The Class FilterComboBox.
 */
public class FilterComboBox extends ComboBox< String > 
{

    /** The items. */
    private ObservableList< String >    items;

    /**
     * The Class KeyHandler.
     */
    private class KeyHandler implements EventHandler< KeyEvent > 
    {

        /** The sm. */
        private SingleSelectionModel< String >  sm;

        /** The s. */
        private String                          s;

        /**
         * Instantiates a new key handler.
         */
        public KeyHandler() 
        {
            sm = getSelectionModel();
            s = "";
        }

        /* (non-Javadoc)
         * @see javafx.event.EventHandler#handle(javafx.event.Event)
         */
        @Override
        public void handle( KeyEvent event ) 
        {
            // handle non alphanumeric keys like backspace, delete etc
            if( event.getCode() == KeyCode.BACK_SPACE && s.length()>0)
            {
                s = s.substring( 0, s.length() - 1 );
            }
            else if(event.getCode() != KeyCode.TAB )
            {
                s += event.getText();
            }

            if( s.length() == 0 ) 
            {
                sm.selectFirst();
                return;
            }
            System.out.println( s );
            for( String item: items ) 
            {
                if( item.startsWith( s ) ) 
                {
                    sm.select( item );
                    return;
                }
            }
        }

    }

    /**
     * Instantiates a new filter combo box.
     *
     * @param items the items
     */
    public FilterComboBox( ObservableList< String > items ) 
    {
        super( items );
        this.items = items;

        setOnKeyReleased( new KeyHandler() );
    }
}

This component is a ComboBox that only takes String as an input and which can be filtered by typing some character. All credit goes to Bhupendra, I only posted this code so as to prevent other people from having to think too much about this common problem. Last edit : added a test to prevent TAB from being considered as a character (allow to navigate in a form without breaking the component)

Upperclassman answered 11/12, 2013 at 15:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.