In developing a new application, I ran into the need for a NSPopupButton for selecting a preference. The menu that you see popup when you click on a NSPopupButton is really just an NSMenu full of NSMenuItems. The same menu items that you might see in a contextual menu or even from the main menu at the top of the screen.
I was looking for some way to display a Title as regular, then right under it some kind of subtitle. Naturally, NSMenuItems are designed to handle just a single title. It seemed like if I wanted to display anything other than a single title, I was going to have to take advantage of NSMenuItem’s new ability to host a generic NSView.
Problem
Getting your custom NSView in to a NSMenuItem is easy. I actually got it displaying rather nicely. The problems came when I wanted my NSView and NSMenuItem to behave like they would as per regular.
Passing clicks from my NSView to the NSPopupButton proved to be rather difficult. Moreover, it felt like a huge kludge as it tied these items to a single control. I also wanted to reuse these in the menu up top.
Even with the kludge, I decided to proceed on. Next up was displaying the checkbox (NSOnState) image. For some reason, the checkbox image returned by [NSImage imageNamed:NSOnStateImage] is different from the checkbox image used in the rest of the system. It was skinner and looked out of place.
Overall, making a custom view with this much kludge and not quite feeling right grated on me. I wasted 2 days to get this far, but it doesn’t live up to the user experience that I want to give users and expect from software on the Mac.
Solution
In my rush to get my NSMenuItem running, I overlooked a a critical feature: NSMenuItem titles can take an NSAttributedString.
When initializing your NSMenuItem we can use an attributed string to set our colors. \n also works for creating new lines.
// assume we have an NSMenuItem alloc'ed/init'ed named item.
// we also have a model objected that responds to the "title" and "subtitle" messages
// named myObject
NSString *title = [NSString stringWithFormat:@"%@\n%@",[myObject title],[myObject subtitle]];
NSMutableAttributedString *attributed_title = [[NSMutableAttributedString alloc] initWithString:title];
// This is the system default for controls. anything else and it looks off
NSDictionary *title_options = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:@"Lucida Grande" size:13.0f],NSFontAttributeName,nil];
//make our subtitle a different color as it is just auxillary information
NSDictionary *sub_title_options = [NSDictionary dictionaryWithObjectsAndKeys:[NSColor disabledControlTextColor],NSForegroundColorAttributeName,nil];
// apply our color attributes to the ranges of the string they are applicable to...
[attributed_title addAttributes:title_options range:[title rangeOfString:[myObject title]]];
[attributed_title addAttributes:sub_title_options range:[title rangeOfString:[myObject subtitle]]];
// finally set our attributed to the menu item
[item setAttributedTitle:attributed_title];
[item setRepresentedObject:myObject];
// clean up
[attributed_title release];
With the above in place we are halfway there. Our new shiny menu item looks great and it fits in. Most importantly there is zero kludge. Actions go where we want them when we want them. The NSOnState image looks like it does in the rest of the system. Life is hunky-dory.
![]()
Until we highlight it.
Our title and state images turn white when highlighted as expected. However, our subtitle…not so much.
Luckily we can fix this without too much trouble. NSMenu’s has a lovely delegate that tells us when it is about to highlight something with it’s menu:willHighlightItem: call. “Will” is the keyword that we tips us of we can use this. It means that our currently highlighted item is still selected, so we can revert any changes made.
*Update: * I had left a huge block of code that smelled bad and left it as an exercise to the reader to clean it up. I decided to remove some of the smell myself. If you have ideas on how the code below could be refactored even better, please let a comment below.
// set our controller to be the delegate of our NSMenu. In the case of a NSPopupButton,
// you can access it with the [myButton menu] method.
- (void)setForegroundColor:(NSColor *)foregroundColor
onAttributedString:(NSMutableAttributedString *)attributedString
forRangeOfString:(NSString *)string {
NSRange range = [[attributedString mutableString] rangeOfString:string];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:foregroundColor,NSForegroundColorAttributeName,nil];
[attributedString setAttributes:attributes range:range];
}
- (void)willHighlightItem:(NSMenuItem *)item currentlyHighlightedItem:(NSMenuItem *)currentItem {
if (currentItem != nil) {
NSString *subtitle = [(MyModelObject *)[currentItem representedObject] subtitle];
NSMutableAttributedString *attributedString = [[currentItem attributedTitle] mutableCopy];
[self setForegroundColor:[NSColor disabledControlTextColor] onAttributedString:attributedString forRangeOfString:subtitle];
[currentItem setAttributedTitle:attributedString];
[attributedString release];
}
NSString *subtitle = [(MyModelObject *)[item representedObject] subtitle];
NSMutableAttributedString *attributedString = [[item attributedTitle] mutableCopy];
[self setForegroundColor:[NSColor selectedMenuItemTextColor] onAttributedString:attributedString forRangeOfString:subtitle];
[item setAttributedTitle:attributedString];
[attributedString release];
}

Conclusion
With just the few lines of code above, we can create a custom looking NSMenuItem that behaves exactly as we would expect it to. What’s more by using an NSAttributedString isntead of a custom view, our “custom control” get accessibility for free. This means users with impaired sight get the same first-class experience as everyone else. Talk about a win-win-win.






