Search code examples
javaswingjtablerenderernimbus

Render TableCell with JComboBox and selection background


I have a requirement to render selected/focused cells in JTable with a JComboBox if the column in question uses combo as editor. The intent of this is to give hint to users that cells in the column is edited with a combo rather than a JTextField.

My problem is that for other LookAndFeels than Metal it's impossible to set selection background of the table to the combobox. If one click or navigates with the keyboard to a "combo column", the selected cell gets rendered by the combo with the default background.

Metal L&F works as I want it to, but that's not an option since our customers uses either Substance/Nimbus/Windows L&F and it doesn't work for any of them.

Hope I was clear enough, any help is appreciated!

Select cells in "Combo column" of this SSCCE to see what I mean:

import java.awt.Component;

import javax.swing.DefaultCellEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;

import com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel;


public class RenderSelectedCellWithComboSSCCE extends JFrame
{   
    RenderSelectedCellWithComboSSCCE()
    {
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        add(new JScrollPane(createTable()));
        pack();
    }

    private JTable createTable()
    {
        TableModel model = new DefaultTableModel(
                new Object [][] {{"A", "Item 0"},
                                 {"B", "Item 1"},
                                 {"C", "Item 2"},
                                 {"D", "Item 3"},
                                 {"E", "Item 4"}},
                new String [] {"TextField", "Combo"});
        JTable table = new JTable(model);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        DefaultCellEditor editor = new DefaultCellEditor(new JComboBox(new Object[]{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4"}));
        editor.setClickCountToStart(2);
        table.getColumnModel().getColumn(1).setCellEditor(editor);
        table.getColumnModel().getColumn(1).setCellRenderer(new ComboCellRenderer());
        return table;
    }

    /**
     * Renderer that demonstrates problem rendering selected cells with a combo with selection background.
     */
    private static class ComboCellRenderer implements TableCellRenderer
    {
        final TableCellRenderer defaultRenderer;
        final JComboBox combo;

        ComboCellRenderer()
        {
            defaultRenderer = new DefaultTableCellRenderer();
            combo = new JComboBox();
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
        {
            if (hasFocus && table != null && table.isCellEditable(row, column))
            {
                combo.setModel(new DefaultComboBoxModel(new Object[]{value}));
                combo.setSelectedItem(value);
                combo.setBackground(table.getSelectionBackground());    // This only have desired effect for Metal L&F
                return combo;
            }
            else
            {
                return defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            }
        }
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                setLookAndFeel();
                new RenderSelectedCellWithComboSSCCE().setVisible(true);
            }

            private void setLookAndFeel()
            {
                try
                {
                    //UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());       // Metal works fine
                    UIManager.setLookAndFeel(new NimbusLookAndFeel());
                    //UIManager.setLookAndFeel(new WindowsLookAndFeel());
                    //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    //UIManager.setLookAndFeel("org.jvnet.substance.SubstanceLookAndFeel");
                }
                catch (Exception e)
                {
                    throw new RuntimeException("Failed to set LookAndFeel", e);
                }
            }
        });
    }
}

Solution

  • ComboBox rendering does pose problems, typically the LAF has very special ideas about how they want to show the the combo itself, many are styled, have rounded corners ... it's unprobable to it done right for all. While playing a bit with Stani's suggestion I ran into all the nasty (and quickly forgotten :-) details, like

    • Windows turns off opacity when rendering the combo, so here it would help to wrap the combo's renderer into one that sets opacity to true after calling super
    • Nimbus ignores the component's background which is had set itself, no matter the opacity: your renderer would work as expected (okay, nearly, modulo unexpected color changes :-) if you set a selection color (on the table) which differs from the default
    • ...

    On the other hand, you don't need a fully functional JComboBox, all you need is an visual clue that there is something to open. So you might get away with a custom component - at its simplest a JLabel with some arrows - mocking a JComboBox, something like:

    public static class MockCombo extends JLabel {
    
        private JButton arrow;
    
        public MockCombo() {
            JComboBox box = new JComboBox();
            box.setEditable(true);
            arrow = (JButton) box.getComponent(0);
            setLayout(new BorderLayout());
            add(arrow, BorderLayout.LINE_END);
            setOpaque(true);
        }
    
    }
    

    Still needs to be fine-tuned by LAF, but a far easier task than tweak a JComboBox