diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 550d1cca7..9d650f4c3 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -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) diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j index 9487e4e46..18b7fe3cb 100644 --- a/Tests/AppKit/CPTreeControllerTest.j +++ b/Tests/AppKit/CPTreeControllerTest.j @@ -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 /* diff --git a/Tests/Manual/CPTreeControllerTest/AppController.j b/Tests/Manual/CPTreeControllerTest/AppController.j index 15e24c0e4..65640056f 100644 --- a/Tests/Manual/CPTreeControllerTest/AppController.j +++ b/Tests/Manual/CPTreeControllerTest/AppController.j @@ -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:@"", [self name]]; +} + @end