new: _CPKVOCollectionOperatorObserver

This commit is contained in:
daboe01
2026-04-03 13:30:13 +02:00
parent 9ecc739e8d
commit 043029cf41
+174 -2
View File
@@ -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;