Files
cappuccino/AppKit/CPArrayController.j
T

877 lines
24 KiB
Plaintext

/*
* 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 <Foundation/CPIndexSet.j>
@import "CPObjectController.j"
@import "CPKeyValueBinding.j"
/*!
@class CPArrayController
CPArrayController is a bindings compatible class that manages an array.
CPArrayController also provides selection management and sorting capabilities.
*/
@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", "filterPredicate", "sortDescriptors"];
}
+ (CPSet)keyPathsForValuesAffectingSelection
{
return [CPSet setWithObjects:"selectionIndexes"];
}
+ (CPSet)keyPathsForValuesAffectingSelectionIndex
{
return [CPSet setWithObjects:"selectionIndexes"];
}
+ (CPSet)keyPathsForValuesAffectingSelectionIndexes
{
// When the arranged objects change, selection preservation may cause the indexes
// to change.
return [CPSet setWithObjects:"arrangedObjects"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
{
// Don't need to depend on arrangedObjects here because selectionIndexes already does.
return [CPSet setWithObjects:"selectionIndexes"];
}
+ (CPSet)keyPathsForValuesAffectingCanRemove
{
return [CPSet setWithObjects:"selectionIndexes"];
}
+ (CPSet)keyPathsForValuesAffectingCanSelectNext
{
return [CPSet setWithObjects:"selectionIndexes"];
}
+ (CPSet)keyPathsForValuesAffectingCanSelectPrevious
{
return [CPSet setWithObjects:"selectionIndexes"];
}
- (id)init
{
self = [super init];
if (self)
{
_sortDescriptors = [CPArray array];
_selectionIndexes = [CPIndexSet indexSet];
}
return self;
}
- (void)prepareContent
{
[self _setContentArray:[[self newObject]]];
}
/*!
Returns YES if the selection should try to be preserved when the content changes, otherwise NO.
@return BOOL YES if the selection is preserved, otherwise NO.
*/
- (BOOL)preservesSelection
{
return _preservesSelection;
}
/*!
Sets whether the selection is kept when the content changes.
@param BOOL aFlag - YES if the selection should be kept, otherwise NO.
*/
- (void)setPreservesSelection:(BOOL)value
{
_preservesSelection = value;
}
/*!
@return BOOL - Returns YES if new objects are automatically selected, otherwise NO.
*/
- (BOOL)selectsInsertedObjects
{
return _selectsInsertedObjects;
}
/*!
Sets whether the controller will automatically select objects as they are inserted.
@return BOOL aFlag - YES if new objects are selected, otherwise NO.
*/
- (void)setSelectsInsertedObjects:(BOOL)value
{
_selectsInsertedObjects = value;
}
/*!
@return BOOL aFlag - Returns YES if the controller should try to avoid an empty selection otherwise NO.
*/
- (BOOL)avoidsEmptySelection
{
return _avoidsEmptySelection;
}
/*!
Sets whether the controller should try to avoid an empty selection.
@param BOOL aFlag - YES if the reciver should attempt to avoid an empty selection, otherwise NO.
*/
- (void)setAvoidsEmptySelection:(BOOL)value
{
_avoidsEmptySelection = value;
}
/*!
Sets the controller's content object.
@param id value - the content object of the controller.
*/
- (void)setContent:(id)value
{
if (value === nil)
value = [];
if (![value isKindOfClass:[CPArray class]])
value = [value];
var oldSelectedObjects = nil,
oldSelectionIndexes = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexes = [self selectionIndexes];
/*
When the contents are changed, the selected indexes may no longer refer to the
same items. This would cause problems when setSelectedObjects is called below.
Any KVO observation would try to retrieve the 'before' value which could be
wrong or even throw an exception for no longer existing indexes.
To avoid that, use the internal __setSelectedObjects which fires no notifications.
The selectionIndexes notifications will fire later since they depend on the
content key. This pattern is also applied for many other methods throughout this
class.
*/
if (_clearsFilterPredicateOnInsertion)
[self willChangeValueForKey:@"filterPredicate"];
// Don't use [super setContent:] as that would fire the contentObject change.
// We need to be in control of when notifications fire.
_contentObject = value;
if (_clearsFilterPredicateOnInsertion)
[self __setFilterPredicate:nil]; // Causes a _rearrangeObjects.
else
[self _rearrangeObjects];
if ([self preservesSelection])
[self __setSelectedObjects:oldSelectedObjects];
else
[self __setSelectionIndexes:oldSelectionIndexes];
if (_clearsFilterPredicateOnInsertion)
[self didChangeValueForKey:@"filterPredicate"];
}
/*!
@ignore
*/
- (void)_setContentArray:(id)anArray
{
[self setContent:anArray];
}
/*!
@ignore
*/
- (void)_setContentSet:(id)aSet
{
[self setContent:[aSet allObjects]];
}
/*!
Returns the content array of the controller.
@return id the content array of the reciever
*/
- (id)contentArray
{
return [self content];
}
/*!
Returns the content of the reciever as a CPSet.
@return id - the content of the controller as a set.
*/
- (id)contentSet
{
return [CPSet setWithArray:[self content]];
}
/*!
Sorts and filters a given array and returns it.
@param CPArray anArray - an array of objects.
@return CPArray - the array of sorted objects.
*/
- (CPArray)arrangeObjects:(CPArray)objects
{
var filterPredicate = [self filterPredicate],
sortDescriptors = [self sortDescriptors];
if (filterPredicate && sortDescriptors)
{
var sortedObjects = [objects filteredArrayUsingPredicate:filterPredicate];
[sortedObjects sortUsingDescriptors:sortDescriptors];
return sortedObjects;
}
else if (filterPredicate)
return [objects filteredArrayUsingPredicate:filterPredicate];
else if (sortDescriptors)
return [objects sortedArrayUsingDescriptors:sortDescriptors];
return [objects copy];
}
/*!
Triggers the filtering of the objects in the controller.
*/
- (void)rearrangeObjects
{
[self willChangeValueForKey:@"arrangedObjects"];
[self _rearrangeObjects];
[self didChangeValueForKey:@"arrangedObjects"];
}
/*
Like rearrangeObjects but don't fire any change notifications.
@ignore
*/
- (void)_rearrangeObjects
{
/*
Rearranging reapplies the selection criteria and may cause objects to disappear,
so take care of the selection.
*/
var oldSelectedObjects = nil,
oldSelectionIndexes = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexes = [self selectionIndexes];
[self __setArrangedObjects:[self arrangeObjects:[self contentArray]]];
if ([self preservesSelection])
[self __setSelectedObjects:oldSelectedObjects];
else
[self __setSelectionIndexes:oldSelectionIndexes];
}
/*!
@ignore
*/
- (void)__setArrangedObjects:(id)value
{
if (_arrangedObjects === value)
return;
_arrangedObjects = [[_CPObservableArray alloc] initWithArray:value];
}
/*!
Returns an array of the controller's objects sorted and filtered.
@return - array of objects
*/
- (id)arrangedObjects
{
return _arrangedObjects;
}
/*!
Returns the receiver's array of sort descriptors.
@return CPArray an array of sort descriptors
*/
- (CPArray)sortDescriptors
{
return _sortDescriptors;
}
/*!
Sets the sort descriptors for the controller.
@param CPArray descriptors - the new sort descriptors.
*/
- (void)setSortDescriptors:(CPArray)value
{
if (_sortDescriptors === value)
return;
_sortDescriptors = [value copy];
// Use the non-notification version since arrangedObjects already depends
// on sortDescriptors.
[self _rearrangeObjects];
}
/*!
Returns the predicate used by the controller to filter the contents of the reciever.
If no predicate is set nil is returned.
@return CPPredicate the predicate used by the controller
*/
- (CPPredicate)filterPredicate
{
return _filterPredicate;
}
/*!
Sets the predicate for the controller to filter the content.
Passing nil will remove an existing prediate.
@param CPPrediate aPredicate - the new predicate.
*/
- (void)setFilterPredicate:(CPPredicate)value
{
[self __setFilterPredicate:value];
}
/*
Like setFilterPredicate but don't fire any change notifications.
@ignore
*/
- (void)__setFilterPredicate:(CPPredicate)value
{
if (_filterPredicate === value)
return;
_filterPredicate = value;
// Use the non-notification version since arrangedObjects already depends
// on filterPredicate.
[self _rearrangeObjects];
}
/*!
Returns a BOOL indicating whether the receiver always returns the multiple values marker when multiple objects are selected.
@return BOOL YES is the controller always uses multiple value markers, otherwise NO.
*/
- (BOOL)alwaysUsesMultipleValuesMarker
{
return _alwaysUsesMultipleValuesMarker;
}
//Selection
/*!
Returns the index of the first object in the controller's selection.
@return unsigned - Index of the first selected object.
*/
- (unsigned)selectionIndex
{
return [_selectionIndexes firstIndex];
}
/*!
Sets the selected index
@param unsided anIndex - the new index to select
@return BOOL - Returns YES if the selection was changed, otherwise NO.
*/
- (BOOL)setSelectionIndex:(unsigned)index
{
return [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
}
/*!
Returns an index set of the selected indexes.
@return CPIndexSet - The selected indexes.
*/
- (CPIndexSet)selectionIndexes
{
return _selectionIndexes;
}
/*!
Sets the selection indexes of the controller.
@param CPIndexSet indexes - the indexes to select
@return BOOL - Returns YES if the selection changed, otherwise NO.
*/
- (BOOL)setSelectionIndexes:(CPIndexSet)indexes
{
[self _selectionWillChange]
[self __setSelectionIndexes:indexes];
[self _selectionDidChange];
}
/*
Like setSelectionIndex but don't fire any change notifications.
@ignore
*/
- (BOOL)__setSelectionIndex:(int)theIndex
{
[self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:theIndex]];
}
/*
Like setSelectionIndexes but don't fire any change notifications.
@ignore
*/
- (BOOL)__setSelectionIndexes:(CPIndexSet)indexes
{
if (!indexes)
indexes = [CPIndexSet indexSet];
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];
}
if ([_selectionIndexes isEqualToIndexSet:indexes])
return NO;
_selectionIndexes = [indexes copy];
// 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.
var binderClass = [[self class] _binderClassForBinding:@"selectionIndexes"];
[[binderClass getBinding:@"selectionIndexes" forObject:self] reverseSetValueFor:@"selectionIndexes"];
return YES;
}
/*!
Returns an array of the selected objects.
@return CPArray - the selected objects.
*/
- (CPArray)selectedObjects
{
var objects = [[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]];
return [_CPObservableArray arrayWithArray:(objects || [])];
}
/*!
Sets the selected objects of the controller.
@param CPArray anArray - the objects to select
@return BOOL - Returns YES if the selection was changed, otherwise NO.
*/
- (BOOL)setSelectedObjects:(CPArray)objects
{
[self willChangeValueForKey:@"selectionIndexes"];
[self _selectionWillChange];
[self __setSelectedObjects:objects];
[self didChangeValueForKey:@"selectionIndexes"];
[self _selectionDidChange];
}
/*
Like setSelectedObjects but don't fire any change notifications.
@ignore
*/
- (BOOL)__setSelectedObjects:(CPArray)objects
{
var set = [CPIndexSet indexSet],
count = [objects count],
arrangedObjects = [self arrangedObjects];
for (var i = 0; i < count; i++)
{
var index = [arrangedObjects indexOfObject:[objects objectAtIndex:i]];
if (index !== CPNotFound)
[set addIndex:index];
}
[self __setSelectionIndexes:set];
return YES;
}
//Moving selection
/*!
Returns YES if the previous object, relative to the current selection, in the controller's content array can be selected.
@return BOOL - YES if the object can be selected, otherwise NO.
*/
- (BOOL)canSelectPrevious
{
return [[self selectionIndexes] firstIndex] > 0
}
/*!
Selects the previous object, relative to the current selection, in the controllers arranged content.
@param id sender - the sender of the message.
*/
- (void)selectPrevious:(id)sender
{
var index = [[self selectionIndexes] firstIndex] - 1;
if (index >= 0)
[self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
}
/*!
Returns YES if the next object, relative to the current selection, in the controller's content array can be selected.
@return BOOL - YES if the object can be selected, otherwise NO.
*/
- (BOOL)canSelectNext
{
return [[self selectionIndexes] firstIndex] < [[self arrangedObjects] count] - 1;
}
/*!
Selects the next object, relative to the current selection, in the controllers arranged content.
@param id sender - the sender of the message.
*/
- (void)selectNext:(id)sender
{
var index = [[self selectionIndexes] firstIndex] + 1;
if (index < [[self arrangedObjects] count])
[self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
}
//Add/Remove
/*!
Adds object to the receiverÕs content collection and the arranged objects array.
@param id anObject - the object to add the controller.
*/
- (void)addObject:(id)object
{
if (![self canAdd])
return;
if (_clearsFilterPredicateOnInsertion)
[self willChangeValueForKey:@"filterPredicate"];
[self willChangeValueForKey:@"content"];
[_contentObject addObject:object];
if (_clearsFilterPredicateOnInsertion)
[self __setFilterPredicate:nil];
if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
{
var pos = [_arrangedObjects insertObject:object inArraySortedByDescriptors:_sortDescriptors];
// selectionIndexes change notification will be fired as a result of the
// content change. Don't fire manually.
if (_selectsInsertedObjects)
[self __setSelectionIndex:pos];
else
[_selectionIndexes shiftIndexesStartingAtIndex:pos by:1];
}
else
[self _rearrangeObjects];
[self didChangeValueForKey:@"content"];
if (_clearsFilterPredicateOnInsertion)
[self didChangeValueForKey:@"filterPredicate"];
}
/*!
Adds an object at a given index to the reciever's collection.
@param id anObject - The object to add to the collection.
@param int anIndex - The index to insert the object at.
*/
- (void)insertObject:(id)anObject atArrangedObjectIndex:(int)anIndex
{
if (![self canAdd])
return;
if (_clearsFilterPredicateOnInsertion)
[self willChangeValueForKey:@"filterPredicate"];
[self willChangeValueForKey:@"content"];
[_contentObject insertObject:anObject atIndex:anIndex];
if (_clearsFilterPredicateOnInsertion)
[self __setFilterPredicate:nil];
[[self arrangedObjects] insertObject:anObject atIndex:anIndex];
// selectionIndexes change notification will be fired as a result of the
// content change. Don't fire manually.
if ([self selectsInsertedObjects])
[self __setSelectionIndex:anIndex];
else
[[self selectionIndexes] shiftIndexesStartingAtIndex:anIndex by:1];
if ([self avoidsEmptySelection] && [[self selectionIndexes] count] <= 0 && [_contentObject count] > 0)
[self __setSelectionIndexes:[CPIndexSet indexSetWithIndex:0]];
[self didChangeValueForKey:@"content"];
if (_clearsFilterPredicateOnInsertion)
[self didChangeValueForKey:@"filterPredicate"];
}
/*!
Removes a given object from the reciever's collection.
@param id anObject - The object to remove from the collection.
*/
- (void)removeObject:(id)object
{
[self willChangeValueForKey:@"content"];
[_contentObject removeObject:object];
if (_filterPredicate === nil || [_filterPredicate evaluateWithObject:object])
{
// selectionIndexes change notification will be fired as a result of the
// content change. Don't fire manually.
var pos = [_arrangedObjects indexOfObject:object];
[_arrangedObjects removeObjectAtIndex:pos];
[_selectionIndexes shiftIndexesStartingAtIndex:pos by:-1];
}
[self didChangeValueForKey:@"content"];
}
/*!
Creates and adds a new object to the receiverÕs content and arranged objects.
@param id sender - The sender of the message.
*/
- (void)add:(id)sender
{
if (![self canAdd])
return;
[self insert:sender];
}
/*!
Creates a new object and inserts it into the receiverÕs content array.
@param id sender - The sender of the message.
*/
- (void)insert:(id)sender
{
if (![self canInsert])
return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject];
[self addObject:newObject];
}
/*!
Removes the controller's selected objects from the controller's collection.
@param id sender - The sender of the message.
*/
- (void)remove:(id)sender
{
[self removeObjects:[[self arrangedObjects] objectsAtIndexes:[self selectionIndexes]]];
}
/*!
Removes the objects at the specified indexes in the controller's arranged objects from the content array.
@param CPIndexSet indexes - indexes of the objects to remove.
*/
- (void)removeObjectsAtArrangedObjectIndexes:(CPIndexSet)indexes
{
[self _removeObjects:[[self arrangedObjects] objectsAtIndexes:indexes]];
}
/*!
Adds an array of objects to the controller's collection.
@param CPArray anArray - The array of objects to add to the collection.
*/
- (void)addObjects:(CPArray)objects
{
if (![self canAdd])
return;
var contentArray = [self contentArray],
count = [objects count];
for (var i = 0; i < count; i++)
[contentArray addObject:[objects objectAtIndex:i]];
[self setContent:contentArray];
}
/*!
Removes an array of objects from the collection.
@param CPArray anArray - The array of objects to remove
*/
- (void)removeObjects:(CPArray)objects
{
[self _removeObjects:objects];
}
/*!
@ignore
*/
- (void)_removeObjects:(CPArray)objects
{
[self willChangeValueForKey:@"content"];
[_contentObject removeObjectsInArray:objects];
var arrangedObjects = [self arrangedObjects],
position = [arrangedObjects indexOfObject:[objects objectAtIndex:0]];
[arrangedObjects removeObjectsInArray:objects];
var objectsCount = [arrangedObjects count],
selectionIndexes = [CPIndexSet indexSet];
if ([self preservesSelection] || [self avoidsEmptySelection])
{
selectionIndexes = [CPIndexSet indexSetWithIndex:position];
// Remove the selection if there are no objects
if (objectsCount <= 0)
selectionIndexes = [CPIndexSet indexSet];
// Shift selection to last object if position is out of bounds
else if (position >= objectsCount)
selectionIndexes = [CPIndexSet indexSetWithIndex:objectsCount - 1];
}
_selectionIndexes = selectionIndexes;
[self didChangeValueForKey:@"content"];
}
/*!
Returns a BOOL indicating whether an object can be inserted into the controller's collection.
@return BOOL - YES if an object can be inserted, otherwise NO.
*/
- (BOOL)canInsert
{
return [self isEditable];
}
@end
var CPArrayControllerAvoidsEmptySelection = @"CPArrayControllerAvoidsEmptySelection",
CPArrayControllerClearsFilterPredicateOnInsertion = @"CPArrayControllerClearsFilterPredicateOnInsertion",
CPArrayControllerFilterRestrictsInsertion = @"CPArrayControllerFilterRestrictsInsertion",
CPArrayControllerPreservesSelection = @"CPArrayControllerPreservesSelection",
CPArrayControllerSelectsInsertedObjects = @"CPArrayControllerSelectsInsertedObjects",
CPArrayControllerAlwaysUsesMultipleValuesMarker = @"CPArrayControllerAlwaysUsesMultipleValuesMarker";
@implementation CPArrayController (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_avoidsEmptySelection = [aCoder decodeBoolForKey:CPArrayControllerAvoidsEmptySelection];
_clearsFilterPredicateOnInsertion = [aCoder decodeBoolForKey:CPArrayControllerClearsFilterPredicateOnInsertion];
_filterRestrictsInsertion = [aCoder decodeBoolForKey:CPArrayControllerFilterRestrictsInsertion];
_preservesSelection = [aCoder decodeBoolForKey:CPArrayControllerPreservesSelection];
_selectsInsertedObjects = [aCoder decodeBoolForKey:CPArrayControllerSelectsInsertedObjects];
_alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
if (![self content] && [self automaticallyPreparesContent])
[self prepareContent];
else if (![self content])
[self _setContentArray:[]];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeBool:_avoidsEmptySelection forKey:CPArrayControllerAvoidsEmptySelection];
[aCoder encodeBool:_clearsFilterPredicateOnInsertion forKey:CPArrayControllerClearsFilterPredicateOnInsertion];
[aCoder encodeBool:_filterRestrictsInsertion forKey:CPArrayControllerFilterRestrictsInsertion];
[aCoder encodeBool:_preservesSelection forKey:CPArrayControllerPreservesSelection];
[aCoder encodeBool:_selectsInsertedObjects forKey:CPArrayControllerSelectsInsertedObjects];
[aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPArrayControllerAlwaysUsesMultipleValuesMarker];
}
- (void)awakeFromCib
{
[self _selectionWillChange];
[self _selectionDidChange];
}
@end