mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 04:57:03 +00:00
• Refactor @implementation _CPKVCSet and CPObject (CPSetKVO) from _CPSet.j to the tail of CPMutableSet.j. • Fix legacy compiler parse failures by ensuring the CPMutableSet superclass symbol is fully resolved within the translation unit prior to proxy evaluation. • Maintain strict DAG compliance across the modern toolchain pipeline topology. • Embed explicit #pragma mark navigation landmarks and enforce Allman brace layout to improve human structural ergonomics.
1570 lines
55 KiB
Plaintext
1570 lines
55 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+KVO.j"
|
|
@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,
|
|
collectionOperatorMatch = aPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/);
|
|
|
|
if (collectionOperatorMatch)
|
|
forwarder = [[_CPKVOCollectionOperatorObserver alloc] initWithTarget:_targetObject observer:anObserver keyPath:aPath options:options context:aContext];
|
|
else 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 = [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 = [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 _CPKVOCollectionOperatorObserver : CPObject
|
|
{
|
|
id _target;
|
|
id _originalObserver;
|
|
CPString _fullKeyPath;
|
|
unsigned _options;
|
|
id _context;
|
|
|
|
CPString _collectionKeyPath;
|
|
CPString _operator;
|
|
CPString _valueKeyPath;
|
|
|
|
CPArray _observedItems;
|
|
id _cachedValue;
|
|
}
|
|
|
|
- (id)initWithTarget:(id)aTarget observer:(id)anObserver keyPath:(CPString)aKeyPath options:(unsigned)options context:(id)aContext
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
_target = aTarget;
|
|
_originalObserver = anObserver;
|
|
_fullKeyPath = aKeyPath;
|
|
_options = options;
|
|
_context = aContext;
|
|
_observedItems = [CPArray array];
|
|
|
|
var match = aKeyPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/);
|
|
if (match)
|
|
{
|
|
_collectionKeyPath = match[1];
|
|
_operator = match[2];
|
|
_valueKeyPath = match[3];
|
|
}
|
|
|
|
_cachedValue = [_target valueForKeyPath:_fullKeyPath];
|
|
|
|
[_target addObserver:self forKeyPath:_collectionKeyPath options:_options context:nil];
|
|
|
|
[self _setupItemObservers];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_setupItemObservers
|
|
{
|
|
var collection = [_target valueForKeyPath:_collectionKeyPath];
|
|
|
|
if (collection && [collection respondsToSelector:@selector(objectEnumerator)])
|
|
{
|
|
var enumerator = [collection objectEnumerator],
|
|
item;
|
|
|
|
while ((item = [enumerator nextObject]) !== nil)
|
|
{
|
|
if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)])
|
|
[item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil];
|
|
|
|
[_observedItems addObject:item];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)_tearDownItemObservers
|
|
{
|
|
if (_valueKeyPath)
|
|
{
|
|
var count = [_observedItems count];
|
|
while (count--)
|
|
{
|
|
var item = [_observedItems objectAtIndex:count];
|
|
if ([item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
|
|
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
|
|
}
|
|
}
|
|
|
|
[_observedItems removeAllObjects];
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
|
|
{
|
|
var isPrior = [[change objectForKey:CPKeyValueChangeNotificationIsPriorKey] boolValue];
|
|
|
|
if (object === _target && [keyPath isEqualToString:_collectionKeyPath])
|
|
{
|
|
// Only tear down/rebuild observers when the actual change happens, not during the "Prior" notification
|
|
if (!isPrior)
|
|
{
|
|
var kind = [change objectForKey:CPKeyValueChangeKindKey];
|
|
|
|
if (kind === CPKeyValueChangeSetting)
|
|
{
|
|
[self _tearDownItemObservers];
|
|
[self _setupItemObservers];
|
|
}
|
|
else if (kind === CPKeyValueChangeInsertion || kind === CPKeyValueChangeReplacement)
|
|
{
|
|
if (kind === CPKeyValueChangeReplacement)
|
|
{
|
|
var oldItems = [change objectForKey:CPKeyValueChangeOldKey];
|
|
if (oldItems)
|
|
{
|
|
for (var i = 0, len = oldItems.length; i < len; i++)
|
|
{
|
|
var item = oldItems[i];
|
|
if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
|
|
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
|
|
[_observedItems removeObject:item];
|
|
}
|
|
}
|
|
}
|
|
|
|
var newItems = [change objectForKey:CPKeyValueChangeNewKey];
|
|
if (newItems)
|
|
{
|
|
for (var i = 0, len = newItems.length; i < len; i++)
|
|
{
|
|
var item = newItems[i];
|
|
if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)])
|
|
[item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil];
|
|
[_observedItems addObject:item];
|
|
}
|
|
}
|
|
}
|
|
else if (kind === CPKeyValueChangeRemoval)
|
|
{
|
|
var oldItems = [change objectForKey:CPKeyValueChangeOldKey];
|
|
if (oldItems)
|
|
{
|
|
for (var i = 0, len = oldItems.length; i < len; i++)
|
|
{
|
|
var item = oldItems[i];
|
|
if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
|
|
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
|
|
[_observedItems removeObject:item];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[self _notifyOriginalObserverIsPrior:isPrior];
|
|
}
|
|
else if (_valueKeyPath && [keyPath isEqualToString:_valueKeyPath] && [_observedItems containsObject:object])
|
|
{
|
|
[self _notifyOriginalObserverIsPrior:isPrior];
|
|
}
|
|
}
|
|
|
|
- (void)_notifyOriginalObserverIsPrior:(BOOL)isPrior
|
|
{
|
|
var change = [CPMutableDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
|
|
|
|
if (isPrior)
|
|
[change setObject:YES forKey:CPKeyValueChangeNotificationIsPriorKey];
|
|
|
|
if (_options & CPKeyValueObservingOptionOld)
|
|
[change setObject:(_cachedValue !== nil ? _cachedValue : [CPNull null]) forKey:CPKeyValueChangeOldKey];
|
|
|
|
if (!isPrior)
|
|
{
|
|
var newValue = [_target valueForKeyPath:_fullKeyPath];
|
|
if (_options & CPKeyValueObservingOptionNew)
|
|
[change setObject:(newValue !== nil ? newValue : [CPNull null]) forKey:CPKeyValueChangeNewKey];
|
|
|
|
[_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context];
|
|
|
|
// Update the cache for the next notification
|
|
_cachedValue = newValue;
|
|
}
|
|
else
|
|
{
|
|
[_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context];
|
|
}
|
|
}
|
|
|
|
- (void)finalize
|
|
{
|
|
[_target removeObserver:self forKeyPath:_collectionKeyPath];
|
|
[self _tearDownItemObservers];
|
|
}
|
|
|
|
@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
|
|
};
|
|
};
|