/* * CPKeyValueCoding.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 "CPDictionary.j" @import "CPException.j" @import "CPNull.j" @import "CPObject.j" @import "CPSet.j" @implementation CPObject (KeyValueObserving) - (void)willChangeValueForKey:(CPString)aKey { } - (void)didChangeValueForKey:(CPString)aKey { } - (void)willChange:(CPKeyValueChange)aChange valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey { } - (void)didChange:(CPKeyValueChange)aChange valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey { } - (void)willChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)aMutationKind usingObjects:(CPSet)objects { } - (void)didChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)aMutationKind usingObjects:(CPSet)objects { } - (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext { if (!anObserver || !aPath) return; [[_CPKVOProxy proxyForObject:self] _addObserver:anObserver forKeyPath:aPath options:options context:aContext]; } - (void)removeObserver:(id)anObserver forKeyPath:(CPString)aPath { if (!anObserver || !aPath) return; [self[KVOProxyKey] _removeObserver:anObserver forKeyPath:aPath]; } + (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey { return YES; } + (CPSet)keyPathsForValuesAffectingValueForKey:(CPString)aKey { var capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1), selector = "keyPathsForValuesAffecting" + capitalizedKey; if ([[self class] respondsToSelector:selector]) return objj_msgSend([self class], selector); return [CPSet set]; } - (void)applyChange:(CPDictionary)aChange toKeyPath:(CPString)aKeyPath { var changeKind = [aChange objectForKey:CPKeyValueChangeKindKey], oldValue = [aChange objectForKey:CPKeyValueChangeOldKey], newValue = [aChange objectForKey:CPKeyValueChangeNewKey]; if (newValue === [CPNull null]) newValue = nil; if (changeKind === CPKeyValueChangeSetting) return [self setValue:newValue forKeyPath:aKeyPath]; var indexes = [aChange objectForKey:CPKeyValueChangeIndexesKey]; // If we have an indexes entry, then we have an ordered to-many relationship if (indexes) { if (changeKind === CPKeyValueChangeInsertion) [[self mutableArrayValueForKeyPath:aKeyPath] insertObjects:newValue atIndexes:indexes]; else if (changeKind === CPKeyValueChangeRemoval) [[self mutableArrayValueForKeyPath:aKeyPath] removeObjectsAtIndexes:indexes]; else if (changeKind === CPKeyValueChangeReplacement) [[self mutableArrayValueForKeyPath:aKeyPath] replaceObjectAtIndexes:indexes withObjects:newValue]; } else { if (changeKind === CPKeyValueChangeInsertion) [[self mutableSetValueForKeyPath:aKeyPath] unionSet:newValue]; else if (changeKind === CPKeyValueChangeRemoval) [[self mutableSetValueForKeyPath:aKeyPath] minusSet:oldValue]; else if (changeKind === CPKeyValueChangeReplacement) [[self mutableSetValueForKeyPath:aKeyPath] setSet:newValue]; } } @end @implementation CPDictionary (KeyValueObserving) - (CPDictionary)inverseChangeDictionary { var inverseChangeDictionary = [self mutableCopy], changeKind = [self objectForKey:CPKeyValueChangeKindKey]; if (changeKind === CPKeyValueChangeSetting || changeKind === CPKeyValueChangeReplacement) { [inverseChangeDictionary setObject:[self objectForKey:CPKeyValueChangeOldKey] forKey:CPKeyValueChangeNewKey]; [inverseChangeDictionary setObject:[self objectForKey:CPKeyValueChangeNewKey] forKey:CPKeyValueChangeOldKey]; } else if (changeKind === CPKeyValueChangeInsertion) { [inverseChangeDictionary setObject:CPKeyValueChangeRemoval forKey:CPKeyValueChangeKindKey]; [inverseChangeDictionary setObject:[self objectForKey:CPKeyValueChangeNewKey] forKey:CPKeyValueChangeOldKey]; [inverseChangeDictionary removeObjectForKey:CPKeyValueChangeNewKey]; } else if (changeKind === CPKeyValueChangeRemoval) { [inverseChangeDictionary setObject:CPKeyValueChangeInsertion forKey:CPKeyValueChangeKindKey]; [inverseChangeDictionary setObject:[self objectForKey:CPKeyValueChangeOldKey] forKey:CPKeyValueChangeNewKey]; [inverseChangeDictionary removeObjectForKey:CPKeyValueChangeOldKey]; } return inverseChangeDictionary; } @end // KVO Options CPKeyValueObservingOptionNew = 1 << 0; CPKeyValueObservingOptionOld = 1 << 1; CPKeyValueObservingOptionInitial = 1 << 2; CPKeyValueObservingOptionPrior = 1 << 3; // KVO Change Dictionary Keys CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey"; CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey"; CPKeyValueChangeOldKey = @"CPKeyValueChangeOldKey"; CPKeyValueChangeIndexesKey = @"CPKeyValueChangeIndexesKey"; CPKeyValueChangeNotificationIsPriorKey = @"CPKeyValueChangeNotificationIsPriorKey"; // KVO Change Types CPKeyValueChangeSetting = 1; CPKeyValueChangeInsertion = 2; CPKeyValueChangeRemoval = 3; CPKeyValueChangeReplacement = 4; // CPKeyValueSetMutationKind CPKeyValueUnionSetMutation = 1; CPKeyValueMinusSetMutation = 2; CPKeyValueIntersectSetMutation = 3; CPKeyValueSetSetMutation = 4; //FIXME: "secret" dict ivar-keys are workaround to support unordered to-many relationships without too many modifications _CPKeyValueChangeSetMutationObjectsKey = @"_CPKeyValueChangeSetMutationObjectsKey"; _CPKeyValueChangeSetMutationKindKey = @"_CPKeyValueChangeSetMutationKindKey"; _CPKeyValueChangeSetMutationNewValueKey = @"_CPKeyValueChangeSetMutationNewValueKey"; var _changeKindForSetMutationKind = function(mutationKind) { switch (mutationKind) { case CPKeyValueUnionSetMutation: return CPKeyValueChangeInsertion; case CPKeyValueMinusSetMutation: return CPKeyValueChangeRemoval; case CPKeyValueIntersectSetMutation: return CPKeyValueChangeRemoval; case CPKeyValueSetSetMutation: return CPKeyValueChangeReplacement; } } var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld, DependentKeysKey = "$KVODEPENDENT", KVOProxyKey = "$KVOPROXY"; //rule of thumb: _ methods are called on the real proxy object, others are called on the "fake" proxy object (aka the real object) /* @ignore */ @implementation _CPKVOProxy : CPObject { id _targetObject; Class _nativeClass; CPDictionary _changesForKey; Object _observersForKey; int _observersForKeyLength; CPSet _replacedKeys; } + (id)proxyForObject:(CPObject)anObject { var proxy = anObject[KVOProxyKey]; if (proxy) return proxy; return [[self alloc] initWithTarget:anObject]; } - (id)initWithTarget:(id)aTarget { if (self = [super init]) { _targetObject = aTarget; _nativeClass = [aTarget class]; _observersForKey = {}; _changesForKey = {}; _observersForKeyLength = 0; [self _replaceClass]; aTarget[KVOProxyKey] = self; } return self; } - (void)_replaceClass { var currentClass = _nativeClass, kvoClassName = "$KVO_" + class_getName(_nativeClass), existingKVOClass = objj_lookUpClass(kvoClassName); if (existingKVOClass) { _targetObject.isa = existingKVOClass; _replacedKeys = existingKVOClass._replacedKeys; return; } var kvoClass = objj_allocateClassPair(currentClass, kvoClassName); objj_registerClassPair(kvoClass); _replacedKeys = [CPSet set]; kvoClass._replacedKeys = _replacedKeys; //copy in the methods from our model subclass var methods = class_copyMethodList(_CPKVOModelSubclass); if ([_targetObject isKindOfClass:[CPDictionary class]]) methods = methods.concat(class_copyMethodList(_CPKVOModelDictionarySubclass)); class_addMethods(kvoClass, methods); _targetObject.isa = kvoClass; } - (void)_replaceModifiersForKey:(CPString)aKey { if ([_replacedKeys containsObject:aKey] || ![_nativeClass automaticallyNotifiesObserversForKey:aKey]) return; [_replacedKeys addObject:aKey]; var theClass = _nativeClass, KVOClass = _targetObject.isa, capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1); // Attribute and To-One Relationships var setKey_selector = sel_getUid("set" + capitalizedKey + ":"), setKey_method = class_getInstanceMethod(theClass, setKey_selector); if (setKey_method) { var setKey_method_imp = setKey_method.method_imp; class_addMethod(KVOClass, setKey_selector, function(self, _cmd, anObject) { [self willChangeValueForKey:aKey]; setKey_method_imp(self, _cmd, anObject); [self didChangeValueForKey:aKey]; }, ""); } // FIXME: Deprecated. var _setKey_selector = sel_getUid("_set" + capitalizedKey + ":"), _setKey_method = class_getInstanceMethod(theClass, _setKey_selector); if (_setKey_method) { var _setKey_method_imp = _setKey_method.method_imp; class_addMethod(KVOClass, _setKey_selector, function(self, _cmd, anObject) { [self willChangeValueForKey:aKey]; _setKey_method_imp(self, _cmd, anObject); [self didChangeValueForKey:aKey]; }, ""); } // Ordered To-Many Relationships var insertObject_inKeyAtIndex_selector = sel_getUid("insertObject:in" + capitalizedKey + "AtIndex:"), insertObject_inKeyAtIndex_method = class_getInstanceMethod(theClass, insertObject_inKeyAtIndex_selector), insertKey_atIndexes_selector = sel_getUid("insert" + capitalizedKey + ":atIndexes:"), insertKey_atIndexes_method = class_getInstanceMethod(theClass, insertKey_atIndexes_selector), removeObjectFromKeyAtIndex_selector = sel_getUid("removeObjectFrom" + capitalizedKey + "AtIndex:"), removeObjectFromKeyAtIndex_method = class_getInstanceMethod(theClass, removeObjectFromKeyAtIndex_selector), removeKeyAtIndexes_selector = sel_getUid("remove" + capitalizedKey + "AtIndexes:"), removeKeyAtIndexes_method = class_getInstanceMethod(theClass, removeKeyAtIndexes_selector); if ((insertObject_inKeyAtIndex_method || insertKey_atIndexes_method) && (removeObjectFromKeyAtIndex_method || removeKeyAtIndexes_method)) { if (insertObject_inKeyAtIndex_method) { var insertObject_inKeyAtIndex_method_imp = insertObject_inKeyAtIndex_method.method_imp; class_addMethod(KVOClass, insertObject_inKeyAtIndex_selector, function(self, _cmd, anObject, anIndex) { [self willChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; insertObject_inKeyAtIndex_method_imp(self, _cmd, anObject, anIndex); [self didChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; }, ""); } if (insertKey_atIndexes_method) { var insertKey_atIndexes_method_imp = insertKey_atIndexes_method.method_imp; class_addMethod(KVOClass, insertKey_atIndexes_selector, function(self, _cmd, objects, indexes) { [self willChange:CPKeyValueChangeInsertion valuesAtIndexes:[indexes copy] forKey:aKey]; insertKey_atIndexes_method_imp(self, _cmd, objects, indexes); [self didChange:CPKeyValueChangeInsertion valuesAtIndexes:[indexes copy] forKey:aKey]; }, ""); } if (removeObjectFromKeyAtIndex_method) { var removeObjectFromKeyAtIndex_method_imp = removeObjectFromKeyAtIndex_method.method_imp; class_addMethod(KVOClass, removeObjectFromKeyAtIndex_selector, function(self, _cmd, anIndex) { [self willChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; removeObjectFromKeyAtIndex_method_imp(self, _cmd, anIndex); [self didChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; }, ""); } if (removeKeyAtIndexes_method) { var removeKeyAtIndexes_method_imp = removeKeyAtIndexes_method.method_imp; class_addMethod(KVOClass, removeKeyAtIndexes_selector, function(self, _cmd, indexes) { [self willChange:CPKeyValueChangeRemoval valuesAtIndexes:[indexes copy] forKey:aKey]; removeKeyAtIndexes_method_imp(self, _cmd, indexes); [self didChange:CPKeyValueChangeRemoval valuesAtIndexes:[indexes copy] forKey:aKey]; }, ""); } // These are optional. var replaceObjectInKeyAtIndex_withObject_selector = sel_getUid("replaceObjectIn" + capitalizedKey + "AtIndex:withObject:"), replaceObjectInKeyAtIndex_withObject_method = class_getInstanceMethod(theClass, replaceObjectInKeyAtIndex_withObject_selector); if (replaceObjectInKeyAtIndex_withObject_method) { var replaceObjectInKeyAtIndex_withObject_method_imp = replaceObjectInKeyAtIndex_withObject_method.method_imp; class_addMethod(KVOClass, replaceObjectInKeyAtIndex_withObject_selector, function(self, _cmd, anIndex, anObject) { [self willChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; replaceObjectInKeyAtIndex_withObject_method_imp(self, _cmd, anIndex, anObject); [self didChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] forKey:aKey]; }, ""); } var replaceKeyAtIndexes_withKey_selector = sel_getUid("replace" + capitalizedKey + "AtIndexes:with" + capitalizedKey + ":"), replaceKeyAtIndexes_withKey_method = class_getInstanceMethod(theClass, replaceKeyAtIndexes_withKey_selector); if (replaceKeyAtIndexes_withKey_method) { var replaceKeyAtIndexes_withKey_method_imp = replaceKeyAtIndexes_withKey_method.method_imp; class_addMethod(KVOClass, replaceKeyAtIndexes_withKey_selector, function(self, _cmd, indexes, objects) { [self willChange:CPKeyValueChangeReplacement valuesAtIndexes:[indexes copy] forKey:aKey]; replaceObjectInKeyAtIndex_withObject_method_imp(self, _cmd, indexes, objects); [self didChange:CPKeyValueChangeReplacement valuesAtIndexes:[indexes copy] forKey:aKey]; }, ""); } } // Unordered To-Many Relationships var addKeyObject_selector = sel_getUid("add" + capitalizedKey + "Object:"), addKeyObject_method = class_getInstanceMethod(theClass, addKeyObject_selector), addKey_selector = sel_getUid("add" + capitalizedKey + ":"), addKey_method = class_getInstanceMethod(theClass, addKey_selector), removeKeyObject_selector = sel_getUid("remove" + capitalizedKey + "Object:"), removeKeyObject_method = class_getInstanceMethod(theClass, removeKeyObject_selector), removeKey_selector = sel_getUid("remove" + capitalizedKey + ":"), removeKey_method = class_getInstanceMethod(theClass, removeKey_selector); if ((addKeyObject_method || addKey_method) && (removeKeyObject_method || removeKey_method)) { if (addKeyObject_method) { var addKeyObject_method_imp = addKeyObject_method.method_imp; class_addMethod(KVOClass, addKeyObject_selector, function(self, _cmd, anObject) { [self willChangeValueForKey:aKey withSetMutation:CPKeyValueUnionSetMutation usingObjects:[CPSet setWithObject:anObject]]; addKeyObject_method_imp(self, _cmd, anObject); [self didChangeValueForKey:aKey withSetMutation:CPKeyValueUnionSetMutation usingObjects:[CPSet setWithObject:anObject]]; }, ""); } if (addKey_method) { var addKey_method_imp = addKey_method.method_imp; class_addMethod(KVOClass, addKey_selector, function(self, _cmd, objects) { [self willChangeValueForKey:aKey withSetMutation:CPKeyValueUnionSetMutation usingObjects:[objects copy]]; addKey_method_imp(self, _cmd, objects); [self didChangeValueForKey:aKey withSetMutation:CPKeyValueUnionSetMutation usingObjects:[objects copy]]; }, ""); } if (removeKeyObject_method) { var removeKeyObject_method_imp = removeKeyObject_method.method_imp; class_addMethod(KVOClass, removeKeyObject_selector, function(self, _cmd, anObject) { [self willChangeValueForKey:aKey withSetMutation:CPKeyValueMinusSetMutation usingObjects:[CPSet setWithObject:anObject]]; removeKeyObject_method_imp(self, _cmd, anObject); [self didChangeValueForKey:aKey withSetMutation:CPKeyValueMinusSetMutation usingObjects:[CPSet setWithObject:anObject]]; }, ""); } if (removeKey_method) { var removeKey_method_imp = removeKey_method.method_imp; class_addMethod(KVOClass, removeKey_selector, function(self, _cmd, objects) { [self willChangeValueForKey:aKey withSetMutation:CPKeyValueMinusSetMutation usingObjects:[objects copy]]; removeKey_method_imp(self, _cmd, objects); [self didChangeValueForKey:aKey withSetMutation:CPKeyValueMinusSetMutation usingObjects:[objects copy]]; }, ""); } // intersect: is optional. var intersectKey_selector = sel_getUid("intersect" + capitalizedKey + ":"), intersectKey_method = class_getInstanceMethod(theClass, intersectKey_selector); if (intersectKey_method) { var intersectKey_method_imp = intersectKey_method.method_imp; class_addMethod(KVOClass, intersectKey_selector, function(self, _cmd, aSet) { [self willChangeValueForKey:aKey withSetMutation:CPKeyValueIntersectSetMutation usingObjects:[aSet copy]]; intersectKey_method_imp(self, _cmd, aSet); [self didChangeValueForKey:aKey withSetMutation:CPKeyValueIntersectSetMutation usingObjects:[aSet copy]]; }, ""); } } var affectingKeys = [[_nativeClass keyPathsForValuesAffectingValueForKey:aKey] allObjects], affectingKeysCount = affectingKeys ? affectingKeys.length : 0; if (!affectingKeysCount) return; var dependentKeysForClass = _nativeClass[DependentKeysKey]; if (!dependentKeysForClass) { dependentKeysForClass = {}; _nativeClass[DependentKeysKey] = dependentKeysForClass; } while (affectingKeysCount--) { var affectingKey = affectingKeys[affectingKeysCount], affectedKeys = dependentKeysForClass[affectingKey]; if (!affectedKeys) { affectedKeys = [CPSet new]; dependentKeysForClass[affectingKey] = affectedKeys; } [affectedKeys addObject:aKey]; //observe key paths of objects other then ourselves, so we are notified of the changes //use CPKeyValueObservingOptionPrior to ensure proper wrapping around changes //so CPKeyValueObservingOptionPrior and CPKeyValueObservingOptionOld can be fulfilled even for dependent keys if (affectingKey.indexOf(@".") !== -1) [_targetObject addObserver:self forKeyPath:affectingKey options:CPKeyValueObservingOptionPrior | kvoNewAndOld context:nil]; else [self _replaceModifiersForKey:affectingKey]; } } - (void)observeValueForKeyPath:(CPString)theKeyPath ofObject:(id)theObject change:(CPDictionary)theChanges context:(id)theContext { // Fire change events for the dependent keys var dependentKeysForClass = _nativeClass[DependentKeysKey], dependantKeys = [dependentKeysForClass[theKeyPath] allObjects]; var isBeforeFlag = !![theChanges objectForKey:CPKeyValueChangeNotificationIsPriorKey]; for (var i = 0; i < [dependantKeys count]; i++) { var dependantKey = [dependantKeys objectAtIndex:i]; [self _sendNotificationsForKey:dependantKey changeOptions:theChanges isBefore:isBeforeFlag]; } } - (void)_addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext { if (!anObserver) return; var forwarder = nil; if (aPath.indexOf('.') != CPNotFound) forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext]; else [self _replaceModifiersForKey:aPath]; var observers = _observersForKey[aPath]; if (!observers) { observers = [CPDictionary dictionary]; _observersForKey[aPath] = observers; _observersForKeyLength++; } [observers setObject:_CPKVOInfoMake(anObserver, options, aContext, forwarder) forKey:[anObserver UID]]; if (options & CPKeyValueObservingOptionInitial) { var newValue = [_targetObject valueForKeyPath:aPath]; if (newValue === nil || newValue === undefined) newValue = [CPNull null]; var changes = [CPDictionary dictionaryWithObject:newValue forKey:CPKeyValueChangeNewKey]; [anObserver observeValueForKeyPath:aPath ofObject:_targetObject change:changes context:aContext]; } } - (void)_removeObserver:(id)anObserver forKeyPath:(CPString)aPath { var observers = _observersForKey[aPath]; if (aPath.indexOf('.') != CPNotFound) { var forwarder = [observers objectForKey:[anObserver UID]].forwarder; [forwarder finalize]; } [observers removeObjectForKey:[anObserver UID]]; if (![observers count]) { _observersForKeyLength--; delete _observersForKey[aPath]; } if (!_observersForKeyLength) { _targetObject.isa = _nativeClass; //restore the original class delete _targetObject[KVOProxyKey]; } } //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[aKey]; if (isBefore) { changes = changeOptions; var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey], setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey]; if (setMutationKind) { var setMutationObjects = [changes[_CPKeyValueChangeSetMutationObjectsKey] copy], setExistingObjects = [[_targetObject valueForKey: aKey] copy]; if (setMutationKind == CPKeyValueMinusSetMutation) { [setExistingObjects intersectSet: setMutationObjects]; [changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey]; } else if (setMutationKind === CPKeyValueIntersectSetMutation || setMutationKind === CPKeyValueSetSetMutation) { [setExistingObjects minusSet: setMutationObjects]; [changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey]; } //for unordered to-many relationships (CPSet) even new values can only be calculated before!!! if (setMutationKind === CPKeyValueUnionSetMutation || setMutationKind === CPKeyValueSetSetMutation) { [setMutationObjects minusSet: setExistingObjects]; //hide new value (for CPKeyValueObservingOptionPrior messages) //as long as "didChangeValue..." is not yet called! changes[_CPKeyValueChangeSetMutationNewValueKey] = setMutationObjects; } } else if (indexes) { var type = [changes objectForKey:CPKeyValueChangeKindKey]; // for ordered 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 newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes]; [changes setValue:newValues forKey:CPKeyValueChangeOldKey]; } } else { var oldValue = [_targetObject valueForKey:aKey]; if (oldValue === nil || oldValue === undefined) oldValue = [CPNull null]; [changes setObject:oldValue forKey:CPKeyValueChangeOldKey]; } [changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey]; _changesForKey[aKey] = changes; } else { if (!changes) [CPException raise:@"CPKeyValueObservingException" reason:@"'didChange...' message called without prior call of 'willChange...'"]; [changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey]; var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey], setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey]; if (setMutationKind) { //old and new values for unordered to-many relationships can only be calculated before //set recalculated hidden new value as soon as "didChangeValue..." is called! var newValue = changes[_CPKeyValueChangeSetMutationNewValueKey]; [changes setValue:newValue forKey:CPKeyValueChangeNewKey]; //delete hidden values delete changes[_CPKeyValueChangeSetMutationNewValueKey]; delete changes[_CPKeyValueChangeSetMutationObjectsKey]; delete changes[_CPKeyValueChangeSetMutationKindKey]; } else if (indexes) { var type = [changes objectForKey:CPKeyValueChangeKindKey]; // for ordered to-many relationships, newvalue is only sensible for replace and insert if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeInsertion) { //FIXME: do we need to go through and replace "" with CPNull? var newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes]; [changes setValue:newValues forKey:CPKeyValueChangeNewKey]; } } else { var newValue = [_targetObject valueForKey:aKey]; if (newValue === nil || newValue === undefined) newValue = [CPNull null]; [changes setObject:newValue forKey:CPKeyValueChangeNewKey]; } } var observers = [_observersForKey[aKey] allValues], count = observers ? observers.length : 0; while (count--) { var observerInfo = observers[count]; if (isBefore && (observerInfo.options & CPKeyValueObservingOptionPrior)) [observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context]; else if (!isBefore) [observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context]; } var dependentKeysMap = _nativeClass[DependentKeysKey]; if (!dependentKeysMap) return; var dependentKeyPaths = [dependentKeysMap[aKey] allObjects]; if (!dependentKeyPaths) return; var index = 0, count = [dependentKeyPaths count]; for (; index < count; ++index) { var keyPath = dependentKeyPaths[index]; [self _sendNotificationsForKey:keyPath changeOptions:isBefore ? [changeOptions copy] : _changesForKey[keyPath] isBefore:isBefore]; } } @end @implementation _CPKVOModelSubclass { } - (void)willChangeValueForKey:(CPString)aKey { var superClass = [self class], methodSelector = @selector(willChangeValueForKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, aKey); if (!aKey) return; var changeOptions = [CPDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey]; [[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES]; } - (void)didChangeValueForKey:(CPString)aKey { var superClass = [self class], methodSelector = @selector(didChangeValueForKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, aKey); if (!aKey) return; [[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO]; } - (void)willChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey { var superClass = [self class], methodSelector = @selector(willChange:valuesAtIndexes:forKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, change, indexes, 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 { var superClass = [self class], methodSelector = @selector(didChange:valuesAtIndexes:forKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, change, indexes, aKey); if (!aKey) return; [[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO]; } - (void)willChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)mutationKind usingObjects:(CPSet)objects { var superClass = [self class], methodSelector = @selector(willChangeValueForKey:withSetMutation:usingObjects:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, aKey, mutationKind, objects); if (!aKey) return; var changeKind = _changeKindForSetMutationKind(mutationKind), changeOptions = [CPDictionary dictionaryWithObject:changeKind forKey:CPKeyValueChangeKindKey]; //set hidden change-dict ivars to support unordered to-many relationships changeOptions[_CPKeyValueChangeSetMutationObjectsKey] = objects; changeOptions[_CPKeyValueChangeSetMutationKindKey] = mutationKind; [[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES]; } - (void)didChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)mutationKind usingObjects:(CPSet)objects { var superClass = [self class], methodSelector = @selector(didChangeValueForKey:withSetMutation:usingObjects:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, aKey, mutationKind, objects); if (!aKey) return; [[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO]; } - (Class)class { return self[KVOProxyKey]._nativeClass; } - (Class)superclass { return [[self class] superclass]; } - (BOOL)isKindOfClass:(Class)aClass { return [[self class] isSubclassOfClass:aClass]; } - (BOOL)isMemberOfClass:(Class)aClass { return [self class] == aClass; } - (CPString)className { return [self class].name; } @end @implementation _CPKVOModelDictionarySubclass { } - (void)removeAllObjects { var keys = [self allKeys], count = [keys count], i = 0; for (; i < count; i++) [self willChangeValueForKey:keys[i]]; var superClass = [self class], methodSelector = @selector(removeAllObjects), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector); for (i = 0; i < count; i++) [self didChangeValueForKey:keys[i]]; } - (void)removeObjectForKey:(id)aKey { [self willChangeValueForKey:aKey]; var superClass = [self class], methodSelector = @selector(removeObjectForKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, aKey); [self didChangeValueForKey:aKey]; } - (void)setObject:(id)anObject forKey:(id)aKey { [self willChangeValueForKey:aKey]; var superClass = [self class], methodSelector = @selector(setObject:forKey:), methodImp = class_getMethodImplementation(superClass, methodSelector); methodImp(self, methodSelector, anObject, aKey); [self didChangeValueForKey:aKey]; } @end @implementation _CPKVOForwardingObserver : CPObject { id _object; id _observer; id _context; unsigned _options; //a.b CPString _firstPart; //a CPString _secondPart; //b id _value; } - (id)initWithKeyPath:(CPString)aKeyPath object:(id)anObject observer:(id)anObserver options:(unsigned)options context:(id)aContext { self = [super init]; _context = aContext; _observer = anObserver; _object = anObject; _options = options; var dotIndex = aKeyPath.indexOf('.'); if (dotIndex == CPNotFound) [CPException raise:CPInvalidArgumentException reason:"Created _CPKVOForwardingObserver without compound key path: "+aKeyPath]; _firstPart = aKeyPath.substring(0, dotIndex); _secondPart = aKeyPath.substring(dotIndex + 1); //become an observer of the first part of our key (a) [_object addObserver:self forKeyPath:_firstPart options:_options context:nil]; //the current value of a (not the value of a.b) _value = [_object valueForKey:_firstPart]; if (_value) [_value addObserver:self forKeyPath:_secondPart options:_options context:nil]; //we're observing b on current a return self; } - (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext { if (aKeyPath === _firstPart) { [_observer observeValueForKeyPath:_firstPart ofObject:_object change:changes context:_context]; //since a has changed, we should remove ourselves as an observer of the old a, and observe the new one if (_value) [_value removeObserver:self forKeyPath:_secondPart]; _value = [_object valueForKey:_firstPart]; if (_value) [_value addObserver:self forKeyPath:_secondPart options:_options context:nil]; } else { //a is the same, but a.b has changed -- nothing to do but forward this message along [_observer observeValueForKeyPath:_firstPart+"."+aKeyPath ofObject:_object change:changes context:_context]; } } - (void)finalize { if (_value) [_value removeObserver:self forKeyPath:_secondPart]; [_object removeObserver:self forKeyPath:_firstPart]; _object = nil; _observer = nil; _context = nil; _value = nil; } @end var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, aForwarder) { return { observer: anObserver, options: theOptions, context: aContext, forwarder: aForwarder }; } @import "CPArray+KVO.j" @import "CPSet+KVO.j"