Wednesday, June 17, 2009

NSTreeController remove: does not call outlineViewSelectionDidChange: when removing the last child

After a hiatus from Cocoa development, I’ve recently been trying out some sample projects. And, as luck would have it, I found a bug in the Cocoa library when trying to play with Apple’s SourceView sample code.

The problem is that you can delete a group’s last child and not get a notification sent to your delegate’s method outlineViewSelectionDidChange:. In order to see this happen, just download the source code from Apple’s web site at http://developer.apple.com/samplecode/SourceView/ and build it or run the pre-built version of the application in the folder.

  1. Select the first item under PLACES.

  2. Press the “-” button to remove the selected item.

  3. Keep doing this until the child nodes under PLACES is empty.

  4. Notice the “-” button is still active even though there is no selection; the state of this button is updated by calls to the method outlineViewSelectionDidChange:.

  5. Press the “-” button once again and notice it removes the DEVICES group.


In Step 2 and 3, the notification NSOutlineViewSelectionDidChangeNotification is sent after each removal except for when the last node is removed, thereby causing nothing to be selected; most people would define the removal of the last child a change in selection, but it appears there’s a bug in Cocoa that prevents that from happening.

I added the following code as an action method for the “-” button in order to fix this. Note that I’ve only been trying this on Mac OS X 10.5.7, so other versions of Mac OS X may work differently.


// --------------------------------------------------
// removeSelectionAction:sender:
// --------------------------------------------------
- (IBAction)removeSelectionAction:(id)sender
{
NSArray* selectedItems;
NSUInteger numberOfSelectedItems;
BOOL needToSendTheNotificationOurselves = NO;

selectedItems = [treeController selectedNodes];
numberOfSelectedItems = [selectedItems count];
if (0 == numberOfSelectedItems)
{
// Not sure how we could have gotten here when nothing is
// selected, so we better send out a notification to all
// controllers that they better get on the ball.
needToSendTheNotificationOurselves = YES;
}
else
{
NSTreeNode* firstSelectedNode;
NSTreeNode* parentNode;

// Let's see if the node about to be removed is the only
// child of its parent; if so, we'll need to send out a
// notification ourselves to the controllers to handle the
// change.
firstSelectedNode = [selectedItems objectAtIndex:0];
parentNode = [firstSelectedNode parentNode];
if (nil != parentNode)
{
if (1 == [[parentNode childNodes] count])
{
needToSendTheNotificationOurselves = YES;
}
}
}

[treeController remove:sender];

if (needToSendTheNotificationOurselves)
{
NSNotification* theMissingNotificationWeNeed;

// Looks like whatever we removed has placed us in the
// state where we won't get the notification we would
// normally get, so we'll have to send out a notification
// ourselves.
theMissingNotificationWeNeed =
[NSNotification
notificationWithName:
NSOutlineViewSelectionDidChangeNotification
object:outlineView];
if (nil != theMissingNotificationWeNeed)
{
[[NSNotificationCenter defaultCenter]
postNotification:theMissingNotificationWeNeed];
}
}
}

No comments: