mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-01 04:47:36 +00:00
Merge remote-tracking branch 'upstream/main' into Aristo3
This commit is contained in:
commit
8a4213ced1
3 files changed
+331
-112
No files matched your search
+90
-71
@@ -50,6 +50,9 @@
|
||||
|
||||
[self exposeBinding:@"contentArray"];
|
||||
[self exposeBinding:@"sortDescriptors"];
|
||||
[self exposeBinding:@"selectionIndexPaths"];
|
||||
[self exposeBinding:@"selectionIndexPath"];
|
||||
[self exposeBinding:@"selectedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingContentArray
|
||||
@@ -59,7 +62,7 @@
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
|
||||
return [CPSet setWithObjects:@"content", @"contentArray", @"sortDescriptors", @"childrenKeyPath"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
|
||||
@@ -69,27 +72,27 @@
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsert
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
@@ -115,7 +118,8 @@
|
||||
}
|
||||
|
||||
- (void)prepareContent
|
||||
{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
{
|
||||
[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
}
|
||||
|
||||
- (BOOL)preservesSelection { return _preservesSelection; }
|
||||
@@ -144,7 +148,8 @@
|
||||
- (void)setChildrenKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (_childrenKeyPath === aKeyPath) return;
|
||||
_childrenKeyPath = aKeyPath;[self rearrangeObjects];
|
||||
_childrenKeyPath = aKeyPath;
|
||||
[self rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)countKeyPath { return _countKeyPath; }
|
||||
@@ -166,14 +171,21 @@
|
||||
if (![value isKindOfClass:[CPArray class]])
|
||||
value = [CPArray arrayWithObject:value];
|
||||
|
||||
if (_contentObject === value)
|
||||
return;
|
||||
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
[self willChangeValueForKey:@"contentArray"];
|
||||
|
||||
_contentObject = value;
|
||||
|
||||
[self _rearrangeObjects];
|
||||
@@ -182,23 +194,32 @@
|
||||
[self __setSelectedObjects:oldSelectedObjects];
|
||||
else
|
||||
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
|
||||
[self didChangeValueForKey:@"contentArray"];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
|
||||
|
||||
- (void)_setContentArray:(id)anArray { [self setContent:anArray]; }
|
||||
- (id)contentArray { return _contentObject; }
|
||||
- (id)arrangedObjects { return _arrangedObjects; }
|
||||
|
||||
- (void)rearrangeObjects
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"arrangedObjects"];
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
[self didChangeValueForKey:@"arrangedObjects"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)_rearrangeObjects
|
||||
{
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
@@ -216,7 +237,7 @@
|
||||
- (void)__rebuildArrangedObjectsTree
|
||||
{
|
||||
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
|
||||
contentArray = [self contentArray];
|
||||
contentArray = [self contentArray];
|
||||
|
||||
if (contentArray && [contentArray count] > 0)
|
||||
{
|
||||
@@ -244,7 +265,7 @@
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var obj = [sortedObjects objectAtIndex:i],
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
|
||||
if (_childrenKeyPath)
|
||||
{
|
||||
@@ -278,11 +299,7 @@
|
||||
|
||||
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return result;
|
||||
return [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
}
|
||||
|
||||
- (void)_ensureTreeNodesExistForIndexPaths:(CPArray)indexPaths
|
||||
@@ -334,7 +351,8 @@
|
||||
if (needsRebuild)
|
||||
{
|
||||
[childNodes removeAllObjects];
|
||||
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];[childNodes addObjectsFromArray:newNodes];
|
||||
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];
|
||||
[childNodes addObjectsFromArray:newNodes];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,21 +384,13 @@
|
||||
if ([_selectionIndexPaths isEqualToArray:newPaths])
|
||||
return NO;
|
||||
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"selectionIndexPaths"];
|
||||
|
||||
_selectionIndexPaths = [newPaths copy];
|
||||
|
||||
var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"];
|
||||
|
||||
if (binderClass)
|
||||
{
|
||||
var binding = [binderClass getBinding:@"selectionIndexPaths" forObject:self];
|
||||
|
||||
if (binding)
|
||||
[binding reverseSetValueFor:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
[self didChangeValueForKey:@"selectionIndexPaths"];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -388,9 +398,7 @@
|
||||
- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
var newPaths = [_selectionIndexPaths mutableCopy];
|
||||
|
||||
[newPaths addObjectsFromArray:indexPaths];
|
||||
|
||||
return [self setSelectionIndexPaths:newPaths];
|
||||
}
|
||||
|
||||
@@ -404,7 +412,7 @@
|
||||
- (CPArray)selectedNodes
|
||||
{
|
||||
var nodes = [CPMutableArray array],
|
||||
count = [_selectionIndexPaths count];
|
||||
count = [_selectionIndexPaths count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -418,13 +426,17 @@
|
||||
- (CPArray)selectedObjects
|
||||
{
|
||||
var objects = [CPMutableArray array],
|
||||
nodes = [self selectedNodes],
|
||||
count = [nodes count];
|
||||
nodes = [self selectedNodes],
|
||||
count = [nodes count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
[objects addObject:[[nodes objectAtIndex:i] representedObject]];
|
||||
{
|
||||
var representedObject = [[nodes objectAtIndex:i] representedObject];
|
||||
if (representedObject)
|
||||
[objects addObject:representedObject];
|
||||
}
|
||||
|
||||
return objects;
|
||||
return [_CPObservableArray arrayWithArray:objects];
|
||||
}
|
||||
|
||||
- (BOOL)__setSelectedObjects:(CPArray)objects
|
||||
@@ -462,22 +474,22 @@
|
||||
}
|
||||
|
||||
- (BOOL)canInsert { return [self isEditable]; }
|
||||
- (BOOL)canInsertChild { return [self isEditable] &&[_selectionIndexPaths count] > 0; }
|
||||
- (BOOL)canInsertChild { return [self isEditable] && [_selectionIndexPaths count] > 0; }
|
||||
- (BOOL)canAddChild { return [self canInsertChild]; }
|
||||
|
||||
- (void)add:(id)sender
|
||||
{
|
||||
if (![self canInsert]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
|
||||
selectionPath = [self selectionIndexPath];
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
selectionPath = [self selectionIndexPath];
|
||||
|
||||
if (!selectionPath)
|
||||
selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]];
|
||||
|
||||
var length = [selectionPath length],
|
||||
lastIndex = [selectionPath indexAtPosition:length - 1],
|
||||
insertPath = [selectionPath indexPathByRemovingLastIndex];
|
||||
lastIndex = [selectionPath indexAtPosition:length - 1],
|
||||
insertPath = [selectionPath indexPathByRemovingLastIndex];
|
||||
|
||||
insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
|
||||
|
||||
@@ -489,10 +501,10 @@
|
||||
if (![self canAddChild])
|
||||
return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]],
|
||||
childCount = [[parentNode childNodes] count],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount];
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]],
|
||||
childCount = [[parentNode childNodes] count],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
@@ -501,8 +513,8 @@
|
||||
{
|
||||
if (![self canInsert]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
|
||||
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:indexPath];
|
||||
}
|
||||
@@ -512,7 +524,7 @@
|
||||
if (![self canInsertChild]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
@@ -524,6 +536,7 @@
|
||||
|
||||
- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
@@ -531,21 +544,22 @@
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var object = [objects objectAtIndex:i],
|
||||
path = [indexPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
path = [indexPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
|
||||
{
|
||||
[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1];
|
||||
childIndex = [path indexAtPosition:length - 1];
|
||||
|
||||
var children = [parentObj valueForKeyPath:_childrenKeyPath];
|
||||
if (!children)
|
||||
@@ -568,9 +582,11 @@
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths];
|
||||
if ([self selectsInsertedObjects])
|
||||
[self setSelectionIndexPaths:indexPaths];
|
||||
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)remove:(id)sender
|
||||
@@ -585,16 +601,17 @@
|
||||
|
||||
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
|
||||
count = [sortedPaths count];
|
||||
count = [sortedPaths count];
|
||||
|
||||
for (var i = count - 1; i >= 0; i--)
|
||||
{
|
||||
var path = [sortedPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{
|
||||
@@ -603,15 +620,15 @@
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1],
|
||||
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
childIndex = [path indexAtPosition:length - 1],
|
||||
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (mutableChildren && childIndex <[mutableChildren count])
|
||||
if (mutableChildren && childIndex < [mutableChildren count])
|
||||
[mutableChildren removeObjectAtIndex:childIndex];
|
||||
}
|
||||
}
|
||||
@@ -624,6 +641,7 @@
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
|
||||
@@ -632,18 +650,19 @@
|
||||
}
|
||||
|
||||
- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
|
||||
{[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
|
||||
{
|
||||
[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTreeControllerAvoidsEmptySelection = @"CPTreeControllerAvoidsEmptySelection",
|
||||
CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
|
||||
CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
|
||||
CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
|
||||
CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
|
||||
CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
|
||||
CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
|
||||
CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
|
||||
CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
|
||||
CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
|
||||
CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
|
||||
CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
|
||||
CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
|
||||
|
||||
@implementation CPTreeController (CPCoding)
|
||||
|
||||
|
||||
@@ -17,22 +17,21 @@
|
||||
CPTreeController _treeController @accessors(property=treeController);
|
||||
CPArray _contentArray @accessors(property=contentArray);
|
||||
|
||||
CPArray observations;
|
||||
int aCount @accessors;
|
||||
CPMutableArray _observedKeyPaths;
|
||||
}
|
||||
|
||||
- (CPArray)makeTestTree
|
||||
{
|
||||
var engineering = [OrgNode nodeWithName:@"Engineering"],
|
||||
marketing = [OrgNode nodeWithName:@"Marketing"];
|
||||
marketing = [OrgNode nodeWithName:@"Marketing"];
|
||||
|
||||
var webTeam = [OrgNode nodeWithName:@"Web Team"],
|
||||
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
|
||||
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
|
||||
|
||||
[engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
|
||||
|
||||
var dev1 = [OrgNode nodeWithName:@"Francisco"],
|
||||
dev2 = [OrgNode nodeWithName:@"Ross"];
|
||||
dev2 = [OrgNode nodeWithName:@"Ross"];
|
||||
|
||||
[webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
|
||||
|
||||
@@ -43,12 +42,23 @@
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
_observedKeyPaths = [CPMutableArray array];
|
||||
_contentArray = [self makeTestTree];
|
||||
_treeController = [[CPTreeController alloc] init];
|
||||
[_treeController setChildrenKeyPath:@"children"];
|
||||
[_treeController setContent:[_contentArray copy]];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
_observedKeyPaths = nil;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext
|
||||
{
|
||||
[_observedKeyPaths addObject:aKeyPath];
|
||||
}
|
||||
|
||||
- (void)testInitWithContent
|
||||
{
|
||||
[self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
|
||||
@@ -99,7 +109,7 @@
|
||||
[controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
backendTeam = [[engineering children] objectAtIndex:1];
|
||||
backendTeam = [[engineering children] objectAtIndex:1];
|
||||
[self assert:1 equals:[[backendTeam children] count] message:@"Child should be added to the model object's children array"];
|
||||
[self assert:@"Tom" equals:[[[backendTeam children] objectAtIndex:0] name]];
|
||||
}
|
||||
@@ -132,7 +142,7 @@
|
||||
[controller removeObjectAtArrangedObjectIndexPath:path];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
webTeam = [[engineering children] objectAtIndex:0];
|
||||
webTeam = [[engineering children] objectAtIndex:0];
|
||||
|
||||
[self assert:1 equals:[[webTeam children] count] message:@"Francisco should be removed, leaving only Ross"];
|
||||
[self assert:@"Ross" equals:[[[webTeam children] objectAtIndex:0] name]];
|
||||
@@ -176,7 +186,8 @@
|
||||
// "Marketing" is now at index 0
|
||||
[self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]];
|
||||
|
||||
// Test behavior when AvoidsEmptySelection is NO[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
// Test behavior when AvoidsEmptySelection is NO
|
||||
[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
[controller setAvoidsEmptySelection:NO];
|
||||
|
||||
// Reselect "Marketing" at index 0
|
||||
@@ -229,6 +240,130 @@
|
||||
[self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
|
||||
}
|
||||
|
||||
/*
|
||||
* New Tests for Selection Bindings, KVO, and UI Action Status
|
||||
*/
|
||||
|
||||
- (void)testExposedBindings
|
||||
{
|
||||
var exposedBindings = [CPTreeController exposedBindings];
|
||||
|
||||
[self assertTrue:[exposedBindings containsObject:@"contentArray"] message:@"contentArray should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"sortDescriptors"] message:@"sortDescriptors should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectionIndexPaths"] message:@"selectionIndexPaths should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectionIndexPath"] message:@"selectionIndexPath should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectedObjects"] message:@"selectedObjects should be exposed"];
|
||||
}
|
||||
|
||||
- (void)testDetailBindingToSelectionProxyUpdatesOnSelectionChange
|
||||
{
|
||||
var controller = [self treeController],
|
||||
textField = [[CPTextField alloc] init];
|
||||
|
||||
[textField bind:@"value" toObject:controller withKeyPath:@"selection.name" options:nil];
|
||||
|
||||
// Select "Engineering" (index 0)
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assert:@"Engineering" equals:[textField stringValue] message:@"Bound view should reflect root selection"];
|
||||
|
||||
// Select "Web Team" (index [0, 0])
|
||||
var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
|
||||
[controller setSelectionIndexPath:nestedPath];
|
||||
[self assert:@"Web Team" equals:[textField stringValue] message:@"Bound view should reflect nested selection"];
|
||||
|
||||
// Select "Marketing" (index 1)
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
[self assert:@"Marketing" equals:[textField stringValue] message:@"Bound view should reflect changed selection"];
|
||||
}
|
||||
|
||||
- (void)testDetailBindingUpdatesOnSetContent
|
||||
{
|
||||
var controller = [self treeController],
|
||||
textField = [[CPTextField alloc] init];
|
||||
|
||||
[textField bind:@"value" toObject:controller withKeyPath:@"selection.name" options:nil];
|
||||
|
||||
// Select "Engineering"
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assert:@"Engineering" equals:[textField stringValue]];
|
||||
|
||||
// Replace content
|
||||
var newDept = [OrgNode nodeWithName:@"Design Dept"];
|
||||
[controller setContent:[CPMutableArray arrayWithObject:newDept]];
|
||||
|
||||
// Detail binding should update to the preserved selection or new root
|
||||
[self assert:@"Design Dept" equals:[textField stringValue] message:@"Detail binding must update when content changes"];
|
||||
}
|
||||
|
||||
- (void)testSelectedObjectsKVOTriggeredOnContentChange
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"selectedObjects" options:0 context:nil];
|
||||
|
||||
// Set new content
|
||||
var newDept = [OrgNode nodeWithName:@"Operations"];
|
||||
[controller setContent:[CPMutableArray arrayWithObject:newDept]];
|
||||
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"selectedObjects"] message:@"selectedObjects KVO notification must fire when content changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"selectedObjects"];
|
||||
}
|
||||
|
||||
- (void)testCanInsertDependsOnEditable
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"canInsert" options:0 context:nil];
|
||||
|
||||
[controller setEditable:YES];
|
||||
[self assertTrue:[controller canInsert]];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
[controller setEditable:NO];
|
||||
[self assertFalse:[controller canInsert] message:@"canInsert should be NO when editable is NO"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsert"] message:@"canInsert KVO must fire when editable changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"canInsert"];
|
||||
}
|
||||
|
||||
- (void)testCanAddChildAndCanInsertChildDependOnEditableAndSelection
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setEditable:YES];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"canAddChild" options:0 context:nil];
|
||||
[controller addObserver:self forKeyPath:@"canInsertChild" options:0 context:nil];
|
||||
|
||||
// Empty selection -> canAddChild/canInsertChild should be NO
|
||||
[controller setSelectionIndexPaths:[CPArray array]];
|
||||
[self assertFalse:[controller canAddChild]];
|
||||
[self assertFalse:[controller canInsertChild]];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
// Select an item -> canAddChild/canInsertChild should become YES
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assertTrue:[controller canAddChild]];
|
||||
[self assertTrue:[controller canInsertChild]];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canAddChild"] message:@"canAddChild KVO should fire on selection change"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsertChild"] message:@"canInsertChild KVO should fire on selection change"];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
// Set editable to NO -> canAddChild/canInsertChild should become NO
|
||||
[controller setEditable:NO];
|
||||
[self assertFalse:[controller canAddChild]];
|
||||
[self assertFalse:[controller canInsertChild]];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canAddChild"] message:@"canAddChild KVO should fire when editable changes"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsertChild"] message:@"canInsertChild KVO should fire when editable changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"canAddChild"];
|
||||
[controller removeObserver:self forKeyPath:@"canInsertChild"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
{
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 850, 400) styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask],
|
||||
contentView = [theWindow contentView];
|
||||
|
||||
[theWindow setTitle:@"CPTreeController Bindings Test"];
|
||||
|
||||
// 1. Create the Data Model
|
||||
var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
|
||||
child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
|
||||
@@ -32,36 +34,39 @@
|
||||
|
||||
// 2. Setup the Tree Controller
|
||||
treeController = [[CPTreeController alloc] init];
|
||||
[treeController setObjectClass:[Node class]];
|
||||
[treeController setChildrenKeyPath:@"children"];
|
||||
[treeController setContent:contentArray];
|
||||
|
||||
// 3. Setup the Outline View
|
||||
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
|
||||
[scrollView setAutohidesScrollers:YES];
|
||||
// 3. Setup Outline View 1 (Left)
|
||||
var scrollView1 = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 240, 300)];
|
||||
[scrollView1 setAutohidesScrollers:YES];
|
||||
|
||||
var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
|
||||
var column = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column headerView] setStringValue:@"Node Name"];
|
||||
[column setWidth:240];
|
||||
[column setEditable:YES]; // Editable to test bidirectional bindings in the tree
|
||||
|
||||
[outlineView addTableColumn:column];
|
||||
[outlineView setOutlineTableColumn:column];
|
||||
[outlineView setAllowsMultipleSelection:YES];
|
||||
[scrollView setDocumentView:outlineView];
|
||||
[contentView addSubview:scrollView];
|
||||
var outlineView1 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 240, 300)],
|
||||
column1 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column1 headerView] setStringValue:@"Outline View 1"];
|
||||
[column1 setWidth:230];
|
||||
[column1 setEditable:YES];
|
||||
|
||||
// 4. Establish Bindings for the Outline View
|
||||
[outlineView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
[outlineView1 addTableColumn:column1];
|
||||
[outlineView1 setOutlineTableColumn:column1];
|
||||
[outlineView1 setAllowsMultipleSelection:YES];
|
||||
[scrollView1 setDocumentView:outlineView1];
|
||||
[contentView addSubview:scrollView1];
|
||||
|
||||
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)];
|
||||
// Bind Outline View 1 (Content & Selection)
|
||||
[outlineView1 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView1 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
// 4. Setup Outline View 2 (Middle - Synced)
|
||||
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(280, 20, 240, 300)];
|
||||
[scrollView2 setAutohidesScrollers:YES];
|
||||
var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
|
||||
var column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column2 headerView] setStringValue:@"Node Name"];
|
||||
[column2 setWidth:240];
|
||||
[column2 setEditable:YES]; // Editable to test bidirectional bindings in the tree
|
||||
|
||||
var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 240, 300)],
|
||||
column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column2 headerView] setStringValue:@"Outline View 2 (Synced)"];
|
||||
[column2 setWidth:230];
|
||||
[column2 setEditable:YES];
|
||||
|
||||
[outlineView2 addTableColumn:column2];
|
||||
[outlineView2 setOutlineTableColumn:column2];
|
||||
@@ -69,20 +74,68 @@
|
||||
[scrollView2 setDocumentView:outlineView2];
|
||||
[contentView addSubview:scrollView2];
|
||||
|
||||
// 4. Establish Bindings for the Outline View
|
||||
// Bind Outline View 2 (Content & Selection)
|
||||
[outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
// 5. Setup Inspector / Detail Controls (Right Panel)
|
||||
var panelX = 540;
|
||||
|
||||
// Label: Detail Inspector
|
||||
var detailLabel = [CPTextField labelWithTitle:@"Detail Binding (selection.name):"];
|
||||
[detailLabel setFrameOrigin:CGPointMake(panelX, 20)];
|
||||
[contentView addSubview:detailLabel];
|
||||
|
||||
// 2-Way Detail Binding TextField
|
||||
var nameField = [CPTextField textFieldWithStringValue:@"" placeholder:@"No Selection" width:260];
|
||||
[nameField setFrameOrigin:CGPointMake(panelX, 45)];
|
||||
[nameField setEditable:YES];
|
||||
[nameField bind:@"value" toObject:treeController withKeyPath:@"selection.name" options:nil];
|
||||
[contentView addSubview:nameField];
|
||||
|
||||
// Programmatic Selection Button
|
||||
var selectBtn = [CPButton buttonWithTitle:@"Select 'Child 1.2' [0, 1]"];
|
||||
[selectBtn setFrame:CGRectMake(panelX, 90, 260, 28)];
|
||||
[selectBtn setTarget:self];
|
||||
[selectBtn setAction:@selector(selectSpecificNode:)];
|
||||
[contentView addSubview:selectBtn];
|
||||
|
||||
// Add Child Button (Action + Enabled Binding)
|
||||
var addChildBtn = [CPButton buttonWithTitle:@"Add Child to Selection"];
|
||||
[addChildBtn setFrame:CGRectMake(panelX, 125, 260, 28)];
|
||||
[addChildBtn setTarget:treeController];
|
||||
[addChildBtn setAction:@selector(addChild:)];
|
||||
[addChildBtn bind:@"enabled" toObject:treeController withKeyPath:@"canAddChild" options:nil];
|
||||
[contentView addSubview:addChildBtn];
|
||||
|
||||
// Remove Selected Button
|
||||
var removeBtn = [CPButton buttonWithTitle:@"Remove Selected"];
|
||||
[removeBtn setFrame:CGRectMake(panelX, 160, 260, 28)];
|
||||
[removeBtn setTarget:treeController];
|
||||
[removeBtn setAction:@selector(remove:)];
|
||||
[contentView addSubview:removeBtn];
|
||||
|
||||
// Selection Log Label
|
||||
var logHeaderLabel = [CPTextField labelWithTitle:@"KVO selectionIndexPaths Log:"];
|
||||
[logHeaderLabel setFrameOrigin:CGPointMake(panelX, 210)];
|
||||
[contentView addSubview:logHeaderLabel];
|
||||
|
||||
logField = [CPTextField labelWithTitle:@"Selected: Root 1"];
|
||||
[logField setFrame:CGRectMake(panelX, 235, 260, 60)];
|
||||
[logField setLineBreakMode:CPLineBreakByWordWrapping];
|
||||
[contentView addSubview:logField];
|
||||
|
||||
// 6. Register Observer for Live Selection Log
|
||||
[treeController addObserver:self forKeyPath:@"selectionIndexPaths" options:CPKeyValueObservingOptionNew context:nil];
|
||||
|
||||
[theWindow center];
|
||||
[theWindow orderFront:self];
|
||||
}
|
||||
|
||||
- (void)selectSpecificNode:(id)sender
|
||||
{
|
||||
// Programmatically select index path [0, 1] which is "Child 1.2"
|
||||
// This tests the `_CPOutlineViewSelectionIndexPathsBinder` auto-expand logic.
|
||||
var path =[CPIndexPath indexPathWithIndexes:[0, 1]];
|
||||
var path = [CPIndexPath indexPathWithIndexes:[0, 1]];
|
||||
[treeController setSelectionIndexPath:path];
|
||||
}
|
||||
|
||||
@@ -94,12 +147,14 @@
|
||||
if ([selectedObjects count] > 0)
|
||||
{
|
||||
var names = [CPMutableArray array];
|
||||
for (var i = 0; i <[selectedObjects count]; i++)
|
||||
[names addObject:[selectedObjects[i] name]];[logField setStringValue:[names componentsJoinedByString:@", "]];
|
||||
for (var i = 0; i < [selectedObjects count]; i++)
|
||||
[names addObject:[selectedObjects[i] name]];
|
||||
|
||||
[logField setStringValue:[CPString stringWithFormat:@"Selected: %@", [names componentsJoinedByString:@", "]]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[logField setStringValue:@"Nothing selected"];
|
||||
[logField setStringValue:@"Selected: (Nothing selected)"];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,16 +176,21 @@
|
||||
if (self)
|
||||
{
|
||||
name = aName;
|
||||
children = someChildren;
|
||||
children = someChildren || [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithName:@"New Node" children:[]];
|
||||
}
|
||||
|
||||
- (void)setName:(CPString)aName
|
||||
{
|
||||
[self willChangeValueForKey:@"name"];
|
||||
name = aName;[self didChangeValueForKey:@"name"];
|
||||
name = aName;
|
||||
[self didChangeValueForKey:@"name"];
|
||||
}
|
||||
|
||||
- (CPString)name
|
||||
@@ -150,4 +210,9 @@
|
||||
return children;
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
return [CPString stringWithFormat:@"<Node %@>", [self name]];
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in new issue
Block a user