JavaFX TextField Auto-suggestions

后端 未结 5 718
我寻月下人不归
我寻月下人不归 2020-11-29 07:12

I want to make this TextField have suggestions feature just like in Lucene. I\'ve searched all the web and I just find it for ComboBox.

TextField instNameTxt         


        
相关标签:
5条回答
  • 2020-11-29 07:24

    There is another solution with JFoenix. Since February 2018 they added autocompletion class. This is implementation of it.

    // when initializing the window or in some other method
    void initialize() {
        JFXAutoCompletePopup<String> autoCompletePopup = new JFXAutoCompletePopup<>();
        autoCompletePopup.getSuggestions().addAll("option1", "option2", "...");
    
        autoCompletePopup.setSelectionHandler(event -> {
            textField.setText(event.getObject());
    
            // you can do other actions here when text completed
        });
    
        // filtering options
        textField.textProperty().addListener(observable -> {
            autoCompletePopup.filter(string -> string.toLowerCase().contains(textField.getText().toLowerCase()));
            if (autoCompletePopup.getFilteredSuggestions().isEmpty() || textField.getText().isEmpty()) {
                autoCompletePopup.hide();
                // if you remove textField.getText.isEmpty() when text field is empty it suggests all options
                // so you can choose
            } else {
                autoCompletePopup.show(textField);
            }
        });
    }
    

    This is a bit new approach and worked fine with me. Hope it will help and thanks to JFoenix developers.

    0 讨论(0)
  • 2020-11-29 07:36

    This example https://gist.github.com/floralvikings/10290131 only allowed Strings.

    I edited it to allow any object and just use that object's toString method. This allows you to use the object selected to do other things rather than just populate the TextField. Also fixed a bug where an exception would occur if you are deleting a String in the TextField that is not a part of the entries.

    Example usage:

    SortedSet<Address> entries = new TreeSet<>((Address o1, Address o2) -> o1.toString().compareTo(o2.toString()));
    
    entries.add(new Address(50, "Main Street", "Oakville", "Ontario", "T6P4K9"));
    entries.add(new Address(3, "Fuller Road", "Toronto", "Ontario", "B6S4T9"));
    
    AutoCompleteTextField<Address> text = new AutoCompleteTextField(entries);
    
    text.getEntryMenu().setOnAction(e ->
    {
        ((MenuItem) e.getTarget()).addEventHandler(Event.ANY, event ->
        {
             if (text.getLastSelectedObject() != null)
             {
                text.setText(text.getLastSelectedObject().toString());
                System.out.println(text.getLastSelectedObject().getProvince());
             }
        });
    });
    

    AutoCompleteTextField.java

    import javafx.beans.value.ObservableValue;
    import javafx.event.ActionEvent;
    import javafx.geometry.Side;
    import javafx.scene.control.ContextMenu;
    import javafx.scene.control.CustomMenuItem;
    import javafx.scene.control.TextField;
    
    import java.util.LinkedList;
    import java.util.List;
    import java.util.SortedSet;
    import java.util.TreeSet;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.text.Text;
    import javafx.scene.text.TextFlow;
    
    /**
     * This class is a TextField which implements an "autocomplete" functionality,
     * based on a supplied list of entries.<p>
     *
     * If the entered text matches a part of any of the supplied entries these are
     * going to be displayed in a popup. Further the matching part of the entry is
     * going to be displayed in a special style, defined by
     * {@link #textOccurenceStyle textOccurenceStyle}. The maximum number of
     * displayed entries in the popup is defined by
     * {@link #maxEntries maxEntries}.<br>
     * By default the pattern matching is not case-sensitive. This behaviour is
     * defined by the {@link #caseSensitive caseSensitive}
     * .<p>
     *
     * The AutoCompleteTextField also has a List of
     * {@link #filteredEntries filteredEntries} that is equal to the search results
     * if search results are not empty, or {@link #filteredEntries filteredEntries}
     * is equal to {@link #entries entries} otherwise. If
     * {@link #popupHidden popupHidden} is set to true no popup is going to be
     * shown. This list can be used to bind all entries to another node (a ListView
     * for example) in the following way:
     * <pre>
     * <code>
     * AutoCompleteTextField auto = new AutoCompleteTextField(entries);
     * auto.setPopupHidden(true);
     * SimpleListProperty filteredEntries = new SimpleListProperty(auto.getFilteredEntries());
     * listView.itemsProperty().bind(filteredEntries);
     * </code>
     * </pre>
     *
     * @author Caleb Brinkman
     * @author Fabian Ochmann
     * @param <S>
     */
    public class AutoCompleteTextField<S> extends TextField
    {
    
        private final ObjectProperty<S> lastSelectedItem = new SimpleObjectProperty<>();
    
        /**
         * The existing autocomplete entries.
         */
        private final SortedSet<S> entries;
    
        /**
         * The set of filtered entries:<br>
         * Equal to the search results if search results are not empty, equal to
         * {@link #entries entries} otherwise.
         */
        private ObservableList<S> filteredEntries
                = FXCollections.observableArrayList();
    
        /**
         * The popup used to select an entry.
         */
        private ContextMenu entriesPopup;
    
        /**
         * Indicates whether the search is case sensitive or not. <br>
         * Default: false
         */
        private boolean caseSensitive = false;
    
        /**
         * Indicates whether the Popup should be hidden or displayed. Use this if
         * you want to filter an existing list/set (for example values of a
         * {@link javafx.scene.control.ListView ListView}). Do this by binding
         * {@link #getFilteredEntries() getFilteredEntries()} to the list/set.
         */
        private boolean popupHidden = false;
    
        /**
         * The CSS style that should be applied on the parts in the popup that match
         * the entered text. <br>
         * Default: "-fx-font-weight: bold; -fx-fill: red;"
         * <p>
         * Note: This style is going to be applied on an
         * {@link javafx.scene.text.Text Text} instance. See the <i>JavaFX CSS
         * Reference Guide</i> for available CSS Propeties.
         */
        private String textOccurenceStyle = "-fx-font-weight: bold; "
                + "-fx-fill: red;";
    
        /**
         * The maximum Number of entries displayed in the popup.<br>
         * Default: 10
         */
        private int maxEntries = 10;
    
        /**
         * Construct a new AutoCompleteTextField.
         *
         * @param entrySet
         */
        public AutoCompleteTextField(SortedSet<S> entrySet)
        {
            super();
            this.entries = (entrySet == null ? new TreeSet<>() : entrySet);
            this.filteredEntries.addAll(entries);
    
            entriesPopup = new ContextMenu();
    
            textProperty().addListener((ObservableValue<? extends String> observableValue, String s, String s2) ->
            {
    
                if (getText() == null || getText().length() == 0)
                {
                    filteredEntries.clear();
                    filteredEntries.addAll(entries);
                    entriesPopup.hide();
                } else
                {
                    LinkedList<S> searchResult = new LinkedList<>();
                    //Check if the entered Text is part of some entry
                    String text1 = getText();
                    Pattern pattern;
                    if (isCaseSensitive())
                    {
                        pattern = Pattern.compile(".*" + text1 + ".*");
                    } else
                    {
                        pattern = Pattern.compile(".*" + text1 + ".*", Pattern.CASE_INSENSITIVE);
                    }
                    for (S entry : entries)
                    {
                        Matcher matcher = pattern.matcher(entry.toString());
                        if (matcher.matches())
                        {
                            searchResult.add(entry);
                        }
                    }
                    if (!entries.isEmpty())
                    {
                        filteredEntries.clear();
                        filteredEntries.addAll(searchResult);
                        //Only show popup if not in filter mode
                        if (!isPopupHidden())
                        {
                            populatePopup(searchResult, text1);
                            if (!entriesPopup.isShowing())
                            {
                                entriesPopup.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
                            }
                        }
                    } else
                    {
                        entriesPopup.hide();
                    }
                }
            });
    
            focusedProperty().addListener((ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean aBoolean2) ->
            {
                entriesPopup.hide();
            });
    
        }
    
        /**
         * Get the existing set of autocomplete entries.
         *
         * @return The existing autocomplete entries.
         */
        public SortedSet<S> getEntries()
        {
            return entries;
        }
    
        /**
         * Populate the entry set with the given search results. Display is limited
         * to 10 entries, for performance.
         *
         * @param searchResult The set of matching strings.
         */
        private void populatePopup(List<S> searchResult, String text)
        {
            List<CustomMenuItem> menuItems = new LinkedList<>();
            int count = Math.min(searchResult.size(), getMaxEntries());
            for (int i = 0; i < count; i++)
            {
                final String result = searchResult.get(i).toString();
                final S itemObject = searchResult.get(i);
                int occurence;
    
                if (isCaseSensitive())
                {
                    occurence = result.indexOf(text);
                } else
                {
                    occurence = result.toLowerCase().indexOf(text.toLowerCase());
                }
                if (occurence < 0)
                {
                    continue;
                }
                //Part before occurence (might be empty)
                Text pre = new Text(result.substring(0, occurence));
                //Part of (first) occurence
                Text in = new Text(result.substring(occurence, occurence + text.length()));
                in.setStyle(getTextOccurenceStyle());
                //Part after occurence
                Text post = new Text(result.substring(occurence + text.length(), result.length()));
    
                TextFlow entryFlow = new TextFlow(pre, in, post);
    
                CustomMenuItem item = new CustomMenuItem(entryFlow, true);
                item.setOnAction((ActionEvent actionEvent) ->
                {
                    lastSelectedItem.set(itemObject);
                    entriesPopup.hide();
                });
                menuItems.add(item);
            }
            entriesPopup.getItems().clear();
            entriesPopup.getItems().addAll(menuItems);
    
        }
    
        public S getLastSelectedObject()
        {
            return lastSelectedItem.get();
        }
    
        public ContextMenu getEntryMenu()
        {
            return entriesPopup;
        }
    
        public boolean isCaseSensitive()
        {
            return caseSensitive;
        }
    
        public String getTextOccurenceStyle()
        {
            return textOccurenceStyle;
        }
    
        public void setCaseSensitive(boolean caseSensitive)
        {
            this.caseSensitive = caseSensitive;
        }
    
        public void setTextOccurenceStyle(String textOccurenceStyle)
        {
            this.textOccurenceStyle = textOccurenceStyle;
        }
    
        public boolean isPopupHidden()
        {
            return popupHidden;
        }
    
        public void setPopupHidden(boolean popupHidden)
        {
            this.popupHidden = popupHidden;
        }
    
        public ObservableList<S> getFilteredEntries()
        {
            return filteredEntries;
        }
    
        public int getMaxEntries()
        {
            return maxEntries;
        }
    
        public void setMaxEntries(int maxEntries)
        {
            this.maxEntries = maxEntries;
        }
    
    }
    
    0 讨论(0)
  • 2020-11-29 07:38

    You can use ControlsFX --> maven

    Solution:

    TextFields.bindAutoCompletion(textfield,"text to suggest", "another text to suggest");
    
    0 讨论(0)
  • 2020-11-29 07:38

    Here is my solution - a complete method only with a ComboBox parameter:

     /**
     * My own autocomplete combobox
     *
     * @param categoryComboBox
     */
    public static void bindAutoCompleteToComboBox(ComboBox<String> categoryComboBox) {
    
        /**
         * backup the original list
         */
        List<String> categoryComboBoxItemsList = new ArrayList<String>(categoryComboBox.getItems());
    
        /**
         * if mouse pressed: select all of the text field
         */
        categoryComboBox.getEditor().setOnMousePressed(new EventHandler<MouseEvent>() {
    
            @Override
            public void handle(MouseEvent event) {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        if (categoryComboBox.getEditor().isFocused() && !categoryComboBox.getEditor().getText().isEmpty()) {
                            categoryComboBox.getEditor().selectAll();
                        }
                    }
                });
            }
        });
    
        /**
         * events on text input
         */
        categoryComboBox.setOnKeyReleased(new EventHandler<KeyEvent>() {
    
            private List<String> reducedList = new ArrayList<String>();
    
            @Override
            public void handle(KeyEvent event) {
    
                if (event.getCode().isLetterKey() || event.getCode().isDigitKey() || event.getCode().equals(KeyCode.BACK_SPACE)) {
    
                    /**
                     * Open comboBox if letter, number or backspace
                     */
                    categoryComboBox.show();
    
                    String temp = categoryComboBox.getEditor().getText();
                    reducedList = new ArrayList<String>();
    
                    /**
                     * If backspace pressed, selection refers to the basic list again
                     */
                    if (event.getCode().equals(KeyCode.BACK_SPACE)) {
                        categoryComboBox.getItems().clear();
                        categoryComboBox.getItems().addAll(categoryComboBoxItemsList);
    
                        // java fx workaround to restore the default list height of 10
                        categoryComboBox.hide();
                        categoryComboBox.setVisibleRowCount(10);
                        categoryComboBox.show();
                    }
    
                    /**
                     * loop through all entrys and look whether input contains this text.
                     *
                     * after that, entry will be added to the reduced list
                     */
                    for (String element : categoryComboBox.getItems()) {
                        if (StringUtils.containsIgnoreCase(element, temp)) {
                            reducedList.add(element);
                        }
                    }
    
                    /**
                     * all elements are cleared, the reduced list will be added. First element is selected
                     */
                    categoryComboBox.getItems().clear();
                    categoryComboBox.getItems().addAll(reducedList);
                    categoryComboBox.getSelectionModel().select(0);
                    categoryComboBox.getEditor().setText(temp);
    
                } else if (event.getCode().equals(KeyCode.ENTER)) {
    
                    /**
                     * if enter, the element which is selected will be applied to the text field and the dropdown will be closed
                     */
                    if (categoryComboBox.getSelectionModel().getSelectedIndex() != -1) {
                        categoryComboBox.getEditor().setText(categoryComboBox.getItems().get((categoryComboBox.getSelectionModel().getSelectedIndex())));
                    } else {
                        categoryComboBox.getEditor().setText(categoryComboBox.getItems().get(0));
                    }
    
                } else if (event.getCode().equals(KeyCode.DOWN)) {
    
                    /**
                     * arrow down shows the dropdown
                     */
                    categoryComboBox.show();
                }
    
                /**
                 * Tab marks everything (when tabbing into the field
                 */
                if (event.getCode().equals(KeyCode.TAB)) {
    
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            if (categoryComboBox.getEditor().isFocused() && !categoryComboBox.getEditor().getText().isEmpty()) {
                                categoryComboBox.getEditor().selectAll();
                            }
                        }
                    });
    
                } else {
                    /**
                     * all entries except for tab put the caret on the last character
                     */
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            categoryComboBox.getEditor().positionCaret(categoryComboBox.getEditor().getText().length());
                        }
                    });
                }
    
            }
        });
    
        /**
         * focus lost
         */
        categoryComboBox.focusedProperty().addListener(new ChangeListener<Boolean>() {
    
            /**
             * if focus lost: refill the category combo box with the original items
             */
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
    
                if (oldValue) {
    
                    /**
                     * saves whether textfield was empty before reset the comboBox
                     */
                    boolean emptyTextField = categoryComboBox.getEditor().getText().isEmpty();
    
                    if (categoryComboBox.getSelectionModel().getSelectedIndex() != -1) {
                        categoryComboBox.getEditor().setText(categoryComboBox.getItems().get(categoryComboBox.getSelectionModel().getSelectedIndex()));
                    }
    
                    String temp = categoryComboBox.getEditor().getText();
    
                    categoryComboBox.getItems().clear();
                    categoryComboBox.getItems().addAll(categoryComboBoxItemsList);
    
                    if (!emptyTextField) {
                        categoryComboBox.getSelectionModel().select(temp);
                    } else {
                        categoryComboBox.getEditor().setText("");
                    }
                }
            }
        });
    }
    
    0 讨论(0)
  • 2020-11-29 07:47

    Here is my solution based on This.

    public class AutocompletionlTextField extends TextFieldWithLengthLimit {
        //Local variables
        //entries to autocomplete
        private final SortedSet<String> entries;      
        //popup GUI
        private ContextMenu entriesPopup;
    
    
        public AutocompletionlTextField() {
            super();
            this.entries = new TreeSet<>();
            this.entriesPopup = new ContextMenu();
    
            setListner();
        }  
    
    
        /**
         * wrapper for default constructor with setting of "TextFieldWithLengthLimit" LengthLimit
         * 
         * @param lengthLimit 
         */
        public AutocompletionlTextField(int lengthLimit) {        
            this();
            super.setLengthLimit(lengthLimit);                
        }
    
    
        /**
         * "Suggestion" specific listners
         */
        private void setListner() {     
            //Add "suggestions" by changing text
            textProperty().addListener((observable, oldValue, newValue) -> {
                String enteredText = getText();
                //always hide suggestion if nothing has been entered (only "spacebars" are dissalowed in TextFieldWithLengthLimit)
                if (enteredText == null || enteredText.isEmpty()) {
                    entriesPopup.hide();
                } else {
                    //filter all possible suggestions depends on "Text", case insensitive
                    List<String> filteredEntries = entries.stream()
                            .filter(e -> e.toLowerCase().contains(enteredText.toLowerCase()))
                            .collect(Collectors.toList());
                    //some suggestions are found
                    if (!filteredEntries.isEmpty()) {
                        //build popup - list of "CustomMenuItem"
                        populatePopup(filteredEntries, enteredText);
                        if (!entriesPopup.isShowing()) { //optional
                            entriesPopup.show(AutocompletionlTextField.this, Side.BOTTOM, 0, 0); //position of popup
                        }
                    //no suggestions -> hide
                    } else {
                        entriesPopup.hide();
                    }
                }
            });
    
            //Hide always by focus-in (optional) and out
            focusedProperty().addListener((observableValue, oldValue, newValue) -> {
                entriesPopup.hide();
            });
        }             
    
    
        /**
        * Populate the entry set with the given search results. Display is limited to 10 entries, for performance.
        * 
        * @param searchResult The set of matching strings.
        */
        private void populatePopup(List<String> searchResult, String searchReauest) {
            //List of "suggestions"
            List<CustomMenuItem> menuItems = new LinkedList<>();
            //List size - 10 or founded suggestions count
            int maxEntries = 10;
            int count = Math.min(searchResult.size(), maxEntries);
            //Build list as set of labels
            for (int i = 0; i < count; i++) {
              final String result = searchResult.get(i);
              //label with graphic (text flow) to highlight founded subtext in suggestions
              Label entryLabel = new Label();
              entryLabel.setGraphic(Styles.buildTextFlow(result, searchReauest));  
              entryLabel.setPrefHeight(10);  //don't sure why it's changed with "graphic"
              CustomMenuItem item = new CustomMenuItem(entryLabel, true);
              menuItems.add(item);
    
              //if any suggestion is select set it into text and close popup
              item.setOnAction(actionEvent -> {
                  setText(result);
                  positionCaret(result.length());
                  entriesPopup.hide();
              });
            }
    
            //"Refresh" context menu
            entriesPopup.getItems().clear();
            entriesPopup.getItems().addAll(menuItems);
        }
    
    
        /**
        * Get the existing set of autocomplete entries.
        * 
        * @return The existing autocomplete entries.
        */
        public SortedSet<String> getEntries() { return entries; }
    }
    

    You must extends from "TextField" instead of "TextFieldWithLengthLimit" and delete constructor with "Length limit".

    I use static methods to work with Styles. It's used here to "highlight" entered text inside suggestion results. Here is the code of methos from this class:

    /**
     * Build TextFlow with selected text. Return "case" dependent.
     * 
     * @param text - string with text
     * @param filter - string to select in text
     * @return - TextFlow
     */
    public static TextFlow buildTextFlow(String text, String filter) {        
        int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
        Text textBefore = new Text(text.substring(0, filterIndex));
        Text textAfter = new Text(text.substring(filterIndex + filter.length()));
        Text textFilter = new Text(text.substring(filterIndex,  filterIndex + filter.length())); //instead of "filter" to keep all "case sensitive"
        textFilter.setFill(Color.ORANGE);
        textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));  
        return new TextFlow(textBefore, textFilter, textAfter);
    }    
    

    You may add this "AutocompletionlTextField" in FXML (dont forget about "imports") or inside constructor. To set "suggestions" list on use "entries" getter:

    AutocompletionlTextField field = new AutocompletionlTextField();
    field.getEntries().addAll(YOUR_ARRAY_OF_STRINGS);
    

    It seems like that in my application:

    Hope it helps.

    0 讨论(0)
提交回复
热议问题