mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-07 09:28:02 +00:00
Add KVC+KVO support for "To Many" keys (i.e. Arrays).
This commit is contained in:
1 parent
490ad97f7b
commit
0b05b4b7d8
4 files changed
+648
-142
No files matched your search
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
@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] valueForKeyPath:lastPart];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPKVCArray : CPArray
|
||||
{
|
||||
id _proxyObject;
|
||||
id _key;
|
||||
|
||||
SEL _insertSEL;
|
||||
Function _insert;
|
||||
|
||||
SEL _removeSEL;
|
||||
Function _remove;
|
||||
|
||||
SEL _replaceSEL;
|
||||
Function _replace;
|
||||
|
||||
SEL _objectAtIndexSEL;
|
||||
Function _objectAtIndex;
|
||||
|
||||
SEL _countSEL;
|
||||
Function _count;
|
||||
|
||||
SEL _accessSEL;
|
||||
Function _access;
|
||||
|
||||
SEL _setSEL;
|
||||
Function _set;
|
||||
}
|
||||
|
||||
+ (id)alloc
|
||||
{
|
||||
var a = [];
|
||||
a.isa = self;
|
||||
|
||||
var ivars = class_copyIvarList(self),
|
||||
count = ivars.length;
|
||||
|
||||
while (count--)
|
||||
a[ivar_getName(ivars[count])] = nil;
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
-(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:selector])
|
||||
_insert = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_removeSEL = sel_getName(@"removeObjectFrom"+capitalizedKey+"AtIndex:");
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_remove = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_replaceSEL = sel_getName(@"replaceObjectFrom"+capitalizedKey+"AtIndex:withObject:");
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_replace = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_objectAtIndexSEL = sel_getName(@"objectIn"+capitalizedKey+"AtIndex:");
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_objectAtIndex = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_countSEL = sel_getName(@"countOf"+capitalizedKey);
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_count = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_accessSEL = sel_getName(_key);
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_access = [_proxyObject methodForSelector:selector];
|
||||
|
||||
_setSEL = sel_getName(@"set"+capitalizedKey+":");
|
||||
if ([_proxyObject respondsToSelector:selector])
|
||||
_set = [_proxyObject methodForSelector:selector];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
var theCopy = [],
|
||||
count = [self count];
|
||||
|
||||
for (var i=0; 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];
|
||||
}
|
||||
|
||||
- (id)objectAtIndex:(unsigned)anIndex
|
||||
{
|
||||
if(_objectAtIndex)
|
||||
return _objectAtIndex(_proxyObject, _objectAtIndexSEL, anIndex);
|
||||
|
||||
return [[self _representedObject] objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)addObject:(id)anObject
|
||||
{
|
||||
if (_insert)
|
||||
return _insert(_proxyObject, _insertSEL, anObject, [self count]);
|
||||
|
||||
var target = [[self _representedObject] copy];
|
||||
|
||||
[target addObject:anObject];
|
||||
[_self _setRepresentedObject:target];
|
||||
}
|
||||
|
||||
- (void)insertObject:(id)anObject atIndex:(unsigned)anIndex
|
||||
{
|
||||
if (_insert)
|
||||
return _insert(_proxyObject, _insertSEL, anObject, anIndex);
|
||||
|
||||
var target = [[self _representedObject] copy];
|
||||
|
||||
[target insertObject:anObject atIndex:anIndex];
|
||||
[_self _setRepresentedObject:target];
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
- (CPArray)objectsAtIndexes:(CPIndexSet)indexes
|
||||
{
|
||||
var index = [indexes firstIndex],
|
||||
objects = [];
|
||||
|
||||
while(index != CPNotFound)
|
||||
{
|
||||
[objects addObject:[self objectAtIndex:index]];
|
||||
index = [indexes indexGreaterThanIndex:index];
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
@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])
|
||||
{
|
||||
var value = [object valueForKey:aKey];
|
||||
|
||||
if (!value && value !== "")
|
||||
value = [NSNull null];
|
||||
|
||||
newArray.push(value);
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)valueForKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (aKey.indexOf("@") === 0)
|
||||
{
|
||||
var dotIndex = akey.indexOf("."),
|
||||
operator = aKey.substring(1, dotIndex),
|
||||
parameter = aKey.substring(dotIndex+1);
|
||||
|
||||
if (kvoOperators[operator])
|
||||
return kvoOperators[operator](self, _cmd, parameter);
|
||||
|
||||
return nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newArray = [],
|
||||
enumerator = [self objectEnumerator],
|
||||
object;
|
||||
|
||||
while (object = [enumerator nextObject])
|
||||
{
|
||||
var value = [object valueForKeyPath:aKeyPath];
|
||||
|
||||
if (!value && value !== "")
|
||||
value = [NSNull 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 = [];
|
||||
|
||||
kvoOperators["avg"] = function avgOperator(self, _cmd, param)
|
||||
{
|
||||
var objects = [self valueForKeyPath:param],
|
||||
length = [objects count],
|
||||
average = 0.0;
|
||||
|
||||
while(length--)
|
||||
average += [objects[length] doubleValue] / count;
|
||||
|
||||
return average;
|
||||
}
|
||||
|
||||
kvoOperators["max"] = function maxOperator(self, _cmd, param)
|
||||
{
|
||||
var objects = [self valueForKeyPath:param],
|
||||
length = [objects count] - 1,
|
||||
max = [objects lastObject];
|
||||
|
||||
while (length--)
|
||||
{
|
||||
var item = objects[length];
|
||||
if ([max compare:item] < 0)
|
||||
max = item;
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
kvoOperators["min"] = function minOperator(self, _cmd, param)
|
||||
{
|
||||
var objects = [self valueForKeyPath:param],
|
||||
length = [objects count] - 1,
|
||||
min = [objects lastObject];
|
||||
|
||||
while (length--)
|
||||
{
|
||||
var item = objects[length];
|
||||
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],
|
||||
length = [objects count],
|
||||
sum = 0.0;
|
||||
|
||||
while(length--)
|
||||
sum += [objects[length] 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]==[NSArray 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]==[NSArray instanceMethodForSelector:_cmd])
|
||||
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
|
||||
else
|
||||
[super removeObserver:observer forKeyPath:aKeyPath];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -278,81 +278,4 @@ CPUnknownUserInfoKey = @"CPUnknownUserInfoKey";
|
||||
|
||||
@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 == "@count")
|
||||
return length;
|
||||
|
||||
return nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newArray = [],
|
||||
enumerator = [self objectEnumerator],
|
||||
object;
|
||||
|
||||
while (object = [enumerator nextObject])
|
||||
{
|
||||
var value = [object valueForKey:aKey];
|
||||
|
||||
if (!value && value !== "")
|
||||
value = [NSNull null];
|
||||
|
||||
newArray.push(value);
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)valueForKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (aKey.indexOf("@") == 0)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newArray = [],
|
||||
enumerator = [self objectEnumerator],
|
||||
object;
|
||||
|
||||
while (object = [enumerator nextObject])
|
||||
{
|
||||
var value = [object valueForKeyPath:aKeyPath];
|
||||
|
||||
if (!value && value !== "")
|
||||
value = [NSNull 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
|
||||
@import "CPArray_KVO.j"
|
||||
@@ -31,12 +31,18 @@
|
||||
|
||||
- (void)willChangeValueForKey:(CPString)aKey
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
- (void)didChangeValueForKey:(CPString)aKey
|
||||
{
|
||||
}
|
||||
|
||||
- (void)willChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)key
|
||||
{
|
||||
}
|
||||
|
||||
- (void)didChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)key
|
||||
{
|
||||
}
|
||||
|
||||
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
|
||||
@@ -79,9 +85,6 @@ CPKeyValueObservingOptionOld = 1 << 1;
|
||||
CPKeyValueObservingOptionInitial = 1 << 2;
|
||||
CPKeyValueObservingOptionPrior = 1 << 3;
|
||||
|
||||
//convenience
|
||||
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
|
||||
|
||||
// KVO Change Dictionary Keys
|
||||
CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey";
|
||||
CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey";
|
||||
@@ -89,6 +92,15 @@ CPKeyValueChangeOldKey = @"CPKeyValueChangeOldKey";
|
||||
CPKeyValueChangeIndexesKey = @"CPKeyValueChangeIndexesKey";
|
||||
CPKeyValueChangeNotificationIsPriorKey = @"CPKeyValueChangeNotificationIsPriorKey";
|
||||
|
||||
// KVO Change Types
|
||||
CPKeyValueChangeSetting = 1;
|
||||
CPKeyValueChangeInsertion = 2;
|
||||
CPKeyValueChangeRemoval = 3;
|
||||
CPKeyValueChangeReplacement = 4;
|
||||
|
||||
//convenience
|
||||
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
|
||||
|
||||
// Map of real objects to their KVO proxy
|
||||
var KVOProxyMap = [CPDictionary dictionary],
|
||||
DependentKeysMap = [CPDictionary dictionary];
|
||||
@@ -173,22 +185,23 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1),
|
||||
found = false,
|
||||
replacementMethods = [
|
||||
"set"+capitalizedKey+":",
|
||||
"_set"+capitalizedKey+":",
|
||||
"insertObject:in"+capitalizedKey+"AtIndex:",
|
||||
"replaceObjectIn"+capitalizedKey+"AtIndex:",
|
||||
"removeObjectFrom"+capitalizedKey+"AtIndex:"
|
||||
"set"+capitalizedKey+":", _kvoMethodForMethod,
|
||||
"_set"+capitalizedKey+":", _kvoMethodForMethod,
|
||||
"insertObject:in"+capitalizedKey+"AtIndex:", _kvoInsertMethodForMethod,
|
||||
"replaceObjectIn"+capitalizedKey+"AtIndex:withObject:", _kvoReplaceMethodForMethod,
|
||||
"removeObjectFrom"+capitalizedKey+"AtIndex:", _kvoRemoveMethodForMethod
|
||||
];
|
||||
|
||||
for (var i=0, count=replacementMethods.length; i<count; i++)
|
||||
for (var i=0, count=replacementMethods.length; i<count; i+=2)
|
||||
{
|
||||
var theSelector = sel_getName(replacementMethods[i]);
|
||||
var theSelector = sel_getName(replacementMethods[i]),
|
||||
theReplacementMethod = replacementMethods[i+1];
|
||||
|
||||
if ([_nativeClass instancesRespondToSelector:theSelector])
|
||||
{
|
||||
var theMethod = class_getInstanceMethod(_nativeClass, theSelector);
|
||||
|
||||
class_addMethod(_targetObject.isa, theSelector, _kvoMethodForMethod(aKey, theMethod), "");
|
||||
class_addMethod(_targetObject.isa, theSelector, theReplacementMethod(aKey, theMethod), "");
|
||||
|
||||
found = true;
|
||||
}
|
||||
@@ -282,21 +295,41 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_sendNotificationsForKey:(CPString)aKey isBefore:(BOOL)isBefore
|
||||
//FIXME: We do not compute and cache if CPKeyValueObservingOptionOld is needed, so we may do unnecessary work
|
||||
|
||||
- (void)_sendNotificationsForKey:(CPString)aKey changeOptions:(CPDictionary)changeOptions isBefore:(BOOL)isBefore
|
||||
{
|
||||
var changes = [_changesForKey objectForKey:aKey];
|
||||
|
||||
if (isBefore)
|
||||
{
|
||||
changes = [CPDictionary dictionary];
|
||||
changes = changeOptions;
|
||||
|
||||
var oldValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
if (!oldValue && oldValue !== "")
|
||||
oldValue = [CPNull null];
|
||||
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
|
||||
|
||||
if (indexes)
|
||||
{
|
||||
var type = [changes objectForKey:CPKeyValueChangeKindKey];
|
||||
|
||||
// for to-many relationships, oldvalue is only sensible for replace and remove
|
||||
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeRemoval)
|
||||
{
|
||||
//FIXME: do we need to go through and replace "" with CPNull?
|
||||
var oldValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
|
||||
[changes setValue:oldValues forKey:CPKeyValueChangeOldKey];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
if (!oldValue && oldValue !== "")
|
||||
oldValue = [CPNull null];
|
||||
|
||||
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
|
||||
}
|
||||
|
||||
[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
|
||||
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
|
||||
|
||||
[_changesForKey setObject:changes forKey:aKey];
|
||||
}
|
||||
@@ -304,12 +337,29 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
{
|
||||
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
|
||||
|
||||
var newValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
if (!newValue && newValue !== "")
|
||||
newValue = [CPNull null];
|
||||
|
||||
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
|
||||
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
|
||||
|
||||
if (indexes)
|
||||
{
|
||||
var type = [changes objectForKey:CPKeyValueChangeKindKey];
|
||||
|
||||
// for to-many relationships, oldvalue is only sensible for replace and remove
|
||||
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeInsertion)
|
||||
{
|
||||
//FIXME: do we need to go through and replace "" with CPNull?
|
||||
var oldValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
|
||||
[changes setValue:oldValues forKey:CPKeyValueChangeNewKey];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newValue = [_targetObject valueForKey:aKey];
|
||||
|
||||
if (!newValue && newValue !== "")
|
||||
newValue = [CPNull null];
|
||||
|
||||
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
|
||||
}
|
||||
}
|
||||
|
||||
var observers = [[_observersForKey objectForKey:aKey] allValues],
|
||||
@@ -331,7 +381,7 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
return;
|
||||
|
||||
for (var i=0, count=keysComposedOfKey.length; i<count; i++)
|
||||
[self _sendNotificationsForKey:keysComposedOfKey[i] isBefore:isBefore];
|
||||
[self _sendNotificationsForKey:keysComposedOfKey[i] changeOptions:changeOptions isBefore:isBefore];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -345,7 +395,9 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:YES];
|
||||
var changeOptions = [CPDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES];
|
||||
}
|
||||
|
||||
- (void)didChangeValueForKey:(CPString)aKey
|
||||
@@ -353,7 +405,25 @@ var KVOProxyMap = [CPDictionary dictionary],
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:NO];
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO];
|
||||
}
|
||||
|
||||
- (void)willChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
|
||||
{
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
var changeOptions = [CPDictionary dictionaryWithObjects:[change, indexes] forKeys:[CPKeyValueChangeKindKey, CPKeyValueChangeIndexesKey]];
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES];
|
||||
}
|
||||
|
||||
- (void)didChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
|
||||
{
|
||||
if (!aKey)
|
||||
return;
|
||||
|
||||
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO];
|
||||
}
|
||||
|
||||
- (Class)class
|
||||
@@ -474,48 +544,42 @@ var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, a
|
||||
|
||||
var _kvoMethodForMethod = function _kvoMethodForMethod(theKey, theMethod)
|
||||
{
|
||||
return function(self)
|
||||
return function(self, _cmd, object)
|
||||
{
|
||||
[self willChangeValueForKey:theKey];
|
||||
theMethod.method_imp.apply(self, arguments);
|
||||
theMethod.method_imp(self, _cmd, object);
|
||||
[self didChangeValueForKey:theKey];
|
||||
}
|
||||
}
|
||||
|
||||
@implementation CPArray (KeyValueObserving)
|
||||
|
||||
- (void)addObserver:(id)anObserver toObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
|
||||
var _kvoInsertMethodForMethod = function _kvoInsertMethodForMethod(theKey, theMethod)
|
||||
{
|
||||
var index = [indexes firstIndex];
|
||||
|
||||
while (index >= 0)
|
||||
return function(self, _cmd, object, index)
|
||||
{
|
||||
[self[index] addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
|
||||
|
||||
index = [indexes indexGreaterThanIndex:index];
|
||||
[self willChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
|
||||
theMethod.method_imp(self, _cmd, object, index);
|
||||
[self didChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObserver:(id)anObserver fromObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath
|
||||
var _kvoReplaceMethodForMethod = function _kvoReplaceMethodForMethod(theKey, theMethod)
|
||||
{
|
||||
var index = [indexes firstIndex];
|
||||
|
||||
while (index >= 0)
|
||||
return function(self, _cmd, index, object)
|
||||
{
|
||||
[self[index] removeObserver:anObserver forKeyPath:aKeyPath];
|
||||
|
||||
index = [indexes indexGreaterThanIndex:index];
|
||||
[self willChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
|
||||
theMethod.method_imp(self, _cmd, index, object);
|
||||
[self didChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
|
||||
}
|
||||
}
|
||||
|
||||
-(void)addObserver:(id)observer forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
|
||||
var _kvoRemoveMethodForMethod = function _kvoRemoveMethodForMethod(theKey, theMethod)
|
||||
{
|
||||
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
|
||||
return function(self, _cmd, index)
|
||||
{
|
||||
[self willChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
|
||||
theMethod.method_imp(self, _cmd, index);
|
||||
[self didChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
|
||||
}
|
||||
}
|
||||
|
||||
-(void)removeObserver:(id)observer forKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
|
||||
}
|
||||
|
||||
@end
|
||||
@import "CPArray_KVO.j"
|
||||
@@ -1,5 +1,5 @@
|
||||
import <Foundation/CPKeyValueCoding.j>
|
||||
import <Foundation/CPKeyValueObserving.j>
|
||||
@import <Foundation/CPKeyValueCoding.j>
|
||||
@import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
@implementation CPKVOTest : OJTestCase
|
||||
{
|
||||
@@ -14,6 +14,11 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
id focus;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
_sawObservation = NO;
|
||||
}
|
||||
|
||||
- (void)testAddObserver
|
||||
{
|
||||
bob = [[PersonTester alloc] init];
|
||||
@@ -167,7 +172,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testMultipartKey"];
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:0 context:"testMultipartKey"];
|
||||
|
||||
[bob setName:@"bob"];
|
||||
|
||||
@@ -184,7 +189,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[cs101 setTeacher:bob];
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testThreePartKey"];
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:0 context:"testThreePartKey"];
|
||||
|
||||
[focus setValue:"ford focus" forKey:"model"];
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
@@ -199,7 +204,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[cs101 setTeacher:bob];
|
||||
[focus setValue:"2000" forKey:"year"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:nil context:"testThreePartKeyPart2"];
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:0 context:"testThreePartKeyPart2"];
|
||||
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
@@ -213,7 +218,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
[cs101 setTeacher:bob];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testRemoveMultipartKey"];
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.name" options:0 context:"testRemoveMultipartKey"];
|
||||
|
||||
[cs101 removeObserver:self forKeyPath:@"teacher.name"];
|
||||
|
||||
@@ -231,7 +236,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[cs101 setTeacher:bob];
|
||||
[bob setValue:focus forKey:"car"];
|
||||
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testRemoveThreePartKey"];
|
||||
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:0 context:"testRemoveThreePartKey"];
|
||||
[cs101 removeObserver:self forKeyPath:@"teacher.car.model"];
|
||||
|
||||
[focus setValue:"ford focus" forKey:"model"];
|
||||
@@ -250,7 +255,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges"];
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges"];
|
||||
|
||||
[a setValue:[D new] forKeyPath:"b.c.d"];
|
||||
|
||||
@@ -267,7 +272,7 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges2"];
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges2"];
|
||||
|
||||
[a setValue:nil forKeyPath:"b.c"];
|
||||
|
||||
@@ -284,13 +289,42 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
[a setValue:[E new] forKeyPath:"b.c.d.e"];
|
||||
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges3"];
|
||||
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges3"];
|
||||
|
||||
[a setValue:7 forKeyPath:"b.c.d.e.f"];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testInsertIntoToManyProperty
|
||||
{
|
||||
var tester = [ToManyTester new];
|
||||
|
||||
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
|
||||
|
||||
|
||||
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:"testInsertIntoToManyProperty"];
|
||||
|
||||
[tester insertObject:5 inManagedObjectsAtIndex:4];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testRemoveFromToManyProperty
|
||||
{
|
||||
var tester = [ToManyTester new];
|
||||
|
||||
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
|
||||
|
||||
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:"testRemoveFromToManyProperty"];
|
||||
|
||||
[tester removeObjectFromManagedObjectsAtIndex:0];
|
||||
|
||||
[self assertTrue: _sawObservation message:"Never recieved an observation"];
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)testPerformance
|
||||
{
|
||||
bob = [PersonTester new];
|
||||
@@ -432,6 +466,25 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
_sawDependentObservation = YES;
|
||||
break;
|
||||
|
||||
case "testInsertIntoToManyProperty":
|
||||
var type = [changes objectForKey:CPKeyValueChangeKindKey];
|
||||
[self assertTrue: type == CPKeyValueChangeInsertion message: "Should have been an insertion, was: "+type];
|
||||
|
||||
var values = [changes objectForKey:CPKeyValueChangeNewKey];
|
||||
[self assertTrue: [values isEqual:[5]] message: "array should have contained 5, was: "+values+" type: "+[values.isa description]+" length: "+values.length];
|
||||
|
||||
break;
|
||||
|
||||
case "testRemoveFromToManyProperty":
|
||||
var type = [changes objectForKey:CPKeyValueChangeKindKey];
|
||||
[self assertTrue: type == CPKeyValueChangeRemoval message: "Should have been a removal, was: "+type];
|
||||
|
||||
var values = [changes objectForKey:CPKeyValueChangeOldKey];
|
||||
[self assertTrue: [values isEqual:[1]] message: "array should have contained 1, was: "+values+" type: "+[values.isa description]+" length: "+values.length];
|
||||
[[anObject valueForKey:@"managedObjects"] isEqual:[2, 3, 4]];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
[self assertFalse:YES message:"unhandled observation, must be an error"];
|
||||
return;
|
||||
@@ -501,6 +554,33 @@ import <Foundation/CPKeyValueObserving.j>
|
||||
|
||||
@end
|
||||
|
||||
@implementation ToManyTester : CPObject
|
||||
{
|
||||
CPArray managedObjects;
|
||||
}
|
||||
|
||||
- (unsigned int)countOfManagedObjects
|
||||
{
|
||||
return [managedObjects count];
|
||||
}
|
||||
|
||||
- (id)objectInManagedObjectsAtIndex:(unsigned)anIndex
|
||||
{
|
||||
return [managedObjects objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)removeObjectFromManagedObjectsAtIndex:(unsigned)anIndex
|
||||
{
|
||||
[managedObjects removeObjectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)insertObject:(id)anObject inManagedObjectsAtIndex:(unsigned)anIndex
|
||||
{
|
||||
[managedObjects insertObject:anObject atIndex:anIndex];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation A : CPObject
|
||||
{
|
||||
id b;
|
||||
|
||||
Reference in new issue
Block a user