4

I'm using the "Add a JSeparator together with the ComboBoxItem-to-render on a JPanel"-ListCellRenderer approach to display separators in a JComboBox.

I noticed that the algorithm on MacOS to vertically center the selected item on the PopUp gets confused by the changed height of the JSeparator-ComboBoxItems.

Is there a way to fix the wrong position of the PopUps seen on the right-hand side of this screenshot? If the "Spain"-Item is selected it is painted slightly too high; the "Cars"-Item much too high.

JComboBox Separator MacOs Bug

The sourcecode:

import java.awt.BorderLayout;
import java.awt.Component;
import java.util.Arrays;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.ListCellRenderer;

public class JComboBoxSeparatorMacOs {

    public static void main(String[] args) {
    JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.add(new JComboBox<String>("A,Normal,Combo Box,without Separators".split(",")), BorderLayout.WEST);

        JComboBox<String> comboBox = new JComboBox<String>("Spain,Italy,Car,Peru".split(","));
        ListCellRenderer<String> renderer = new SeparatorListCellRenderer<String>(comboBox.getRenderer(), 0);
        comboBox.setRenderer(renderer);
        frame.add(comboBox);

        frame.pack();
        frame.setVisible(true);
    }
}

class SeparatorListCellRenderer<E> implements ListCellRenderer<E> {
    private final ListCellRenderer<? super E> delegate;
    private final int[] indexes;
    private final JPanel panel = new JPanel(new BorderLayout());

    public SeparatorListCellRenderer(ListCellRenderer<? super E> delegate, int... indexes) {
        Arrays.sort(indexes);
        this.delegate = delegate;
        this.indexes = indexes;
        panel.setOpaque(false);  //for rendering of selected item on MSWindows
    }

    @Override
    public Component getListCellRendererComponent(JList list, E value, int index, boolean isSelected, boolean cellHasFocus) {
        panel.removeAll();
        panel.add(delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus));
        if (Arrays.binarySearch(indexes, index) >= 0)
            panel.add(new JSeparator(), BorderLayout.PAGE_END);

        return panel;
    }
}
bobndrew
  • 395
  • 10
  • 32

2 Answers2

2

This looks like a feature of com.apple.laf.AquaComboBoxUI that tries to expose more of the list surrounding the current selection. It just doesn't expect the list to include a a JSeparator having a com.apple.laf.AquaPopupMenuSeparatorUI.

As an alternative, consider one of these approaches:

  • Use HTML to decorate the entry, e.g.

    new JComboBox("<html><b>Spain</b></html>,Italy,Car,Peru"…
    
  • Alter the Font in the renderer, as shown here.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
1
  • I think that this issue has nothing with Platoform / Native OS / Look and Feel

  • JComboBox, JPopup could / couldn't be restricted, changed, overrode some of methods in some Swing GUI Builders

  • for JComboBoxes JPopup to use Combo Box Popup by @camickr

  • maybe JSeparator in Renderer could be little bit different

    a) for possible output from ActionListener or ItemListener

    b) notice this couldn't works for proper KeyListener added to the derived JList (maybe not important)

images

enter image description hereenter image description here

code

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class ComboBoxWithSeparator extends JFrame {

    private static final long serialVersionUID = 1L;
    final String SEPARATOR = "SEPARATOR";

    public ComboBoxWithSeparator() {
        super("Block ComboBox Example");
        String[][] str = {{"A", "B", "C"}, {"1", "2", "3"}, {"abc", "def", "ghi"}};
        JComboBox combo = new JComboBox(makeVectorData(str));
        combo.setRenderer(new ComboBoxRenderer());
        combo.addActionListener(new BlockComboListener(combo));
        combo.setPrototypeDisplayValue("XXXXXXXXXXXXXXXXX");
        setLayout(new FlowLayout());
        JComboBox combo1 = new JComboBox(makeVectorData(str));
        combo1.setRenderer(new ComboBoxRenderer());
        combo1.addActionListener(new BlockComboListener(combo));
        combo1.setPrototypeDisplayValue("XXXXXXXXXXXXXXXXX");
        BoundsPopupMenuListener listener = new BoundsPopupMenuListener(true, true);
        combo1.addPopupMenuListener(listener);
        combo1.setPrototypeDisplayValue("ItemWWW");
        add(combo);
        add(combo1);
        pack();
        setVisible(true);
    }

    private Vector<String> makeVectorData(String[][] str) {
        boolean needSeparator = false;
        Vector<String> data = new Vector<String>();
        for (int i = 0; i < str.length; i++) {
            if (needSeparator) {
                data.addElement(SEPARATOR);
            }
            for (int j = 0; j < str[i].length; j++) {
                data.addElement(str[i][j]);
                needSeparator = true;
            }
        }
        return data;
    }

    public static void main(String args[]) {
        try {
            for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(laf.getName())) {
                    UIManager.setLookAndFeel(laf.getClassName());
                    //UIManager.getLookAndFeelDefaults().put("Panel.background", Color.white);
                    //UIManager.getLookAndFeelDefaults().put("Button.contentMargins", new InsetsUIResource(0,0,0,0));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        ComboBoxWithSeparator frame = new ComboBoxWithSeparator();
        frame.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
    }

    private class ComboBoxRenderer extends JLabel implements ListCellRenderer {

        private static final long serialVersionUID = 1L;
        private JSeparator separator;

        public ComboBoxRenderer() {
            setOpaque(true);
            setBorder(new EmptyBorder(1, 1, 1, 1));
            separator = new JSeparator(JSeparator.HORIZONTAL);
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value,
                int index, boolean isSelected, boolean cellHasFocus) {
            String str = (value == null) ? "" : value.toString();
            if (SEPARATOR.equals(str)) {
                return separator;
            }
            if (isSelected) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }
            setFont(list.getFont());
            setText(str);
            return this;
        }
    }

    private class BlockComboListener implements ActionListener {

        private JComboBox combo;
        private Object currentItem;

        BlockComboListener(JComboBox combo) {
            this.combo = combo;
            combo.setSelectedIndex(0);
            currentItem = combo.getSelectedItem();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            String tempItem = (String) combo.getSelectedItem();
            if (SEPARATOR.equals(tempItem)) {
                combo.setSelectedItem(currentItem);
            } else {
                currentItem = tempItem;
            }
        }
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • 2
    Actually it really is dependent of the L&F. For example, in Metal, the combo box popup is always located at the same place. On MacOS, the L&F tries to locate the popup so that the selected value is placed exactly on top of the combobox. Adding the separator seems to cause an issue on Aqua L&F. – Guillaume Polet Oct 09 '12 at 11:51
  • @Guillaume Polet JSeparator is chameleon, one as container, another as components, if isn't there used LayoutManager then fill all available space (JComponent), are caused above code example some issue on Mac OSX ??? – mKorbel Oct 09 '12 at 11:54
  • @Guillaume Polet in the case that is Quaua Look and Feel rellated issue (very scallable L&F) then feel free to edit my post with your printscreen – mKorbel Oct 09 '12 at 11:58
  • unfortunately (or not :-p), I don't have a Mac right now. ;-) – Guillaume Polet Oct 09 '12 at 11:58
  • @Guillaume Polet excelent comment :-) nothing personally please, have to waiting for comments by OP, or hoping (maybe) is (@trashgod) around – mKorbel Oct 09 '12 at 12:01
  • @mKorbel - thank you for your answer, but omg, it is as cryptic as an answer can be... ;-) _first point:_ of course it has to do with the MacOS-Look And Feel, because on no other OS the PopUp is adjusted like this. _second:_ what do you mean by "Swing GUI Builders"? _third:_ the camickr example places the popup **above** and not **in an selection-adjusted position over** the combobox (and it's not working with Java >1.6.0_24 or 7) – bobndrew Oct 10 '12 at 10:03
  • @mKorbel - _fourth:_ I have absolutely no clue what you are writing about: "possible output from ActionListener"?!? Your example breaks the keyboard navigation in the left combobox and let's me select the Separator in the right combobox. Because of these problems I want to use the "The Separator is no ComboBox-Item" method. – bobndrew Oct 10 '12 at 10:03
  • my code example demonstrated possible scenarios, because you can listening by ActionListener or ItemListener, there I assumed that no issue with JSeparator, and in the case if yes is there an issue with Aqua & JSeparator, then override JSeparator with proper LayoutManager and override isFocusable, or easiest way is to use CompounBorders (Matte & Empty) to simulating JSeparator on SOUTH area in the derived JList from JComboBox, there no issue stick Popup to the rellative Point (JComboBox), – mKorbel Oct 10 '12 at 11:02