objective-ccocoamulti-selectnsmatrixnsbuttoncell

NSMatrix with multiple toggle buttons?


I am trying to create an NSMatrix of NSButtonCells where between zero and four buttons can be selected (toggled on). I have tried the following (test) code, but am not sure how I can provide the functionality I require. Perhaps it's not possible with NSMatrix and I need to look at an alternative control, or create my own?

@interface MatrixView : NSView
{
    NSScrollView *_scrollView;
    NSMatrix *_matrixView;
}
@end

@implementation MatrixView

- (id)initWithFrame:(NSRect)frameRect
{
    NSLog(@"initWithFrame. frameRect=%@", NSStringFromRect(frameRect));
    self = [super initWithFrame:frameRect];
    if (self != nil)
    {
        _scrollView = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, frameRect.size.width, frameRect.size.height)];
        [_scrollView setBorderType:NSNoBorder];
        [_scrollView setHasVerticalScroller:YES];
        [_scrollView setHasHorizontalScroller:NO];
        [_scrollView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];

        NSSize contentSize = [_scrollView contentSize];
        contentSize.height = 300;

        // Make it 3 x however-many-buttons-will-fit-the-height
        CGFloat gap = 8.0;
        CGFloat width = (contentSize.width / 3.0) - (gap * 2.0);
        NSUInteger rows = (contentSize.height / (width + gap));

        NSLog(@"width=%f, rows=%lu", width, rows);

        NSButtonCell *prototype = [[NSButtonCell alloc] init];
        [prototype setTitle:@"Hello"];
        [prototype setButtonType:NSToggleButton];
        [prototype setShowsStateBy:NSChangeGrayCellMask];
        _matrixView = [[NSMatrix alloc] initWithFrame:NSMakeRect(0, 0, contentSize.width, contentSize.height)
                                                 mode:NSListModeMatrix
                                            prototype:prototype
                                         numberOfRows:rows
                                      numberOfColumns:3];
        [_matrixView setCellSize:NSMakeSize(width, width)];
        [_matrixView setIntercellSpacing:NSMakeSize(gap, gap)];
        [_matrixView setAllowsEmptySelection:YES];
        [_matrixView sizeToCells];
        [_scrollView setDocumentView:_matrixView];
        [self addSubview:_scrollView];
        [self setAutoresizesSubviews:YES];
        [prototype release];
    }

    return self;
}

...

Solution

  • I got this to work with the following subclass of NSMatrix. I added one property, onCount, to keep track of how many buttons were in the on state:

    @implementation RDMatrix
    @synthesize onCount;
    
    -(id) initWithParentView:(NSView *) cv {
        NSButtonCell *theCell = [[NSButtonCell alloc ]init];
        theCell.bezelStyle = NSSmallSquareBezelStyle;
        theCell.buttonType = NSPushOnPushOffButton;
        theCell.title = @"";
        if (self = [super initWithFrame:NSMakeRect(200,150,1,1) mode:2 prototype:theCell numberOfRows:4 numberOfColumns:4]){ 
            [self setSelectionByRect:FALSE];
            [self setCellSize:NSMakeSize(40,40)];
            [self sizeToCells];
            self.target = self;
            self.action = @selector(buttonClick:);
            self.drawsBackground = FALSE;
            self.autoresizingMask = 8;
            self.allowsEmptySelection = TRUE;
            self.mode = NSHighlightModeMatrix;
            self.onCount = 0;
            [cv addSubview:self];
            return self;
        }
        return nil;
    }
    
    
    -(IBAction)buttonClick:(NSMatrix *)sender {
        NSUInteger onOrOff =[sender.selectedCells.lastObject state];
        if (onOrOff) {
            self.onCount += 1;
        }else{
            self.onCount -= 1;
        }
        NSLog(@"%ld",self.onCount);
        if (self.onCount == 5) {
            [sender.selectedCells.lastObject setState:0];
            self.onCount -= 1;
        }    
    }
    

    When you try to select the 5th button it will flash on, but then go off. This could be a problem depending on how you are using the state of these buttons. I just logged them with this method:

    -(IBAction)checkMatrix:(id)sender {
        NSIndexSet *indxs = [self.mat.cells indexesOfObjectsPassingTest:^BOOL(NSButtonCell *cell, NSUInteger idx, BOOL *stop) {
            return cell.state == NSOnState;
        }];
        NSLog(@"%@",indxs);
    }
    

    After Edit: I didn't like the way my first method flashed the button on briefly before turning it off again when you try to click the 5th button. I found what I think is a better solution that involves overriding mouseDown in the matrix subclass (if you want to try this, you should delete the setAction and setTarget statements and delete the buttonClick method):

    -(void)mouseDown:(NSEvent *) event {
        NSPoint matPoint = [self convertPoint:event.locationInWindow fromView:nil];
        NSInteger row;
        NSInteger column;
        [self getRow:&row column:&column forPoint:matPoint];
        NSButtonCell *cell = [self cellAtRow:row column:column];
        if (self.onCount < 4 && cell.state == NSOffState) {
            cell.state = NSOnState;
            self.onCount += 1;
        }else if (cell.state == NSOnState) {
            cell.state = NSOffState;
            self.onCount -= 1;
        }
    }