I wrote a custom JComboBox
that display a search text field once the number of items becomes big enough to necessitate a scroll bar
It works, but there’s a small but I can’t fix so far
When you click the first combo for the first time, it contains some extra space at the bottom. It’s because its size hasn’t yet been updated after the vertical scroll bar was hidden
When you click it again, its size is correct
It seems to get rid of that issue, I need to somehow trigger another call to getPreferredSize()
on the popupMenu
at the end of my componentHidden()
implementation. However, neither revalidate()
, repaint()
, nor doLayout()
helped
How do I fix it?
package demos.combo;
import di.ComboBoxes;
import javax.swing.*;
public class SearchableComboDemo {
public static void main(String[] args) {
JComboBox<String> firstCombo = ComboBoxes.searchableComboBox();
JComboBox<String> secondCombo = ComboBoxes.searchableComboBox();
firstCombo.setMaximumRowCount(3);
secondCombo.setMaximumRowCount(3);
firstCombo.addItem("Item A");
firstCombo.addItem("Item B");
firstCombo.addItem("Item C");
secondCombo.addItem("Item D");
secondCombo.addItem("Item E");
secondCombo.addItem("Item F");
secondCombo.addItem("Item G");
JFrame frame = new JFrame();
JPanel mainPanel = new JPanel();
mainPanel.add(firstCombo);
mainPanel.add(secondCombo);
frame.setContentPane(mainPanel);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.pack();
frame.setVisible(true);
}
}
package di;
import demos.combo.SearchableComboPopup;
import javax.swing.*;
import javax.swing.plaf.ComboBoxUI;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.ComboPopup;
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> comboModel = new DefaultComboBoxModel<>(new Vector<>(items));
JComboBox<T> comboBox = new JComboBox<>(comboModel);
SearchableComboPopup<T> comboPopup = searchableComboPopup(comboBox);
comboBox.setUI(searchableComboBoxUi(comboPopup));
return comboBox;
}
private static <T> SearchableComboPopup<T> searchableComboPopup(JComboBox<T> comboBox) {
return new SearchableComboPopup<T>(comboBox);
}
private static <T> ComboBoxUI searchableComboBoxUi(SearchableComboPopup<T> comboPopup) {
return new BasicComboBoxUI() {
@Override
protected ComboPopup createPopup() {
return comboPopup;
}
};
}
}
package demos.combo;
import di.Actions;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.basic.ComboPopup;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
public class SearchableComboPopup<T> implements ComboPopup {
private final JComboBox<T> comboBox;
private final JPopupMenu popupMenu;
private final JTextField searchTextField;
private final Box searchBox;
private final JList<T> itemList;
private final JScrollPane scrollPane;
public SearchableComboPopup(JComboBox<T> comboBox) {
this.comboBox = comboBox;
this.searchTextField = createSearchTextField();
this.searchBox = createSearchBox();
this.itemList = createList(comboBox.getModel());
this.scrollPane = createScrollPane();
this.popupMenu = createPopupMenu();
}
private JTextField createSearchTextField() {
JTextField textField = new JTextField();
textField.addKeyListener(createSearchFieldKeyListener());
textField.getDocument().addDocumentListener(createSearchTextFieldDocumentListener());
return textField;
}
private KeyListener createSearchFieldKeyListener() {
return new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
ensureSelection();
itemList.requestFocusInWindow();
}
}
private void ensureSelection() {
if (itemList.isSelectionEmpty())
itemList.setSelectedIndex(0);
}
};
}
private DocumentListener createSearchTextFieldDocumentListener() {
return new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
updateModel();
}
@Override
public void removeUpdate(DocumentEvent e) {
updateModel();
}
private void updateModel() {
String enteredText = searchTextField.getText();
ArrayList<T> matchingElements = findMatchingElements(enteredText);
DefaultListModel<T> modelWithOnlyMatchingElements = new DefaultListModel<>();
matchingElements.forEach(modelWithOnlyMatchingElements::addElement);
itemList.setModel(modelWithOnlyMatchingElements);
}
private ArrayList<T> findMatchingElements(String enteredText) {
ArrayList<T> matchingElements = new ArrayList<>();
ComboBoxModel<T> comboModel = comboBox.getModel();
for (int i = 0; i < comboModel.getSize(); i++) {
T modelElement = comboModel.getElementAt(i);
if (StringUtils.containsIgnoreCase(modelElement.toString(), enteredText))
matchingElements.add(modelElement);
}
return matchingElements;
}
@Override
public void changedUpdate(DocumentEvent e) {
}
};
}
private Box createSearchBox() {
Box searchBox = new Box(BoxLayout.Y_AXIS);
searchBox.add(searchTextField);
searchBox.add(Box.createRigidArea(new Dimension(0, 3)));
return searchBox;
}
private JList<T> createList(ListModel<T> searchListModel) {
JList<T> list = new JList<>(searchListModel);
list.addMouseListener(createListMouseListener());
list.addKeyListener(createListKeyListener());
configureInputMap(list);
return list;
}
private MouseListener createListMouseListener() {
return new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
selectAndHide();
}
};
}
private void selectAndHide() {
T selectedValue = itemList.getSelectedValue();
if (selectedValue == null) return;
comboBox.setSelectedItem(selectedValue);
hide();
}
private KeyListener createListKeyListener() {
return new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
switchFocusToTextFieldIfNecessary(e);
submitSelectionIfNecessary(e);
}
private void switchFocusToTextFieldIfNecessary(KeyEvent e) {
boolean isUpmostItemSelected = itemList.getSelectedIndex() == 0;
if (e.getKeyCode() == KeyEvent.VK_UP && isUpmostItemSelected)
searchTextField.requestFocusInWindow();
}
private void submitSelectionIfNecessary(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER)
selectAndHide();
}
};
}
private void configureInputMap(JList<T> list) {
String switchFocusToTextFieldKey = "switchFocusToTextField";
list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK), switchFocusToTextFieldKey);
list.getActionMap().put(switchFocusToTextFieldKey, Actions.requestFocusInWindow(searchTextField));
}
private JScrollPane createScrollPane() {
JScrollPane scrollPane = new JScrollPane(itemList);
return scrollPane;
}
private JPopupMenu createPopupMenu() {
JPopupMenu popupMenu = new ComboBoxPopupMenu<>(comboBox);
popupMenu.add(searchBox);
popupMenu.add(scrollPane);
return popupMenu;
}
@Override
public void show() {
comboBox.firePopupMenuWillBecomeVisible();
int itemListVisibleRowCount = Math.min(comboBox.getItemCount(), comboBox.getMaximumRowCount());
itemList.setVisibleRowCount(itemListVisibleRowCount);
popupMenu.show(comboBox, 0, comboBox.getHeight());
searchBox.setVisible(scrollPane.getVerticalScrollBar().isVisible());
searchTextField.requestFocusInWindow();
}
@Override
public void hide() {
comboBox.firePopupMenuWillBecomeInvisible();
searchTextField.setText("");
popupMenu.setVisible(false);
comboBox.firePopupMenuCanceled();
}
public void toggle() {
if (isVisible()) hide();
else show();
}
@Override
public boolean isVisible() {
return popupMenu.isVisible();
}
@Override
@SuppressWarnings("unchecked")
public JList<Object> getList() {
return (JList<Object>) itemList;
}
@Override
public MouseListener getMouseListener() {
return createMouseListener();
}
private MouseListener createMouseListener() {
return new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
toggle();
}
};
}
@Override
public MouseMotionListener getMouseMotionListener() {
return null;
}
@Override
public KeyListener getKeyListener() {
return null;
}
@Override
public void uninstallingUI() {
}
private static class ComboBoxPopupMenu<T> extends JPopupMenu {
private final JComboBox<T> comboBox;
public ComboBoxPopupMenu(JComboBox<T> comboBox) {
this.comboBox = comboBox;
}
@Override
public Dimension getPreferredSize() {
Dimension preferredSize = super.getPreferredSize();
preferredSize.width = comboBox.getWidth();
return preferredSize;
}
}
}
package di;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
public class Actions {
private Actions() {
}
public static Action requestFocusInWindow(Component componentToFocus) {
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
componentToFocus.requestFocusInWindow();
}
};
}
}
<!-- add it to your pom.xml, assuming you use Maven too -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
dropdown menu, first click — extra space
dropdown menu, second click — no extra space
Wacage is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.