2018-05-08

Making buttons part of the key view loop within a view-based NSTableView

If you have a view-based NSTableView, you can place buttons in your table rows, but they do not participate in the key view loop. In a form, you could construct a loop that lets you tab from a text field to a button to any arbitrary control. The table view, however, constructs an ad hoc key view loop from views provided by its delegate. By default, the tab key only allows movement between text fields.

How can you change that? You have to handle three cases:

Let's start with the last case. It can be handled with a variation of the technique from this previous post. First you need a method within your class that calls the table view's -editColumn:row:withEvent:select: method. This will transfer the focus to a button or start editing a text field.

-(void) jumpToTableViewCell: (NSDictionary *) location {
    [self.tagsTable editColumn: [location[@"col"] intValue]
                           row: [location[@"row"] intValue]
                     withEvent: nil
                        select: YES];
}

You should already have a -controlTextDidEndEditing: method to save the value of the text field. There you can examine the type of movement that caused editing to stop. If it was a tab key and the view in the next column will be a button, you can call -jumpToTableViewCell: after a run loop delay.

-(void) controlTextDidEndEditing:(NSNotification *)obj {
    NSInteger col = [self columnForView: obj.object];
    NSInteger row = [self rowForView: obj.object];
    NSString *val = [obj.object stringValue];
    // ... save text value here ...
    
    int textMovement = [obj.userInfo[@"NSTextMovement"] intValue];
    if (NSTabTextMovement == textMovement) {
        if (col < self.tableColumns.count-1) {
            NSView *vp = [self tableView:self viewForTableColumn: self.tableColumns[1+col] row: row];
            if ([vp isKindOfClass: [NSButton class]]) {
                
                // This works for buttons that follow a text field.
                NSDictionary *destination = @{ @"row" : [NSNumber numberWithInteger: row],
                                               @"col" : [NSNumber numberWithInteger: 1+col] };
                [self performSelectorOnMainThread: @selector(jumpToTableViewCell:)
                                       withObject: destination
                                    waitUntilDone: NO];
            }
        }
    }
}

For the other two cases, I think you have to subclass the table view and modify the -keyDown: method. Notice that self.selectedColumn is -1 if no text field is active. Even if you give a button focus, that does not change, but you can distinguish the two cases by looking at the window's firstResponder.

- (void)keyDown:(NSEvent *)theEvent {
    if ([@"\t" isEqualToString: theEvent.characters] && self.selectedRow >= 0 &&  -1 == self.selectedColumn) {
        if (self == self.window.firstResponder) {
	    // No cell is currently selected.
            NSView *vp = [self tableView:self viewForTableColumn: self.tableColumns[0] row: self.selectedRow];
            if ([vp isKindOfClass: [NSButton class]]) {
                NSDictionary *destination = @{ @"row" : [NSNumber numberWithInteger: self.selectedRow],
					       @"col" : @0 };
                [self performSelectorOnMainThread: @selector(jumpToTableViewCell:)
                                       withObject: destination
                                    waitUntilDone: NO];
                return;
            }
        } else if ([self.window.firstResponder isKindOfClass: [NSButton class]]) {
	    // A button subview has focus.
            NSButton *bp = (NSButton *) self.window.firstResponder;
            if ([bp isDescendantOf: self]) {	// Would this ever be false?
                NSInteger col = [self columnForView: bp];
                if (col < self.tableColumns.count-1) {
                    NSView *vp = [self tableView:self viewForTableColumn: self.tableColumns[1+col] row: self.selectedRow];
                    if ([vp isKindOfClass: [NSButton class]]) {
                        NSDictionary *destination = @{ @"row" : [NSNumber numberWithInteger: self.selectedRow],
                                                       @"col" : [NSNumber numberWithInteger: 1+col] };
                        [self performSelectorOnMainThread: @selector(jumpToTableViewCell:)
                                               withObject: destination
                                            waitUntilDone: NO];
                        return;
                    }
                }
            }
        }
    }
    [super keyDown: theEvent];
}