0

So I am writing an AutosuggestMenu that adds a listener to a TextField and recommends suggestions in a popup ContextMenu, based on comparing the keystrokes entered with the Collection of words provided.
Unfortunately, changes to the ContextMenu elements are not displayed, and I suspect this is because I am modifying the elements of the ObservableList associated with the ContextMenu and not the list itself.

Browsing stack has led to believe I should implement an extractor, but based on the examples provided I have no idea how to do this for my specific problem. Any solution would be very much appreciated!

Source:

package com.sknb.gui;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;


public class AutosuggestMenu {
    public final static int DEFAULT_MENU_SIZE = 10;

    //Data members
    private int menuSize;
    private Collection<String> wordList;
    //GUI members
    private ContextMenu menu;
    private final Text textBefore, textMatching, textAfter;


    public AutosuggestMenu(Collection<String> keyList) {
        this(keyList, DEFAULT_MENU_SIZE);
    }

    public AutosuggestMenu(Collection<String> keyList, int numEntries) {
        if (keyList == null) {
            throw new NullPointerException();
        }

        this.wordList = keyList;
        this.menuSize = numEntries;
        this.menu = new ContextMenu();

        for (int i = 0; i < this.menuSize; i++) {
            CustomMenuItem item = new CustomMenuItem(new Label(), true);
            this.menu.getItems().add(item);
        }

        this.textBefore = new Text();
        this.textMatching = new Text();
        this.textAfter = new Text();
    }

    public void addListener(TextField field) {
         field.textProperty().addListener((observable, oldValue, newValue) -> {
            String enteredText = field.getText();

            if (enteredText == null || enteredText.isEmpty()) {
                this.menu.hide();
            } else {
                List<String> filteredEntries = this.wordList.stream()
                        .filter(e -> e.contains(enteredText))
                        .collect(Collectors.toList());

                if (!filteredEntries.isEmpty()) {
                    populatePopup(field, filteredEntries, enteredText);

                    if (!(this.menu.isShowing())) {
                        this.menu.show(field, Side.BOTTOM, 0, 0); 
                    }
                } else {
                    this.menu.hide();
                }
            }
        });    

        field.focusedProperty().addListener((observableValue, oldValue, newValue) -> {
            this.menu.hide();
        });
    }             

    private void populatePopup(TextField field, List<String> matches, String query) {
        int i = 0,
            max = (matches.size() > this.menuSize) ? this.menuSize : 
                                                 matches.size();

        for (MenuItem item : this.menu.getItems()) {
            if (i < max) {
                String result = matches.get(i);
                item.setGraphic(generateTextFlow(result, query));     
                item.setVisible(true);     

                item.setOnAction(actionEvent -> {
                    field.setText(result);
                    field.positionCaret(result.length());
                    this.menu.hide();
                });
            } else {
                item.setVisible(false);
            }

            i++;
        }
    }

    private TextFlow generateTextFlow(String text, String filter) {        
        int filterIndex = text.indexOf(filter);

        this.textBefore.setText(text.substring(0, filterIndex));
        this.textAfter.setText(text.substring(filterIndex + filter.length()));
        this.textMatching.setText(text.substring(filterIndex,  filterIndex + filter.length()));
        textMatching.setFill(Color.BLUE);
        textMatching.setFont(Font.font("Helvetica", FontWeight.BOLD, 12)); 

        return new TextFlow(textBefore, textMatching, textAfter);
    }

    public int getMenuSize() {
        return this.menuSize;
    }

    public void setMenuSize(int size) {
        this.menuSize = size;
    }

    public Collection<String> getKeyList() {
        return this.wordList;
    }

    public void setKeyList(Collection<String> keyList) {
        this.wordList = keyList;
    }

    //To do: add ways to change style of ContextMenu/menu items/text elements
}

Test class using a text file of English words as a dictionary:

package autosuggestfieldtest;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import com.sknb.gui.AutosuggestMenu;


public class AutosuggestFieldTest extends Application {
    private TextField textfield;
    private Collection<String> words;
    private AutosuggestMenu popup;

    @Override
    public void start(Stage primaryStage) {
        String filename = Paths.get("").toAbsolutePath().toString() + "\\words.txt";

        try (Stream<String> stream = Files.lines(Paths.get(filename))) {
            words = stream
                    .collect(Collectors.toSet());
        } catch (IOException e) {
            System.out.println("DERP");
        }

        popup = new AutosuggestMenu(words);
        textfield = new TextField();

        popup.addListener(textfield);

        StackPane root = new StackPane();
        root.getChildren().add(textfield);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("AutocompleteField Test");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

Note: this is a modification of a similar solution by Ruslan, but I was wondering if there is a solution that does not involve clearing/repopulating the menu with every keystroke? I.e. just using the setGraphic and refreshing the ContextMenu?

SKNB
  • 25
  • 7
  • `ContextMenu` is not the right tool for this job. It does not update dynamically. It would be better to use a `Popup` that contains a `ListView`... – fabian Apr 12 '18 at 19:30
  • First of all, thank you for your response. My only concern is how to position the Popup next to the caret? – SKNB Apr 22 '18 at 14:48

0 Answers0