For displaying attributed strings in the menu when it's popped up, I suggest setting the table column containing the pop-up cell to have it's Content
binding pointing to an NSArrayController
which itself is bound to an NSArray
of NSAttributedStrings
containing all the options, and then putting a delegate on the NSMenu
contained by the pop-up cell, and then doing something like this in the delegate:
- (void)menuNeedsUpdate:(NSMenu*)menu
{
for (NSMenuItem* item in menu.itemArray)
if ([item.representedObject isKindOfClass: [NSAttributedString class]])
{
item.attributedTitle = item.representedObject;
}
}
The binding will have put the un-molested NSAttributedString
into the representedObject
property of the NSMenuItem
. You can find it there and put it into the attributedTitle
property, which will make it show the attributed string in the menu. In sum, a menu item, being drawn in a menu, with it's attributedTitle
property appropriately set, will draw the the styled text.
What's a bit more complicated is making the attributed string draw as intended in the pop-up cell when the menu is not popped-up. NSPopUpButtonCell
appears to render by having an NSMenuItem
that draws for it. Unfortunately, the creation of that particular NSMenuItem
doesn't appear to include pushing the un-molested value into it. Instead the title seems to be sent in as a plain, non-attributed string. I've not been able to devise an elegant solution for this, but I did come up with an inelegant workaround:
First add an NSTextField
column to your NSTableView
that draws the currently selected attributed string correctly (i.e. with attributes). Make that column hidden. Subclass NSPopUpButtonCell
or use a category and associated storage to add a new, private property to NSPopUpButtonCell
. This property will hold a block that you can use at draw time to fetch the corresponding cell from the hidden column. Add an NSTableViewDelegate
, and implement -tableView:dataCellForTableColumn:row:
. When that gets called for the pop-up column, create the block to fetch the cell from the hidden column and shove it into the property on your subclass. Then at draw time, if you have a cell fetcher block, clear out the title
on the menuItem
that it would normally use for rendering, call super (to get the little arrows for the pop-up), then fetch the surrogate cell, and have it draw too. Here's what the code looks like:
@interface AppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate, NSTableViewDelegate>
@property (assign) IBOutlet NSTableColumn *popUpColumn;
@property (assign) IBOutlet NSTableColumn *surrogateColumn;
// ...snip...
@end
@interface SOPopUpButtonCell : NSPopUpButtonCell
typedef NSTextFieldCell* (^CellFetcher)();
@property (nonatomic, copy, readwrite) CellFetcher cellFetcherBlock;
@end
@implementation AppDelegate
// ...snip...
- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
if (nil == tableColumn || self.popUpColumn != tableColumn)
return nil;
SOPopUpButtonCell* defaultCell = (SOPopUpButtonCell*)[tableColumn dataCellForRow: row];
const NSUInteger columnIndex = [[tableView tableColumns] indexOfObject: self.surrogateColumn];
CellFetcher f = ^{
return (NSTextFieldCell*)[tableView preparedCellAtColumn: columnIndex row: row];
};
defaultCell.cellFetcherBlock = f;
return defaultCell;
}
@end
@implementation SOPopUpButtonCell
- (void)setCellFetcherBlock:(CellFetcher)cellFetcherBlock
{
if (_cellFetcherBlock != cellFetcherBlock)
{
if (_cellFetcherBlock)
Block_release(_cellFetcherBlock);
_cellFetcherBlock = cellFetcherBlock ? Block_copy(cellFetcherBlock) : nil;
}
}
- (void)dealloc
{
if (_cellFetcherBlock)
Block_release(_cellFetcherBlock);
[super dealloc];
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
CellFetcher f = self.cellFetcherBlock;
if (f)
self.menuItem.title = @"";
[super drawWithFrame:cellFrame inView:controlView];
if (f)
NSTextFieldCell* surrogateCell = f();
[surrogateCell drawWithFrame: cellFrame inView: controlView];
}
@end
I must admit this makes me feel a little dirty, but it seems to get the job done. I've posted all the code, including the xib with all the associated bindings over on github: Example Project
Hope that helps.