mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 13:07:02 +00:00
541 lines
14 KiB
Plaintext
541 lines
14 KiB
Plaintext
/*
|
|
* CPArray+KVO.j
|
|
* Foundation
|
|
*
|
|
* Created by Ross Boucher.
|
|
* Copyright 2008, 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 "CPArray.j"
|
|
@import "CPNull.j"
|
|
|
|
|
|
@implementation CPObject (CPArrayKVO)
|
|
|
|
- (id)mutableArrayValueForKey:(id)aKey
|
|
{
|
|
return [[_CPKVCArray alloc] initWithKey:aKey forProxyObject:self];
|
|
}
|
|
|
|
- (id)mutableArrayValueForKeyPath:(id)aKeyPath
|
|
{
|
|
var dotIndex = aKeyPath.indexOf(".");
|
|
|
|
if (dotIndex < 0)
|
|
return [self mutableArrayValueForKey:aKeyPath];
|
|
|
|
var firstPart = aKeyPath.substring(0, dotIndex),
|
|
lastPart = aKeyPath.substring(dotIndex + 1);
|
|
|
|
return [[self valueForKeyPath:firstPart] mutableArrayValueForKeyPath:lastPart];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _CPKVCArray : CPMutableArray
|
|
{
|
|
id _proxyObject;
|
|
id _key;
|
|
|
|
SEL _insertSEL;
|
|
Function _insert;
|
|
|
|
SEL _removeSEL;
|
|
Function _remove;
|
|
|
|
SEL _replaceSEL;
|
|
Function _replace;
|
|
|
|
SEL _insertManySEL;
|
|
Function _insertMany;
|
|
|
|
SEL _removeManySEL;
|
|
Function _removeMany;
|
|
|
|
SEL _replaceManySEL;
|
|
Function _replaceMany;
|
|
|
|
SEL _objectAtIndexSEL;
|
|
Function _objectAtIndex;
|
|
|
|
SEL _objectsAtIndexesSEL;
|
|
Function _objectsAtIndexes;
|
|
|
|
SEL _countSEL;
|
|
Function _count;
|
|
|
|
SEL _accessSEL;
|
|
Function _access;
|
|
|
|
SEL _setSEL;
|
|
Function _set;
|
|
}
|
|
|
|
+ (id)alloc
|
|
{
|
|
var array = [];
|
|
|
|
array.isa = self;
|
|
|
|
var ivars = class_copyIvarList(self),
|
|
count = ivars.length;
|
|
|
|
while (count--)
|
|
array[ivar_getName(ivars[count])] = nil;
|
|
|
|
return array;
|
|
}
|
|
|
|
- (id)initWithKey:(id)aKey forProxyObject:(id)anObject
|
|
{
|
|
self = [super init];
|
|
|
|
_key = aKey;
|
|
_proxyObject = anObject;
|
|
|
|
var capitalizedKey = _key.charAt(0).toUpperCase() + _key.substring(1);
|
|
|
|
_insertSEL = sel_getName(@"insertObject:in" + capitalizedKey + "AtIndex:");
|
|
if ([_proxyObject respondsToSelector:_insertSEL])
|
|
_insert = [_proxyObject methodForSelector:_insertSEL];
|
|
|
|
_removeSEL = sel_getName(@"removeObjectFrom" + capitalizedKey + "AtIndex:");
|
|
if ([_proxyObject respondsToSelector:_removeSEL])
|
|
_remove = [_proxyObject methodForSelector:_removeSEL];
|
|
|
|
_replaceSEL = sel_getName(@"replaceObjectIn" + capitalizedKey + "AtIndex:withObject:");
|
|
if ([_proxyObject respondsToSelector:_replaceSEL])
|
|
_replace = [_proxyObject methodForSelector:_replaceSEL];
|
|
|
|
_insertManySEL = sel_getName(@"insert" + capitalizedKey + ":atIndexes:");
|
|
if ([_proxyObject respondsToSelector:_insertManySEL])
|
|
_insertMany = [_proxyObject methodForSelector:_insertManySEL];
|
|
|
|
_removeManySEL = sel_getName(@"remove" + capitalizedKey + "AtIndexes:");
|
|
if ([_proxyObject respondsToSelector:_removeManySEL])
|
|
_removeMany = [_proxyObject methodForSelector:_removeManySEL];
|
|
|
|
_replaceManySEL = sel_getName(@"replace" + capitalizedKey + "AtIndexes:with" + capitalizedKey + ":");
|
|
if ([_proxyObject respondsToSelector:_replaceManySEL])
|
|
_replace = [_proxyObject methodForSelector:_replaceManySEL];
|
|
|
|
_objectAtIndexSEL = sel_getName(@"objectIn" + capitalizedKey + "AtIndex:");
|
|
if ([_proxyObject respondsToSelector:_objectAtIndexSEL])
|
|
_objectAtIndex = [_proxyObject methodForSelector:_objectAtIndexSEL];
|
|
|
|
_objectsAtIndexesSEL = sel_getName(_key + "AtIndexes:");
|
|
if ([_proxyObject respondsToSelector:_objectsAtIndexesSEL])
|
|
_objectsAtIndexes = [_proxyObject methodForSelector:_objectsAtIndexesSEL];
|
|
|
|
_countSEL = sel_getName(@"countOf" + capitalizedKey);
|
|
if ([_proxyObject respondsToSelector:_countSEL])
|
|
_count = [_proxyObject methodForSelector:_countSEL];
|
|
|
|
_accessSEL = sel_getName(_key);
|
|
if ([_proxyObject respondsToSelector:_accessSEL])
|
|
_access = [_proxyObject methodForSelector:_accessSEL];
|
|
|
|
_setSEL = sel_getName(@"set" + capitalizedKey + ":");
|
|
if ([_proxyObject respondsToSelector:_setSEL])
|
|
_set = [_proxyObject methodForSelector:_setSEL];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)copy
|
|
{
|
|
var i = 0,
|
|
theCopy = [],
|
|
count = [self count];
|
|
|
|
for (; i < count; i++)
|
|
[theCopy addObject:[self objectAtIndex:i]];
|
|
|
|
return theCopy;
|
|
}
|
|
|
|
- (id)_representedObject
|
|
{
|
|
if (_access)
|
|
return _access(_proxyObject, _accessSEL);
|
|
|
|
return [_proxyObject valueForKey:_key];
|
|
}
|
|
|
|
- (void)_setRepresentedObject:(id)anObject
|
|
{
|
|
if (_set)
|
|
return _set(_proxyObject, _setSEL, anObject);
|
|
|
|
[_proxyObject setValue:anObject forKey:_key];
|
|
}
|
|
|
|
- (unsigned)count
|
|
{
|
|
if (_count)
|
|
return _count(_proxyObject, _countSEL);
|
|
|
|
return [[self _representedObject] count];
|
|
}
|
|
|
|
- (int)indexOfObject:(CPObject)anObject inRange:(CPRange)aRange
|
|
{
|
|
var index = aRange.location,
|
|
count = aRange.length,
|
|
shouldIsEqual = !!anObject.isa;
|
|
|
|
for (; index < count; ++index)
|
|
{
|
|
var object = [self objectAtIndex:index];
|
|
|
|
if (anObject === object || shouldIsEqual && !!object.isa && [anObject isEqual:object])
|
|
return index;
|
|
}
|
|
|
|
return CPNotFound;
|
|
}
|
|
|
|
- (int)indexOfObject:(CPObject)anObject
|
|
{
|
|
return [self indexOfObject:anObject inRange:CPMakeRange(0, [self count])];
|
|
}
|
|
|
|
- (int)indexOfObjectIdenticalTo:(CPObject)anObject inRange:(CPRange)aRange
|
|
{
|
|
var index = aRange.location,
|
|
count = aRange.length;
|
|
|
|
for (; index < count; ++index)
|
|
if (anObject === [self objectAtIndex:index])
|
|
return index;
|
|
|
|
return CPNotFound;
|
|
}
|
|
|
|
- (int)indexOfObjectIdenticalTo:(CPObject)anObject
|
|
{
|
|
return [self indexOfObjectIdenticalTo:anObject inRange:CPMakeRange(0, [self count])];
|
|
}
|
|
|
|
- (id)objectAtIndex:(unsigned)anIndex
|
|
{
|
|
if (_objectAtIndex)
|
|
return _objectAtIndex(_proxyObject, _objectAtIndexSEL, anIndex);
|
|
|
|
return [[self _representedObject] objectAtIndex:anIndex];
|
|
}
|
|
|
|
- (void)addObject:(id)anObject
|
|
{
|
|
[self insertObject:anObject atIndex:[self count]];
|
|
}
|
|
|
|
- (void)addObjectsFromArray:(CPArray)anArray
|
|
{
|
|
var index = 0,
|
|
count = [anArray count];
|
|
|
|
[self insertObjects:anArray atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange([self count], count)]];
|
|
}
|
|
|
|
- (void)insertObject:(id)anObject atIndex:(unsigned)anIndex
|
|
{
|
|
[self insertObjects:[anObject] atIndexes:[CPIndexSet indexSetWithIndex:anIndex]];
|
|
}
|
|
|
|
- (void)insertObjects:(CPArray)theObjects atIndexes:(CPIndexSet)theIndexes
|
|
{
|
|
if (_insertMany)
|
|
_insertMany(_proxyObject, _insertManySEL, theObjects, theIndexes);
|
|
else if (_insert)
|
|
{
|
|
var indexesArray = [];
|
|
[theIndexes getIndexes:indexesArray maxCount:-1 inIndexRange:nil];
|
|
|
|
for (var index = 0; index < [indexesArray count]; index++)
|
|
{
|
|
var objectIndex = [indexesArray objectAtIndex:index],
|
|
object = [theObjects objectAtIndex:index];
|
|
|
|
_insert(_proxyObject, _insertSEL, object, objectIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var target = [[self _representedObject] copy];
|
|
|
|
[target insertObjects:theObjects atIndexes:theIndexes];
|
|
[self _setRepresentedObject:target];
|
|
}
|
|
}
|
|
|
|
- (void)removeObject:(id)anObject
|
|
{
|
|
[self removeObject:anObject inRange:CPMakeRange(0, [self count])];
|
|
}
|
|
|
|
- (void)removeLastObject
|
|
{
|
|
if (_remove)
|
|
return _remove(_proxyObject, _removeSEL, [self count] - 1);
|
|
|
|
var target = [[self _representedObject] copy];
|
|
|
|
[target removeLastObject];
|
|
[self _setRepresentedObject:target];
|
|
}
|
|
|
|
- (void)removeObjectAtIndex:(unsigned)anIndex
|
|
{
|
|
if (_remove)
|
|
return _remove(_proxyObject, _removeSEL, anIndex);
|
|
|
|
var target = [[self _representedObject] copy];
|
|
|
|
[target removeObjectAtIndex:anIndex];
|
|
[self _setRepresentedObject:target];
|
|
}
|
|
|
|
- (void)replaceObjectAtIndex:(unsigned)anIndex withObject:(id)anObject
|
|
{
|
|
if (_replace)
|
|
return _replace(_proxyObject, _replaceSEL, anIndex, anObject);
|
|
|
|
var target = [[self _representedObject] copy];
|
|
|
|
[target replaceObjectAtIndex:anIndex withObject:anObject];
|
|
[self _setRepresentedObject:target];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
//KVC on CPArray objects act on each item of the array, rather than on the array itself
|
|
|
|
@implementation CPArray (KeyValueCoding)
|
|
|
|
- (id)valueForKey:(CPString)aKey
|
|
{
|
|
if (aKey.indexOf("@") === 0)
|
|
{
|
|
if (aKey.indexOf(".") !== -1)
|
|
[CPException raise:CPInvalidArgumentException reason:"called valueForKey: on an array with a complex key ("+aKey+"). use valueForKeyPath:"];
|
|
|
|
if (aKey == "@count")
|
|
return length;
|
|
|
|
return nil;
|
|
}
|
|
else
|
|
{
|
|
var newArray = [],
|
|
enumerator = [self objectEnumerator],
|
|
object;
|
|
|
|
while ((object = [enumerator nextObject]) !== nil)
|
|
{
|
|
var value = [object valueForKey:aKey];
|
|
|
|
if (value === nil || value === undefined)
|
|
value = [CPNull null];
|
|
|
|
newArray.push(value);
|
|
}
|
|
|
|
return newArray;
|
|
}
|
|
}
|
|
|
|
- (id)valueForKeyPath:(CPString)aKeyPath
|
|
{
|
|
if (aKeyPath.charAt(0) === "@")
|
|
{
|
|
var dotIndex = aKeyPath.indexOf("."),
|
|
operator,
|
|
parameter;
|
|
|
|
if (dotIndex !== -1)
|
|
{
|
|
operator = aKeyPath.substring(1, dotIndex);
|
|
parameter = aKeyPath.substring(dotIndex + 1);
|
|
}
|
|
else
|
|
operator = aKeyPath.substring(1);
|
|
|
|
if (kvoOperators[operator])
|
|
return kvoOperators[operator](self, _cmd, parameter);
|
|
|
|
return nil;
|
|
}
|
|
else
|
|
{
|
|
var newArray = [],
|
|
enumerator = [self objectEnumerator],
|
|
object;
|
|
|
|
while ((object = [enumerator nextObject]) !== nil)
|
|
{
|
|
var value = [object valueForKeyPath:aKeyPath];
|
|
|
|
if (value === nil || value === undefined)
|
|
value = [CPNull null];
|
|
|
|
newArray.push(value);
|
|
}
|
|
|
|
return newArray;
|
|
}
|
|
}
|
|
|
|
- (void)setValue:(id)aValue forKey:(CPString)aKey
|
|
{
|
|
var enumerator = [self objectEnumerator],
|
|
object;
|
|
|
|
while (object = [enumerator nextObject])
|
|
[object setValue:aValue forKey:aKey];
|
|
}
|
|
|
|
- (void)setValue:(id)aValue forKeyPath:(CPString)aKeyPath
|
|
{
|
|
var enumerator = [self objectEnumerator],
|
|
object;
|
|
|
|
while (object = [enumerator nextObject])
|
|
[object setValue:aValue forKeyPath:aKeyPath];
|
|
}
|
|
|
|
@end
|
|
|
|
var kvoOperators = [];
|
|
|
|
// HACK: prevent these from becoming globals. workaround for obj-j "function foo(){}" behavior
|
|
var avgOperator,
|
|
maxOperator,
|
|
minOperator,
|
|
countOperator,
|
|
sumOperator;
|
|
|
|
kvoOperators["avg"] = function avgOperator(self, _cmd, param)
|
|
{
|
|
var objects = [self valueForKeyPath:param],
|
|
length = [objects count],
|
|
index = length,
|
|
average = 0.0;
|
|
|
|
if (!length)
|
|
return 0;
|
|
|
|
while (index--)
|
|
average += [objects[index] doubleValue];
|
|
|
|
return average / length;
|
|
}
|
|
|
|
kvoOperators["max"] = function maxOperator(self, _cmd, param)
|
|
{
|
|
var objects = [self valueForKeyPath:param],
|
|
index = [objects count] - 1,
|
|
max = [objects lastObject];
|
|
|
|
while (index--)
|
|
{
|
|
var item = objects[index];
|
|
if ([max compare:item] < 0)
|
|
max = item;
|
|
}
|
|
|
|
return max;
|
|
}
|
|
|
|
kvoOperators["min"] = function minOperator(self, _cmd, param)
|
|
{
|
|
var objects = [self valueForKeyPath:param],
|
|
index = [objects count] - 1,
|
|
min = [objects lastObject];
|
|
|
|
while (index--)
|
|
{
|
|
var item = objects[index];
|
|
if ([min compare:item] > 0)
|
|
min = item;
|
|
}
|
|
|
|
return min;
|
|
}
|
|
|
|
kvoOperators["count"] = function countOperator(self, _cmd, param)
|
|
{
|
|
return [self count];
|
|
}
|
|
|
|
kvoOperators["sum"] = function sumOperator(self, _cmd, param)
|
|
{
|
|
var objects = [self valueForKeyPath:param],
|
|
index = [objects count],
|
|
sum = 0.0;
|
|
|
|
while (index--)
|
|
sum += [objects[index] doubleValue];
|
|
|
|
return sum;
|
|
}
|
|
|
|
@implementation CPArray (KeyValueObserving)
|
|
|
|
- (void)addObserver:(id)anObserver toObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
|
|
{
|
|
var index = [indexes firstIndex];
|
|
|
|
while (index >= 0)
|
|
{
|
|
[self[index] addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
|
|
|
|
index = [indexes indexGreaterThanIndex:index];
|
|
}
|
|
}
|
|
|
|
- (void)removeObserver:(id)anObserver fromObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath
|
|
{
|
|
var index = [indexes firstIndex];
|
|
|
|
while (index >= 0)
|
|
{
|
|
[self[index] removeObserver:anObserver forKeyPath:aKeyPath];
|
|
|
|
index = [indexes indexGreaterThanIndex:index];
|
|
}
|
|
}
|
|
|
|
- (void)addObserver:(id)observer forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
|
|
{
|
|
if ([isa instanceMethodForSelector:_cmd] === [CPArray instanceMethodForSelector:_cmd])
|
|
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
|
|
else
|
|
[super addObserver:observer forKeyPath:aKeyPath options:options context:context];
|
|
}
|
|
|
|
- (void)removeObserver:(id)observer forKeyPath:(CPString)aKeyPath
|
|
{
|
|
if ([isa instanceMethodForSelector:_cmd] === [CPArray instanceMethodForSelector:_cmd])
|
|
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
|
|
else
|
|
[super removeObserver:observer forKeyPath:aKeyPath];
|
|
}
|
|
|
|
@end
|