Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent the parent nodes of my NSOutlineView from getting sorted?

I've would like to not sort the parent nodes of my NSOultineView.

The datasource of my outline view is a NSTreeController.

When clicking on a column header, I would like to sort the tree only from the second level of the hierarchy, and their children and leave the parent nodes in the same order.

UPDATE This is how I bind columns to the values and assign the sort descriptor.

    [newColumn bind:@"value" toObject:currentItemsArrayController withKeyPath:[NSString stringWithFormat:@"arrangedObjects.%@", metadata.columnBindingKeyPath] options:bindingOptions];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:metadata.columnSortKeyPath ascending:YES selector:metadata.columnSortSelector];
    [newColumn setSortDescriptorPrototype:sortDescriptor];
like image 829
aneuryzm Avatar asked May 13 '15 12:05

aneuryzm


1 Answers

You can use custom comparator.

To demonstrate the basic idea of this approach, let's assume you use NSTreeNode as your tree node class. All root nodes are stored in an NSArray named content, and your three root nodes are cat, dog and fish:

NSTreeNode *cat = [NSTreeNode treeNodeWithRepresentedObject:@"cat"];
// ...the same for dog and fish
self.content = @[cat, dog, fish];

Then, create your NSSortDescriptor prototype. Note that you should use self as the key instead of the string you are comparing (in this case representedObject) to get access to the raw node object. In the comparator, check if the object is contained in your root objects array. If YES, just return NSOrderedSame as it will keep the order unchanged from the initial order in your content array, otherwise use the compare: method to do a standard comparison.

NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES comparator:^NSComparisonResult(NSTreeNode *obj1, NSTreeNode *obj2) {

    if ([self.content containsObject:obj1] && [self.content containsObject:obj2]) {
        return NSOrderedSame;
    }

    return [obj1.representedObject compare:obj2.representedObject];
}];

[[outlineView.tableColumns firstObject] setSortDescriptorPrototype:sortDescriptor];

Edit 2

If you have multiple columns that need to be sorted separately, you cannot use self as the key for every column, as the key should be unique among all columns' sortDescriptorPrototypes. In this case, you can create a custom object as the representedObject and wrap all your data including a pointer back to the tree node in the object.

Edit 1

The correctness of the above-mentioned approach requires NSOutlineView to sort its rows using a stable sort algorithm. That is, identical items will always retain the same relative order before and after sorting. However, I couldn't find any evidence in Apple documentation regarding the stability of the sorting algorithm used here, although from my experience the approach above will actually work.

If you are feeling unsafe, you can explicitly compare your root tree nodes based on whether current order is ascending or descending. To do that, you need an ivar to save the current order. Just implement the outlineView:sortDescriptorsDidChange: delegate method:

- (BOOL)outlineView:(NSOutlineView *)outlineView sortDescriptorsDidChange:(NSArray *)oldDescriptors {
    ascending = ![oldDescriptors.firstObject ascending];
    return YES;
}

And change return NSOrderedSame to:

if ([self.content containsObject:obj1] && [self.content containsObject:obj2]) {
    NSUInteger index1 = [self.content indexOfObject:obj1];
    NSUInteger index2 = [self.content indexOfObject:obj2];
    if (ascending)
        return (index1 < index2) ? NSOrderedAscending : NSOrderedDescending;
    else
        return (index1 < index2) ? NSOrderedDescending : NSOrderedAscending;
}

Edit 3

If you cannot implement outlineView:sortDescriptorsDidChange: for whatever reason, you can manually attach an observer to your outlineView's sortDescriptors array:

[outlineView addObserver:self forKeyPath:@"sortDescriptors" options:NSKeyValueObservingOptionNew context:nil];

In this way you can get notified when user clicked the header as well. After that don't forget to implement the following observing method, as a part of KVO process:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"sortDescriptors"]) {
        // Again, as a simple demonstration, the following line of code only deals with the first sort descriptor. You should modify it to suit your need.
        ascending = [[change[NSKeyValueChangeNewKey] firstObject] ascending];
    }
}

To prevent memory leaking, you have to remove this observer before outlineView is deallocated(if not earlier). If you are not familiar with KVO, please make sure to check out Apple's guide.

like image 193
Renfei Song Avatar answered Nov 07 '22 09:11

Renfei Song