diff --git a/Foundation/CPKeyValueObserving.j b/Foundation/CPKeyValueObserving.j index f789fcbf9..3d3aa4315 100644 --- a/Foundation/CPKeyValueObserving.j +++ b/Foundation/CPKeyValueObserving.j @@ -763,9 +763,12 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti if (!anObserver) return; - var forwarder = nil; + var forwarder = nil, + collectionOperatorMatch = aPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/); - if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@') + 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]; @@ -1265,6 +1268,175 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti @end + +@implementation _CPKVOCollectionOperatorObserver : CPObject +{ + id _target; + id _originalObserver; + CPString _fullKeyPath; + unsigned _options; + id _context; + + CPString _collectionKeyPath; + CPString _operator; + CPString _valueKeyPath; + + CPArray _observedItems; +} + +- (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]; + + // Parse key path like "collectionKeyPath.@sum.valueKeyPath" + // Captures: 1=collectionKeyPath, 2=@sum, 3=valueKeyPath + var match = aKeyPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/); + if (match) + { + _collectionKeyPath = match[1]; + _operator = match[2]; + _valueKeyPath = match[3]; + } + + // Observe the collection itself on the target to catch array resets/mutations + [_target addObserver:self forKeyPath:_collectionKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld 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 +{ + // Branch 1: The collection on the target was modified + if (object === _target && [keyPath isEqualToString:_collectionKeyPath]) + { + 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 _notifyOriginalObserver]; + } + // Branch 2: One of the children items has updated + else if (_valueKeyPath && [keyPath isEqualToString:_valueKeyPath] && [_observedItems containsObject:object]) + { + [self _notifyOriginalObserver]; + } +} + +- (void)_notifyOriginalObserver +{ + // Ask standard KVC to evaluate the entire aggregate string (e.g., @sum.value) + var newValue = [_target valueForKeyPath:_fullKeyPath], + change = @{ + CPKeyValueChangeKindKey: CPKeyValueChangeSetting, + CPKeyValueChangeNewKey: (newValue !== nil ? newValue : [CPNull null]) + }; + + [_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context]; +} + +- (void)finalize +{ + [_target removeObserver:self forKeyPath:_collectionKeyPath]; + [self _tearDownItemObservers]; +} + +@end + + @implementation _CPKVOForwardingObserver : CPObject { id _object;