How To Add Separators To A ComboBox(18:58, 01. Jun. 2011)

This article explains how to add separators to the list of a JComboBox.

JComboBox Separator Demo

Generally there are two different approaches - model dependent and model independent. First we want to take a look at the model independent solution.

Model independent solution

The basic idea is to create a special renderer which uses a JPanel as container, a JLabel for the text and a JSeparator at the bottom of the panel. An array which takes the index numbers for the separators is passed to the constructor of the renderer .

JComboBox combo = new JComboBox(new String[]{"Item 1", "Item 2", "Item 3", "Item 1-1", "Item 1-2", "Item 2-1"});
combo.setRenderer(new SeparatorRenderer(combo.getRenderer(), 2, 4));    
...
  //
  // Renderer Class
  //  
  private static class SeparatorRenderer implements ListCellRenderer
  {
    private ListCellRenderer delegate;
    private JPanel container;
    private JLabel label;
    private HashSet<Integer> separatorIndexes; 
    private boolean cellRendererSelectionBackgroundEnabled;
    private boolean isSelected;

    public SeparatorRenderer(ListCellRenderer delegate, Integer... separatorIndexes)
    {
      this.delegate = delegate;
      this.separatorIndexes = new HashSet<Integer>(Arrays.asList(separatorIndexes));
      cellRendererSelectionBackgroundEnabled = SyntheticaLookAndFeel.getBoolean("Synthetica.cellRenderer.selectionBackground.enabled", null, false);
      container = new JPanel();
      label = new JLabel()
      {
        protected void paintComponent(Graphics g) 
        {
          //required for special background rendering enabled themes like SyntheticaClassy
          if (cellRendererSelectionBackgroundEnabled && isSelected)
          {
            String imagePath = SyntheticaLookAndFeel.getString("Synthetica.comboBox.listSelectionBackground", null);
            Insets sInsets = SyntheticaLookAndFeel.getInsets("Synthetica.comboBox.listSelectionBackground.insets", null, false);
            ImagePainter imagePainter = new ImagePainter(g, 0, 0, getWidth(), getHeight(), imagePath, sInsets, sInsets, ImagePainter.STRETCHED, ImagePainter.STRETCHED);
            imagePainter.draw();
          }
          super.paintComponent(g);
        }
      };
      container.setLayout(new BorderLayout());
      container.setOpaque(false);
      container.add(label);
      container.add(new JSeparator(), BorderLayout.SOUTH);
    }

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)
    {
      JLabel c = (JLabel)delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
      if (index >= 0 && separatorIndexes.contains(index))
      {
        this.isSelected = isSelected;
        label.setText(c.getText());
        label.setForeground(c.getForeground());
        label.setBackground(c.getBackground());
        label.setBorder(c.getBorder());
        label.setOpaque(isSelected && !cellRendererSelectionBackgroundEnabled);
        return container;
      }
      return c;
    }
  }

Because themes like SyntheticaClassy do some special background rendering for selected items, we have to override #paintComponent() of our custom renderer label and add related painting code - this is not needed for regular themes.

Model dependent solution

In the model dependent approach separator placeholders are part of the ComboBox model - we use the minus character in the demo application to represent separators. We also have to override #setSelectedItem() to make the separator unselectable. The renderer simply returns a JSeparator instance instead of the separator placeholder.

private static final String SEPARATOR_MARK = "-";
  ...
  JYComboBox combo = new JYComboBox()
  {       
    @Override
    public void setSelectedItem(Object item)
    {
      if (!SEPARATOR_MARK.equals(item.toString()))
       super.setSelectedItem(item);
    }
  };
    
  combo.setModel(new DefaultComboBoxModel(new String[]{"Item 1", "Item 2", "Item 3", SEPARATOR_MARK, "Item 1-1", "Item 1-2", SEPARATOR_MARK, "Item 2-1"}));
    
  final ListCellRenderer l = combo.getRenderer();
  combo.setRenderer(new ListCellRenderer()
  {
    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)
    {
      Component c = l.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
      if (index >= 0 && SEPARATOR_MARK.equals(value.toString()))
        return new JSeparator();
      return c;
    }
  });

This works fine except the fact that the key binding (up/down) doesn't work as expected. To solve the issue we have to create and install a separator aware UI-delegate. The base class (SyntheticaJYComboBoxUI) is part of our SyntheticaAddons package.

comboBox.setUI(new SeparatorComboBoxUI());
  ...
  //
  //Separator aware UI-delegate class
  //
  private static class SeparatorComboBoxUI extends SyntheticaJYComboBoxUI 
  {
    @Override
    protected void selectNextPossibleValue()
    {
      int i = comboBox.getSelectedIndex();
      if (i < comboBox.getModel().getSize()-1)
      {        
        if (SEPARATOR_MARK.equals(comboBox.getItemAt(i+1)))
          i++;
        comboBox.setSelectedIndex(i+1);
        comboBox.repaint();
      }
    }

    @Override
    protected void selectPreviousPossibleValue()
    {
      int i = comboBox.getSelectedIndex();
      if (i > 0)
      {        
        if (SEPARATOR_MARK.equals(comboBox.getItemAt(i-1)))
          i--;
        comboBox.setSelectedIndex(i-1);
        comboBox.repaint();
      }
    }
  }   

Download Demo Sourcecode

Related Links

TableCellRenderer, Part 5 - Helpful Hints