mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 04:57:03 +00:00
994 lines
28 KiB
Plaintext
994 lines
28 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;
|
|
|
|
BOOL _automaticallyRearrangesObjects; // FIXME: Not in use
|
|
|
|
CPIndexSet _selectionIndexes;
|
|
CPArray _sortDescriptors;
|
|
CPPredicate _filterPredicate;
|
|
CPArray _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)
|
|
{
|
|
_preservesSelection = YES;
|
|
_selectsInsertedObjects = YES;
|
|
_avoidsEmptySelection = YES;
|
|
_clearsFilterPredicateOnInsertion = YES;
|
|
_alwaysUsesMultipleValuesMarker = NO;
|
|
_automaticallyRearrangesObjects = NO;
|
|
|
|
_filterRestrictsInsertion = YES; // FIXME: Not in use
|
|
|
|
[self _init];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_init
|
|
{
|
|
_sortDescriptors = [CPArray array];
|
|
_filterPredicate = nil;
|
|
_selectionIndexes = [CPIndexSet indexSet];
|
|
_arrangedObjects = [CPArray array];
|
|
}
|
|
|
|
- (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 - YES if new objects are selected, otherwise NO.
|
|
*/
|
|
- (void)setSelectsInsertedObjects:(BOOL)value
|
|
{
|
|
_selectsInsertedObjects = value;
|
|
}
|
|
|
|
/*!
|
|
@return BOOL - 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 receiver should attempt to avoid an empty selection, otherwise NO.
|
|
*/
|
|
- (void)setAvoidsEmptySelection:(BOOL)value
|
|
{
|
|
_avoidsEmptySelection = value;
|
|
}
|
|
|
|
/*!
|
|
Whether the receiver will clear its filter predicate when a new object is inserted.
|
|
|
|
@return BOOL YES if the receiver clears filter predicates on insert
|
|
*/
|
|
- (BOOL)clearsFilterPredicateOnInsertion
|
|
{
|
|
return _clearsFilterPredicateOnInsertion;
|
|
}
|
|
|
|
/*!
|
|
Sets whether the receiver should clear its filter predicate when a new object is inserted.
|
|
|
|
@param BOOL YES if the receiver should clear filter predicates on insert
|
|
*/
|
|
- (void)setClearsFilterPredicateOnInsertion:(BOOL)aFlag
|
|
{
|
|
_clearsFilterPredicateOnInsertion = aFlag;
|
|
}
|
|
|
|
/*!
|
|
Whether the receiver will always return the multiple values marker when multiple
|
|
items are selected, even if the items have the same value.
|
|
|
|
@return BOOL YES if the receiver always uses the multiple values marker
|
|
*/
|
|
- (BOOL)alwaysUsesMultipleValuesMarker
|
|
{
|
|
return _alwaysUsesMultipleValuesMarker;
|
|
}
|
|
|
|
/*!
|
|
Sets whether the receiver should always return the multiple values marker when multiple
|
|
items are selected, even if the items have the same value.
|
|
|
|
@param BOOL aFlag YES if the receiver should always use the multiple values marker
|
|
*/
|
|
- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag
|
|
{
|
|
_alwaysUsesMultipleValuesMarker = aFlag;
|
|
}
|
|
|
|
/*!
|
|
Whether the receiver will rearrange its contents automatically whenever the sort
|
|
descriptors or filter predicates are changed.
|
|
|
|
NOTE: not yet implemented. Cappuccino always act as if this value was YES.
|
|
|
|
@return BOOL YES if the receiver will automatically rearrange its content on new sort
|
|
descriptors or filter predicates
|
|
*/
|
|
- (BOOL)automaticallyRearrangesObjects
|
|
{
|
|
return _automaticallyRearrangesObjects;
|
|
}
|
|
|
|
/*!
|
|
Sets whether the receiver should rearrange its contents automatically whenever the sort
|
|
descriptors or filter predicates are changed.
|
|
|
|
NOTE: not yet implemented. Cappuccino always act as if this value was YES.
|
|
|
|
@param BOOL YES if the receiver should automatically rearrange its content on new sort
|
|
descriptors or filter predicates
|
|
*/
|
|
- (void)setAutomaticallyRearrangesObjects:(BOOL)aFlag
|
|
{
|
|
_automaticallyRearrangesObjects = aFlag;
|
|
}
|
|
|
|
/*!
|
|
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 && _filterPredicate != nil)
|
|
[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 receiver
|
|
*/
|
|
- (id)contentArray
|
|
{
|
|
return [self content];
|
|
}
|
|
|
|
/*!
|
|
Returns the content of the receiver 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 count] > 0)
|
|
{
|
|
var sortedObjects = [objects filteredArrayUsingPredicate:filterPredicate];
|
|
[sortedObjects sortUsingDescriptors:sortDescriptors];
|
|
return sortedObjects;
|
|
}
|
|
else if (filterPredicate)
|
|
return [objects filteredArrayUsingPredicate:filterPredicate];
|
|
else if ([sortDescriptors count] > 0)
|
|
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 receiver.
|
|
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 unsigned 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 receiver'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 receiver'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
|
|
|
|
@implementation CPArrayController (CPBinder)
|
|
|
|
+ (Class)_binderClassForBinding:(CPString)theBinding
|
|
{
|
|
if (theBinding == @"contentArray")
|
|
return [_CPArrayControllerContentBinder class];
|
|
|
|
return [super _binderClassForBinding:theBinding];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _CPArrayControllerContentBinder : CPBinder
|
|
|
|
- (void)setValueFor:(CPString)aBinding
|
|
{
|
|
var destination = [_info objectForKey:CPObservedObjectKey],
|
|
keyPath = [_info objectForKey:CPObservedKeyPathKey],
|
|
options = [_info objectForKey:CPOptionsKey],
|
|
newValue = [destination mutableArrayValueForKeyPath:keyPath];
|
|
|
|
newValue = [self transformValue:newValue withOptions:options];
|
|
[_source setValue:newValue forKey:aBinding];
|
|
}
|
|
|
|
@end
|
|
|
|
var CPArrayControllerAvoidsEmptySelection = @"CPArrayControllerAvoidsEmptySelection",
|
|
CPArrayControllerClearsFilterPredicateOnInsertion = @"CPArrayControllerClearsFilterPredicateOnInsertion",
|
|
CPArrayControllerFilterRestrictsInsertion = @"CPArrayControllerFilterRestrictsInsertion",
|
|
CPArrayControllerPreservesSelection = @"CPArrayControllerPreservesSelection",
|
|
CPArrayControllerSelectsInsertedObjects = @"CPArrayControllerSelectsInsertedObjects",
|
|
CPArrayControllerAlwaysUsesMultipleValuesMarker = @"CPArrayControllerAlwaysUsesMultipleValuesMarker",
|
|
CPArrayControllerAutomaticallyRearrangesObjects = @"CPArrayControllerAutomaticallyRearrangesObjects";
|
|
|
|
@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];
|
|
_automaticallyRearrangesObjects = [aCoder decodeBoolForKey:CPArrayControllerAutomaticallyRearrangesObjects];
|
|
_sortDescriptors = [CPArray array];
|
|
|
|
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];
|
|
[aCoder encodeBool:_automaticallyRearrangesObjects forKey:CPArrayControllerAutomaticallyRearrangesObjects];
|
|
}
|
|
|
|
- (void)awakeFromCib
|
|
{
|
|
[self _selectionWillChange];
|
|
[self _selectionDidChange];
|
|
}
|
|
|
|
@end
|