Question

I have the classic setup of an NSTableView with the columns bound to various keyPaths of the arrangedObjects of an NSArrayController, and the NSArrayController bound to an array of dictionaries.

With this setup, selecting one or multiple rows in the tableview works automatically. The table view and array controller work together and it's easy to query the NSArrayController to get the list of selected objects.

One of my table columns contains NSButtonCells, the checkbox kind. Is there a way to use Cocoa Bindings to bind the checkbox in each row to that row's selection state ? I know that I could add another value to the NSDictionary representing each row, but that would duplicate the selection information that is already available in NSArrayController.

If it is necessary to do that, would also appreciate a quick sketch of your implementation.

Thanks

Was it helpful?

Solution

So, the answer to this is not for the faint of heart. The reason is you are trying to get NSTableView to do something it doesn't naturally want to do.

Start out by using cocoa's native NSTableView multiple selection behavior: - clicking on a row selects it and deselects other rows - holding control and clicking on a column toggles the selection state of that row only

Now, add a column of checkboxes. For this row, the rules are different: - clicking on a checkbox toggles the selection state of that row only

This would be easy if we could capture clicks to the checkboxes and process them ourselves. Now we can, but the problem is that after we process them, they still get forwarded on to the NSTableView, altering the selection in the usual way. [Note: there may be some way to avoid this forwarding - if you know of one, please let me know]

So here's how you can (finally) accomplished this: - add a "selectedInView" field to each object in the underlying object array. Add an observer to the associated NSArrayController for the keyPath: "selectedObjects". When the selection changes, set the selectedInView field accordingly for each object. Something like this:

if([keyPath isEqualToString:@"selectedObjects"]) {
// For table view checkbox's: keep "selectedInView" of song dictionaries up to date
[_arrayController.arrangedObjects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
  BOOL sel = [_arrayController.selectedObjects containsObject:obj];
  if([[obj objectForKey:@"selectedInView"] boolValue] != sel)[obj setValue:[NSNumber numberWithBool:sel] forKey:@"selectedInView"];
}];

Now comes the tricky part: the only time the checkboxes malfunction are when there is already a selection present. Here are the types of cases:

Setup: Row's 1,2,3 are selected. Checkbox clicked on row 4. Result: Checkbox on row four is selected. Row four is selected. Row's 1,2,3 are deselected (because that's what NSTableView does naturally)

To solve this, whenever a checkbox is clicked you need to create a temporary array to remember the current selection, plus or minus the checkbox that just got clicked:

- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
  if([tableColumn.identifier isEqualToString:@"CheckBox"]) {
    NSMutableDictionary *song = [_arrayController.arrangedObjects objectAtIndex:row];
    if(!_tempSelectedSongs && _arrayController.selectedObjects) _tempSelectedSongs = [[NSMutableArray arrayWithArray:_arrayController.selectedObjects] retain];
    if(_tempSelectedSongs) {
      if([_tempSelectedSongs containsObject:song]) {
        [_tempSelectedSongs removeObject:song];
      } else if(![_tempSelectedSongs containsObject:song]) {
        [_tempSelectedSongs addObject:song];
      }
    }
  }
}

Now after the tableview has done it's selection processing, we want to set the selection to what it should be. There is a promising looking function that allows you to modify the tableview selection BEFORE it actually does the selecting. You can modify it like so:

- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes {
  NSMutableIndexSet *newSet = [NSMutableIndexSet indexSet];
  if(_tempSelectedSongs) {
    NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
    [_tempSelectedSongs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
      NSUInteger index = [_arrayController.arrangedObjects indexOfObject:obj];
      if(index != NSNotFound) [indexSet addIndex:index];
    }];
    proposedSelectionIndexes = indexSet;
    [_tempSelectedSongs release]; _tempSelectedSongs = nil; [_tempSelectedSongsTimer invalidate]; [_tempSelectedSongsTimer release]; _tempSelectedSongsTimer = nil;
  }
  [proposedSelectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
    NSProgressIndicator *progress = ((BDDiscreteProgressCell *)[[_arrayController.arrangedObjects objectAtIndex:idx] objectForKey:@"BDCell"]).progress;
    if(!progress)
      [newSet addIndex:idx];
  }];
  return newSet;
}

This works great, however there is a problem with the order in which the NSTableView delegate functions are called. Obviously we need the first function - where we setup the temporary array - to be called BEFORE the second function - where we use the information.

For whatever reason, it turns out that when you DE-select a checkbox, this is how things work, but when you SELECT a checkbox, the opposite occurs. So for this case, you can add some more code to your above keyPath observer:

if([keyPath isEqualToString:@"selectedObjects"]) {
  if(_tempSelectedSongs) {
    _arrayController.selectedObjects = _tempSelectedSongs;
    [_tempSelectedSongs release]; _tempSelectedSongs = nil;
  }
  // For table view checkbox's: keep "selectedInView" of song dictionaries up to date
  [_arrayController.arrangedObjects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    BOOL sel = [_arrayController.selectedObjects containsObject:obj];
    if([[obj objectForKey:@"selectedInView"] boolValue] != sel)[obj setValue:[NSNumber numberWithBool:sel] forKey:@"selectedInView"];
  }];
}

Edit: turns out there is an additional case: if a single row is selected and it's checkbox is "unclicked," this does not automatically trigger a selectedObjects notification, so you must run a function on a timer to implement the new selection.

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