Question

I've got a NSArrayController dynamically populating a table with a bunch of columns, one of them has a popup button. The content of the popup button cell needs to use NSAttributedString as I need to display a scientific variable with subscript (X1 with lowered 1, for example).

Binding the pop up cell's content values to an array of NSAttributedString yields gibberish in the UI as it only understands plain NSString objects.

The menu of the popup button isn't bindable (i.e. not possible to assign dynamically via bindings).

The contents of the popup button menu can't be bound dynamically either.

Can anybody suggest a way (sticking with bindings for at least the rest of the table content) to dynamically populate the NSPopUpButtonCell menu with NSAttributedString objects?

Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top