Search code examples
javaswingnimbus

How do I correctly alter checkbox text rendering behaviour?


I am using the Nimbus look and feel.

I have two JCheckBoxes, and by default, the text is black when the JCheckBox is enabled, and greyed out when it is disabled. My new requirement is that the program should ignore the enabled state of the JCheckBox, and instead be greyed out when isSelected() is false, and display the text in a specified colour when isSelected() is true.

I have attempted to do this by:

  • Extending BasicCheckBoxUI
  • Overriding the paintText method
  • Copying the contents of BasicButtonUI.paintText() and modifying the behaviour
  • Calling setUI on the JCheckBoxes in question with an instance of my new UI class

    private static class MyCheckBoxUI extends BasicCheckBoxUI
    {
        private Color selectedColor;
    
        public MyCheckBoxUI( Color selectedColor )
        {
            this.selectedColor = selectedColor;
        }
    
        @Override
        protected void paintText( Graphics g, AbstractButton b, Rectangle textRect, String text )
        {
            ButtonModel model = b.getModel();
            FontMetrics fm = SwingUtilities2.getFontMetrics(b, g);
            int mnemonicIndex = b.getDisplayedMnemonicIndex();
    
            if( model.isSelected() ) 
            {
                /*** paint the text normally */
                g.setColor( selectedColor );
                SwingUtilities2.drawStringUnderlineCharAt(b, g,text, mnemonicIndex,
                                          textRect.x + getTextShiftOffset(),
                                          textRect.y + fm.getAscent() + getTextShiftOffset());
            }
            else 
            {
                /*** paint the text disabled ***/
                g.setColor(b.getBackground().brighter());
                SwingUtilities2.drawStringUnderlineCharAt(b, g,text, mnemonicIndex,
                                          textRect.x, textRect.y + fm.getAscent());
                g.setColor(b.getBackground().darker());
                SwingUtilities2.drawStringUnderlineCharAt(b, g,text, mnemonicIndex,
                                          textRect.x - 1, textRect.y + fm.getAscent() - 1);
            }
        }
    }
    

In the constructor of my JPanel, I have the following:

        jCheckBox1.setUI( new MyCheckBoxUI( Color.red ) );
        jCheckBox2.setUI( new MyCheckBoxUI( Color.black ) );

This appears to work as expected, except there is a side effect. Now, the check box will not render the tick in the box when it is selected like it used to (I did not expect this as I have only overridden the paintText method). What have I missed?

Additionally, the use of SwingUtilities2 disturbs me, as I am warned that it is an internal proprietary API that could be removed in a future release. Is there a better way to do this?


Solution

  • You need to play with the UIManager color properties...

    Object disabledTextForeground = UIManager.get("CheckBox[Disabled].textForeground");
    Object enabledTextForeground =  UIManager.get("CheckBox.foreground");
    UIManager.put("CheckBox[Disabled].textForeground", UIManager.get("CheckBox.foreground"));
    UIManager.put("CheckBox[Enabled].textForeground", disabledTextForeground);
    UIManager.put("CheckBox[Selected+Enabled].textForeground", enabledTextForeground);
    

    This example will allow you to modify the values at a global level, meaning that every JCheckBox you create will have the same values

    Options

    For example...

    import java.awt.Color;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import javax.swing.JCheckBox;
    import javax.swing.JFrame;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class TestNimbus {
    
        public static void main(String[] args) {
            new TestNimbus();
        }
    
        public TestNimbus() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    }
    
                    Object disabledTextForeground = UIManager.get("CheckBox[Disabled].textForeground");
                    Object enabledTextForeground =  UIManager.get("CheckBox.foreground");
                    UIManager.put("CheckBox[Disabled].textForeground", UIManager.get("CheckBox.foreground"));
                    UIManager.put("CheckBox[Enabled].textForeground", disabledTextForeground);
                    UIManager.put("CheckBox[Selected+Enabled].textForeground", enabledTextForeground);
    
                    JCheckBox cb1 = new JCheckBox("Option #1");
                    JCheckBox cb2 = new JCheckBox("Option #2");
                    JCheckBox cb3 = new JCheckBox("Option #3");
    
                    cb1.setSelected(true);
                    cb3.setEnabled(false);
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setLayout(new GridBagLayout());
                    GridBagConstraints gbc = new GridBagConstraints();
                    gbc.gridwidth = gbc.REMAINDER;
                    frame.add(cb1, gbc);
                    frame.add(cb2, gbc);
                    frame.add(cb3, gbc);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
    }
    

    To affect only given instance of JCheckBoxs, you would need to supply the overrides directly to each instance of the JCheckBox you want to effect...

    LocalOptions

    The top three have their values overridden, while the bottom three remain unaffected and use the UI default values

    import java.awt.Color;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import javax.swing.JCheckBox;
    import javax.swing.JFrame;
    import javax.swing.UIDefaults;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class TestNimbus {
    
        public static void main(String[] args) {
            new TestNimbus();
        }
    
        public TestNimbus() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    }
    
                    UIDefaults checkBoxDefaults = new UIDefaults();
    
                    Object disabledTextForeground = UIManager.get("CheckBox[Disabled].textForeground");
                    Object enabledTextForeground = UIManager.get("CheckBox.foreground");
                    checkBoxDefaults.put("CheckBox[Disabled].textForeground", UIManager.get("CheckBox.foreground"));
                    checkBoxDefaults.put("CheckBox[Enabled].textForeground", disabledTextForeground);
                    checkBoxDefaults.put("CheckBox[Selected+Enabled].textForeground", enabledTextForeground);
    
                    JCheckBox cb1 = new JCheckBox("Option #1");
                    JCheckBox cb2 = new JCheckBox("Option #2");
                    JCheckBox cb3 = new JCheckBox("Option #3");
    
                    JCheckBox cb4 = new JCheckBox("Normal #1");
                    JCheckBox cb5 = new JCheckBox("Normal #2");
                    JCheckBox cb6 = new JCheckBox("Normal #3");
    
                    configure(cb1, checkBoxDefaults);
                    configure(cb2, checkBoxDefaults);
                    configure(cb3, checkBoxDefaults);
    
                    cb1.setSelected(true);
                    cb4.setSelected(true);
                    cb3.setEnabled(false);
                    cb6.setEnabled(false);
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setLayout(new GridBagLayout());
                    GridBagConstraints gbc = new GridBagConstraints();
                    gbc.gridwidth = gbc.REMAINDER;
                    frame.add(cb1, gbc);
                    frame.add(cb2, gbc);
                    frame.add(cb3, gbc);
                    frame.add(cb4, gbc);
                    frame.add(cb5, gbc);
                    frame.add(cb6, gbc);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
    
                private void configure(JCheckBox checkbox, UIDefaults uiDefaults) {
                    checkbox.putClientProperty("Nimbus.Overrides", uiDefaults);
    //                checkbox.putClientProperty("Nimbus.Overrides.InheritDefaults", false);
                }
            });
        }
    
    }