/* * CPArrayController.j * AppKit * * Adapted from Cocotron, by Johannes Fortmann * * Created by Ross Boucher. * Copyright 2009, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import @import @implementation CPArrayController : CPObjectController { BOOL _avoidsEmptySelection; BOOL _clearsFilterPredicateOnInsertion; BOOL _filterRestrictsInsertion; BOOL _preservesSelection; BOOL _selectsInsertedObjects; BOOL _alwaysUsesMultipleValuesMarker; id _selectionIndexes; id _sortDescriptors; id _filterPredicate; id _arrangedObjects; } + (void)initialize { if (self !== [CPArrayController class]) return; [self exposeBinding:@"contentArray"]; [self exposeBinding:@"contentSet"]; } + (CPSet)keyPathsForValuesAffectingContentArray { return [CPSet setWithObjects:"content"]; } + (CPSet)keyPathsForValuesAffectingArrangedObjects { return [CPSet setWithObjects:"content", "contentArray", "contentSet", "filterPredicate", "sortDescriptors"]; } + (CPSet)keyPathsForValuesAffectingSelection { return [CPSet setWithObjects:"content", "contentArray", "contentSet", "selectionIndexes"]; } + (CPSet)keyPathsForValuesAffectingSelectionIndex { return [CPSet setWithObjects:"content", "contentArray", "contentSet", "selectionIndexes", "selection"]; } + (CPSet)keyPathsForValuesAffectingSelectedObjects { return [CPSet setWithObjects:"content", "contentArray", "contentSet", "selectionIndexes", "selection"]; } + (CPSet)keyPathsForValuesAffectingCanRemove { return [CPSet setWithObjects:"selectionIndexes"]; } + (CPSet)keyPathsForValuesAffectingCanSelectNext { return [CPSet setWithObjects:"selectionIndexes"]; } + (CPSet)keyPathsForValuesAffectingCanSelectPrevious { return [CPSet setWithObjects:"selectionIndexes"]; } + (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey { if (![super automaticallyNotifiesObserversForKey:aKey]) return NO; if (aKey === @"selectionIndexes") return NO; return YES; } - (id)init { self = [super init]; if (self) { _selectionIndexes = [CPIndexSet indexSet]; } return self; } -(void)prepareContent { [self _setContentArray:[[self newObject]]]; } - (BOOL)preservesSelection { return _preservesSelection; } - (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; } - (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; } - (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; } - (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; } - (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; } - (void)setContent:(id)value { if(![value isKindOfClass:[CPArray class]]) value = [value]; var oldSelection = nil, oldSelectionIndexes = [[self selectionIndexes] copy]; if ([self preservesSelection]) oldSelection = [self selectedObjects]; // Avoid out of bounds selections. _selectionIndexes = [CPIndexSet indexSet]; //FIXME: copy? [super setContent:value]; if(_clearsFilterPredicateOnInsertion) [self setFilterPredicate:nil]; [self rearrangeObjects]; if (oldSelection) [self setSelectedObjects:oldSelection]; else [self setSelectionIndexes:oldSelectionIndexes]; } - (void)_setContentArray:(id)anArray { [self setContent:anArray]; } - (void)_setContentSet:(id)aSet { [self setContent:aSet]; } - (id)contentArray { return [self content]; } - (id)contentSet { return [self content]; } - (CPArray)arrangeObjects:(CPArray)objects { var sortedObjects = objects; if ([self filterPredicate]) sortedObjects = [sortedObjects filteredArrayUsingPredicate:[self filterPredicate]]; if ([self sortDescriptors]) sortedObjects = [sortedObjects sortedArrayUsingDescriptors:[self sortDescriptors]]; return sortedObjects; } - (void)rearrangeObjects { // Rearranging reapplies the selection criteria and may cause objects to disappear, // so take care of the selection. // // Sometimes rearrangeObjects is called by setContent which may cause two rounds of // selection preservation. This is okay because setContent temporarily clears the // selection and so this code below ends up preserving nothing in that case. var oldSelection = nil, oldSelectionIndexes = [[self selectionIndexes] copy]; if ([self preservesSelection]) oldSelection = [self selectedObjects]; // Avoid out of bounds selections. _selectionIndexes = [CPIndexSet indexSet]; [self _setArrangedObjects:[self arrangeObjects:[self contentArray]]]; if (oldSelection) [self setSelectedObjects:oldSelection]; else [self setSelectionIndexes:oldSelectionIndexes]; } - (void)_setArrangedObjects:(id)value { if (_arrangedObjects === value) return; _arrangedObjects = [[_CPObservableArray alloc] initWithArray:value]; } - (id)arrangedObjects { return _arrangedObjects; } - (CPArray)sortDescriptors { return _sortDescriptors; } - (void)setSortDescriptors:(CPArray)value { if (_sortDescriptors === value) return; _sortDescriptors = [value copy]; [self rearrangeObjects]; } - (CPPredicate)filterPredicate { return _filterPredicate; } - (void)setFilterPredicate:(CPPredicate)value { if (_filterPredicate === value) return; _filterPredicate = value; [self rearrangeObjects]; } - (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; } //Selection - (unsigned)selectionIndex { return [_selectionIndexes firstIndex]; } - (BOOL)setSelectionIndex:(unsigned)index { return [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; } - (CPIndexSet)selectionIndexes { return _selectionIndexes; } - (BOOL)setSelectionIndexes:(CPIndexSet)indexes { if ([_selectionIndexes isEqualToIndexSet:indexes]) return NO; if (![indexes count]) { if(_avoidsEmptySelection && [[self arrangedObjects] count]) indexes = [CPIndexSet indexSetWithIndex:0]; } else { var objectsCount = [[self arrangedObjects] count]; // Remove out of bounds indexes. [indexes removeIndexesInRange:CPMakeRange(objectsCount, [indexes lastIndex]+1)]; // When avoiding empty selection and the deleted selection was at the bottom, select the last item. if(![indexes count] && _avoidsEmptySelection && objectsCount) indexes = [CPIndexSet indexSetWithIndex:objectsCount-1]; } [self willChangeValueForKey:@"selectionIndexes"]; [self _selectionWillChange]; _selectionIndexes = [indexes copy]; [self _selectionDidChange]; [self didChangeValueForKey:@"selectionIndexes"]; // Push back the new selection to the model for selectionIndexes if we have one. // There won't be an infinite loop because of the equality check above. [[CPKeyValueBinding getBinding:@"selectionIndexes" forObject:self] reverseSetValueFor:@"selectionIndexes"]; return YES; } - (CPArray)selectedObjects { var objects = [[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]]; return objects || [_CPObservableArray array]; } - (BOOL)setSelectedObjects:(CPArray)objects { var set = [CPIndexSet indexSet], count = [objects count], arrangedObjects = [self arrangedObjects]; for (var i=0; i 0 } -(BOOL)canSelectNext { return [[self selectionIndexes] firstIndex] < [[self arrangedObjects] count] -1; } -(void)selectNext:(id)sender { var index = [[self selectionIndexes] firstIndex] + 1 || 0; if (index < [[self arrangedObjects] count]) [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; } -(void)selectPrevious:(id)sender { var index = [[self selectionIndexes] firstIndex] - 1 || [[self arrangedObjects] count] - 1; if (index >= 0) [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; } //Add/Remove - (void)addObject:(id)object { if (![self canAdd]) return; [self willChangeValueForKey:@"content"]; [_contentObject addObject:object]; [self didChangeValueForKey:@"content"]; if (_clearsFilterPredicateOnInsertion) [self setFilterPredicate:nil]; if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object]) { var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors]; if (_selectsInsertedObjects) { [self setSelectionIndex:pos]; } else { [self willChangeValueForKey:@"selectionIndexes"]; [_selectionIndexes shiftIndexesStartingAtIndex:pos by:1]; [self didChangeValueForKey:@"selectionIndexes"]; } } else [self rearrangeObjects]; } - (void)removeObject:(id)object { if (![self canRemove]) return; [self willChangeValueForKey:@"content"]; [_contentObject removeObject:object]; [self didChangeValueForKey:@"content"]; if ([_filterPredicate evaluateWithObject:object]) { [self willChangeValueForKey:@"selectionIndexes"]; var pos = [_arrangedObjects indexOfObject:object]; [_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1]; [self didChangeValueForKey:@"selectionIndexes"]; } } -(void)add:(id)sender { if(![self canAdd]) return; [self insert:sender]; } - (void)insert:(id)sender { if(![self canInsert]) return; var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject]; [self addObject:newObject]; } - (void)remove:(id)sender { [self removeObjects:[[self contentArray] objectsAtIndexes:[self selectionIndexes]]]; } - (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)indexes { [self _removeObjects:[[self contentArray] objectsAtIndexes:indexes]]; } - (void)addObjects:(CPArray)objects { if(![self canAdd]) return; var contentArray = [self contentArray], count = [objects count]; for (var i=0; i