Creating a UnifiedToolBarButtonUI
July 18, 2008
A little while back, I talked about creating a Unified Toolbar. In that post I included a class called an EmphasizedLabel, which was an extension of JLabel that drew emphasis color (i.e. a shadow) underneath the text.
The time came to implement a Unified Toolbar button, and I ran up against the same issue, namely the need to draw emphasized text. This time, I decided not to subclass, but instead to write a custom UI. In fact I liked the simplicity and elegance of the UI implementation so much, that I went back and created an EmphasizedLabelUI to replace EmphasizedLabel, which has the nice side effect of working with any extension of JLabel.
Extending BasicButtonUI is quite easy – much easier than trying to implement the full ButtonUI interface. Because we’re extending BasicButtonUI, though, we don’t get the icon effects provided by the Mac ButtonUI, like the pressed icon and disabled icon.
Creating the pressed and disabled versions of the icons is easy to achieve using a BufferedImage and a mask. Simply do the following:
- In the paintIcon method, create a BufferedImage
- Draw the icon into that image
- Fill a rectangle with the appropriate transparency over the icon
- Draw the buffered image into the passed graphics context
We need the extra step of drawing into a BufferedImage of type BufferedImage.TYPE_INT_ARGB because the standard buffer a component draws into doesn’t have an alpha channel (Chet Hasse explains this in greater detail in his book, Filthy Rich Clients).
Finally, we override the paintText method and do the following:
- Set the graphics context color to the emphasis color
- Call SwingUtiltites2. drawStringUnderlineCharAt, shifting the y value down a pixel
- Set the graphics context color to the text color
- Call BasicGraphicsUtils.drawStringUnderlineCharAt
Here’s the full code:
public class UnifiedToolbarButtonUI extends BasicButtonUI { private static final Color EMPTY_COLOR = new Color(0,0,0,0); private static final Color PRESSED_BUTTON_MASK_COLOR = new Color(0,0,0,116); private static final Color DISABLED_BUTTON_MASK_COLOR = new Color(0,0,0,39); @Override protected void installDefaults(AbstractButton b) { super.installDefaults(b); // TODO you should save the original values before setting them below. b.setHorizontalTextPosition(AbstractButton.CENTER); b.setVerticalTextPosition(AbstractButton.BOTTOM); b.setIconTextGap(0); b.setMargin(new Insets(0,0,0,0)); b.setFont(UIManager.getFont("Button.font").deriveFont(11.0f)); } @Override protected void uninstallDefaults(AbstractButton b) { super.uninstallDefaults(b); // TODO you should install the original values overridden when install // was called this. } @Override protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect) { AbstractButton b = (AbstractButton) c; ButtonModel model = b.getModel(); // create a buffered image to draw the icon and mask into. BufferedImage image = new BufferedImage(iconRect.width, iconRect.height, BufferedImage.TYPE_INT_ARGB); // create a graphics context from the buffered image. Graphics2D graphics = (Graphics2D) image.getGraphics(); // paint the icon into the buffered image. b.getIcon().paintIcon(c, graphics, 0, 0); // set the composite on the graphics context to SrcAtop which blends the // source with the destination, and thus transparent pixels in the // destination, remain transparent. graphics.setComposite(AlphaComposite.SrcAtop); // set the mask color based on the button models state. if (!model.isEnabled()) { graphics.setColor(DISABLED_BUTTON_MASK_COLOR); } else if (model.isArmed()) { graphics.setColor(PRESSED_BUTTON_MASK_COLOR); } else { graphics.setColor(new Color(0,0,0,0)); } // fill a rectangle with the mask color. graphics.fillRect(0, 0, iconRect.width, iconRect.height); graphics.dispose(); g.drawImage(image, iconRect.x, iconRect.y, null); } @Override protected void paintText(Graphics g, JComponent c, Rectangle textRect, String text) { Graphics2D graphics = (Graphics2D) g.create(); AbstractButton b = (AbstractButton) c; ButtonModel model = b.getModel(); FontMetrics fm = c.getFontMetrics(c.getFont()); // 1) Draw the emphasis text. graphics.setColor(model.isArmed() ? EMPTY_COLOR : EmphasizedLabelUI.DEFAULT_EMPHASIS_COLOR); BasicGraphicsUtils.drawStringUnderlineCharAt(graphics, text, -1, textRect.x, textRect.y + 1 + fm.getAscent()); // 2) Draw the text. graphics.setColor(model.isEnabled() ? EmphasizedLabelUI.DEFAULT_FOCUSED_FONT_COLOR : EmphasizedLabelUI.DEFAULT_DISABLED_FONT_COLOR); BasicGraphicsUtils.drawStringUnderlineCharAt(graphics, text, -1, textRect.x, textRect.y + fm.getAscent()); graphics.dispose(); } }
July 18, 2008 at 2:07 pm
I like this.
Maybe – just an idea – it would be possible to set some client properties to make it a (almost) “real” Mac Toolbar button: small sizes for the icons and switchable text display. Best would be to put them on the JToolBar itself and the TBUI reads them from the parent (this is no good Swing, I think, but should work).
To add something more, a popup-menu would be cool. But this is overkill for just a good looking toolbar. Especially because JPopUpMenus don’t look good at all on Leopard (no round borders and so on).
July 18, 2008 at 2:08 pm
I’ll llok into this the next days.
July 18, 2008 at 2:14 pm
Sorry, my first comment was not forwarded (no idea why).
It would be cool — just an idea — to set client properties on the JToolBar to specify the size of the icons and wether text should be displayed or not. I don’t know if it’s good Swing style to let ButtonUIs read the client properties of their parent. For those properties a pop up menu would be useful (although JPopUpMenus on Leopard look quite not-native as they have no round corners).
I’ll look into this the next days.
July 18, 2008 at 5:41 pm
Great thoughts Felix. I was also thinking that it would be useful to encapsulate the popdown button that Apple usesin toolbars. I don’t know if I’d do this with a client property – that could work. It might be more straight forward to just create a component that aggregates a button and a popup menu.
Let me know what you come up with.
-Ken
July 21, 2008 at 6:11 am
[…] Orr writes about unified toolbar buttons in his quest to emulate the appearance of native Mac applications. Surprisingly, this entry […]
July 21, 2008 at 5:00 pm
Kirill Grouchnikov correctly pointed out on his blog this week that I should have used BasicGraphicsUtils.drawStringUnderlineCharAt instead of SwingUtilities2.drawStringUnderlineCharAt, which is an unsupported class.
July 21, 2008 at 7:35 pm
And maybe you could also call an additional model.isArmed() every time you call model.isPressed(); the visual feedback now is not how it’s for “normal” tbButtons (the native ones).
July 21, 2008 at 8:00 pm
Hey Felix,
Great point…Apple’s behavior uses the isArmed state. I’ve updated the code to reflect your suggestion. I’ve also incorporated Kirill’s suggestion of using BasicGraphicUtils.drawStringUnderlineCharAt.
-Ken
July 28, 2008 at 10:41 am
[…] 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): […]
August 4, 2008 at 7:17 pm
The MacColorUtils class referenced in the code doesn’t appear elsewhere in your blog. So I’m wondering, what’s the definition of MacColorUtils.EMPTY_COLOR?
August 4, 2008 at 7:22 pm
Sorry about that…MacColorUtils.EMPTY_COLOR = new Color(0,0,0,0). This is a fully transparent color.
I’ve updated the post with the change.
August 4, 2008 at 7:24 pm
I expected it was something like that, i.e., alpha = 0. Thanks.
August 24, 2008 at 8:07 pm
[…] which will make the pixels in the image transparent (I briefly discussed this technique here). Note that we only use this punch-out effect when the list item is […]