Merge remote-tracking branch 'upstream/main'

This commit is contained in:
daboe01
2026-03-08 21:03:35 +01:00
12 changed files with 1936 additions and 47 deletions
+1
View File
@@ -116,3 +116,4 @@
@import "CPWindowController.j"
@import "CPWorkspace.j"
@import "CPFontPanel.j"
@import "CPTreeController.j"
+311 -7
View File
@@ -773,6 +773,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self reloadItem:anItem reloadChildren:NO];
}
- (int)_numberOfRows
{
return _itemsForRows ? _itemsForRows.length : 0;
}
/*!
Reloads the data for a given item and optionally the children.
@@ -784,6 +789,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
_pendingItemToClean = [];
_itemAddedDuringLastLoading = [];
var previousRowCount = _itemsForRows.length;
if (!!shouldReloadChildren || !anItem)
[self _loadItemInfoForItem:anItem intermediate:NO];
else
@@ -791,6 +798,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self _cleanPendingItem];
// Safely update the table size and force a synchronous layout recalculation
// BEFORE the views are reloaded, avoiding the clipping issue.
if (_itemsForRows.length !== previousRowCount)
[self noteNumberOfRowsChanged];
[super _reloadDataViews];
}
@@ -837,9 +849,20 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
for (var i = [previousItems count] - 1; i >= 0; i--)
{
var item = previousItems[i];
var item = previousItems[i],
found = NO;
if (![children containsObject:item])
// Use strict identity (===) instead of containsObject: (which triggers isEqual:)
for (var j = 0, count = children.length; j < count; j++)
{
if (children[j] === item)
{
found = YES;
break;
}
}
if (!found)
[self _addPendingItem:item];
}
}
@@ -853,7 +876,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var children = itemInfo.children;
for (var i = [children count]; i >= 0; i--)
// Fixed out-of-bounds index (was previously [children count])
for (var i = children.length - 1; i >= 0; i--)
{
var child = children[i];
[self _addPendingItem:child];
@@ -864,7 +888,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
- (void)_cleanPendingItem
{
for (var i = [_pendingItemToClean count]; i >= 0; i--)
for (var i = [_pendingItemToClean count] - 1; i >= 0; i--)
{
var item = _pendingItemToClean[i];
@@ -908,7 +932,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var weight = itemInfo.weight,
descendants = anItem ? [anItem] : [];
[_itemAddedDuringLastLoading addObject:anItem];
if (anItem)
[_itemAddedDuringLastLoading addObject:anItem];
if (itemInfo.isExpanded && [self _sendDataSourceShouldDeferDisplayingChildrenOfItem:anItem])
{
@@ -1085,7 +1110,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var parent = itemInfo.parent;
// Check if the parent is the root item because we never return the actual root item
if (itemInfo[[parent UID]] === _rootItemInfo)
if (parent && _itemInfosForItems[[parent UID]] === _rootItemInfo)
parent = nil;
return parent;
@@ -2179,7 +2204,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_))
{
var item = [_outlineView itemAtRow:aRow];
return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item]
return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item];
}
// We reimplement CPView menuForEvent: because we can't call it directly. CPTableView implements menuForEvent:
@@ -2344,3 +2369,282 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted)
? [CPColor colorWithCalibratedWhite:0.4 alpha: 1.0]
: [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]);
};
@implementation CPOutlineView (CPBindings)
+ (void)initialize
{
if (self !== [CPOutlineView class])
return;
[self exposeBinding:@"content"];
[self exposeBinding:@"selectionIndexPaths"];
[self exposeBinding:@"sortDescriptors"];
}
/*!
Returns the currently selected index paths.
This allows the outline view to be KVC-compliant for `selectionIndexPaths`.
*/
- (CPArray)selectionIndexPaths
{
var indexes = [self selectedRowIndexes],
paths = [CPMutableArray array],
index = [indexes firstIndex];
while (index !== CPNotFound)
{
var item = [self itemAtRow:index];
// Check if the item is a CPTreeNode proxy (which it will be when bound to CPTreeController)
if ([item respondsToSelector:@selector(indexPath)])
[paths addObject:[item indexPath]];
index = [indexes indexGreaterThanIndex:index];
}
return paths;
}
@end
@implementation CPOutlineView (CPBinder)
- (id)content { return nil; }
- (void)setContent:(id)aContent { }
- (void)setSelectionIndexPaths:(CPArray)paths { }
+ (Class)_binderClassForBinding:(CPString)aBinding
{
if (aBinding === @"content")
return [_CPOutlineViewContentBinder class];
if (aBinding === @"selectionIndexPaths")
return [_CPOutlineViewSelectionIndexPathsBinder class];
return [super _binderClassForBinding:aBinding];
}
@end
// --- Content Binder ---
/*!
_CPOutlineViewContentBinder acts as the CPOutlineViewDataSource when the outline view
is bound to a CPTreeController's arrangedObjects.
*/
@implementation _CPOutlineViewContentBinder : CPBinder
{
CPTreeNode _rootNode;
}
- (void)bind
{
// 1. Set the data source FIRST so that it is ready when
//[super bind] triggers the initial synchronous setValueFor:
[_source setDataSource:self];
// 2. Establish KVO (which immediately triggers setValueFor:)
[super bind];
}
- (void)unbind
{
if ([_source dataSource] === self)
[_source setDataSource:nil];
[super unbind];
}
- (void)setValueFor:(CPString)aBinding
{
var destination = [_info objectForKey:CPObservedObjectKey],
keyPath =[_info objectForKey:CPObservedKeyPathKey],
value = [destination valueForKeyPath:keyPath];
if (!value || ![value isKindOfClass:[CPTreeNode class]])
_rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil];
else
_rootNode = value;
// Because CPBinder triggers setValueFor: synchronously during its initialization
// (before -bind is ever called), we must lazily assign the data source here.
if ([_source dataSource] !== self)
{
// Assigning the data source automatically triggers [_source reloadData]
// inside CPOutlineView, so we don't need to call it manually here.
[_source setDataSource:self];
}
else
{
// If it was already set, we just manually trigger the reload.
[_source reloadData];
}
}
- (CPTreeNode)rootNode
{
return _rootNode;
}
// -- CPOutlineViewDataSource implementation --
- (id)outlineView:(CPOutlineView)outlineView child:(CPInteger)index ofItem:(id)item
{
var node = item || _rootNode;
return [[node childNodes] objectAtIndex:index];
}
- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
{
var node = item || _rootNode;
return ![node isLeaf];
}
- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
{
var node = item || _rootNode;
return [[node childNodes] count];
}
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
if ([item respondsToSelector:@selector(representedObject)])
return [item representedObject];
return item;
}
@end
// --- Selection Index Paths Binder ---
/*!
_CPOutlineViewSelectionIndexPathsBinder listens for selection changes on the CPOutlineView
and translates the selected rows into CPIndexPaths to push to the CPTreeController.
It also intercepts changes from the CPTreeController and auto-expands the tree to highlight them.
*/
@implementation _CPOutlineViewSelectionIndexPathsBinder : CPBinder
{
BOOL _isSyncingFromModel;
}
- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
{
self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource];
[[CPNotificationCenter defaultCenter]
addObserver:self
selector:@selector(outlineViewSelectionDidChange:)
name:CPOutlineViewSelectionDidChangeNotification
object:aSource];
}
+ (void)unbind:(CPString)aBinding forObject:(id)anObject
{
if (aBinding === "selectionIndexPaths")
[[CPNotificationCenter defaultCenter]
removeObserver:self
name:CPOutlineViewSelectionDidChangeNotification
object:anObject];
[super unbind:aBinding forObject:anObject];
}
- (void)setValueFor:(CPString)aBinding
{
// 1. SUPPRESS KVO AT THE VERY TOP to avoid circular updates when expanding parents
_isSyncingFromModel = YES;
var destination = [_info objectForKey:CPObservedObjectKey],
keyPath = [_info objectForKey:CPObservedKeyPathKey],
indexPaths = [destination valueForKeyPath:keyPath] || [],
indexes = [CPMutableIndexSet indexSet];
// 2. Fetch the root node directly from the CPTreeController (destination)
var rootNode = [destination respondsToSelector:@selector(arrangedObjects)] ? [destination arrangedObjects] : nil;
if (rootNode)
{
for (var i = 0, count = [indexPaths count]; i < count; i++)
{
var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]];
if (item)
{
var parentsToExpand = [CPMutableArray array],
parent = [item parentNode];
while (parent && parent !== rootNode)
{
[parentsToExpand insertObject:parent atIndex:0];
parent = [parent parentNode];
}
for (var j = 0; j < [parentsToExpand count]; j++)
[_source expandItem:parentsToExpand[j]];
var row = [_source rowForItem:item];
if (row !== CPNotFound && row >= 0)
[indexes addIndex:row];
}
}
}
// Adjust the CPOutlineView selection
[_source selectRowIndexes:indexes byExtendingSelection:NO];
// 3. Re-enable KVO after adjustments are done
_isSyncingFromModel = NO;
}
- (void)outlineViewSelectionDidChange:(CPNotification)note
{
// We only want to push the change back if we aren't currently syncing down from the model
if (_isSyncingFromModel)
return;
// In CPBinder, reverseSetValueFor: takes the name of the property on _source
// it should fetch the updated value from. Since CPOutlineView has the selectionIndexPaths method:
[self reverseSetValueFor:@"selectionIndexPaths"];
}
@end
@implementation _CPOutlineViewContentBinder (DynamicColumns)
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
var rep = [item respondsToSelector:@selector(representedObject)] ? [item representedObject] : item;
// Dynamically fetch the value using the column's identifier (e.g., "name")
if (rep && [tableColumn identifier])
return [rep valueForKey:[tableColumn identifier]];
return rep;
}
// Add this to support inline bidirectional editing in the outline view
- (void)outlineView:(CPOutlineView)outlineView setObjectValue:(id)value forTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
var rep = [item respondsToSelector:@selector(representedObject)] ?[item representedObject] : item;
// Push the inline edit back to the model using the column's identifier
if (rep && [tableColumn identifier])
[rep setValue:value forKey:[tableColumn identifier]];
}
- (id)content
{
// CPTableView internals probe the binder for its flat content to draw rows.
// For an outline view, the flat content is exactly the internally mapped items for rows.
if (_source && _source._itemsForRows)
return _source._itemsForRows;
return [];
}
@end
+628
View File
@@ -0,0 +1,628 @@
/*
* CPTreeController.j
* AppKit
*
* Daniel Boehringer Mar/2026
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import <Foundation/CPArray.j>
@import <Foundation/CPIndexPath.j>
@import "CPObjectController.j"
@import "CPKeyValueBinding.j"
@import "CPTreeNode.j"
@implementation CPTreeController : CPObjectController
{
BOOL _avoidsEmptySelection;
BOOL _preservesSelection;
BOOL _selectsInsertedObjects;
BOOL _alwaysUsesMultipleValuesMarker;
CPString _childrenKeyPath;
CPString _countKeyPath;
CPString _leafKeyPath;
CPArray _sortDescriptors;
id _arrangedObjects;
CPArray _selectionIndexPaths;
BOOL _disableSetContent;
}
+ (void)initialize
{
if (self !== [CPTreeController class])
return;
[self exposeBinding:@"contentArray"];
[self exposeBinding:@"sortDescriptors"];
}
+ (CPSet)keyPathsForValuesAffectingContentArray
{
return[CPSet setWithObjects:@"content"];
}
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
{
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
}
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
{
return[CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanAddChild
{
return[CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanInsert
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
- (id)init
{
if (self = [super init])
{
_preservesSelection = YES;
_selectsInsertedObjects = YES;
_avoidsEmptySelection = YES;
_alwaysUsesMultipleValuesMarker = NO;
_childrenKeyPath = @"children";
[self _init];
}
return self;
}
- (void)_init
{
_sortDescriptors = [CPArray array];
_selectionIndexPaths = [CPArray array];
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
}
- (void)prepareContent
{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
}
- (BOOL)preservesSelection { return _preservesSelection; }
- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; }
- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; }
- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; }
- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; }
- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; }
- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; }
- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; }
- (CPArray)sortDescriptors { return _sortDescriptors; }
- (void)setSortDescriptors:(CPArray)value
{
if (_sortDescriptors === value)
return;
_sortDescriptors = [value copy];
[self _rearrangeObjects];
}
- (CPString)childrenKeyPath { return _childrenKeyPath; }
- (void)setChildrenKeyPath:(CPString)aKeyPath
{
if (_childrenKeyPath === aKeyPath) return;
_childrenKeyPath = aKeyPath;[self rearrangeObjects];
}
- (CPString)countKeyPath { return _countKeyPath; }
- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; }
- (CPString)leafKeyPath { return _leafKeyPath; }
- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; }
- (CPString)childrenKeyPathForNode:(CPTreeNode)node { return [self childrenKeyPath]; }
- (CPString)countKeyPathForNode:(CPTreeNode)node { return [self countKeyPath]; }
- (CPString)leafKeyPathForNode:(CPTreeNode)node { return [self leafKeyPath]; }
- (void)setContent:(id)value
{
if (_disableSetContent) return;
if (!value)
value = [CPArray array];
if (![value isKindOfClass:[CPArray class]])
value = [CPArray arrayWithObject:value];
var oldSelectedObjects = nil,
oldSelectionIndexPaths = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexPaths = [self selectionIndexPaths];
_contentObject = value;
[self _rearrangeObjects];
if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
}
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
- (id)contentArray { return _contentObject; }
- (id)arrangedObjects { return _arrangedObjects; }
- (void)rearrangeObjects
{
[self willChangeValueForKey:@"arrangedObjects"];
[self _rearrangeObjects];
[self didChangeValueForKey:@"arrangedObjects"];
}
- (void)_rearrangeObjects
{
var oldSelectedObjects = nil,
oldSelectionIndexPaths = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexPaths = [self selectionIndexPaths];
[self __rebuildArrangedObjectsTree];
if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
}
- (void)__rebuildArrangedObjectsTree
{
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
contentArray = [self contentArray];
if (contentArray && [contentArray count] > 0)
{
var children = [self _buildTreeNodesForObjects:contentArray];
[[rootNode mutableChildNodes] addObjectsFromArray:children];
}
_arrangedObjects = rootNode;
}
- (CPArray)_buildTreeNodesForObjects:(CPArray)objects
{
var count = [objects count];
if (count === 0)
return [];
var sortedObjects = objects;
if (_sortDescriptors && [_sortDescriptors count] > 0)
sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors];
var nodes = [CPMutableArray arrayWithCapacity:count];
for (var i = 0; i < count; i++)
{
var obj = [sortedObjects objectAtIndex:i],
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
if (_childrenKeyPath)
{
var childObjects = [obj valueForKeyPath:_childrenKeyPath];
if (childObjects && [childObjects count] > 0)
{
var childNodes = [self _buildTreeNodesForObjects:childObjects];
[[node mutableChildNodes] addObjectsFromArray:childNodes];
}
}
[nodes addObject:node];
}
return nodes;
}
- (CPIndexPath)selectionIndexPath
{
return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil;
}
- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath
{
var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array];
return[self setSelectionIndexPaths:paths];
}
- (CPArray)selectionIndexPaths { return _selectionIndexPaths; }
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
{
[self _selectionWillChange];
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
[self _selectionDidChange];
return result;
}
- (BOOL)__setSelectionIndexPaths:(CPArray)indexPaths avoidEmpty:(BOOL)avoidEmpty
{
var newPaths = indexPaths;
if (!newPaths)
newPaths = [CPArray array];
if (![newPaths count] && avoidEmpty)
{
if ([[[self arrangedObjects] childNodes] count] > 0)
newPaths = [CPArray arrayWithObject:[CPIndexPath indexPathWithIndex:0]];
}
if ([_selectionIndexPaths isEqualToArray:newPaths])
return NO;
[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"];
return YES;
}
- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
{
var newPaths = [_selectionIndexPaths mutableCopy];
[newPaths addObjectsFromArray:indexPaths];
return [self setSelectionIndexPaths:newPaths];
}
- (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths
{
var newPaths = [_selectionIndexPaths mutableCopy];
[newPaths removeObjectsInArray:indexPaths];
return[self setSelectionIndexPaths:newPaths];
}
- (CPArray)selectedNodes
{
var nodes = [CPMutableArray array],
count = [_selectionIndexPaths count];
for (var i = 0; i < count; i++)
{
var node = [[self arrangedObjects] descendantNodeAtIndexPath:[_selectionIndexPaths objectAtIndex:i]];
if (node)
[nodes addObject:node];
}
return nodes;
}
- (CPArray)selectedObjects
{
var objects = [CPMutableArray array],
nodes = [self selectedNodes],
count = [nodes count];
for (var i = 0; i < count; i++)
[objects addObject:[[nodes objectAtIndex:i] representedObject]];
return objects;
}
- (BOOL)__setSelectedObjects:(CPArray)objects
{
if (!objects || [objects count] === 0)
return[self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection];
var newPaths = [CPMutableArray array];
for (var i = 0, count = [objects count]; i < count; i++)
{
var path = [self _indexPathForObject:[objects objectAtIndex:i] inNode:[self arrangedObjects]];
if (path)
[newPaths addObject:path];
}
return[self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection];
}
- (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node
{
if ([node representedObject] === anObject && [node parentNode] != nil)
return [node indexPath];
var children = [node childNodes];
if (children)
{
for (var i = 0, count = [children count]; i < count; i++)
{
var found = [self _indexPathForObject:anObject inNode:[children objectAtIndex:i]];
if (found)
return found;
}
}
return nil;
}
- (BOOL)canInsert { return[self isEditable]; }
- (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];
if (!selectionPath)
selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]];
var length = [selectionPath length],
lastIndex = [selectionPath indexAtPosition:length - 1],
insertPath = [selectionPath indexPathByRemovingLastIndex];
insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)addChild:(id)sender
{
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];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)insert:(id)sender
{
if (![self canInsert]) return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
[self insertObject:newObject atArrangedObjectIndexPath:indexPath];
}
- (void)insertChild:(id)sender
{
if (![self canInsertChild]) return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)insertObject:(id)anObject atArrangedObjectIndexPath:(CPIndexPath)indexPath
{
[self insertObjects:[CPArray arrayWithObject:anObject] atArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
}
- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
{
[self willChangeValueForKey:@"content"];
_disableSetContent = YES;
var count = [objects count];
for (var i = 0; i < count; i++)
{
var object = [objects objectAtIndex:i],
path = [indexPaths objectAtIndex:i],
length = [path length];
if (length === 1)
{[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
}
else
{
var parentPath = [path indexPathByRemovingLastIndex],
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
if (parentNode)
{
var parentObj = [parentNode representedObject],
childIndex = [path indexAtPosition:length - 1];
var children = [parentObj valueForKeyPath:_childrenKeyPath];
if (!children)
{
children = [CPMutableArray array];
[parentObj setValue:children forKeyPath:_childrenKeyPath];
}
var mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
[mutableChildren insertObject:object atIndex:childIndex];
}
}
}
var binding = [[self class] _binderClassForBinding:@"contentArray"];
if (binding)
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
_disableSetContent = NO;
[self _rearrangeObjects];
if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths];
[self didChangeValueForKey:@"content"];
}
- (void)remove:(id)sender
{
[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths];
}
- (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath
{
[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
}
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
{
[self willChangeValueForKey:@"content"];
_disableSetContent = YES;
var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
count = [sortedPaths count];
for (var i = count - 1; i >= 0; i--)
{
var path = [sortedPaths objectAtIndex:i],
length = [path length];
if (length === 1)
{
[_contentObject removeObjectAtIndex:[path indexAtPosition:0]];
}
else
{
var parentPath = [path indexPathByRemovingLastIndex],
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
if (parentNode)
{
var parentObj = [parentNode representedObject],
childIndex = [path indexAtPosition:length - 1],
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
if (mutableChildren && childIndex <[mutableChildren count])
[mutableChildren removeObjectAtIndex:childIndex];
}
}
}
var binding = [[self class] _binderClassForBinding:@"contentArray"];
if (binding)
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
_disableSetContent = NO;
[self _rearrangeObjects];
[self didChangeValueForKey:@"content"];
}
- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
{
[self moveNodes:[CPArray arrayWithObject:node] toIndexPath:indexPath];
}
- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
{[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";
@implementation CPTreeController (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_avoidsEmptySelection = [aCoder decodeBoolForKey:CPTreeControllerAvoidsEmptySelection];
_preservesSelection = [aCoder decodeBoolForKey:CPTreeControllerPreservesSelection];
_selectsInsertedObjects = [aCoder decodeBoolForKey:CPTreeControllerSelectsInsertedObjects];
_alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
_childrenKeyPath = [aCoder decodeObjectForKey:CPTreeControllerChildrenKeyPath] || @"children";
_countKeyPath = [aCoder decodeObjectForKey:CPTreeControllerCountKeyPath];
_leafKeyPath = [aCoder decodeObjectForKey:CPTreeControllerLeafKeyPath];
_sortDescriptors = [CPArray array];
_selectionIndexPaths = [CPArray array];
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
if (![self content] && [self automaticallyPreparesContent])
[self prepareContent];
else if (![self content])
[self _setContentArray:[CPArray array]];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeBool:_avoidsEmptySelection forKey:CPTreeControllerAvoidsEmptySelection];
[aCoder encodeBool:_preservesSelection forKey:CPTreeControllerPreservesSelection];
[aCoder encodeBool:_selectsInsertedObjects forKey:CPTreeControllerSelectsInsertedObjects];
[aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
[aCoder encodeObject:_childrenKeyPath forKey:CPTreeControllerChildrenKeyPath];
[aCoder encodeObject:_countKeyPath forKey:CPTreeControllerCountKeyPath];
[aCoder encodeObject:_leafKeyPath forKey:CPTreeControllerLeafKeyPath];
}
- (void)awakeFromCib
{
[self _selectionWillChange];
[self _selectionDidChange];
}
@end
+83 -39
View File
@@ -22,13 +22,12 @@
@import <Foundation/CPObject.j>
@import <Foundation/CPIndexPath.j>
@import <Foundation/CPArray.j>
@implementation CPTreeNode : CPObject
{
id _representedObject @accessors(readonly, property=representedObject);
CPTreeNode _parentNode @accessors(readonly, property=parentNode);
id _representedObject @accessors(property=representedObject);
CPTreeNode _parentNode @accessors(property=parentNode);
CPMutableArray _childNodes;
}
@@ -52,36 +51,35 @@
- (CPIndexPath)indexPath
{
if (_parentNode != nil)
// If we have a parent, calculate path based on parent's path + our index
if (_parentNode)
{
var path;
var index;
index = [[_parentNode childNodes] indexOfObject:self];
path = [_parentNode indexPath];
if (path != nil)
{
return [path indexPathByAddingIndex:index];
}
else
{
return [CPIndexPath indexPathWithIndex:index];
}
}
else
{
return nil;
// Search the parent's child nodes, not our own!
var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
// If the parent is the root (and technically has no path itself in some implementations),
// we might get nil. Handle that gracefully.
var parentPath = [_parentNode indexPath];
if (parentPath)
return [parentPath indexPathByAddingIndex:index];
return [CPIndexPath indexPathWithIndex:index];
}
// If we are the root, we don't have an index path in the context of a tree controller usually,
// or we are [] (empty path). Returning nil is acceptable for the absolute root.
return nil;
}
- (BOOL)isLeaf
{
return [_childNodes count] <= 0;
return [_childNodes count] == 0;
}
- (CPArray)childNodes
{
// Return a copy to prevent external modification without KVC
return [_childNodes copy];
}
@@ -90,18 +88,29 @@
return [self mutableArrayValueForKey:@"childNodes"];
}
- (void)insertObject:(id)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
{
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
// MARK: - KVC Compliance Methods
aTreeNode._parentNode = self;
- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
{
// Optional: Auto-detach from old parent if strictly moving nodes
if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
{
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
}
// Direct ivar access is allowed here since we are inside the class implementation
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
aTreeNode._parentNode = self;
[_childNodes insertObject:aTreeNode atIndex:anIndex];
}
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
{
[_childNodes objectAtIndex:anIndex]._parentNode = nil;
var node = [_childNodes objectAtIndex:anIndex];
if ([node isKindOfClass:[CPTreeNode class]])
node._parentNode = nil;
[_childNodes removeObjectAtIndex:anIndex];
}
@@ -110,17 +119,34 @@
{
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
oldTreeNode._parentNode = nil;
aTreeNode._parentNode = self;
if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
oldTreeNode._parentNode = nil;
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
aTreeNode._parentNode = self;
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
}
// MARK: - Convenience Accessors
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
{
return _childNodes[anIndex];
return [_childNodes objectAtIndex:anIndex];
}
- (CPInteger)count
{
return [_childNodes count];
}
- (id)objectAtIndex:(CPInteger)anIndex
{
return [_childNodes objectAtIndex:anIndex];
}
// MARK: - Utility
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
{
[_childNodes sortUsingDescriptors:sortDescriptors];
@@ -129,25 +155,39 @@
return;
var count = [_childNodes count];
while (count--)
[_childNodes[count] sortWithSortDescriptors:sortDescriptors recursively:YES];
{
var child = [_childNodes objectAtIndex:count];
if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
[child sortWithSortDescriptors:sortDescriptors recursively:YES];
}
}
- (CPTreeNode)descendantNodeAtIndexPath:(CPIndexPath)indexPath
{
var index = 0,
count = [indexPath length],
node = self;
if (!indexPath || [indexPath length] == 0)
return self;
for (; index < count; ++index)
node = [node objectInChildNodesAtIndex:[indexPath indexAtPosition:index]];
var node = self,
length = [indexPath length];
for (var i = 0; i < length; i++)
{
var index = [indexPath indexAtPosition:i],
count = [node count];
if (index >= count || index < 0)
return nil;
node = [node objectAtIndex:index];
}
return node;
}
@end
// Coding implementation remains correct
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
@@ -163,6 +203,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
// Safety check to ensure decoding gave us a CPArray
if (!_childNodes)
_childNodes = [[CPMutableArray alloc] init];
}
return self;
+266
View File
@@ -0,0 +1,266 @@
/*
* CPTreeControllerTest.j
*
* Test suite for CPTreeController
*/
@import <Foundation/CPArray.j>
@import <Foundation/CPIndexPath.j>
@import <AppKit/CPTreeController.j>
@import <AppKit/CPTreeNode.j>
@import <AppKit/CPTextField.j>
@class OrgNode
@implementation CPTreeControllerTest : OJTestCase
{
CPTreeController _treeController @accessors(property=treeController);
CPArray _contentArray @accessors(property=contentArray);
CPArray observations;
int aCount @accessors;
}
- (CPArray)makeTestTree
{
var engineering = [OrgNode nodeWithName:@"Engineering"],
marketing = [OrgNode nodeWithName:@"Marketing"];
var webTeam = [OrgNode nodeWithName:@"Web Team"],
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
[engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
var dev1 = [OrgNode nodeWithName:@"Francisco"],
dev2 = [OrgNode nodeWithName:@"Ross"];
[webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
return [CPMutableArray arrayWithObjects:engineering, marketing];
}
- (void)setUp
{
[[CPApplication alloc] init];
_contentArray = [self makeTestTree];
_treeController = [[CPTreeController alloc] init];
[_treeController setChildrenKeyPath:@"children"];
[_treeController setContent:[_contentArray copy]];
}
- (void)testInitWithContent
{
[self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
[self assert:[CPTreeNode class] equals:[[[self treeController] arrangedObjects] class] message:@"arranged objects should be a proxy CPTreeNode root"];
}
- (void)testInitWithoutContent
{
var emptyController = [[CPTreeController alloc] init];
[self assert:[CPArray array] equals:[emptyController contentArray]];
[self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]];
}
- (void)testSetContent
{
var newTree = [CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Solo Department"]];
[[self treeController] setContent:newTree];
[self assert:newTree equals:[[self treeController] contentArray]];
[self assert:1 equals:[[[[self treeController] arrangedObjects] childNodes] count]];
}
- (void)testSelectionPaths
{
var controller = [self treeController];
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
[controller setSelectionIndexPath:path];
var selectedPath = [controller selectionIndexPath];
[self assert:path equals:selectedPath];
var selectedNodes = [controller selectedNodes];
[self assert:1 equals:[selectedNodes count]];
[self assert:@"Web Team" equals:[[[selectedNodes objectAtIndex:0] representedObject] name]];
}
- (void)testAddChild
{
var controller = [self treeController];
[controller setObjectClass:[OrgNode class]];
var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1];
[controller setSelectionIndexPath:parentPath];
var newDev = [OrgNode nodeWithName:@"Tom"];
var insertPath = [parentPath indexPathByAddingIndex:0];
[controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
var engineering = [[controller contentArray] objectAtIndex:0],
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]];
}
- (void)testInsertObjectAtArrangedObjectIndexPath
{
var controller = [self treeController];
var path = [CPIndexPath indexPathWithIndex:1];
var hrDept = [OrgNode nodeWithName:@"Human Resources"];
[controller insertObject:hrDept atArrangedObjectIndexPath:path];
[self assert:3 equals:[[controller contentArray] count]];
[self assert:hrDept equals:[[controller contentArray] objectAtIndex:1]];
var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
var subDept = [OrgNode nodeWithName:@"Sub Dept"];
[controller insertObject:subDept atArrangedObjectIndexPath:nestedPath];
var engChildren = [[[controller contentArray] objectAtIndex:0] children];
[self assert:subDept equals:[engChildren objectAtIndex:0] message:@"Object should be inserted at the correct nested index path"];
}
- (void)testRemoveObjectAtArrangedObjectIndexPath
{
var controller = [self treeController];
var path = [[[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0] indexPathByAddingIndex:0];
[controller removeObjectAtArrangedObjectIndexPath:path];
var engineering = [[controller contentArray] 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]];
}
- (void)testRemoveObjectsAtArrangedObjectIndexPaths
{
var controller = [self treeController];
var paths = [CPArray arrayWithObjects:[CPIndexPath indexPathWithIndex:0], [CPIndexPath indexPathWithIndex:1]];
[controller removeObjectsAtArrangedObjectIndexPaths:paths];
[self assert:0 equals:[[controller contentArray] count] message:@"All root nodes should be removed"];
}
- (void)testSelectingEmptyIndexPathsExplicitlyWithAvoidsEmptySelection
{
var controller = [self treeController];
[controller setAvoidsEmptySelection:YES];
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
[controller setSelectionIndexPaths:[CPArray array]];
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be empty when unselecting explicitly, even with avoidsEmptySelection"];
}
- (void)testAvoidsEmptySelectionWhenRemoving
{
var controller = [self treeController];
[controller setAvoidsEmptySelection:YES];
var path = [CPIndexPath indexPathWithIndex:0];
[controller setSelectionIndexPath:path];
// Remove "Engineering"
[controller removeObjectAtArrangedObjectIndexPath:path];
[self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"];
// "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]];
[controller setAvoidsEmptySelection:NO];
// Reselect "Marketing" at index 0
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
// Remove "Marketing"
[controller removeObjectAtArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:0]];
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be allowed to be empty when avoidsEmptySelection is NO"];
}
- (void)testChildrenKeyPathOverride
{
var controller = [[CPTreeController alloc] init];
[controller setChildrenKeyPath:@"subItems"];
var data = [OrgNode nodeWithName:@"Root"];
[data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"];
[controller setContent:[CPMutableArray arrayWithObject:data]];
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
var newItem = [OrgNode nodeWithName:@"New Sub"];
[controller insertObject:newItem atArrangedObjectIndexPath:path];
var subs = [data valueForKey:@"subItems"];
[self assert:2 equals:[subs count]];
[self assert:newItem equals:[subs objectAtIndex:0] message:@"Object should be inserted using the custom childrenKeyPath"];
}
- (void)testContentBinding
{
var controller = [[CPTreeController alloc] init];
[controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil];
[self assert:[self contentArray] equals:[controller contentArray]];
[self assert:2 equals:[[[controller arrangedObjects] childNodes] count]];
}
- (void)testSelectedObjects
{
var controller = [self treeController];
var path = [CPIndexPath indexPathWithIndex:1];
[controller setSelectionIndexPath:path];
var selectedObjects = [controller selectedObjects];
[self assert:1 equals:[selectedObjects count]];
[self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
}
@end
/*
* Dummy Model Class for Testing
*/
@implementation OrgNode : CPObject
{
CPString _name @accessors(property=name);
CPMutableArray _children @accessors(property=children);
CPMutableArray _subItems @accessors(property=subItems);
}
+ (id)nodeWithName:(CPString)aName
{
return [[self alloc] initWithName:aName];
}
- (id)initWithName:(CPString)aName
{
if (self = [super init])
{
_name = aName;
_children = [CPMutableArray array];
_subItems = [CPMutableArray array];
}
return self;
}
- (CPString)description
{
return [CPString stringWithFormat:@"<OrgNode %@>", [self name]];
}
@end
+1 -1
View File
@@ -25,7 +25,7 @@
indexPath = [CPIndexPath indexPathWithIndex:1];
[self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]];
[self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]];
}
@end
@@ -0,0 +1,153 @@
/*
* AppController.j
* TreeControllerBindingsTest
*
* Created for testing CPOutlineView and CPTreeController bindings.
*/
@import <AppKit/AppKit.j>
@import <AppKit/CPTreeController.j>
@implementation AppController : CPObject
{
CPTreeController treeController;
CPTextField logField;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
// 1. Create the Data Model
var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
child2 = [[Node alloc] initWithName:@"Child 1.2" children:[]],
root2 = [[Node alloc] initWithName:@"Root 2" children:[]],
child3 = [[Node alloc] initWithName:@"Child 2.1" children:[]];
[root1 setChildren:[child1, child2]];
[root2 setChildren:[child3]];
var contentArray = [root1, root2];
// 2. Setup the Tree Controller
treeController = [[CPTreeController alloc] init];
[treeController setChildrenKeyPath:@"children"];
[treeController setContent:contentArray];
// 3. Setup the Outline View
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
[scrollView 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];
// 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];
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 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
[outlineView2 addTableColumn:column2];
[outlineView2 setOutlineTableColumn:column2];
[outlineView2 setAllowsMultipleSelection:YES];
[scrollView2 setDocumentView:outlineView2];
[contentView addSubview:scrollView2];
// 4. Establish Bindings for the Outline View
[outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[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]];
[treeController setSelectionIndexPath:path];
}
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
{
if (keyPath === @"selectionIndexPaths")
{
var selectedObjects = [treeController selectedObjects];
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:@", "]];
}
else
{
[logField setStringValue:@"Nothing selected"];
}
}
}
@end
// --- Custom Data Model ---
@implementation Node : CPObject
{
CPString name;
CPArray children;
}
- (id)initWithName:(CPString)aName children:(CPArray)someChildren
{
self = [super init];
if (self)
{
name = aName;
children = someChildren;
}
return self;
}
// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
- (void)setName:(CPString)aName
{
[self willChangeValueForKey:@"name"];
name = aName;[self didChangeValueForKey:@"name"];
}
- (CPString)name
{
return name;
}
- (void)setChildren:(CPArray)someChildren
{
[self willChangeValueForKey:@"children"];
children = someChildren;
[self didChangeValueForKey:@"children"];
}
- (CPArray)children
{
return children;
}
@end
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPBundleName</key>
<string>treecontroller</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
@@ -0,0 +1,93 @@
/*
* Jakefile
* tooltips
*
* Created by You on April 26, 2011.
* Copyright 2011, Your Company All rights reserved.
*/
var ENV = require("system").env,
FILE = require("file"),
JAKE = require("jake"),
task = JAKE.task,
FileList = JAKE.FileList,
app = require("cappuccino/jake").app,
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
OS = require("os");
app ("tooltips", function(task)
{
task.setBuildIntermediatesPath(FILE.join("Build", "tooltips.build", configuration));
task.setBuildPath(FILE.join("Build", configuration));
task.setProductName("tooltips");
task.setIdentifier("com.yourcompany.tooltips");
task.setVersion("1.0");
task.setAuthor("Your Company");
task.setEmail("feedback @nospam@ yourcompany.com");
task.setSummary("tooltips");
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
task.setResources(new FileList("Resources/**"));
task.setIndexFilePath("index.html");
task.setInfoPlistPath("Info.plist");
if (configuration === "Debug")
task.setCompilerFlags("-DDEBUG -g");
else
task.setCompilerFlags("-O");
});
task ("default", ["tooltips"], function()
{
printResults(configuration);
});
task ("build", ["default"]);
task ("debug", function()
{
ENV["CONFIGURATION"] = "Debug";
JAKE.subjake(["."], "build", ENV);
});
task ("release", function()
{
ENV["CONFIGURATION"] = "Release";
JAKE.subjake(["."], "build", ENV);
});
task ("run", ["debug"], function()
{
OS.system(["open", FILE.join("Build", "Debug", "tooltips", "index.html")]);
});
task ("run-release", ["release"], function()
{
OS.system(["open", FILE.join("Build", "Release", "tooltips", "index.html")]);
});
task ("deploy", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Deployment", "tooltips"));
OS.system(["press", "-f", FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Deployment", "tooltips")]);
printResults("Deployment")
});
task ("desktop", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Desktop", "tooltips"));
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Desktop", "tooltips", "tooltips.app"));
printResults("Desktop")
});
task ("run-desktop", ["desktop"], function()
{
OS.system([FILE.join("Build", "Desktop", "tooltips", "tooltips.app", "Contents", "MacOS", "NativeHost"), "-i"]);
});
function printResults(configuration)
{
print("----------------------------");
print(configuration+" app built at path: "+FILE.join("Build", configuration, "tooltips"));
print("----------------------------");
}
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<!--
index-debug.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
//
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
// the methods in the debugger.
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
// more information on decorators.
//
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
<script type="text/javascript">
objj_msgSend_reset();
// DEBUG OPTIONS:
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
// Uncomment to enable printing of backtraces on exceptions:
//objj_msgSend_decorate(objj_backtrace_decorator);
// Uncomment to supress exceptions that take place inside a message
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
// Uncomment to enable runtime type checking:
//objj_msgSend_decorate(objj_typecheck_decorator);
// Uncomment (along with both above) to print backtraces on type check errors:
//objj_typecheck_prints_backtrace = true;
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
//CPLogUnregister(CPLogDefault);
// Uncomment to enable a specific logger:
//CPLogRegister(CPLogConsole);
//CPLogRegister(CPLogPopup);
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
// the class name of the view that created them. Comment this or set to false to disable.
appkit_tag_dom_elements = true;
</script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<!--
index.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* tooltips
*
* Created by You on April 26, 2011.
* Copyright 2011, Your Company All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}