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 ProjectDependencies and Technologies Used:
|