diff options
| author | Joshua Peek <josh@joshpeek.com> | 2012-07-23 15:52:49 -0500 |
|---|---|---|
| committer | Joshua Peek <josh@joshpeek.com> | 2012-07-23 15:52:49 -0500 |
| commit | 7b6caa0f6c7603aabb5e6ed657e24aaddbeafc78 (patch) | |
| tree | ec0522eae75bb04ae73d28ae3f599682a2bfdaeb /samples/Objective-C/TUITableView.m | |
| parent | 314f0e485227915931db361be14d308a210bedea (diff) | |
Rename samples subdirectories
Diffstat (limited to 'samples/Objective-C/TUITableView.m')
| -rw-r--r-- | samples/Objective-C/TUITableView.m | 1248 |
1 files changed, 1248 insertions, 0 deletions
diff --git a/samples/Objective-C/TUITableView.m b/samples/Objective-C/TUITableView.m new file mode 100644 index 0000000..0ec17d7 --- /dev/null +++ b/samples/Objective-C/TUITableView.m @@ -0,0 +1,1248 @@ +/* + 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 |
