Search code examples
macoscocoanstableviewnspopupbuttoncell

Getting duplicate header button cell in NSTableView when using NSPopUpButtonCell


I have a dynamic NSTableView which can add a number of columns depending on the data provided. For each column I have set the header cell to be a NSPopUpButtonCell. (Side-note: I've had to use a custom subclass class for NSTableHeaderView otherwise the menu doesn't pop-up). All works well, apart from a duplicate or extra header button cell on the top right. It mirrors perfectly the previous column selection as shown in screenshots. My question is how do I stop the NSTableView from recycling the previous popup header cell? (By the way I have tried the setCornerView method but that only effects the header area above the vertical scrollbar.)

Duplicate column header popupbuttoncell

Duplicate column header popupbuttoncell mirrors the selected value of the previous column


Solution

  • I came across the same problem this week. I went with the quick fix,

    [_tableView sizeLastColumnToFit];
    

    (However, after discussion with OP this requires that you use a subclass of NSPopUpButtonCell in the header and also NSTableHeaderView. I attach my solution below)

    You can to this by combining the approaches outlined here,

    1. PopUpTableHeaderCell
    2. DataTableHeaderView

    Here is a simplified snippet,

    // PopUpTableHeaderCell.h
    #import <Cocoa/Cocoa.h>
    /* Credit: http://www.cocoabuilder.com/archive/cocoa/133285-placing-controls-inside-table-header-view-solution.html#133285 */
    
    @interface PopUpTableHeaderCell : NSPopUpButtonCell
    @property (strong) NSTableHeaderCell *tableHeaderCell; // Just used for drawing the background
    
    @end
    
    // PopUpTableHeaderCell.m
    @implementation PopUpTableHeaderCell
    
    - (id)init {
        if (self = [super init]){
    
            // Init our table header cell and set a blank title, ready for drawing
            _tableHeaderCell = [[NSTableHeaderCell alloc] init];
            [_tableHeaderCell setTitle:@""];
    
            // Set up the popup cell attributes
            [self setControlSize:NSMiniControlSize];
            [self setArrowPosition:NSPopUpNoArrow];
            [self setBordered:NO];
            [self setBezeled:NO];
            [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
        }
        return self;
    }
    
    // We do all drawing ourselves to make our popup cell look like a header cell
    - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView{
    
        [_tableHeaderCell drawWithFrame:cellFrame inView:controlView];
    
        // Now draw the text and image over the top
        [self drawInteriorWithFrame:cellFrame inView:controlView];
    }
    
    @end
    

    Now for the NSTableViewHeader subclass.

    //DataTableHeaderView.h
    #import <Cocoa/Cocoa.h>
    
    /* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
    
    @interface DataTableHeaderView : NSTableHeaderView
    @end
    
    //DataTableHeaderView.m
    #import "DataTableHeaderView.h"
    
    /* Credit: http://forums.macnn.com/79/developer-center/304072/problem-of-nspopupbuttoncell-within-nstableheaderview/ */
    
    @implementation DataTableHeaderView
    
    - (id)initWithFrame:(NSRect)frame {
    
        self = [super initWithFrame:frame];
        if (self) {
            // Initialization code here.
        }
        return self;
    }
    
    - (void)mouseDown:(NSEvent *)theEvent {
    
        // Figure which column, if any, was clicked
        NSPoint clickedPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
        NSInteger columnIndex = [self columnAtPoint:clickedPoint];
        if (columnIndex < 0) {
            return [super mouseDown:theEvent];
        }
    
        NSRect columnRect = [self headerRectOfColumn:columnIndex];
    
        // I want to preserve column resizing. If you do not, remove this
        if (![self mouse:clickedPoint inRect:NSInsetRect(columnRect, 3, 0)]) {
            return [super mouseDown:theEvent];
        }
    
        // Now, pop the cell's menu
        [[[self.tableView.tableColumns objectAtIndex:columnIndex] headerCell] performClickWithFrame:columnRect inView:self];
        [self setNeedsDisplay:YES];
    }
    
    - (BOOL)isOpaque {
        return NO;
    }
    
    
    - (void)drawRect:(NSRect)dirtyRect {
        [super drawRect:dirtyRect]; 
        // Drawing code here.
    }
    
    @end
    

    You can tie everything together in the AppDelegate -awakeFromNib or similar,

    -(void) awakeFromNib {
    
        /* NB the NSTableHeaderView class is changed to be an DataTableHeaderView in IB! */
    
        NSUInteger numberOfColumnsWanted = 5;
        for (NSUInteger i=0; i<numberOfColumnsWanted; i++) {
    
            PopUpTableHeaderCell *headerCell;
            headerCell = [[PopUpTableHeaderCell alloc] init];
    
            [headerCell addItemWithTitle:@"item 1"];
            [headerCell addItemWithTitle:@"item 2"];
            [headerCell addItemWithTitle:@"item 3"];
    
            NSTableColumn *column;
            [column setHeaderCell:headerCell];
            [column sizeToFit];
    
            [_tableView addTableColumn:column];
        }
    
        /* If we don't do this we get a final (space filling) column with an unclickable (dummy) header */
        [_tableView sizeLastColumnToFit];
    
    }
    

    Other than that I haven't figured out how to properly correct the drawing in that region.

    It seems like it's the image of the last cell that is being duplicated. So I slightly more hack-ish approach would be to add a extra column to your table view with a blank name and which intentionally ignores the mouse clicks. Hopefully by setting the display properties of the last column you can make it look the way you want.

    I couldn't find any NSTableView or NSTableViewDelegate method that allow control of this region, so may any other solution would be very complicated. I would be interested in a nice solution too, but I hope this gets you started!