bdunagan

Brian Dunagan

September 3 2008
Cocoa Tutorial: Source List Badges

Source lists have become a standard user interface element at this point in the Mac environment. Apple’s Mail, iCal, iTunes, and iPhoto use them, along with many third-party applications including Delicious Monster’s Delicious Library and Panic’s Coda. They’re easily created using an NSTableView or NSOutlineView and changing the selectionHighlightStyle (highlight in Interface Builder) to “Source List”, and soon I’ll post my own version to help anyone along.

However, badges are just as prevalent in current Cocoa applications. They are the small numbers on the right side of source lists, letting you know how many unread emails or new podcasts you have. Yet, I’ve only found one tutorial for how to create them. While it’s an excellent overview, it’s written in Java, so it doesn’t directly port to Objective-C. Fortunately, it’s not difficult, and I’m including the source code from BDBadgeCell.m below for anyone to use (MIT license). I used NSBezierPath to create the oval and a bit of string drawing code to produce the element. Check out the screenshots below.

I included a screenshot of Apple’s Mail badges for reference; the font is slightly different, and I’d appreciate any pointers to get the look exactly right. On the left is Apple’s Mail badges, and on the right is my BDBadge badges. The two applications are in focus in the top row and out of focus in the bottom row.

Add the following code to your cell subclass. (I subclassed NSBrowserCell.)

// snippet from BDBadgeCell.m //

// Initialize badge variables, based on Apple Mail.
static int BADGE_BUFFER_LEFT = 4;
static int BADGE_BUFFER_TOP = 3;
static int BADGE_BUFFER_LEFT_SMALL = 2;
static int BADGE_CIRCLE_BUFFER_RIGHT = 5;
static int BADGE_TEXT_HEIGHT = 14;
static int BADGE_X_RADIUS = 7;
static int BADGE_Y_RADIUS = 8;
static int BADGE_TEXT_SMALL = 20;

- (void)setBadgeCount:(int)newBadgeCount
{
    badgeCount = newBadgeCount;
}

- (NSColor *)highlightColorInView:(NSView *)controlView
{
    return [NSColor clearColor];
}

- (void)drawInteriorWithFrame:(NSRect)aRect inView:(NSView *)controlView
{
    // Draw NSBrowserCell.
    [super drawInteriorWithFrame:aRect inView:controlView];

    // Set up badge string and size.
    NSString *badge = [NSString stringWithFormat:@"%d", badgeCount];
    NSSize badgeNumSize = [badge sizeWithAttributes:nil];

    // Calculate the badge's coordinates.
    int badgeWidth = badgeNumSize.width + BADGE_BUFFER_LEFT * 2;
    if (badgeWidth < BADGE_TEXT_SMALL)
    {
        // The text is too short. Decrease the badge's size.
        badgeWidth = BADGE_TEXT_SMALL;
    }
    int badgeX = aRect.origin.x + aRect.size.width - BADGE_CIRCLE_BUFFER_RIGHT - badgeWidth;
    int badgeY = aRect.origin.y + BADGE_BUFFER_TOP;
    int badgeNumX = badgeX + BADGE_BUFFER_LEFT;
    if (badgeWidth == BADGE_TEXT_SMALL)
    {
        badgeNumX += BADGE_BUFFER_LEFT_SMALL;
    }
    NSRect badgeRect = NSMakeRect(badgeX, badgeY, badgeWidth, BADGE_TEXT_HEIGHT);

    // Draw the badge and number.
    NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeRect xRadius:BADGE_X_RADIUS yRadius:BADGE_Y_RADIUS];
    if ([[NSApp mainWindow] isVisible] && ![self isHighlighted])
    {
        // The row is not selected and the window is in focus.

        [[NSColor colorWithCalibratedRed:.53 green:.60 blue:.74 alpha:1.0] set];
        [badgePath fill];
        NSDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setValue:[NSFont boldSystemFontOfSize:11] forKey:NSFontAttributeName];
        [dict setValue:[NSNumber numberWithFloat:-.25] forKey:NSKernAttributeName];
        [dict setValue:[NSColor whiteColor] forKey:NSForegroundColorAttributeName];
        [badge drawAtPoint:NSMakePoint(badgeNumX,badgeY) withAttributes:dict];
    }
    else if ([[NSApp mainWindow] isVisible])
    {
        // The row is selected and the window is in focus.
        [[NSColor whiteColor] set];
        [badgePath fill];
        NSDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setValue:[NSFont boldSystemFontOfSize:11] forKey:NSFontAttributeName];
        [dict setValue:[NSNumber numberWithFloat:-.25] forKey:NSKernAttributeName];
        [dict setValue:[NSColor alternateSelectedControlColor] forKey:NSForegroundColorAttributeName];
        [badge drawAtPoint:NSMakePoint(badgeNumX,badgeY) withAttributes:dict];
    }
    else if (![[NSApp mainWindow] isVisible] && ![self isHighlighted])
    {
        // The row is not selected and the window is not in focus.
        [[NSColor disabledControlTextColor] set];
        [badgePath fill];
        NSDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setValue:[NSFont boldSystemFontOfSize:11] forKey:NSFontAttributeName];
        [dict setValue:[NSNumber numberWithFloat:-.25] forKey:NSKernAttributeName];
        [dict setValue:[NSColor whiteColor] forKey:NSForegroundColorAttributeName];
        [badge drawAtPoint:NSMakePoint(badgeNumX,badgeY) withAttributes:dict];
    }
    else
    {
        // The row is selected and the window is not in focus.
        [[NSColor whiteColor] set];
        [badgePath fill];
        NSDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setValue:[NSFont boldSystemFontOfSize:11] forKey:NSFontAttributeName];
        [dict setValue:[NSNumber numberWithFloat:-.25] forKey:NSKernAttributeName];
        [dict setValue:[NSColor disabledControlTextColor] forKey:NSForegroundColorAttributeName];
        [badge drawAtPoint:NSMakePoint(badgeNumX,badgeY) withAttributes:dict];
    }
}

2010-01-25 Update: Perspx has a far better implementation of source list badges as part of his PXSourceList project.

fill the void Cocoa Tutorial: iTunes Link Arrows
LinkedIn GitHub Email