Files
linguist/samples/Objective-C/TUITableView.m
2012-07-23 15:52:49 -05:00

1249 lines
37 KiB
Objective-C

/*
Copyright 2011 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "TUITableView.h"
#import "TUITableView+Cell.h"
#import "TUITableViewSectionHeader.h"
#import "TUINSView.h"
// header views need to be above the cells at all times
#define HEADER_Z_POSITION 1000
typedef struct {
CGFloat offset; // from beginning of section
CGFloat height;
} TUITableViewRowInfo;
@interface TUITableViewSection : NSObject
{
__unsafe_unretained TUITableView *_tableView; // weak
TUIView *_headerView; // Not reusable (similar to UITableView)
NSInteger sectionIndex;
NSUInteger numberOfRows;
CGFloat sectionHeight;
CGFloat sectionOffset;
TUITableViewRowInfo *rowInfo;
}
@property (strong, readonly) TUIView *headerView;
@property (nonatomic, assign) CGFloat sectionOffset;
@property (readonly) NSInteger sectionIndex;
@end
@implementation TUITableViewSection
@synthesize sectionOffset;
@synthesize sectionIndex;
- (id)initWithNumberOfRows:(NSUInteger)n sectionIndex:(NSInteger)s tableView:(TUITableView *)t
{
if((self = [super init])){
_tableView = t;
sectionIndex = s;
numberOfRows = n;
rowInfo = calloc(n, sizeof(TUITableViewRowInfo));
}
return self;
}
- (void)dealloc
{
if(rowInfo) free(rowInfo);
}
- (NSUInteger)numberOfRows
{
return numberOfRows;
}
- (void)_setupRowHeights
{
sectionHeight = 0.0;
TUIView *header;
if((header = self.headerView) != nil) {
sectionHeight += roundf(header.frame.size.height);
}
for(int i = 0; i < numberOfRows; ++i) {
CGFloat h = roundf([_tableView.delegate tableView:_tableView heightForRowAtIndexPath:[TUIFastIndexPath indexPathForRow:i inSection:sectionIndex]]);
rowInfo[i].offset = sectionHeight;
rowInfo[i].height = h;
sectionHeight += h;
}
}
- (CGFloat)rowHeight:(NSInteger)i
{
if(i >= 0 && i < numberOfRows) {
return rowInfo[i].height;
}
return 0.0;
}
- (CGFloat)sectionRowOffset:(NSInteger)i
{
if(i >= 0 && i < numberOfRows){
return rowInfo[i].offset;
}
return 0.0;
}
- (CGFloat)tableRowOffset:(NSInteger)i
{
return sectionOffset + [self sectionRowOffset:i];
}
- (CGFloat)sectionHeight
{
return sectionHeight;
}
- (CGFloat)headerHeight
{
return (self.headerView != nil) ? self.headerView.frame.size.height : 0;
}
/**
* @brief Obtain the section header view.
*
* The section header view is created lazily via the data source when this
* method is first called.
*
* @return section header view
*/
- (TUIView *)headerView
{
if(_headerView == nil) {
if(_tableView.dataSource != nil && [_tableView.dataSource respondsToSelector:@selector(tableView:headerViewForSection:)]){
_headerView = [_tableView.dataSource tableView:_tableView headerViewForSection:sectionIndex];
_headerView.autoresizingMask = TUIViewAutoresizingFlexibleWidth;
_headerView.layer.zPosition = HEADER_Z_POSITION;
}
}
return _headerView;
}
@end
@interface TUITableView (Private)
- (void)_updateSectionInfo;
- (void)_updateDerepeaterViews;
@end
@implementation TUITableView
@synthesize pullDownView=_pullDownView;
@synthesize headerView=_headerView;
- (id)initWithFrame:(CGRect)frame style:(TUITableViewStyle)style
{
if((self = [super initWithFrame:frame])) {
_style = style;
_reusableTableCells = [[NSMutableDictionary alloc] init];
_visibleSectionHeaders = [[NSMutableIndexSet alloc] init];
_visibleItems = [[NSMutableDictionary alloc] init];
_tableFlags.animateSelectionChanges = 1;
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
return [self initWithFrame:frame style:TUITableViewStylePlain];
}
- (id<TUITableViewDelegate>)delegate
{
return (id<TUITableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<TUITableViewDelegate>)d
{
_tableFlags.delegateTableViewWillDisplayCellForRowAtIndexPath = [d respondsToSelector:@selector(tableView:willDisplayCell:forRowAtIndexPath:)];
[super setDelegate:d]; // must call super
}
- (id<TUITableViewDataSource>)dataSource
{
return _dataSource;
}
- (void)setDataSource:(id<TUITableViewDataSource>)d
{
_dataSource = d;
_tableFlags.dataSourceNumberOfSectionsInTableView = [_dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)];
}
- (BOOL)animateSelectionChanges
{
return _tableFlags.animateSelectionChanges;
}
- (void)setAnimateSelectionChanges:(BOOL)a
{
_tableFlags.animateSelectionChanges = a;
}
- (NSInteger)numberOfSections
{
return [_sectionInfo count];
}
- (NSInteger)numberOfRowsInSection:(NSInteger)section
{
return [[_sectionInfo objectAtIndex:section] numberOfRows];
}
- (CGRect)rectForHeaderOfSection:(NSInteger)section {
if(section >= 0 && section < [_sectionInfo count]){
TUITableViewSection *s = [_sectionInfo objectAtIndex:section];
CGFloat offset = [s sectionOffset];
CGFloat height = [s headerHeight];
CGFloat y = _contentHeight - offset - height;
return CGRectMake(0, y, self.bounds.size.width, height);
}
return CGRectZero;
}
- (CGRect)rectForSection:(NSInteger)section
{
if(section >= 0 && section < [_sectionInfo count]){
TUITableViewSection *s = [_sectionInfo objectAtIndex:section];
CGFloat offset = [s sectionOffset];
CGFloat height = [s sectionHeight];
CGFloat y = _contentHeight - offset - height;
return CGRectMake(0, y, self.bounds.size.width, height);
}
return CGRectZero;
}
- (CGRect)rectForRowAtIndexPath:(TUIFastIndexPath *)indexPath
{
NSInteger section = indexPath.section;
NSInteger row = indexPath.row;
if(section >= 0 && section < [_sectionInfo count]) {
TUITableViewSection *s = [_sectionInfo objectAtIndex:section];
CGFloat offset = [s tableRowOffset:row];
CGFloat height = [s rowHeight:row];
CGFloat y = _contentHeight - offset - height;
return CGRectMake(0, y, self.bounds.size.width, height);
}
return CGRectZero;
}
/**
* @brief Update section info
*
* The previous section info is released and new section info is created.
*/
- (void)_updateSectionInfo {
if(_sectionInfo != nil){
// remove any visible headers, they should be re-added when the table is laid out
for(TUITableViewSection *section in _sectionInfo){
TUIView *headerView;
if((headerView = [section headerView]) != nil){
[headerView removeFromSuperview];
}
}
// clear visible section headers
[_visibleSectionHeaders removeAllIndexes];
// clear the section info array
_sectionInfo = nil;
}
NSInteger numberOfSections = 1;
if(_tableFlags.dataSourceNumberOfSectionsInTableView){
numberOfSections = [_dataSource numberOfSectionsInTableView:self];
}
NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:numberOfSections];
CGFloat offset = [_headerView bounds].size.height - self.contentInset.top*2;
for(int s = 0; s < numberOfSections; ++s) {
TUITableViewSection *section = [[TUITableViewSection alloc] initWithNumberOfRows:[_dataSource tableView:self numberOfRowsInSection:s] sectionIndex:s tableView:self];
[section _setupRowHeights];
section.sectionOffset = offset;
offset += [section sectionHeight];
[sections addObject:section];
}
_contentHeight = offset - self.contentInset.bottom;
_sectionInfo = sections;
}
- (void)_enqueueReusableCell:(TUITableViewCell *)cell
{
NSString *identifier = cell.reuseIdentifier;
if(!identifier)
return;
NSMutableArray *array = [_reusableTableCells objectForKey:identifier];
if(!array) {
array = [[NSMutableArray alloc] init];
[_reusableTableCells setObject:array forKey:identifier];
}
[array addObject:cell];
}
- (TUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
if(!identifier)
return nil;
NSMutableArray *array = [_reusableTableCells objectForKey:identifier];
if(array) {
TUITableViewCell *c = [array lastObject];
if(c) {
[array removeLastObject];
[c prepareForReuse];
return c;
}
}
return nil;
}
/**
* @brief Obtain the header view for the specified section
*
* If the section has no header, nil is returned.
*
* @param section the section
* @return section header
*/
- (TUIView *)headerViewForSection:(NSInteger)section {
if(section >= 0 && section < [_sectionInfo count]){
return [(TUITableViewSection *)[_sectionInfo objectAtIndex:section] headerView];
}else{
return nil;
}
}
- (TUITableViewCell *)cellForRowAtIndexPath:(TUIFastIndexPath *)indexPath // returns nil if cell is not visible or index path is out of range
{
return [_visibleItems objectForKey:indexPath];
}
- (NSArray *)visibleCells
{
return [_visibleItems allValues];
}
static NSInteger SortCells(TUITableViewCell *a, TUITableViewCell *b, void *ctx)
{
if(a.frame.origin.y > b.frame.origin.y)
return NSOrderedAscending;
return NSOrderedDescending;
}
- (NSArray *)sortedVisibleCells
{
NSArray *v = [self visibleCells];
return [v sortedArrayUsingComparator:(NSComparator)^NSComparisonResult(TUITableViewCell *a, TUITableViewCell *b) {
if(a.frame.origin.y > b.frame.origin.y)
return NSOrderedAscending;
return NSOrderedDescending;
}];
}
#define INDEX_PATHS_FOR_VISIBLE_ROWS [_visibleItems allKeys]
- (NSArray *)indexPathsForVisibleRows
{
return INDEX_PATHS_FOR_VISIBLE_ROWS;
}
- (TUIFastIndexPath *)indexPathForCell:(TUITableViewCell *)c
{
for(TUIFastIndexPath *i in _visibleItems) {
TUITableViewCell *cell = [_visibleItems objectForKey:i];
if(cell == c)
return i;
}
return nil;
}
/**
* @brief Obtain the indexes of sections which intersect @p rect.
*
* @param rect the rect
* @return intersecting sections
*/
- (NSIndexSet *)indexesOfSectionsInRect:(CGRect)rect
{
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init];
for(int i = 0; i < [_sectionInfo count]; i++) {
if(CGRectIntersectsRect([self rectForSection:i], rect)){
[indexes addIndex:i];
}
}
return indexes;
}
/**
* @brief Obtain the indexes of sections whose header views intersect @p rect.
*
* @param rect the rect
* @return intersecting sections
*/
- (NSIndexSet *)indexesOfSectionHeadersInRect:(CGRect)rect
{
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init];
for(int i = 0; i < [_sectionInfo count]; i++) {
if(CGRectIntersectsRect([self rectForHeaderOfSection:i], rect)){
[indexes addIndex:i];
}
}
return indexes;
}
- (NSArray *)indexPathsForRowsInRect:(CGRect)rect
{
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:50];
NSInteger sectionIndex = 0;
for(TUITableViewSection *section in _sectionInfo) {
NSInteger numberOfRows = [section numberOfRows];
for(NSInteger row = 0; row < numberOfRows; ++row) {
TUIFastIndexPath *indexPath = [TUIFastIndexPath indexPathForRow:row inSection:sectionIndex];
CGRect cellRect = [self rectForRowAtIndexPath:indexPath];
if(CGRectIntersectsRect(cellRect, rect)) {
[indexPaths addObject:indexPath];
} else {
// not visible
}
}
++sectionIndex;
}
return indexPaths;
}
/**
* @brief Obtain the index path of the row at the specified point
*
* If the point is not valid or no row exists at that point, nil is
* returned.
*
* @param point location in the table view
* @return index path of the row at @p point
*/
- (TUIFastIndexPath *)indexPathForRowAtPoint:(CGPoint)point {
NSInteger sectionIndex = 0;
for(TUITableViewSection *section in _sectionInfo){
for(NSInteger row = 0; row < [section numberOfRows]; row++){
TUIFastIndexPath *indexPath = [TUIFastIndexPath indexPathForRow:row inSection:sectionIndex];
CGRect cellRect = [self rectForRowAtIndexPath:indexPath];
if(CGRectContainsPoint(cellRect, point)){
return indexPath;
}
}
++sectionIndex;
}
return nil;
}
/**
* @brief Obtain the index path of the row at the specified y-coordinate offset
*
* Unlike #indexPathForRowAtPoint:, this method does not consider the x-coordinate.
* If the offset is not valid or no row exists at that offset, nil is returned.
*
* @param offset y-coordinate offset in the table view
* @return index path of the row at @p offset
*/
- (TUIFastIndexPath *)indexPathForRowAtVerticalOffset:(CGFloat)offset {
NSInteger sectionIndex = 0;
for(TUITableViewSection *section in _sectionInfo){
for(NSInteger row = 0; row < [section numberOfRows]; row++){
TUIFastIndexPath *indexPath = [TUIFastIndexPath indexPathForRow:row inSection:sectionIndex];
CGRect cellRect = [self rectForRowAtIndexPath:indexPath];
if(offset >= cellRect.origin.y && offset <= (cellRect.origin.y + cellRect.size.height)){
return indexPath;
}
}
++sectionIndex;
}
return nil;
}
/**
* @brief Obtain the index of a section whose header is at the specified point
*
* If the point is not valid or no header exists at that point, a negative value
* is returned.
*
* @param point location in the table view
* @return index of the section whose header is at @p point
*/
- (NSInteger)indexOfSectionWithHeaderAtPoint:(CGPoint)point {
NSInteger sectionIndex = 0;
for(TUITableViewSection *section in _sectionInfo){
TUIView *headerView;
if((headerView = section.headerView) != nil){
CGFloat offset = [section sectionOffset];
CGFloat height = [section headerHeight];
CGFloat y = _contentHeight - offset - height;
CGRect frame = CGRectMake(0, y, self.bounds.size.width, height);
if(point.y > frame.origin.y && point.y < (frame.origin.y + frame.size.height)){
return sectionIndex;
}
}
sectionIndex++;
}
return -1;
}
/**
* @brief Obtain the index of a section whose header is at the specified y-coordinate offset
*
* Unlike #indexOfSectionWithHeaderAtPoint:, this method does not consider the x-coordinate.
* If the offset is not valid or no header exists at that offset, a negative value
* is returned.
*
* @param offset y-coordinate offset in the table view
* @return index of the section whose header is at @p offset
*/
- (NSInteger)indexOfSectionWithHeaderAtVerticalOffset:(CGFloat)offset {
NSInteger sectionIndex = 0;
for(TUITableViewSection *section in _sectionInfo){
TUIView *headerView;
if((headerView = section.headerView) != nil){
CGFloat offset = [section sectionOffset];
CGFloat height = [section headerHeight];
CGFloat y = _contentHeight - offset - height;
CGRect frame = CGRectMake(0, y, self.bounds.size.width, height);
if(offset >= frame.origin.y && offset <= (frame.origin.y + frame.size.height)){
return sectionIndex;
}
}
sectionIndex++;
}
return -1;
}
/**
* @brief Enumerate index paths
* @see #enumerateIndexPathsFromIndexPath:toIndexPath:withOptions:usingBlock:
*/
- (void)enumerateIndexPathsUsingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
[self enumerateIndexPathsFromIndexPath:nil toIndexPath:nil withOptions:0 usingBlock:block];
}
/**
* @brief Enumerate index paths
* @see #enumerateIndexPathsFromIndexPath:toIndexPath:withOptions:usingBlock:
*/
- (void)enumerateIndexPathsWithOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
[self enumerateIndexPathsFromIndexPath:nil toIndexPath:nil withOptions:options usingBlock:block];
}
/**
* @brief Enumerate index paths
*
* The provided block is repeatedly invoked with each valid index path between
* the specified bounds. Both bounding index paths are inclusive.
*
* @param fromIndexPath the index path to begin enumerating at or nil to begin at the first index path
* @param toIndexPath the index path to stop enumerating at or nil to stop at the last index path
* @param options enumeration options (not currently supported; pass 0)
* @param block the block to enumerate with
*/
- (void)enumerateIndexPathsFromIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath withOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
NSInteger sectionLowerBound = (fromIndexPath != nil) ? fromIndexPath.section : 0;
NSInteger sectionUpperBound = (toIndexPath != nil) ? toIndexPath.section : [self numberOfSections] - 1;
NSInteger rowLowerBound = (fromIndexPath != nil) ? fromIndexPath.row : 0;
NSInteger rowUpperBound = (toIndexPath != nil) ? toIndexPath.row : -1;
NSInteger irow = rowLowerBound; // start at the lower bound row for the first iteration...
for(NSInteger i = sectionLowerBound; i < [self numberOfSections] && i <= sectionUpperBound /* inclusive */; i++){
NSInteger rowCount = [self numberOfRowsInSection:i];
for(NSInteger j = irow; j < rowCount && j <= ((rowUpperBound < 0 || i < sectionUpperBound) ? rowCount - 1 : rowUpperBound) /* inclusive */; j++){
BOOL stop = FALSE;
block([TUIFastIndexPath indexPathForRow:j inSection:i], &stop);
if(stop) return;
}
irow = 0; // ...then use zero for subsequent iterations
}
}
- (TUIFastIndexPath *)_topVisibleIndexPath
{
TUIFastIndexPath *topVisibleIndex = nil;
NSArray *v = [INDEX_PATHS_FOR_VISIBLE_ROWS sortedArrayUsingSelector:@selector(compare:)];
if([v count])
topVisibleIndex = [v objectAtIndex:0];
return topVisibleIndex;
}
- (void)setFrame:(CGRect)f
{
_tableFlags.forceSaveScrollPosition = 1;
[super setFrame:f];
}
- (void)setContentOffset:(CGPoint)p
{
_tableFlags.didFirstLayout = 1; // prevent the auto-scroll-to-top during the first layout
[super setContentOffset:p];
// if we're currently dragging we need to update the drag operation since the content under
// the mouse has moved; we just call update again with the last mouse location
if([self __isDraggingCell]){
[self __updateDraggingCell:_dragToReorderCell offset:_currentDragToReorderMouseOffset location:_currentDragToReorderLocation];
}
}
- (void)setPullDownView:(TUIView *)p
{
[_pullDownView removeFromSuperview];
_pullDownView = p;
[self addSubview:_pullDownView];
_pullDownView.hidden = YES;
}
- (void)setHeaderView:(TUIView *)h
{
[_headerView removeFromSuperview];
_headerView = h;
[self addSubview:_headerView];
_headerView.hidden = YES;
}
- (BOOL)_preLayoutCells
{
CGRect bounds = self.bounds;
if(!_sectionInfo || !CGSizeEqualToSize(bounds.size, _lastSize)) {
// save scroll position
CGFloat previousOffset = 0.0f;
TUIFastIndexPath *savedIndexPath = nil;
CGFloat relativeOffset = 0.0;
if(_tableFlags.maintainContentOffsetAfterReload) {
previousOffset = self.contentSize.height + self.contentOffset.y;
} else {
if(_tableFlags.forceSaveScrollPosition || [self.nsView inLiveResize]) {
_tableFlags.forceSaveScrollPosition = 0;
NSArray *a = [INDEX_PATHS_FOR_VISIBLE_ROWS sortedArrayUsingSelector:@selector(compare:)];
if([a count]) {
savedIndexPath = [a objectAtIndex:0];
CGRect v = [self visibleRect];
CGRect r = [self rectForRowAtIndexPath:savedIndexPath];
relativeOffset = ((v.origin.y + v.size.height) - (r.origin.y + r.size.height));
relativeOffset += (_lastSize.height - bounds.size.height);
}
} else if(_keepVisibleIndexPathForReload) {
savedIndexPath = _keepVisibleIndexPathForReload;
relativeOffset = _relativeOffsetForReload;
_keepVisibleIndexPathForReload = nil;
}
}
[self _updateSectionInfo]; // clean up any previous section info and recreate it
self.contentSize = CGSizeMake(self.bounds.size.width, _contentHeight);
_lastSize = bounds.size;
if(!_tableFlags.didFirstLayout) {
_tableFlags.didFirstLayout = 1;
[self scrollToTopAnimated:NO];
}
// restore scroll position
if(_tableFlags.maintainContentOffsetAfterReload) {
CGFloat newOffset = previousOffset - self.contentSize.height;
self.contentOffset = CGPointMake(self.contentOffset.x, newOffset);
} else {
if(savedIndexPath) {
CGRect v = [self visibleRect];
CGRect r = [self rectForRowAtIndexPath:savedIndexPath];
r.origin.y -= (v.size.height - r.size.height);
r.size.height += (v.size.height - r.size.height);
r.origin.y += relativeOffset;
[self scrollRectToVisible:r animated:NO];
}
}
return YES; // needs visible cells to be redisplayed
}
return NO; // just need to do the recycling
}
/**
* @brief Layout header views for sections which have one.
*/
- (void)_layoutSectionHeaders:(BOOL)visibleHeadersNeedRelayout
{
CGRect visible = [self visibleRect];
NSIndexSet *oldIndexes = _visibleSectionHeaders;
NSIndexSet *newIndexes = [self indexesOfSectionsInRect:visible];
NSMutableIndexSet *toRemove = [oldIndexes mutableCopy];
[toRemove removeIndexes:newIndexes];
NSMutableIndexSet *toAdd = [newIndexes mutableCopy];
[toAdd removeIndexes:oldIndexes];
// update the placement of all visible headers
__block TUIView *pinnedHeader = nil;
[newIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) {
if(index < [_sectionInfo count]) {
TUITableViewSection *section = [_sectionInfo objectAtIndex:index];
if(section.headerView != nil) {
CGRect headerFrame = [self rectForHeaderOfSection:index];
if(_style == TUITableViewStyleGrouped) {
// check if this header needs to be pinned
if(CGRectGetMaxY(headerFrame) > CGRectGetMaxY(visible)) {
headerFrame.origin.y = CGRectGetMaxY(visible) - headerFrame.size.height;
pinnedHeader = section.headerView;
// if the header is a TUITableViewSectionHeader notify it of it's pinned state
if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){
((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = TRUE;
}
}else if((pinnedHeader != nil) && (CGRectGetMaxY(headerFrame) > pinnedHeader.frame.origin.y)) {
// this header is intersecting with the pinned header, so we push the pinned header upwards.
CGRect pinnedHeaderFrame = pinnedHeader.frame;
pinnedHeaderFrame.origin.y = CGRectGetMaxY(headerFrame);
pinnedHeader.frame = pinnedHeaderFrame;
// if the header is a TUITableViewSectionHeader notify it of it's pinned state
if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){
((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = FALSE;
}
}else{
// if the header is a TUITableViewSectionHeader notify it of it's pinned state
if([section.headerView isKindOfClass:[TUITableViewSectionHeader class]]){
((TUITableViewSectionHeader *)section.headerView).pinnedToViewport = FALSE;
}
}
}
section.headerView.frame = headerFrame;
[section.headerView setNeedsLayout];
if(section.headerView.superview == nil){
[self addSubview:section.headerView];
}
}
}
[_visibleSectionHeaders addIndex:index];
}];
// remove offscreen headers
[toRemove enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) {
if(index < [_sectionInfo count]) {
TUITableViewSection *section = [_sectionInfo objectAtIndex:index];
if(section.headerView != nil) {
[section.headerView removeFromSuperview];
}
}
[_visibleSectionHeaders removeIndex:index];
}];
}
- (void)_layoutCells:(BOOL)visibleCellsNeedRelayout
{
if(visibleCellsNeedRelayout) {
// update remaining visible cells if needed
for(TUIFastIndexPath *i in _visibleItems) {
TUITableViewCell *cell = [_visibleItems objectForKey:i];
cell.frame = [self rectForRowAtIndexPath:i];
cell.layer.zPosition = 0;
[cell setNeedsLayout];
}
}
CGRect visible = [self visibleRect];
// Example:
// old: 0 1 2 3 4 5 6 7
// new: 2 3 4 5 6 7 8 9
// to remove: 0 1
// to add: 8 9
NSArray *oldVisibleIndexPaths = INDEX_PATHS_FOR_VISIBLE_ROWS;
NSArray *newVisibleIndexPaths = [self indexPathsForRowsInRect:visible];
NSMutableArray *indexPathsToRemove = [oldVisibleIndexPaths mutableCopy];
[indexPathsToRemove removeObjectsInArray:newVisibleIndexPaths];
NSMutableArray *indexPathsToAdd = [newVisibleIndexPaths mutableCopy];
[indexPathsToAdd removeObjectsInArray:oldVisibleIndexPaths];
// remove offscreen cells
for(TUIFastIndexPath *i in indexPathsToRemove) {
TUITableViewCell *cell = [self cellForRowAtIndexPath:i];
// don't reuse the dragged cell
if(_dragToReorderCell == nil || ![cell isEqual:_dragToReorderCell]){
[self _enqueueReusableCell:cell];
[cell removeFromSuperview];
[_visibleItems removeObjectForKey:i];
}
}
// add new cells
for(TUIFastIndexPath *i in indexPathsToAdd) {
if([_visibleItems objectForKey:i]) {
NSLog(@"!!! Warning: already have a cell in place for index path %@\n\n\n", i);
} else {
TUITableViewCell *cell = [_dataSource tableView:self cellForRowAtIndexPath:i];
[self.nsView invalidateHoverForView:cell];
cell.frame = [self rectForRowAtIndexPath:i];
cell.layer.zPosition = 0;
[cell setNeedsLayout];
[cell prepareForDisplay];
if([i isEqual:_selectedIndexPath]) {
[cell setSelected:YES animated:NO];
} else {
[cell setSelected:NO animated:NO];
}
if(_tableFlags.delegateTableViewWillDisplayCellForRowAtIndexPath) {
[_delegate tableView:self willDisplayCell:cell forRowAtIndexPath:i];
}
[self addSubview:cell];
if([_indexPathShouldBeFirstResponder isEqual:i]) {
// only make cells first responder if they accept it
if([cell acceptsFirstResponder]){
[self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell withFutureRequestToken:_futureMakeFirstResponderToken];
}
_indexPathShouldBeFirstResponder = nil;
}
[_visibleItems setObject:cell forKey:i];
}
}
// if we have a dragged cell, make sure it's on top of the newly added cells
if([indexPathsToAdd count] > 0 && _dragToReorderCell != nil){
[[_dragToReorderCell superview] bringSubviewToFront:_dragToReorderCell];
}
if(_headerView) {
CGSize s = self.contentSize;
CGRect headerViewRect = CGRectMake(0, s.height - _headerView.frame.size.height, visible.size.width, _headerView.frame.size.height);
if(CGRectIntersectsRect(headerViewRect, visible)) {
_headerView.frame = headerViewRect;
[_headerView setNeedsLayout];
if(_headerView.hidden) {
// show
_headerView.hidden = NO;
}
} else {
if(!_headerView.hidden) {
_headerView.hidden = YES;
}
}
}
if(_pullDownView) {
CGSize s = self.contentSize;
CGRect pullDownRect = CGRectMake(0, s.height, visible.size.width, _pullDownView.frame.size.height);
if([self pullDownViewIsVisible]) {
if(_pullDownView.hidden) {
// show
_pullDownView.frame = pullDownRect;
_pullDownView.hidden = NO;
}
} else {
if(!_pullDownView.hidden) {
_pullDownView.hidden = YES;
}
}
}
}
- (BOOL)pullDownViewIsVisible
{
if(_pullDownView) {
CGSize s = self.contentSize;
CGRect visible = [self visibleRect];
CGRect pullDownRect = CGRectMake(0, s.height, visible.size.width, _pullDownView.frame.size.height);
return CGRectIntersectsRect(pullDownRect, visible);
}
return NO;
}
- (void)reloadDataMaintainingVisibleIndexPath:(TUIFastIndexPath *)indexPath relativeOffset:(CGFloat)relativeOffset
{
_keepVisibleIndexPathForReload = indexPath;
_relativeOffsetForReload = relativeOffset;
[self reloadData];
}
- (void)reloadData
{
// notify our delegate we're about to reload the table
if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableViewWillReloadData:)]){
[self.delegate tableViewWillReloadData:self];
}
_selectedIndexPath = nil;
// need to recycle all visible cells, have them be regenerated on layoutSubviews
// because the same cells might have different content
for(TUIFastIndexPath *i in _visibleItems) {
TUITableViewCell *cell = [_visibleItems objectForKey:i];
[self _enqueueReusableCell:cell];
[cell removeFromSuperview];
}
// if we have a dragged cell, clear it
_dragToReorderCell = nil;
// clear visible cells
[_visibleItems removeAllObjects];
// remove any visible headers, they should be re-added when the table is laid out
for(TUITableViewSection *section in _sectionInfo){
TUIView *headerView;
if((headerView = [section headerView]) != nil){
[headerView removeFromSuperview];
}
}
// clear visible section headers
[_visibleSectionHeaders removeAllIndexes];
_sectionInfo = nil; // will be regenerated on next layout
[self layoutSubviews];
// notify our delegate the table view has been reloaded
if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableViewDidReloadData:)]){
[self.delegate tableViewDidReloadData:self];
}
}
- (void)layoutSubviews
{
if(!_tableFlags.layoutSubviewsReentrancyGuard) {
_tableFlags.layoutSubviewsReentrancyGuard = 1;
[TUIView setAnimationsEnabled:NO block:^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
BOOL visibleCellsNeedRelayout = [self _preLayoutCells];
[super layoutSubviews]; // this will munge with the contentOffset
[self _layoutSectionHeaders:visibleCellsNeedRelayout];
[self _layoutCells:visibleCellsNeedRelayout];
if(_tableFlags.derepeaterEnabled)
[self _updateDerepeaterViews];
[CATransaction commit];
}];
_tableFlags.layoutSubviewsReentrancyGuard = 0;
} else {
// NSLog(@"trying to nest...");
}
}
- (void)reloadLayout
{
_sectionInfo = nil; // will be regenerated on next layout
[self _preLayoutCells];
[super layoutSubviews]; // this will munge with the contentOffset
[self _layoutSectionHeaders:YES];
[self _layoutCells:YES];
}
- (void)scrollToRowAtIndexPath:(TUIFastIndexPath *)indexPath atScrollPosition:(TUITableViewScrollPosition)scrollPosition animated:(BOOL)animated
{
CGRect v = [self visibleRect];
CGRect r = [self rectForRowAtIndexPath:indexPath];
// when the target index path section has a header view, add its height to
// the height of our row to prevent the selected row from being overlapped
// by the pinned header
TUIView *headerView;
if((headerView = [self headerViewForSection:indexPath.section]) != nil){
CGRect headerFrame = [self rectForHeaderOfSection:indexPath.section];
r.size.height += headerFrame.size.height;
}
switch(scrollPosition) {
case TUITableViewScrollPositionNone:
// do nothing
break;
case TUITableViewScrollPositionTop:
r.origin.y -= (v.size.height - r.size.height);
r.size.height += (v.size.height - r.size.height);
[self scrollRectToVisible:r animated:animated];
break;
case TUITableViewScrollPositionToVisible:
default:
[self scrollRectToVisible:r animated:animated];
break;
}
}
- (TUIFastIndexPath *)indexPathForSelectedRow
{
return _selectedIndexPath;
}
- (TUIFastIndexPath *)indexPathForFirstRow
{
return [TUIFastIndexPath indexPathForRow:0 inSection:0];
}
- (TUIFastIndexPath *)indexPathForLastRow
{
NSInteger sec = [self numberOfSections] - 1;
NSInteger row = [self numberOfRowsInSection:sec] - 1;
return [TUIFastIndexPath indexPathForRow:row inSection:sec];
}
- (void)_makeRowAtIndexPathFirstResponder:(TUIFastIndexPath *)indexPath
{
TUITableViewCell *cell = [self cellForRowAtIndexPath:indexPath];
// only cells that accept first responder should be made first responder
if(cell && [cell acceptsFirstResponder]) {
[self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell];
} else {
_indexPathShouldBeFirstResponder = indexPath;
_futureMakeFirstResponderToken = [self.nsWindow futureMakeFirstResponderRequestToken];
}
}
- (void)selectRowAtIndexPath:(TUIFastIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(TUITableViewScrollPosition)scrollPosition
{
TUIFastIndexPath *oldIndexPath = [self indexPathForSelectedRow];
// if([indexPath isEqual:oldIndexPath]) {
// // just scroll to visible
// } else {
[self deselectRowAtIndexPath:[self indexPathForSelectedRow] animated:animated];
TUITableViewCell *cell = [self cellForRowAtIndexPath:indexPath]; // may be nil
[cell setSelected:YES animated:animated];
// should already be nil
_selectedIndexPath = indexPath;
[cell setNeedsDisplay];
// only notify when the selection actually changes
if([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){
[self.delegate tableView:self didSelectRowAtIndexPath:indexPath];
}
// }
NSResponder *firstResponder = [self.nsWindow firstResponder];
if(firstResponder == self || firstResponder == [self cellForRowAtIndexPath:oldIndexPath]) {
// only make cell first responder if the table view already is first responder
[self _makeRowAtIndexPathFirstResponder:indexPath];
}
[self scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];
}
- (void)deselectRowAtIndexPath:(TUIFastIndexPath *)indexPath animated:(BOOL)animated
{
if([indexPath isEqual:_selectedIndexPath]) {
TUITableViewCell *cell = [self cellForRowAtIndexPath:indexPath]; // may be nil
[cell setSelected:NO animated:animated];
_selectedIndexPath = nil;
[cell setNeedsDisplay];
// only notify when the selection actually changes
if([self.delegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)]){
[self.delegate tableView:self didDeselectRowAtIndexPath:indexPath];
}
}
}
- (TUIFastIndexPath *)indexPathForFirstVisibleRow
{
TUIFastIndexPath *firstIndexPath = nil;
for(TUIFastIndexPath *indexPath in _visibleItems) {
if(firstIndexPath == nil || [indexPath compare:firstIndexPath] == NSOrderedAscending) {
firstIndexPath = indexPath;
}
}
return firstIndexPath;
}
- (TUIFastIndexPath *)indexPathForLastVisibleRow
{
TUIFastIndexPath *lastIndexPath = nil;
for(TUIFastIndexPath *indexPath in _visibleItems) {
if(lastIndexPath == nil || [indexPath compare:lastIndexPath] == NSOrderedDescending) {
lastIndexPath = indexPath;
}
}
return lastIndexPath;
}
- (BOOL)performKeyAction:(NSEvent *)event
{
// no selection or selected cell not visible and this is not repeative key press
BOOL noCurrentSelection = (_selectedIndexPath == nil || ([self cellForRowAtIndexPath:_selectedIndexPath] == nil && ![event isARepeat]));;
typedef TUIFastIndexPath * (^TUITableViewCalculateNextIndexPathBlock)(TUIFastIndexPath *lastIndexPath);
void (^selectValidIndexPath)(TUIFastIndexPath *startForNoSelection, TUITableViewCalculateNextIndexPathBlock calculateNextIndexPath) = ^(TUIFastIndexPath *startForNoSelection, TUITableViewCalculateNextIndexPathBlock calculateNextIndexPath) {
NSParameterAssert(calculateNextIndexPath != nil);
BOOL foundValidNextRow = NO;
TUIFastIndexPath *lastIndexPath = _selectedIndexPath;
while(!foundValidNextRow) {
TUIFastIndexPath *newIndexPath;
if(noCurrentSelection && lastIndexPath == nil) {
newIndexPath = startForNoSelection;
} else {
newIndexPath = calculateNextIndexPath(lastIndexPath);
}
if(![_delegate respondsToSelector:@selector(tableView:shouldSelectRowAtIndexPath:forEvent:)] || [_delegate tableView:self shouldSelectRowAtIndexPath:newIndexPath forEvent:event]){
[self selectRowAtIndexPath:newIndexPath animated:self.animateSelectionChanges scrollPosition:TUITableViewScrollPositionToVisible];
foundValidNextRow = YES;
}
if([lastIndexPath isEqual:newIndexPath]) foundValidNextRow = YES;
lastIndexPath = newIndexPath;
}
};
switch([[event charactersIgnoringModifiers] characterAtIndex:0]) {
case NSUpArrowFunctionKey: {
selectValidIndexPath([self indexPathForLastVisibleRow], ^(TUIFastIndexPath *lastIndexPath) {
NSUInteger section = lastIndexPath.section;
NSUInteger row = lastIndexPath.row;
if(row > 0) {
row--;
} else {
while(section > 0) {
section--;
NSUInteger rowsInSection = [self numberOfRowsInSection:section];
if(rowsInSection > 0) {
row = rowsInSection - 1;
break;
}
}
}
return [TUIFastIndexPath indexPathForRow:row inSection:section];
});
return YES;
}
case NSDownArrowFunctionKey: {
selectValidIndexPath([self indexPathForFirstVisibleRow], ^(TUIFastIndexPath *lastIndexPath) {
NSUInteger section = lastIndexPath.section;
NSUInteger row = lastIndexPath.row;
NSUInteger rowsInSection = [self numberOfRowsInSection:section];
if(row + 1 < rowsInSection) {
row++;
} else {
NSUInteger sections = [self numberOfSections];
while(section + 1 < sections) {
section++;
NSUInteger rowsInSection = [self numberOfRowsInSection:section];
if(rowsInSection > 0) {
row = 0;
break;
}
}
}
return [TUIFastIndexPath indexPathForRow:row inSection:section];
});
return YES;
}
}
return [super performKeyAction:event];
}
- (BOOL)maintainContentOffsetAfterReload
{
return _tableFlags.maintainContentOffsetAfterReload;
}
- (void)setMaintainContentOffsetAfterReload:(BOOL)newValue
{
_tableFlags.maintainContentOffsetAfterReload = newValue;
}
@end
@implementation NSIndexPath (TUITableView)
+ (NSIndexPath *)indexPathForRow:(NSUInteger)row inSection:(NSUInteger)section
{
NSUInteger i[] = {section, row};
return [NSIndexPath indexPathWithIndexes:i length:2];
}
- (NSUInteger)section
{
return [self indexAtPosition:0];
}
- (NSUInteger)row
{
return [self indexAtPosition:1];
}
@end