JavaFX TextField Auto-suggestions

2019-01-18 08:02发布

问题:

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 instNameTxtFld = instNameTxtFld();

private TextField instNameTxtFld() {
    TextField txtFld = new TextField();
    txtFld.setPrefSize(600, 75);
    return txtFld;
}

The reason that I can't use the method for ComboBox is because I can't input the value to database below if I use ComboBox.

private void goNext() {

    if (nameTxtFld.getText() == null || nameTxtFld.getText().trim().isEmpty()
            || instNameTxtFld.getText()== null || instNameTxtFld.getText().trim().isEmpty()
            || addTxtArea.getText() == null || addTxtArea.getText().trim().isEmpty()) {
        alertDialog.showAndWait();
    } else {
        String satu = idNumTxtFld.getText();
        String dua = nameTxtFld.getText();
        String tiga = addTxtArea.getText();
        String empat = instNameTxtFld.getText();
        int delapan = idType.getSelectionModel().getSelectedIndex();
        String sembilan = timeStamp.getText();
        try {
            KonekDB.createConnection();
            Statement st = KonekDB.conn.createStatement();
            String sql = "INSERT INTO privateguest"
                    + "(idNumber, name, address, institution, idType, startTime) "
                    + "VALUES "
                    + "('" + satu + "','" + dua + "','" + tiga + "','" + empat + "','" + delapan + "','" + sembilan + "')";

            System.out.println(sql);
            st.executeUpdate(sql);

        } catch (SQLException ex) {

            System.out.println(satu + " " + dua + " " + tiga + " " + empat + " " + delapan + " " + sembilan);
            System.out.println("SQL Exception (next)");
            ex.printStackTrace();
        }
        Frame3Private frame3 = new Frame3Private(english);
        this.getScene().setRoot(frame3);
    }

}

Please help me to make the most simple code for doing TextField suggestions/ auto-complete.

回答1:

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.



回答2:

You can use ControlsFX --> maven

Solution:

TextFields.bindAutoCompletion(textfield,"text to suggest", "another text to suggest");


回答3:

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("");
                }
            }
        }
    });
}


回答4:

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.