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.

At rest
Disabled
Pressed

Creating the pressed and disabled versions of the icons is easy to achieve using a BufferedImage and a mask. Simply do the following:

  1. In the paintIcon method, create a BufferedImage
  2. Draw the icon into that image
  3. Fill a rectangle with the appropriate transparency over the icon
  4. 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:

  1. Set the graphics context color to the emphasis color
  2. Call SwingUtiltites2. drawStringUnderlineCharAt, shifting the y value down a pixel
  3. Set the graphics context color to the text color
  4. 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();
    }
}
Advertisements

13 Responses to “Creating a UnifiedToolBarButtonUI”

  1. Felix Says:

    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).

  2. Felix Says:

    I’ll llok into this the next days.

  3. Felix Says:

    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.

  4. Ken Says:

    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


  5. […] Orr writes about unified toolbar buttons in his quest to emulate the appearance of native Mac applications. Surprisingly, this entry […]

  6. Ken Says:

    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.

  7. Felix Says:

    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).

  8. Ken Says:

    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


  9. […] 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): […]

  10. rbs Says:

    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?

  11. Ken Says:

    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.

  12. rbs Says:

    I expected it was something like that, i.e., alpha = 0. Thanks.


  13. […] 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 […]


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: