Creating a PreferencesTabBarButtonUI

July 28, 2008


The preferences widget in Mac OS X looks really nice. This sexier tab component was designed by one of the guys over at Panic for Coda, and was then rolled into OS X 10.5 (I’d link to the original blog where I read this, but I can’t seem to find it).

Besides the sheer amazing fact that Apple actually incorporated an external idea (and a really good one at that), this tab component is a welcome improvement to the Tiger style preferences widget (still found, unfortunately, in iTunes 7.7).

Here’s the rough anatomy of a Mac OS X Leopard style preferences button (drawn in Java of course!):

All the elements (the outer, inner and center fill) are filled with half-height gradients, allowing them to fade to the top and bottom. This is relatively easy to create using Java 2D (you can find the implementation of UnifiedToolbarButtonUI here):

public class PreferencesTabBarButtonUI extends UnifiedToolbarButtonUI {

    /* a fully transparent color to use when fading colors out.*/
    private static final Color TRANSPARENT_COLOR =
            new Color(0,0,0,0);

    /* colors to use when the button's window doesn't have focus. */
    private static final Color UNFOCUSED_BACKGROUND_CENTER_COLOR =
            new Color(0,0,0,29);
    private static final Color UNFOCUSED_INNER_BORDER_COLOR =
            new Color(0,0,0,38);
    private static final Color UNFOCUSED_OUTER_BORDER_COLOR =
            new Color(0,0,0,63);

    /* colors to use when the button's window does have focus. */
    private static final Color FOCUSED_BACKGROUND_CENTER_COLOR =
            new Color(56);
    private static final Color FOCUSED_INNER_BORDER_COLOR =
            new Color(0,0,0,80);
    private static final Color FOCUSED_OUTER_BORDER_COLOR =
            new Color(0,0,0,130);

    @Override
    protected void installDefaults(AbstractButton b) {
        super.installDefaults(b);
        // TODO you should save the original values.
        b.setMargin(new Insets(5,6,4,6));
    }

    @Override
    protected void uninstallDefaults(AbstractButton b) {
        super.uninstallDefaults(b);
        // TODO you should restore the original values.
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        AbstractButton b = (AbstractButton) c;
        ButtonModel model = b.getModel();
        // if the button is selected, draw the selection background.
        if (model.isSelected()) {
            Graphics2D graphics = (Graphics2D) g.create();
            paintSelectedButtonBackground(b, graphics);
            graphics.dispose();
        }
        super.paint(g, c);
    }

    private static void paintSelectedButtonBackground(AbstractButton button,
                                                      Graphics2D graphics) {
        // determine if the containing window has focus.
        Window window = SwingUtilities.getWindowAncestor(component);
        boolean isButtonsWindowFocused = window != null && window.isFocused();

        // get center graident colors, based on the focus state of the
        // containing window.
        Color centerColor = isButtonsWindowFocused
                ? FOCUSED_BACKGROUND_CENTER_COLOR
                : UNFOCUSED_BACKGROUND_CENTER_COLOR;
        Color innerBorderColor = isButtonsWindowFocused
                ? FOCUSED_INNER_BORDER_COLOR
                : UNFOCUSED_INNER_BORDER_COLOR;
        Color outterBorderColor = isButtonsWindowFocused
                ? FOCUSED_OUTER_BORDER_COLOR
                : UNFOCUSED_OUTER_BORDER_COLOR;

        // calculate the first gradient's stop y position, and the second
        // gradient's start y position. thesve values, shouldn't overlap, as
        // transparent colors are addative, and would thus result in
        // bleed-through.
        int topMiddleY = button.getHeight()/2;
        int bottomMiddleY = button.getHeight()/2+1;

        // create the top and bottom fill paint.
        Paint topCenterPaint = new GradientPaint(
                0f,0f,TRANSPARENT_COLOR,1f,topMiddleY,centerColor);
        Paint bottomCenterPaint = new GradientPaint(
                0f,bottomMiddleY,centerColor,1f,button.getHeight(),TRANSPARENT_COLOR);

        // draw the selection background gradient. note that we don't want to
        // draw where the border is as tranparent colors will bleed together.
        graphics.setPaint(topCenterPaint);
        int borderWidth = 2;
        int fillWidth = button.getWidth() - borderWidth * 2;
        graphics.fillRect(borderWidth,0,fillWidth,topMiddleY);
        graphics.setPaint(bottomCenterPaint);
        graphics.fillRect(borderWidth,topMiddleY,fillWidth,button.getHeight());

        // create the outter border top and bottom paint.
        Paint topOuterBorderPaint = new GradientPaint(
                0f,0f,TRANSPARENT_COLOR,1f,topMiddleY,outterBorderColor);
        Paint bottomOuterBorderPaint = new GradientPaint(
                0f,bottomMiddleY,outterBorderColor,1f,button.getHeight(),TRANSPARENT_COLOR);

        // draw the outter border line.
        graphics.setPaint(topOuterBorderPaint);
        int outterLeftBorderX = 0;
        int outterRightBorderX = button.getWidth() - 1;
        graphics.drawLine(outterLeftBorderX,0,outterLeftBorderX,topMiddleY);
        graphics.drawLine(outterRightBorderX,0,outterRightBorderX,topMiddleY);
        graphics.setPaint(bottomOuterBorderPaint);
        graphics.drawLine(outterLeftBorderX,bottomMiddleY,outterLeftBorderX,button.getHeight());
        graphics.drawLine(outterRightBorderX,bottomMiddleY,outterRightBorderX,button.getHeight());

        // create the inner border top and bottom paint.
        Paint topInnerBorderPaint = new GradientPaint(
                0f,0f,TRANSPARENT_COLOR,1f,topMiddleY,innerBorderColor);
        Paint bottomInnerBorderPaint = new GradientPaint(
                0f,bottomMiddleY,innerBorderColor,1f,button.getHeight(),TRANSPARENT_COLOR);

        // draw the inner border line.
        graphics.setPaint(topInnerBorderPaint);
        int innerLeftBorderX = 1;
        int innerRightBorderX = button.getWidth() - 2;
        graphics.drawLine(innerLeftBorderX,0,innerLeftBorderX,topMiddleY);
        graphics.drawLine(innerRightBorderX,0,innerRightBorderX,topMiddleY);
        graphics.setPaint(bottomInnerBorderPaint);
        graphics.drawLine(innerLeftBorderX,bottomMiddleY,innerLeftBorderX,button.getHeight());
        graphics.drawLine(innerRightBorderX,bottomMiddleY,innerRightBorderX,button.getHeight());
    }
}

You may be wondering why I didn’t create a utility method for some of the line drawing code above. When working with Java 2D, I find it more straight forward to keep all the drawing code, even if it’s redundant, in a sequential (script like) form. I’ve found that this makes the code easier to follow.

Advertisements

11 Responses to “Creating a PreferencesTabBarButtonUI”

  1. Felix Says:

    BTW, Leopard’s Aqua LAF provides the same look for JToogleButton in a JToolBar (I think that’s the reason, couldn’t figure it out totally). Additionally, it has a nice blue, icon shaped focus border.

  2. Ken Says:

    Nice tip Felix! I didn’t know the Aqua L&F provided the gradient button rendering. Seems like Apple should document that, as its a nice little feature. However, it would be nice if this rendering were controlled via a client property instead of, or in addition to, the parent container mechanism.

  3. Ken Says:

    One additional note – with my ButtonUIs, you’ll get the emphasized text rendering, which isn’t provided by the standard rendering. Its unfortunate that this much rework is required to get the high-fidelity apps we desire.

  4. Felix Says:

    Maybe, there is a doc for this: Maybe Apple meant this behaviour with Radar #3479922 on the “Java for Mac OS X v10.5 RN…” (http://developer.apple.com/releasenotes/Java/JavaLeopardRN/ResolvedIssues/chapter_3_section_5.html#//apple_ref/doc/uid/TP40006634-CH3-DontLinkElementID_106). But none of that cool emphasising-stuff : ).
    Yeah, rendering depending on parent components isn’t what I would call good Java-UIing.

  5. Ken Says:

    I think Radar item #347992 is referencing the pressed and disabled icon rendering, which I talked about here. I’ll ping the java-dev mailing list to find out if this has been documented anywhere.


  6. Here’s a video link for a presentation Cabel Sasser gave at C[4] this year where he provides some insight about the origins of this new toolbar style (seek to 0:22:00). The whole thing is actually pretty entertaining and enlightening.

    http://www.viddler.com/explore/rentzsch/videos/14/

  7. Ken Says:

    Very cool…this is much more informative than the original blog posting I read.

  8. data-loss Says:

    Isn’t it better to get the same effect with:
    – fill Rectangle around it with the inside color,
    – draw Rectangle around it with the border color,
    – set the composite to AlphaComposite.DstIn and draw one cyclic gradient from fully opaque to transparent.

    The last operation will remove the alpha component gradually from the bordered rectangle at the top and bottom.

    That will be like 5 lines of code, I guess?

  9. data-loss Says:

    I went ahead and even wrote the code, actually it’s 8 lines of painting, not 5…:

    Dimension dim = button.getSize();
    gr.setColor(Color.LIGHT_GRAY);
    gr.fill(new Rectangle(dim));
    gr.setColor(Color.DARK_GRAY);
    gr.draw(new Rectangle(dim));
    gr.setComposite(AlphaComposite.DstIn);
    gr.setPaint(new GradientPaint(0, 0, new Color(0, 0, 0, 0), 0, dim.height/2f, new Color(0, 0, 0, 1f), true));
    gr.fill(new Rectangle(dim));

    This can be further trimmed down to 5 if you use JDK1.6:

    Dimension dim = button.getSize();
    gr.setPaint(new LinearGradientPaint(0, 0, dim.width, 0, new float[] {0, 1f/dim.width, 1-2f/dim.width, 1f-1f/dim.width}, new Color[]{Color.DARK_GRAY, Color.LIGHT_GRAY, Color.LIGHT_GRAY, Color.DARK_GRAY}));
    gr.fill(new Rectangle(dim));
    gr.setComposite(AlphaComposite.DstIn);
    gr.setPaint(new GradientPaint(0, 0, new Color(0, 0, 0, 0), 0, dim.height/2f, new Color(0, 0, 0, 1f), true));
    gr.fill(new Rectangle(dim));

    Enjoy!

  10. Ken Says:

    I was using JDK 5, so LinearGradient wasn’t available.

  11. data-loss Says:

    The first one (8 lines) does not use LinearGradient.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: