mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-17 16:11:28 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -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,193 @@ 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 _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;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
@class CarTester
|
||||
@class ToManyTester
|
||||
@class AccountTester
|
||||
@class TransactionTester
|
||||
|
||||
@implementation CPKVOTest : OJTestCase
|
||||
{
|
||||
@@ -436,6 +438,35 @@
|
||||
[self assertTrue:_sawObservation message:@"Never recieved an observation"];
|
||||
}
|
||||
|
||||
- (void)testCollectionOperatorKeyPath
|
||||
{
|
||||
var account = [[AccountTester alloc] init],
|
||||
t1 = [[TransactionTester alloc] initWithAmount:10.0],
|
||||
t2 = [[TransactionTester alloc] initWithAmount:20.0];
|
||||
|
||||
[account setTransactions:[t1, t2]];
|
||||
|
||||
// 1. Test updating a child property triggers an aggregate update
|
||||
[account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ItemChange"];
|
||||
|
||||
_sawObservation = NO;
|
||||
[t1 setAmount:15.0]; // KVC sum goes from 30.0 -> 35.0
|
||||
|
||||
[self assertTrue:_sawObservation message:@"Never received an observation when child item's property changed"];
|
||||
[account removeObserver:self forKeyPath:@"transactions.@sum.amount"];
|
||||
|
||||
// 2. Test replacing the whole array triggers an aggregate update
|
||||
[account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ArrayReplace"];
|
||||
|
||||
var t3 = [[TransactionTester alloc] initWithAmount:5.0];
|
||||
|
||||
_sawObservation = NO;
|
||||
[account setTransactions:[t1, t2, t3]]; // KVC sum goes from 35.0 -> 40.0
|
||||
|
||||
[self assertTrue:_sawObservation message:@"Never received an observation when collection was replaced"];
|
||||
[account removeObserver:self forKeyPath:@"transactions.@sum.amount"];
|
||||
}
|
||||
|
||||
- (void)testPerformance
|
||||
{
|
||||
bob = [PersonTester new];
|
||||
@@ -777,6 +808,16 @@
|
||||
testNestedNotificationsBobCount += 1;
|
||||
break;
|
||||
|
||||
case "testCollectionOperatorKeyPath_ItemChange":
|
||||
[self assert:newValue equals:35.0];
|
||||
[self assert:oldValue equals:30.0];
|
||||
break;
|
||||
|
||||
case "testCollectionOperatorKeyPath_ArrayReplace":
|
||||
[self assert:newValue equals:40.0];
|
||||
[self assert:oldValue equals:35.0];
|
||||
break;
|
||||
|
||||
default:
|
||||
[self assertFalse:YES message:@"unhandled observation, must be an error"];
|
||||
return;
|
||||
@@ -955,3 +996,25 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TransactionTester : CPObject
|
||||
{
|
||||
float amount @accessors;
|
||||
}
|
||||
|
||||
- (id)initWithAmount:(float)anAmount
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
amount = anAmount;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountTester : CPObject
|
||||
{
|
||||
CPArray transactions @accessors;
|
||||
}
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user