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.
July 28, 2008 at 9:54 am
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.
July 28, 2008 at 10:51 am
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.
July 28, 2008 at 11:04 am
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.
July 28, 2008 at 12:21 pm
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.
July 28, 2008 at 12:57 pm
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.
July 28, 2008 at 10:59 pm
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/
July 28, 2008 at 11:30 pm
Very cool…this is much more informative than the original blog posting I read.
April 10, 2009 at 4:33 pm
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?
April 10, 2009 at 5:16 pm
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!
April 10, 2009 at 10:31 pm
I was using JDK 5, so LinearGradient wasn’t available.
April 10, 2009 at 10:37 pm
The first one (8 lines) does not use LinearGradient.