Close

Java Swing - Dropdown Text Suggestion Component Example

[Last Updated: Jul 4, 2018]

Following example shows how to display text suggestions in a dropdown list.

Example

The Decorator

Following decorator can work with any kind of component which allows user to enter or type text.

public class SuggestionDropDownDecorator<C extends JComponent> {
    private final C invoker;
    private final SuggestionClient<C> suggestionClient;
    private JPopupMenu popupMenu;
    private JList<String> listComp;
    DefaultListModel<String> listModel;
    private boolean disableTextEvent;

    public SuggestionDropDownDecorator(C invoker, SuggestionClient<C> suggestionClient) {
        this.invoker = invoker;
        this.suggestionClient = suggestionClient;
    }

    public static <C extends JComponent> void decorate(C component, SuggestionClient<C> suggestionClient) {
        SuggestionDropDownDecorator<C> d = new SuggestionDropDownDecorator<>(component, suggestionClient);
        d.init();
    }

    public void init() {
        initPopup();
        initSuggestionCompListener();
        initInvokerKeyListeners();
    }

    private void initPopup() {
        popupMenu = new JPopupMenu();
        listModel = new DefaultListModel<>();
        listComp = new JList<>(listModel);
        listComp.setBorder(BorderFactory.createEmptyBorder(0, 2, 5, 2));
        listComp.setFocusable(false);
        popupMenu.setFocusable(false);
        popupMenu.add(listComp);
    }

    private void initSuggestionCompListener() {
        if (invoker instanceof JTextComponent) {
            JTextComponent tc = (JTextComponent) invoker;
            tc.getDocument().addDocumentListener(new DocumentListener() {
                @Override
                public void insertUpdate(DocumentEvent e) {
                    update(e);
                }

                @Override
                public void removeUpdate(DocumentEvent e) {
                    update(e);
                }

                @Override
                public void changedUpdate(DocumentEvent e) {
                    update(e);
                }

                private void update(DocumentEvent e) {
                    if (disableTextEvent) {
                        return;
                    }
                    SwingUtilities.invokeLater(() -> {
                        List<String> suggestions = suggestionClient.getSuggestions(invoker);
                        if (suggestions != null && !suggestions.isEmpty()) {
                            showPopup(suggestions);
                        } else {
                            popupMenu.setVisible(false);
                        }
                    });
                }
            });
        }//todo init invoker components other than text components
    }

    private void showPopup(List<String> suggestions) {
        listModel.clear();
        suggestions.forEach(listModel::addElement);
        Point p = suggestionClient.getPopupLocation(invoker);
        if (p == null) {
            return;
        }
        popupMenu.pack();
        listComp.setSelectedIndex(0);
        popupMenu.show(invoker, (int) p.getX(), (int) p.getY());
    }

    private void initInvokerKeyListeners() {
        //not using key inputMap cause that would override the original handling
        invoker.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == VK_ENTER) {
                    selectFromList(e);
                } else if (e.getKeyCode() == VK_UP) {
                    moveUp(e);
                } else if (e.getKeyCode() == VK_DOWN) {
                    moveDown(e);
                } else if (e.getKeyCode() == VK_ESCAPE) {
                    popupMenu.setVisible(false);
                }
            }
        });
    }

    private void selectFromList(KeyEvent e) {
        if (popupMenu.isVisible()) {
            int selectedIndex = listComp.getSelectedIndex();
            if (selectedIndex != -1) {
                popupMenu.setVisible(false);
                String selectedValue = listComp.getSelectedValue();
                disableTextEvent = true;
                suggestionClient.setSelectedText(invoker, selectedValue);
                disableTextEvent = false;
                e.consume();
            }
        }
    }

    private void moveDown(KeyEvent keyEvent) {
        if (popupMenu.isVisible() && listModel.getSize() > 0) {
            int selectedIndex = listComp.getSelectedIndex();
            if (selectedIndex < listModel.getSize()) {
                listComp.setSelectedIndex(selectedIndex + 1);
                keyEvent.consume();
            }
        }
    }

    private void moveUp(KeyEvent keyEvent) {
        if (popupMenu.isVisible() && listModel.getSize() > 0) {
            int selectedIndex = listComp.getSelectedIndex();
            if (selectedIndex > 0) {
                listComp.setSelectedIndex(selectedIndex - 1);
                keyEvent.consume();
            }
        }
    }
}

The SuggestionClient interface

This is a hookup interface for the decorator. An implementation works on a specific component. This interface also allows the component on how it wants to display suggestions e.g. word by word or on the entire text etc.

public interface SuggestionClient<C extends JComponent> {

    Point getPopupLocation(C invoker);

    void setSelectedText(C invoker, String selectedValue);

    java.util.List<String> getSuggestions(C invoker);

}

The SuggestionClient Implementations

Following implementation is for any JTextComponent. It shows the suggestions on entire text.

/**
 * Matches entire text instead of separate words
 */
public class TextComponentSuggestionClient implements SuggestionClient<JTextComponent> {

    private Function<String, List<String>> suggestionProvider;

    public TextComponentSuggestionClient(Function<String, List<String>> suggestionProvider) {
        this.suggestionProvider = suggestionProvider;
    }

    @Override
    public Point getPopupLocation(JTextComponent invoker) {
        return new Point(0, invoker.getPreferredSize().height);
    }

    @Override
    public void setSelectedText(JTextComponent invoker, String selectedValue) {
        invoker.setText(selectedValue);
    }

    @Override
    public List<String> getSuggestions(JTextComponent invoker) {
        return suggestionProvider.apply(invoker.getText().trim());
    }
}

Following implementation is also for a JTextComponent. It shows the suggestions on each words as we type.

/**
 * Matches individual words instead of complete text
 */
public class TextComponentWordSuggestionClient implements SuggestionClient<JTextComponent> {
    private Function<String, List<String>> suggestionProvider;

    public TextComponentWordSuggestionClient(Function<String, List<String>> suggestionProvider) {
        this.suggestionProvider = suggestionProvider;
    }

    @Override
    public Point getPopupLocation(JTextComponent invoker) {
        int caretPosition = invoker.getCaretPosition();
        try {
            Rectangle2D rectangle2D = invoker.modelToView(caretPosition);
            return new Point((int) rectangle2D.getX(), (int) (rectangle2D.getY() + rectangle2D.getHeight()));
        } catch (BadLocationException e) {
            System.err.println(e);
        }
        return null;
    }

    @Override
    public void setSelectedText(JTextComponent tp, String selectedValue) {
        int cp = tp.getCaretPosition();
        try {
            if (cp == 0 || tp.getText(cp - 1, 1).trim().isEmpty()) {
                tp.getDocument().insertString(cp, selectedValue, null);
            } else {
                int previousWordIndex = Utilities.getPreviousWord(tp, cp);
                String text = tp.getText(previousWordIndex, cp - previousWordIndex);
                if (selectedValue.startsWith(text)) {
                    tp.getDocument().insertString(cp, selectedValue.substring(text.length()), null);
                } else {
                    tp.getDocument().insertString(cp, selectedValue, null);
                }
            }
        } catch (BadLocationException e) {
            System.err.println(e);
        }
    }

    @Override
    public List<String> getSuggestions(JTextComponent tp) {
        try {
            int cp = tp.getCaretPosition();
            if (cp != 0) {
                String text = tp.getText(cp - 1, 1);
                if (text.trim().isEmpty()) {
                    return null;
                }
            }
            int previousWordIndex = Utilities.getPreviousWord(tp, cp);
            String text = tp.getText(previousWordIndex, cp - previousWordIndex);
            return suggestionProvider.apply(text.trim());
        } catch (BadLocationException e) {
            System.err.println(e);
        }
        return null;
    }
}

Example main class

public class SuggestionExampleMain {
    public static void main(String[] args) {
        JFrame frame = createFrame();
        JTextField textField = new JTextField(10);
        SuggestionDropDownDecorator.decorate(textField,
                new TextComponentSuggestionClient(SuggestionExampleMain::getSuggestions));
        JTextPane textPane = new JTextPane();
        SuggestionDropDownDecorator.decorate(textPane,
                new TextComponentWordSuggestionClient(SuggestionExampleMain::getSuggestions));
        frame.add(textField, BorderLayout.NORTH);
        frame.add(new JScrollPane(textPane));
        frame.setVisible(true);
    }

    private static List<String> words =
            RandomUtil.getWords(2, 400).stream().map(String::toLowerCase).collect(Collectors.toList());

    private static List<String> getSuggestions(String input) {
        //the suggestion provider can control text search related stuff, e.g case insensitive match, the search  limit etc.
        if (input.isEmpty()) {
            return null;
        }
        return words.stream()
                    .filter(s -> s.startsWith(input))
                    .limit(20)
                    .collect(Collectors.toList());
    }

    private static JFrame createFrame() {
        JFrame frame = new JFrame("Suggestion Dropdown Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(600, 300));
        return frame;
    }
}

Output

Example Project

Dependencies and Technologies Used:

  • JDK 1.8
  • Maven 3.3.9

Text Suggestion Example Select All Download
  • text-suggestion-dropdown-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
              • uicommon
                • SuggestionDropDownDecorator.java
                • util

    See Also