Java Swing - JComboBox Filtering and Highlighting Example

[Updated: Dec 4, 2017, Created: Nov 28, 2017]

This example implements filtering and highlighting functionality for JComboBox component. We are going to create a custom ComboBoxEditor having JLabel as an editor component. Like other examples it also uses a custom renderer with Html highlighting as well. The filtering is based on KeyListener, so if the combo box is in focus, pressing keys will start the filtering. Also to maintain the default JComboBox up/down arrow key navigation/selection functionality, this example does not remove the unmatched items, instead it appends them to the end of the list. After selection, the original order of the items is restored. The selection can be made by hitting enter key or mouse click. Backspace key will remove the characters from end. Escape key will rollback the search.

Main class

public class JComboBoxFilterMain {
  public static void main(String[] args) {
      List<Employee> employees = EmployeeDataAccess.getEmployees();
      JComboBox<Employee> comboBox = new JComboBox<>(
              employees.toArray(new Employee[employees.size()]));

      ComboBoxFilterDecorator<Employee> decorate = ComboBoxFilterDecorator.decorate(comboBox,
              CustomComboRenderer::getEmployeeDisplayText,
              JComboBoxFilterMain::employeeFilter);

      comboBox.setRenderer(new CustomComboRenderer(decorate.getFilterTextSupplier()));

      JPanel panel = new JPanel();
      panel.add(comboBox);

      JFrame frame = createFrame();
      frame.add(panel);
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
  }

  private static boolean employeeFilter(Employee emp, String textToFilter) {
      if (textToFilter.isEmpty()) {
          return true;
      }
      return CustomComboRenderer.getEmployeeDisplayText(emp).toLowerCase()
                                .contains(textToFilter.toLowerCase());
  }

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

The Filtering decorator

public class ComboBoxFilterDecorator<T> {
    private JComboBox<T> comboBox;
    private BiPredicate<T, String> userFilter;
    private Function<T, String> comboDisplayTextMapper;
    java.util.List<T> originalItems;
    private Object selectedItem;
    private FilterEditor filterEditor;

    public ComboBoxFilterDecorator(JComboBox<T> comboBox,
                                   BiPredicate<T, String> userFilter,
                                   Function<T, String> comboDisplayTextMapper) {
        this.comboBox = comboBox;
        this.userFilter = userFilter;
        this.comboDisplayTextMapper = comboDisplayTextMapper;
    }

    public static <T> ComboBoxFilterDecorator<T> decorate(JComboBox<T> comboBox,
                                                          Function<T, String> comboDisplayTextMapper,
                                                          BiPredicate<T, String> userFilter) {
        ComboBoxFilterDecorator decorator =
                new ComboBoxFilterDecorator(comboBox, userFilter, comboDisplayTextMapper);
        decorator.init();
        return decorator;
    }

    private void init() {
        prepareComboFiltering();
        initComboPopupListener();
        initComboKeyListener();
    }

    private void prepareComboFiltering() {
        DefaultComboBoxModel<T> model = (DefaultComboBoxModel<T>) comboBox.getModel();
        this.originalItems = new ArrayList<>();
        for (int i = 0; i < model.getSize(); i++) {
            this.originalItems.add(model.getElementAt(i));
        }


        filterEditor = new FilterEditor(comboDisplayTextMapper, new Consumer<Boolean>() {
            //editing mode (commit/cancel) change listener
            @Override
            public void accept(Boolean aBoolean) {
                if (aBoolean) {//commit
                    selectedItem = comboBox.getSelectedItem();
                } else {//rollback to the last one
                    comboBox.setSelectedItem(selectedItem);
                    filterEditor.setItem(selectedItem);
                }
            }
        });

        JLabel filterLabel = filterEditor.getFilterLabel();
        filterLabel.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                filterLabel.setBorder(BorderFactory.createLoweredBevelBorder());
            }

            @Override
            public void focusLost(FocusEvent e) {
                filterLabel.setBorder(UIManager.getBorder("TextField.border"));
                resetFilterComponent();
            }
        });
        comboBox.setEditor(filterEditor);
        comboBox.setEditable(true);
    }

    private void initComboKeyListener() {
        filterEditor.getFilterLabel().addKeyListener(
                new KeyAdapter() {
                    @Override
                    public void keyPressed(KeyEvent e) {
                        char keyChar = e.getKeyChar();
                        if (!Character.isDefined(keyChar)) {
                            return;
                        }
                        int keyCode = e.getKeyCode();
                        switch (keyCode) {
                            case KeyEvent.VK_DELETE:
                                return;
                            case KeyEvent.VK_ENTER:
                                selectedItem = comboBox.getSelectedItem();
                                resetFilterComponent();
                                return;
                            case KeyEvent.VK_ESCAPE:
                                resetFilterComponent();
                                return;
                            case KeyEvent.VK_BACK_SPACE:
                                filterEditor.removeCharAtEnd();
                                break;
                            default:
                                filterEditor.addChar(keyChar);
                        }
                        if (!comboBox.isPopupVisible()) {
                            comboBox.showPopup();
                        }
                        if (filterEditor.isEditing() && filterEditor.getText().length() > 0) {
                            applyFilter();
                        } else {
                            comboBox.hidePopup();
                            resetFilterComponent();
                        }
                    }
                }
        );
    }

    public Supplier<String> getFilterTextSupplier() {
        return () -> {
            if (filterEditor.isEditing()) {
                return filterEditor.getFilterLabel().getText();
            }
            return "";
        };
    }

    private void initComboPopupListener() {
        comboBox.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                resetFilterComponent();
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                resetFilterComponent();
            }
        });
    }

    private void resetFilterComponent() {
        if (!filterEditor.isEditing()) {
            return;
        }
        //restore original order
        DefaultComboBoxModel<T> model = (DefaultComboBoxModel<T>) comboBox.getModel();
        model.removeAllElements();
        for (T item : originalItems) {
            model.addElement(item);
        }
        filterEditor.reset();
    }

    private void applyFilter() {
        DefaultComboBoxModel<T> model = (DefaultComboBoxModel<T>) comboBox.getModel();
        model.removeAllElements();
        java.util.List<T> filteredItems = new ArrayList<>();
        //add matched items at top
        for (T item : originalItems) {
            if (userFilter.test(item, filterEditor.getFilterLabel().getText())) {
                model.addElement(item);
            } else {
                filteredItems.add(item);
            }
        }

        //red color when no match
        filterEditor.getFilterLabel()
                    .setForeground(model.getSize() == 0 ?
                            Color.red : UIManager.getColor("Label.foreground"));
        //add unmatched items
        filteredItems.forEach(model::addElement);
    }
}

A custom ComboBoxEditor implementation

public class FilterEditor<T> extends BasicComboBoxEditor {
    private JLabel filterLabel = new JLabel();
    private String text = "";
    boolean editing;
    private Function<T, String> displayTextFunction;
    private Consumer<Boolean> editingChangeListener;
    private Object selected;

    FilterEditor(Function<T, String> displayTextFunction,
                 Consumer<Boolean> editingChangeListener) {
        this.displayTextFunction = displayTextFunction;
        this.editingChangeListener = editingChangeListener;
    }

    public void addChar(char c) {
        text += c;
        if (!editing) {
            enableEditingMode();
        }
    }

    public void removeCharAtEnd() {
        if (text.length() > 0) {
            text = text.substring(0, text.length() - 1);
            if (!editing) {
                enableEditingMode();
            }
        }
    }

    private void enableEditingMode() {
        editing = true;
        filterLabel.setFont(filterLabel.getFont().deriveFont(Font.PLAIN));
        editingChangeListener.accept(true);
    }

    public void reset() {
        if (editing) {
            filterLabel.setFont(UIManager.getFont("ComboBox.font"));
            filterLabel.setForeground(UIManager.getColor("Label.foreground"));
            text = "";
            editing = false;
            editingChangeListener.accept(false);
        }
    }

    @Override
    public Component getEditorComponent() {
        return filterLabel;
    }

    public JLabel getFilterLabel() {
        return filterLabel;
    }

    @Override
    public void setItem(Object anObject) {
        if (editing) {
            filterLabel.setText(text);
        } else {
            T t = (T) anObject;
            filterLabel.setText(displayTextFunction.apply(t));
        }
        this.selected = anObject;
    }

    @Override
    public Object getItem() {
        return selected;
    }

    @Override
    public void selectAll() {
    }

    @Override
    public void addActionListener(ActionListener l) {
    }

    @Override
    public void removeActionListener(ActionListener l) {
    }

    public boolean isEditing() {
        return editing;
    }

    public String getText() {
        return text;
    }
}

Output

Example Project

Dependencies and Technologies Used :

  • datafactory 0.8: Library to generate data for testing.
  • JDK 1.8
  • Maven 3.3.9

JCombBox Filtering With Custom Editor Component Example Select All Download
  • combo-box-filter
    • src
      • main
        • java
          • com
            • logicbig
              • example

See Also