I need to implement a "searchable" JComboBox
: one that filters its displayed items as you type
Inspired by this answer, I wrote this implementation
package di;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Vector;
public class ComboBoxes {
private ComboBoxes() {
}
public static <T> JComboBox<T> searchableComboBox() {
return searchableComboBox(new ArrayList<>());
}
public static <T> JComboBox<T> searchableComboBox(Collection<T> items) {
ComboBoxModel<T> model = new DefaultComboBoxModel<>(new Vector<>(items));
JComboBox<T> comboBox = new JComboBox<>(model);
comboBox.setEditable(true);
ComboBoxEditor comboBoxEditor = comboBox.getEditor();
JTextField editorTextField = (JTextField) comboBoxEditor.getEditorComponent();
KeyAdapter searchableComboBoxKeyListener = createSearchableComboBoxKeyListener(comboBox, model);
editorTextField.addKeyListener(searchableComboBoxKeyListener);
return comboBox;
}
private static <T> KeyAdapter createSearchableComboBoxKeyListener(JComboBox<T> comboBox, ComboBoxModel<T> initialModel) {
return new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
JTextField editorTextField = (JTextField) comboBox.getEditor().getEditorComponent();
String enteredText = editorTextField.getText();
ArrayList<T> matchingElements = findMatchingElements(enteredText);
DefaultComboBoxModel<T> modelWithOnlyMatchingElements = new DefaultComboBoxModel<>(new Vector<>(matchingElements));
comboBox.setModel(modelWithOnlyMatchingElements);
comboBox.setSelectedItem(enteredText);
comboBox.showPopup();
}
private ArrayList<T> findMatchingElements(String enteredText) {
ArrayList<T> matchingElements = new ArrayList<>();
for (int i = 0; i < initialModel.getSize(); i++) {
T modelElement = initialModel.getElementAt(i);
if (StringUtils.containsIgnoreCase(modelElement.toString(), enteredText))
matchingElements.add(modelElement);
}
return matchingElements;
}
};
}
}
It works great with strings. It works sort of ok with custom objects, but there's a small problem
The key listener sets the entered string as a new selected item. It can get away with that since setSelectedItem()
accepts an Object
, even though JComboBox
is a parameterized type (as generics were introduced far later than Swing, JComboBox
is not really generic)
So if you listen to new selections and expect selected items to be of JComboBox
's type argument, you'll get a ClassCastException
as strings cannot be cast to that type. Here's a demo
package demos.combo;
import di.ComboBoxes;
import javax.swing.*;
import java.awt.event.ItemEvent;
import java.util.ArrayList;
public class SearchableComboBoxDemo {
public static void main(String[] args) {
JFrame frame = new JFrame("Searchable Combo Box Demo");
JPanel mainPanel = new JPanel();
JComboBox<Plant> searchableCombo = ComboBoxes.searchableComboBox();
searchableCombo.addItemListener(SearchableComboBoxDemo::onItemSelection);
comboBoxItems().forEach(searchableCombo::addItem);
mainPanel.add(searchableCombo);
frame.setContentPane(mainPanel);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static void onItemSelection(ItemEvent event) {
if (event.getStateChange() != ItemEvent.SELECTED) return;
Plant selectedItem = (Plant) event.getItem(); // throws
System.out.printf("Selected item: " + selectedItem + "\n");
}
private static ArrayList<Plant> comboBoxItems() {
ArrayList<Plant> items = new ArrayList<>();
items.add(new Plant("Potato"));
items.add(new Plant("Peach"));
items.add(new Plant("Banana"));
items.add(new Plant("Orange"));
items.add(new Plant("Carrot"));
items.add(new Plant("Cabbage"));
return items;
}
}
package demos.combo;
public class Plant {
private final String name;
public Plant(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
The setSelectedItem()
call is important. If you remove it, the text field will restore a valid value after you try to delete a character. It's a side effect of setting a new model
One would imagine that setting the text field's text directly would fix it
comboBox.setModel(modelWithOnlyMatchingElements);
editorTextField.setText(enteredText); // like this
comboBox.showPopup();
but then JComboBox
doesn't fire ItemEvent
s as expected. Try typing "o", then click "Orange", then select and delete all text, then pick "Potato". You won't receive a "potato selected" event. It's because when the field is cleared and a new model is set, the new model's selected item (the first item in this case) is automatically assigned to JComboBox
's selectedItemReminder
field
/*
the invoked DefaultComboBoxModel constructor automatically selects the first element,
there's nothing we can do about it
*/
public DefaultComboBoxModel(Vector<E> v) {
objects = v;
if ( getSize() > 0 ) {
selectedObject = getElementAt( 0 );
}
}
If the user's selection matches it (and it does), then no IventItem
is ever fired (see javax.swing.JComboBox#setSelectedItem
)
So, in summary, how do I implement a searchable JComboBox
:
ItemListener
can listen to?
One workaround is to set selectedItem
to null
comboBox.setModel(modelWithOnlyMatchingElements);
comboBox.setSelectedItem(null);
editorTextField.setText(enteredText);
comboBox.showPopup();