Files
cappuccino/Foundation/CPKeyValueObserving.j
T
Martin CarlbergandGitHub fed0703ecf New: Allow lazy loading control for combined observer key paths (#2854)
This pull request adds a kvoValueForKey: method used by _CPKVOForwardingObserver for getting values.
The default action is to just return the valueForKey value.
Override this method if you want any other behavior.
It can be used to just return nil if you are implementing a lazy load behavior on a class.
It can allow a combined binding to observe this object without trigger any lazy loading.
2019-11-28 09:38:55 +01:00

1382 lines
48 KiB
Plaintext

/*
* CPKeyValueObserving.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 "CPIndexSet.j"
@import "CPNull.j"
@import "CPObject.j"
@import "CPSet.j"
@implementation CPObject (KeyValueObserving)
- (void)willChangeValueForKey:(CPString)aKey
{
if (!aKey)
return;
if (!self[KVOProxyKey])
{
if (!self._willChangeMessageCounter)
self._willChangeMessageCounter = new Object();
if (!self._willChangeMessageCounter[aKey])
self._willChangeMessageCounter[aKey] = 1;
else
self._willChangeMessageCounter[aKey] += 1;
}
}
- (void)didChangeValueForKey:(CPString)aKey
{
if (!aKey)
return;
if (!self[KVOProxyKey])
{
if (self._willChangeMessageCounter && self._willChangeMessageCounter[aKey])
{
self._willChangeMessageCounter[aKey] -= 1;
if (!self._willChangeMessageCounter[aKey])
delete self._willChangeMessageCounter[aKey];
}
else
[CPException raise:@"CPKeyValueObservingException" reason:@"'didChange...' message called without prior call of 'willChange...'"];
}
}
- (void)willChange:(CPKeyValueChange)aChange valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
{
if (!aKey)
return;
if (!self[KVOProxyKey])
{
if (!self._willChangeMessageCounter)
self._willChangeMessageCounter = new Object();
if (!self._willChangeMessageCounter[aKey])
self._willChangeMessageCounter[aKey] = 1;
else
self._willChangeMessageCounter[aKey] += 1;
}
}
- (void)didChange:(CPKeyValueChange)aChange valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
{
if (!aKey)
return;
if (!self[KVOProxyKey])
{
if (self._willChangeMessageCounter && self._willChangeMessageCounter[aKey])
{
self._willChangeMessageCounter[aKey] -= 1;
if (!self._willChangeMessageCounter[aKey])
delete self._willChangeMessageCounter[aKey];
}
else
[CPException raise:@"CPKeyValueObservingException" reason:@"'didChange...' message called without prior call of 'willChange...'"];
}
}
- (void)willChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)aMutationKind usingObjects:(CPSet)objects
{
if (!aKey)
return;
if (!self[KVOProxyKey])
{
if (!self._willChangeMessageCounter)
self._willChangeMessageCounter = new Object();
if (!self._willChangeMessageCounter[aKey])
self._willChangeMessageCounter[aKey] = 1;
else
self._willChangeMessageCounter[aKey] += 1;
}
}
- (void)didChangeValueForKey:(CPString)aKey withSetMutation:(CPKeyValueSetMutationKind)aMutationKind usingObjects:(CPSet)objects
{
if (!self[KVOProxyKey])
{
if (self._willChangeMessageCounter && self._willChangeMessageCounter[aKey])
{
self._willChangeMessageCounter[aKey] -= 1;
if (!self._willChangeMessageCounter[aKey])
delete self._willChangeMessageCounter[aKey];
}
else
[CPException raise:@"CPKeyValueObservingException" reason:@"'didChange...' message called without prior call of 'willChange...'"];
}
}
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(CPKeyValueObservingOptions)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];
}
/*!
Whether -willChangeValueForKey/-didChangeValueForKey should automatically be invoked when the
setter of the given key is used. The default is YES. If you override this method to return NO
for some key, you will need to call -willChangeValueForKey/-didChangeValueForKey manually to
be KVO compliant.
The default implementation of this method will check if the receiving class implements
`+ (BOOL)automaticallyNotifiesObserversOf<aKey>` and return the response of that method if it
exists.
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(CPString)aKey
{
var capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1),
selector = "automaticallyNotifiesObserversOf" + capitalizedKey,
aClass = [self class];
if ([aClass respondsToSelector:selector])
return aClass.isa.objj_msgSend0(aClass, selector);
return YES;
}
+ (CPSet)keyPathsForValuesAffectingValueForKey:(CPString)aKey
{
var capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1),
selector = "keyPathsForValuesAffecting" + capitalizedKey,
aClass = [self class];
if ([aClass respondsToSelector:selector])
return aClass.isa.objj_msgSend0(aClass, 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] replaceObjectsAtIndexes: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];
}
}
/*!
This valueForKey method is used by _CPKVOForwardingObserver when getting values.
The default action is to just return the valueForKey value.
Override this method if you want any other behavior.
It can be used to just return nil if you are implementing a lazy load behaviour on a class.
It can allow a combined binding to observe this object without trigger any lazy loading.
*/
- (id)kvoValueForKey:(CPString)aKey
{
return [self valueForKey:aKey];
}
@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;
CPDictionary _nestingForKey;
CPDictionary _minOptionsForKey;
Object _observersForKey;
int _observersForKeyLength;
CPSet _replacedKeys;
// TODO: Remove this line when granular notifications are implemented
BOOL _adding @accessors(property=adding);
}
+ (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 = {};
_nestingForKey = {};
_minOptionsForKey = {};
_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])
{
[_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];
}, setKey_method.method_types);
}
// 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];
}, _setKey_method.method_types);
}
// 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];
}, insertObject_inKeyAtIndex_method.method_types);
}
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];
}, insertKey_atIndexes_method.method_types);
}
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];
}, removeObjectFromKeyAtIndex_method.method_types);
}
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];
}, removeKeyAtIndexes_method.method_types);
}
// 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];
}, replaceObjectInKeyAtIndex_withObject_method.method_types);
}
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];
}, replaceKeyAtIndexes_withKey_method.method_types);
}
}
// 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]];
}, addKeyObject_method.method_types);
}
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]];
}, addKey_method.method_types);
}
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]];
}, removeKeyObject_method.method_types);
}
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]];
}, removeKey_method.method_types);
}
// intersect<Key>: 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]];
}, intersectKey_method.method_types);
}
}
}
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],
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:(CPKeyValueObservingOptions)options context:(id)aContext
{
if (!anObserver)
return;
var forwarder = nil;
if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@')
forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext];
else
[self _replaceModifiersForKey:aPath];
var observers = _observersForKey[aPath];
if (!observers)
{
observers = @{};
_observersForKey[aPath] = observers;
_observersForKeyLength++;
}
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext, forwarder) forKey:[anObserver UID]];
if (options & CPKeyValueObservingOptionInitial)
{
var changes;
if (options & CPKeyValueObservingOptionNew)
{
var newValue = [_targetObject valueForKeyPath:aPath];
if (newValue == nil)
newValue = [CPNull null];
changes = @{ CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNewKey: newValue };
} else {
changes = @{ CPKeyValueChangeKindKey: CPKeyValueChangeSetting };
}
[anObserver observeValueForKeyPath:aPath ofObject:_targetObject change:changes context:aContext];
}
}
- (void)_removeObserver:(id)anObserver forKeyPath:(CPString)aPath
{
var observers = _observersForKey[aPath];
if (!observers)
{
// TODO: Remove this line when granular notifications are implemented
if (!_adding)
CPLog.warn(@"Cannot remove an observer %@ for the key path \"%@\" from %@ because it is not registered as an observer.", _targetObject, aPath, anObserver);
return;
}
if (aPath.indexOf('.') != CPNotFound)
{
// During cib instantiation, it is possible for the forwarder to not yet be available,
// so we have to check for nil.
var observer = [observers objectForKey:[anObserver UID]],
forwarder = observer ? observer.forwarder : nil;
[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],
observers = [_observersForKey[aKey] allValues],
observersMinimumOptions = 0;
if (isBefore)
{
if (changes)
{
// "willChange:X" nesting.
var level = _nestingForKey[aKey];
if (!level)
[CPException raise:CPInternalInconsistencyException reason:@"_changesForKey without _nestingForKey"];
_nestingForKey[aKey] = level + 1;
// Only notify on the first willChange..., silently note any following nested calls.
return;
}
_nestingForKey[aKey] = 1;
// Get the combined minimum of the ...Old and ...New options for all observers
var count = observers ? observers.length : 0;
while (count--)
{
var observerInfo = observers[count];
observersMinimumOptions |= observerInfo.options & kvoNewAndOld;
}
_minOptionsForKey[aKey] = observersMinimumOptions;
changes = changeOptions;
if (observersMinimumOptions & CPKeyValueObservingOptionOld)
{
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;
// Clear ...New option as it should never be sent for a ...Prior option
observersMinimumOptions &= ~CPKeyValueObservingOptionNew;
}
else
{
var level = _nestingForKey[aKey];
if (!changes || !level)
{
if (_targetObject._willChangeMessageCounter && _targetObject._willChangeMessageCounter[aKey])
{
// Close unobserved willChange for a given key.
_targetObject._willChangeMessageCounter[aKey] -= 1;
if (!_targetObject._willChangeMessageCounter[aKey])
delete _targetObject._willChangeMessageCounter[aKey];
return;
}
else
[CPException raise:@"CPKeyValueObservingException" reason:@"'didChange...' message called without prior call of 'willChange...'"];
}
_nestingForKey[aKey] = level - 1;
if (level - 1 > 0)
{
// willChange... was called multiple times. Only fire observation notifications when
// didChange... has been called an equal number of times.
return;
}
delete _nestingForKey[aKey];
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
observersMinimumOptions = _minOptionsForKey[aKey];
if (observersMinimumOptions & CPKeyValueObservingOptionNew)
{
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];
}
}
delete _minOptionsForKey[aKey];
delete _changesForKey[aKey];
}
var count = observers ? observers.length : 0,
changesCache = {};
while (count--)
{
var observerInfo = observers[count],
options = observerInfo.options,
onlyNewAndOldOptions = options & kvoNewAndOld,
observerChanges = nil;
if (isBefore)
{
// Only send 'observeValueForKeyPath:' for '...Prior' option when handling 'willChangeValue...'
if (options & CPKeyValueObservingOptionPrior)
{
observerChanges = changes;
// The new values are not yet created in the change dictionary so remove ...New option to get a working cache below
onlyNewAndOldOptions &= ~CPKeyValueObservingOptionNew;
}
}
else
{
observerChanges = changes;
}
if (observerChanges)
{
// Don't change the 'change' dictionary when the observer wants the minimum options.
// The ...New option is remved above for the ...Prior case
if (onlyNewAndOldOptions !== observersMinimumOptions)
{
// Use a subset of the 'change' dictionary. First try to find it in the cache
observerChanges = changesCache[onlyNewAndOldOptions];
if (!observerChanges)
{
// Not in the cache. Build a new dictionary and store it in the cache
changesCache[onlyNewAndOldOptions] = observerChanges = [changes mutableCopy];
if (!(onlyNewAndOldOptions & CPKeyValueObservingOptionOld))
[observerChanges removeObjectForKey:CPKeyValueChangeOldKey];
if (!(onlyNewAndOldOptions & CPKeyValueObservingOptionNew))
[observerChanges removeObjectForKey:CPKeyValueChangeNewKey];
}
}
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:observerChanges 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 : CPObject
{
}
- (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 = @{ CPKeyValueChangeKindKey: CPKeyValueChangeSetting };
[[_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 = @{ CPKeyValueChangeKindKey: change, CPKeyValueChangeIndexesKey: indexes };
[[_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 = @{ CPKeyValueChangeKindKey: changeKind };
//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 : CPObject
{
}
- (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 kvoValueForKey:_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)
{
var pathChanges = [CPMutableDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
var isBeforeFlag = !![changes objectForKey:CPKeyValueChangeNotificationIsPriorKey];
if (isBeforeFlag)
[pathChanges setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
if (_options & CPKeyValueObservingOptionOld)
{
var oldValue = [_value valueForKeyPath:_secondPart];
[pathChanges setObject:oldValue != null ? oldValue : [CPNull null] forKey:CPKeyValueChangeOldKey];
}
if (!isBeforeFlag && (_options & CPKeyValueObservingOptionNew))
{
var newValue = [[_object kvoValueForKey:_firstPart] valueForKeyPath:_secondPart];
[pathChanges setObject:newValue != null ? newValue : [CPNull null] forKey:CPKeyValueChangeNewKey];
}
[_observer observeValueForKeyPath:_firstPart + "." + _secondPart ofObject:_object change:pathChanges context:_context];
// Nothing has changed yet when doing willChange....
if (!isBeforeFlag) {
//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 kvoValueForKey:_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(anObserver, theOptions, aContext, aForwarder)
{
return {
observer: anObserver,
options: theOptions,
context: aContext,
forwarder: aForwarder
};
};
@import "CPArray+KVO.j"
@import "CPSet+KVO.j"