First pass at CPOutlineView.

Reviewed by me.
This commit is contained in:
Francisco Ryan Tolmasky I committed 2009-09-09 17:48:59 -07:00
1 parent b77eef0cd3
commit 041f8200df
2 files changed
+414 -62

No files matched your search

+387 -57
View File
@@ -3,7 +3,7 @@
* AppKit
*
* Created by Francisco Tolmasky.
* Copyright 2008, 280 North, Inc.
* Copyright 2009, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -20,93 +20,423 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CPTableColumn.j"
@import "CPTableView.j"
/*!
@ignore
This class is a subclass of CPTableView which provides the user with a way to display
tree structured data in an outline format. It is particularly useful for displaying hierarchical data
such as a class inheritance tree or any other set of relationships.
*/
#include "CoreGraphics/CGGeometry.h"
@implementation CPOutlineView : CPTableView
{
id _outlineDataSource;
CPArray _itemsByRow;
id _outlineViewDataSource;
CPTableColumn _outlineTableColumn;
float _indentationPerLevel;
Object _rootItemInfo;
CPMutableArray _itemsForRows;
Object _itemInfosForItems;
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
{
[super setDataSource:self];
_itemsByRow = [[CPArray alloc] init];
_rootItemInfo = { isExpanded:YES, isExpandable:NO, level:-1, row:-1, children:[], weight:1 };
_itemsForRows = [nil];
_itemInfosForItems = { };
[self setIndentationPerLevel:25.0];
[super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]];
}
return self;
}
/*!
@ignore
Sets the outline's data source. The data source must implement the following methods:
<pre>
- (id)outlineView:(CPOutlineView)outlineView child:(int)index ofItem:(id)item
- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
</pre>
@param aDataSource the outline's data source
@throws CPInternalInconsistencyException if the data source does not implement all the required methods
*/
- (void)setDataSource:(id)aDataSource
{
if (![aDataSource respondsToSelector:@selector(outlineView:child:ofItem)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:child:ofItem'"];
if (![aDataSource respondsToSelector:@selector(outlineView:isItemExpandable)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:isItemExpandable'"];
if (![aDataSource respondsToSelector:@selector(outlineView:numberOfChildrenOfItem)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:numberOfChildrenOfItem'"];
if (![aDataSource respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:objectValueForTableColumn:byItem'"];
if (_outlineViewDataSource === aDataSource)
return;
_outlineDataSource = aDataSource;
if (![aDataSource respondsToSelector:@selector(outlineView:child:ofItem:)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:child:ofItem:'"];
if (![aDataSource respondsToSelector:@selector(outlineView:isItemExpandable:)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:isItemExpandable:'"];
if (![aDataSource respondsToSelector:@selector(outlineView:numberOfChildrenOfItem:)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:numberOfChildrenOfItem:'"];
if (![aDataSource respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem:)])
[CPException raise:CPInternalInconsistencyException reason:"Data source must implement 'outlineView:objectValueForTableColumn:byItem:'"];
_outlineViewDataSource = aDataSource;
[self reloadData];
}
/* @ignore */
- (id)dataSource
{
return _outlineViewDataSource;
}
- (void)isItemExpanded:(id)anItem
{
if (!anItem)
return YES;
var itemInfo = _itemInfosForItems[[anItem UID]];
if (!itemInfo)
return NO;
return itemInfo.isExpanded;
}
- (void)expandItem:(id)anItem
{
if (!anItem)
return;
var itemInfo = _itemInfosForItems[[anItem UID]];
if (!itemInfo)
return;
if (itemInfo.isExpanded)
return;
itemInfo.isExpanded = YES;
[self reloadItem:anItem reloadChildren:YES];
}
- (void)collapseItem:(id)anItem
{
if (!anItem)
return;
var itemInfo = _itemInfosForItems[[anItem UID]];
if (!itemInfo)
return;
if (!itemInfo.isExpanded)
return;
itemInfo.isExpanded = NO;
[self reloadItem:anItem reloadChildren:YES];
}
- (void)mouseDown:(CPEvent)anEvent
{
var row = [self rowAtPoint:[self convertPoint:[anEvent locationInWindow] fromView:nil]];
if ([self isItemExpanded:[self itemAtRow:row]])
[self collapseItem:[self itemAtRow:row]];
else
[self expandItem:[self itemAtRow:row]];
}
- (void)reloadItem:(id)anItem
{
[self reloadItem:anItem reloadChildren:NO];
}
- (void)reloadItem:(id)anItem reloadChildren:(BOOL)shouldReloadChildren
{
_loadItemInfoForItem(self, anItem, shouldReloadChildren);
[super reloadData];
}
- (id)itemAtRow:(CPInteger)aRow
{
return _itemsForRows[aRow + 1] || nil;
}
- (CPInteger)rowForItem:(id)aItem
{
if (!anItem)
return _rootItemInfo.row;
var itemInfo = _itemInfosForItems[[anItem UID]];
if (typeof itemInfo === "undefined")
return CPNotFound;
return itemInfo.row;
}
- (void)setOutlineTableColumn:(CPTableColumn)aTableColumn
{
if (_outlineTableColumn === aTableColumn)
return;
_outlineTableColumn = aTableColumn;
// FIXME: efficiency.
[self reloadData];
}
- (CPTableColumn)outlineTableColumn
{
return _outlineTableColumn;
}
- (CPInteger)levelForItem:(id)anItem
{
if (!anItem)
return _rootItemInfo.level;
var itemInfo = _itemInfosForItems[[anItem UID]];
if (typeof itemInfo === "undefined")
return CPNotFound;
return itemInfo.level;
}
- (CPInteger)levelForRow:(CPInteger)aRow
{
return [self levelForItem:[self itemAtRow:aRow]];
}
- (void)setIndentationPerLevel:(float)anIndentationWidth
{
if (_indentationPerLevel === anIndentationWidth)
return;
_indentationPerLevel = anIndentationWidth;
// FIXME: efficiency!!!!
[self reloadData];
}
- (float)indentationPerLevel
{
return _indentationPerLevel;
}
- (void)reloadData
{
_numberOfVisibleItems = [_outlineDataSource outlineView:self numberOfChildrenOfItem:nil];
_numberOfRows = _numberOfVisibleItems;
var i = 0;
for (; i < _numberOfVisibleItems; ++i)
_itemsByRow[i] = [_outlineDataSource outlineView:self child:i ofItem:nil];
[self loadTableCellsInRect:[self bounds]];
[self reloadItem:nil reloadChildren:YES];
}
- (void)_enqueueReusableDataView:(CPView)aDataView
{
if ([aDataView isKindOfClass:[_CPOutlineViewHierarchicalView class]])
{
[aDataView removeFromSuperview];
[super _enqueueReusableDataView:[aDataView dataView]];
}
else
[super _enqueueReusableDataView:aDataView];
}
- (CPView)_newDataViewForRow:(CPInteger)aRow tableColumn:(CPTableColumn)aTableColumn
{
var dataView = [super _newDataViewForRow:aRow tableColumn:aTableColumn];
if (aTableColumn !== _outlineTableColumn)
return dataView;
var hierarchicalView = [[_CPOutlineViewHierarchicalView alloc] init];
[hierarchicalView setIndentationWidth:[self levelForRow:aRow] * [self indentationPerLevel]];
[hierarchicalView setDataView:dataView];
return hierarchicalView;
}
@end
/* @ignore */
@implementation CPOutlineView (CPTableDataSource)
/*
FIXME
*/
/* @ignore */
- (void)numberOfRowsInTableView:(CPTableView)aTableView
var _loadItemInfoForItem = function(/*CPOutlineView*/ anOutlineView, /*id*/ anItem, /*BOOL*/ shouldLoadChildren, /*id*/ parentItemInfo)
{
return _numberOfVisibleItems;
// FIXME: remote...
var dataSource = anOutlineView._outlineViewDataSource,
itemInfosForItems = anOutlineView._itemInfosForItems,
shouldReloadData = !parentItemInfo;
// If we are the root, just use the "static" root item info.
if (!anItem)
var itemInfo = anOutlineView._rootItemInfo;
else
{
// Get the existing info if it exists.
var itemUID = [anItem UID],
itemInfo = itemInfosForItems[itemUID];
// If we're not in the tree, then just bail.
if (!itemInfo)
return [];
// If no state, then we are the initiator, no need to update row/level.
if (!parentItemInfo)
parentItemInfo = itemInfo.parentItemInfo;
itemInfo.isExpandable = [dataSource outlineView:anOutlineView isItemExpandable:anItem];
// If we were previously expanded, but now no longer expandable, "de-expand".
// NOTE: we are *not* collapsing, thus no notification is posted.
if (!itemInfo.isExpandable && itemInfo.isExpanded)
{
itemInfo.isExpanded = NO;
itemInfo.children = [];
}
}
var weight = itemInfo.weight,
descendants = [anItem];
if (itemInfo.isExpanded && shouldLoadChildren)
{
var index = 0,
count = [dataSource outlineView:anOutlineView numberOfChildrenOfItem:anItem],
level = itemInfo.level + 1;
itemInfo.children = [];
for (; index < count; ++index)
{
var childItem = [dataSource outlineView:anOutlineView child:index ofItem:anItem],
childItemInfo = itemInfosForItems[[childItem UID]];
if (!childItemInfo)
{
childItemInfo = { isExpanded:NO, isExpandable:NO, children:[], weight:1 };
itemInfosForItems[[childItem UID]] = childItemInfo;
}
itemInfo.children[index] = childItem;
var childDescendants = _loadItemInfoForItem(anOutlineView, childItem, shouldLoadChildren, itemInfo);
childItemInfo.parent = anItem;
childItemInfo.level = level;
descendants = descendants.concat(childDescendants);
}
}
itemInfo.weight = descendants.length;
if (shouldReloadData)
{
var index = itemInfo.row + 1,
itemsForRows = anOutlineView._itemsForRows;
descendants.unshift(index, weight);
itemsForRows.splice.apply(itemsForRows, descendants);
var count = itemsForRows.length;
for (; index < count; ++index)
if (index > 0)
itemInfosForItems[[itemsForRows[index] UID]].row = index - 1;
var deltaWeight = itemInfo.weight - weight;
if (deltaWeight !== 0)
{
var parent = itemInfo.parent;
while (parent)
{
var parentItemInfo = itemInfosForItems[[parent UID]];
parentItemInfo.weight += deltaWeight;
parent = parentItemInfo.parent;
}
}
}
return descendants;
}
/* @ignore */
- (void)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(int)aRowIndex
@implementation _CPOutlineViewTableViewDataSource : CPObject
{
return [_outlineDataSource outlineView:self objectValueForTableColumn:aTableColumn byItem:_itemsByRow[aRowIndex]];
}
@end
- (id)initWithOutlineView:(CPOutlineView)anOutlineView
{
self = [super init];
if (self)
_outlineView = anOutlineView;
return self;
}
- (CPInteger)numberOfRowsInTableView:(CPTableView)anOutlineView
{
return _outlineView._itemsForRows.length - 1;
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
return [_outlineView._outlineViewDataSource outlineView:_outlineView objectValueForTableColumn:aTableColumn byItem:_outlineView._itemsForRows[aRow + 1]];
}
@end
@implementation _CPOutlineViewHierarchicalView : CPView
{
float _indentationWidth;
CPView _dataView;
}
- (void)updateDataViewFrame
{
var size = [self bounds].size;
[_dataView setFrame:_CGRectMake(_indentationWidth, 0.0, size.width - _indentationWidth, size.height)];
}
- (void)setIndentationWidth:(float)aWidth
{
if (_indentationWidth === aWidth)
return;
_indentationWidth = aWidth;
[self updateDataViewFrame];
}
- (void)setDataView:(CPView)aDataView
{
if (_dataView === aDataView)
return;
[_dataView removeFromSuperview];
_dataView = aDataView;
[self updateDataViewFrame];
[self addSubview:_dataView];
}
- (CPView)dataView
{
return _dataView;
}
- (void)setObjectValue:(id)anObjectValue
{
[_dataView setObjectValue:anObjectValue];
}
- (void)setFrameSize:(CGSize)aSize
{
[super setFrameSize:aSize];
[self updateDataViewFrame];
}
@end
+27 -5
View File
@@ -258,6 +258,14 @@ CPTableViewSolidHorizontalGridLineMask = 1 << 1;
//Loading Data
- (void)reloadDataForRowIndexes:(CPIndexSet)rowIndexes columnIndexes:(CPIndexSet)columnIndexes
{
[self reloadData];
// [_previouslyExposedRows removeIndexes:rowIndexes];
// [_previouslyExposedColumns removeIndexes:columnIndexes];
}
- (void)reloadData
{
if (!_dataSource)
@@ -1256,10 +1264,8 @@ CPTableViewSolidHorizontalGridLineMask = 1 << 1;
dataView = _dataViewsForTableColumns[tableColumnUID][row];
_dataViewsForTableColumns[tableColumnUID][row] = nil;
if (!_cachedDataViews[dataView.identifier])
_cachedDataViews[dataView.identifier] = [dataView];
else
_cachedDataViews[dataView.identifier].push(dataView);
[self _enqueueReusableDataView:dataView];
}
}
}
@@ -1297,7 +1303,7 @@ _cachedDataViews[dataView.identifier].push(dataView);
for (; rowIndex < rowsCount; ++rowIndex)
{
var row = rowArray[rowIndex],
dataView = [tableColumn _newDataViewForRow:row],
dataView = [self _newDataViewForRow:row tableColumn:tableColumn],
rectOfRow = rowRects[row];
if (!rectOfRow)
@@ -1314,6 +1320,22 @@ _cachedDataViews[dataView.identifier].push(dataView);
}
}
- (CPView)_newDataViewForRow:(CPInteger)aRow tableColumn:(CPTableColumn)aTableColumn
{
return [aTableColumn _newDataViewForRow:aRow];
}
- (void)_enqueueReusableDataView:(CPView)aDataView
{
// FIXME: yuck!
var identifier = aDataView.identifier;
if (!_cachedDataViews[identifier])
_cachedDataViews[identifier] = [aDataView];
else
_cachedDataViews[identifier].push(aDataView);
}
- (void)setFrameSize:(CGSize)aSize
{
[super setFrameSize:aSize];