Java Swing - Dropdown Text Suggestion Component Example [Updated: May 11, 2018, Created: Apr 27, 2018] |
|
||
Following example shows how to display text suggestions in a dropdown list. ExampleThe DecoratorFollowing 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 interfaceThis 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 ImplementationsFollowing 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 classpublic 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 ProjectDependencies and Technologies Used:
|
|
||
|
|||
|