views:

48

answers:

3

Take the following example code. There is some bug in Swing which doesn't render disabled components as disabled if the component contains HTML. Aside from reporting the issue, which I hope a colleague has already taken care of, is there some good way to work around the problem?

Whatever solution I take, I want it to be a global fix as opposed to something that needs to be hacked into every check box in the application.

I tried making a custom UI for the check box which calls setForeground before and after the painting, but it turns out that by calling setForeground, it fires an event which ultimately results in it calling repaint(), which calls the renderer, ...

import java.awt.GridLayout;
import java.util.Arrays;

import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class TestCheckBoxes extends JFrame
{
    public TestCheckBoxes()
    {
        JCheckBox checkBox1 = new JCheckBox("Enabled, plain text");
        JCheckBox checkBox2 = new JCheckBox("<html><p>Enabled, HTML");
        JCheckBox checkBox3 = new JCheckBox("Disabled, plain text");
        checkBox3.setEnabled(false);
        JCheckBox checkBox4 = new JCheckBox("<html><p>Disabled, HTML");
        checkBox4.setEnabled(false);

        setLayout(new GridLayout(4, 1));

        for (JCheckBox checkBox : Arrays.asList(checkBox1, checkBox2, checkBox3, checkBox4))
        {
            checkBox.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
            add(checkBox);
        }

        ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
        pack();
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                TestCheckBoxes frame = new TestCheckBoxes();
                frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
                frame.setVisible(true);
            }
        });
    }
}
A: 

I would suggest to start using renderers instead of that. You can use HTML inside the renderer (such as JLabel) and your problem will go away by itself :)

More information is at http://java.sun.com/docs/books/tutorial/uiswing/components/combobox.html#renderer

eugener
Sadly, JCheckBox doesn't support a cell renderer. Otherwise I would definitely use that, making a custom UI which replaces the default renderer.
Trejkaz
What do you mean? I gave you the link on how to accomplish that.Just create your custom ComboBoxRendeder :) The renderer should be based ListCellRenderer. It definitely works - I used it many times.
eugener
How do you suppose I would apply a combo box renderer to a check box?
Trejkaz
Sorry :) For some stupid reason I thought we're talking about JComboBox. As far as JCheckBox it is a well known long standing bug :(
eugener
+1  A: 

You could separate the checkbox and label into their own components and simply make a checkbox without a label. You could also maybe add them to a panel of their own and override the setEnabled() method of the panel to simply enable/disable the checkbox and change the label's color. Take this code snippet for example:

final JCheckBox checkbox = new JCheckBox();
final JLabel label = new JLabel();
JPanel panel = new JPanel() {
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        checkbox.setEnabled(enabled);
        if (enabled)
            label.setForeground(Color.BLACK);
        else
            label.setForeground(Color.GRAY);
    }
};
panel.add(checkbox);
panel.add(label);

Note that checkbox and label must be final to use them in our panel's setEnabled()` method. Depending on how often you're inserting HTML into your checkboxes, you can always create your own component class to do this as well.

public class HTMLCheckBox extends JPanel {
    private JCheckBox checkbox = new JCheckBox();
    private JLabel label = new JLabel();
    private Color disabledColor = Color.GRAY;
    private Color enabledColor = Color.BLACK;

    public HTMLCheckBox(String text) {
        label.setText(text);
        add(checkbox);
        add(label);
    }

    public boolean isSelected() {
        return checkbox.isSelected();
    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        checkbox.setEnabled(enabled);
        if (enabled)
            label.setForeground(enabledColor);
        else
            label.setForeground(disabledColor);
    }
}

And then add your own constructors and methods as you so desire. For example, override setBackground() to have it set the background for the panel, checkbox, and label. A setText() method to change the label text would probably also be convenient. Whatever you'd want it to do. And maybe even setters for enabledColor and disabledColor to allow you to change these at will.

peppertherj
Modifying renderers is also very useful, as in eugener's suggestion. I've come across problems where modifying the renderer made it display my HTML tags, though. It's a little tricky at times, but it does look a lot cleaner. :)
peppertherj
This solution sounds like it will require modifying every view in the application.
Trejkaz
A: 

I gotta stop answering my own questions... must be something to do with time zones and getting time to think about it during sleep.

Somewhere else in the application...

UIManager.put("CheckBoxUI", "package.for.CustomisedWindowsCheckBoxUI");

And then this is the implementation, but this is still pretty hacky and it uses a utility method to generate the HTML colour string which is less than great for posting here.

Note that this only fixes it for Windows L&F. Metal L&F has also been witnessed to have a problem but the solution is the same, just subclass BasicCheckBoxUI instead.

import java.awt.Graphics;

import javax.swing.AbstractButton;
import javax.swing.JComponent;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.View;

import com.sun.java.swing.plaf.windows.WindowsCheckBoxUI;

import com.blah.util.ColourUtils;

/**
 * Customisation of Windows check box UI to fix bugs.
 */
public class CustomisedWindowsCheckBoxUI extends WindowsCheckBoxUI {
    /**
     * Factory method called from Swing.
     *
     * @param b the check box.
     * @return the UI.
     */
    public static ComponentUI createUI(JComponent b) {
        // TODO: Sun have an AppContext they use to store these once per app.
        //  Might be more sociable to use something like that.
        return new CustomisedWindowsCheckBoxUI();
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        AbstractButton b = (AbstractButton) c;

        // Works around a bug in BasicButtonUI where a disabled button with HTML markup in the text will
        // not appear to be disabled.

        // TODO: Find a way to fix this globally for HTML rendering.  It seems odd that it isn't working.
        // I can see the code in BasicHTML.createHTMLView which uses the foreground colour, which is
        // obviously why setForeground() works as a workaround.

        if (b.getForeground() instanceof ColorUIResource) {
            View view = (View) c.getClientProperty(BasicHTML.propertyKey);
            if (view != null) {
                // Ensure that we don't update the renderer if the value hasn't changed.
                String cachedHtmlFor = (String) c.getClientProperty("cachedHtmlFor");
                String key = String.format("%s:%s", c.isEnabled(), b.getText());
                if (!key.equals(cachedHtmlFor)) {
                    c.putClientProperty("cachedHtmlFor", key);
                    if (c.isEnabled()) {
                        BasicHTML.updateRenderer(c, b.getText());
                    } else {
                        BasicHTML.updateRenderer(c, String.format("<html><div style='color: %s'>%s",
                                                                  ColourUtils.toHtmlColour(b.getBackground().darker()),
                                                                  b.getText()));
                    }
                }
            }
        }

        super.paint(g, c);
    }
}
Trejkaz