diff --git a/Foundation/CPArray_KVO.j b/Foundation/CPArray_KVO.j new file mode 100644 index 000000000..5386f427d --- /dev/null +++ b/Foundation/CPArray_KVO.j @@ -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 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 diff --git a/Foundation/CPKeyValueCoding.j b/Foundation/CPKeyValueCoding.j index 9fccd8845..6a74cfb3f 100644 --- a/Foundation/CPKeyValueCoding.j +++ b/Foundation/CPKeyValueCoding.j @@ -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" diff --git a/Foundation/CPKeyValueObserving.j b/Foundation/CPKeyValueObserving.j index 4e6fc1cdd..e40cb50e3 100644 --- a/Foundation/CPKeyValueObserving.j +++ b/Foundation/CPKeyValueObserving.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= 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" diff --git a/Tests/Foundation/CPKVOTest.j b/Tests/Foundation/CPKVOTest.j index b10b986c8..909c01ec5 100644 --- a/Tests/Foundation/CPKVOTest.j +++ b/Tests/Foundation/CPKVOTest.j @@ -1,5 +1,5 @@ -import -import +@import +@import @implementation CPKVOTest : OJTestCase { @@ -14,6 +14,11 @@ import id focus; } +- (void)setUp +{ + _sawObservation = NO; +} + - (void)testAddObserver { bob = [[PersonTester alloc] init]; @@ -167,7 +172,7 @@ import [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 [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 [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 [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 [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 [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 [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 [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 _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 @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;