JavaFx:自动完成多选文本字段

时间:2019-06-11 09:44:36

标签: java javafx autocomplete javafx-8 textfield

我必须使用具有自动完成功能和多项选择功能的组件,我将附上一张图片以显示我的意思:

enter image description here

我知道基本JavaFx不支持它,但也许您知道我在哪里可以找到如何做的建议。

如果有任何具有此功能的第三方库,请提供链接,或者如果没有,则请提供任何建议 / 想法来帮助我实现它。

自动完成部分已经实现并在此处回答:JavaFX TextField Auto-suggestions,因此请不要提出建议。我对多选部分感兴趣,因此在找到一个要在文本字段中显示的元素之后,我可以寻找其他项目。

2 个答案:

答案 0 :(得分:1)

这是结合了自动完成和标记栏属性的解决方案。

public class AutocompleteMultiSelectionBox extends HBox {

    private final ObservableList<String> tags;
    private final ObservableSet<String> suggestions;
    private ContextMenu entriesPopup;
    private static final int MAX_ENTRIES = 10;

    private final TextField inputTextField;

    public AutocompleteMultiSelectionBox() {
        getStyleClass().setAll("tag-bar");
        getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        tags = FXCollections.observableArrayList();
        suggestions = FXCollections.observableSet();
        inputTextField = new TextField();
        this.entriesPopup = new ContextMenu();
        setListner();
        inputTextField.setOnKeyPressed(event -> {
            // Remove last element with backspace
            if (event.getCode().equals(KeyCode.BACK_SPACE) && !tags.isEmpty() && inputTextField.getText().isEmpty()) {
                String last = tags.get(tags.size() - 1);
                suggestions.add(last);
                tags.remove(last);
            }
        });

        inputTextField.prefHeightProperty().bind(this.heightProperty());
        HBox.setHgrow(inputTextField, Priority.ALWAYS);
        inputTextField.setBackground(null);

        tags.addListener((ListChangeListener.Change<? extends String> change) -> {
            while (change.next()) {
                if (change.wasPermutated()) {
                    ArrayList<Node> newSublist = new ArrayList<>(change.getTo() - change.getFrom());
                    for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
                        newSublist.add(null);
                    }
                    for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
                        newSublist.set(change.getPermutation(i), getChildren().get(i));
                    }
                    getChildren().subList(change.getFrom(), change.getTo()).clear();
                    getChildren().addAll(change.getFrom(), newSublist);
                } else {
                    if (change.wasRemoved()) {
                        getChildren().subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
                    }
                    if (change.wasAdded()) {
                        getChildren().addAll(change.getFrom(), change.getAddedSubList().stream().map(Tag::new).collect(
                                Collectors.toList()));
                    }
                }
            }
        });
        getChildren().add(inputTextField);
    }

    /**
     * Build TextFlow with selected text. Return "case" dependent.
     *
     * @param text   - string with text
     * @param filter - string to select in text
     * @return - TextFlow
     */
    private 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);
    }

    /**
     * "Suggestion" specific listners
     */
    private void setListner() {
        //Add "suggestions" by changing text
        inputTextField.textProperty().addListener((observable, oldValue, newValue) -> {
            //always hide suggestion if nothing has been entered (only "spacebars" are dissalowed in TextFieldWithLengthLimit)
            if (newValue.isEmpty()) {
                entriesPopup.hide();
            } else {
                //filter all possible suggestions depends on "Text", case insensitive
                List<String> filteredEntries = suggestions.stream()
                        .filter(e -> e.toLowerCase().contains(newValue.toLowerCase()))
                        .collect(Collectors.toList());
                //some suggestions are found
                if (!filteredEntries.isEmpty()) {
                    //build popup - list of "CustomMenuItem"
                    populatePopup(filteredEntries, newValue);
                    if (!entriesPopup.isShowing()) { //optional
                        entriesPopup.show(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 searchRequest) {
        //List of "suggestions"
        List<CustomMenuItem> menuItems = new LinkedList<>();
        //Build list as set of labels
        searchResult.stream()
                .limit(MAX_ENTRIES) // Limit to MAX_ENTRIES in the suggestions
                .forEach(result -> {
                    //label with graphic (text flow) to highlight founded subtext in suggestions
                    TextFlow textFlow = buildTextFlow(result, searchRequest);
                    textFlow.prefWidthProperty().bind(AutocompleteMultiSelectionBox.this.widthProperty());
                    CustomMenuItem item = new CustomMenuItem(textFlow, true);
                    menuItems.add(item);

                    //if any suggestion is select set it into text and close popup
                    item.setOnAction(actionEvent -> {
                        tags.add(result);
                        suggestions.remove(result);
                        inputTextField.clear();
                        entriesPopup.hide();
                    });
                });

        //"Refresh" context menu
        entriesPopup.getItems().clear();
        entriesPopup.getItems().addAll(menuItems);
    }

    public final ObservableList<String> getTags() {
        return tags;
    }

    public final ObservableSet<String> getSuggestions() {
        return suggestions;
    }

    /**
     * Clears then repopulates the entries with the new set of data.
     *
     * @param suggestions set of items.
     */

    public final void setSuggestions(ObservableSet<String> suggestions) {
        this.suggestions.clear();
        this.suggestions.addAll(suggestions);
    }

    private class Tag extends HBox {
        Tag(String tag) {
            // Style
            getStyleClass().add("tag");

            // Remove item button
            Button removeButton = new Button("x");
            removeButton.setBackground(null);
            removeButton.setOnAction(event -> {
                tags.remove(tag);
                suggestions.add(tag);
                inputTextField.requestFocus();
            });

            // Displayed text
            Text text = new Text(tag);
            text.setFill(Color.WHITE);
            text.setFont(Font.font(text.getFont().getFamily(), FontWeight.BOLD, text.getFont().getSize()));

            // Children position
            setAlignment(Pos.CENTER);
            setSpacing(5);
            setPadding(new Insets(0, 0, 0, 5));

            getChildren().addAll(text, removeButton);
        }
    }

}

.css

.tag-bar {
    -fx-border-color: lightblue;
    -fx-spacing: 3;
    -fx-padding: 3;
    -fx-max-height: 30;
}

.tag-bar .tag {
    -fx-background-color: -fx-selection-bar;
    -fx-border-radius: 5 5 5 5;
}

.tag-bar .tag {
    -fx-text-fill: white;
}

.tag-bar .tag .button{
    -fx-text-fill: orange;
    -fx-font-weight: bold;
}

答案 1 :(得分:0)

添加到@Sunflame的答案中(对Kotlin代码表示抱歉)

如果要添加新项目,请在构造函数上创建弹出窗口后添加此内容

    inputTextField.onKeyTyped = EventHandler { event ->
        if ("\r" == event.character && inputTextField.text.isNotEmpty()) {
            val newTag = inputTextField.text
            suggestions.add(newTag)
            tags.add(newTag)
            inputTextField.text = ""
        }
    }

感谢您的辛勤工作,这只是添加的次要功能