diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index d82d737c9..3650055a1 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -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: -
-- (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 -- @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 \ No newline at end of file +- (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 diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index bf9b53ab0..65fc07074 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -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];