Files
twui/lib/UIKit/TUITableView.m
T

967 lines
27 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 "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
{
TUITableView *_tableView; // weak
TUIView *_headerView; // Not reusable (similar to UITableView)
NSInteger sectionIndex;
NSUInteger numberOfRows;
CGFloat sectionHeight;
CGFloat sectionOffset;
TUITableViewRowInfo *rowInfo;
}
@property (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);
[_headerView release];
[super dealloc];
}
- (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] retain];
_headerView.autoresizingMask = TUIViewAutoresizingFlexibleWidth;
_headerView.layer.zPosition = HEADER_Z_POSITION;
}
}
return _headerView;
}
@end
@interface TUITableView (Private)
- (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];
}
- (void)dealloc
{
[_sectionInfo release];
[_visibleSectionHeaders release];
[_visibleItems release];
[_reusableTableCells release];
[_selectedIndexPath release];
[_indexPathShouldBeFirstResponder release];
[_keepVisibleIndexPathForReload release];
[_pullDownView release];
[_headerView release];
[super dealloc];
}
- (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)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)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)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;
}
- (NSArray *)_freshSectionInfo
{
NSInteger numberOfSections = 1;
if(_tableFlags.dataSourceNumberOfSectionsInTableView){
numberOfSections = [_dataSource numberOfSectionsInTableView:self];
}
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:numberOfSections];
int s;
CGFloat offset = [_headerView bounds].size.height - self.contentInset.top;
for(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];
[section release];
}
_contentHeight = offset - self.contentInset.bottom;
return 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 release];
}
[array addObject:cell];
}
- (TUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
if(!identifier)
return nil;
NSMutableArray *array = [_reusableTableCells objectForKey:identifier];
if(array) {
TUITableViewCell *c = [array lastObject];
if(c) {
[c retain];
[array removeLastObject];
[c prepareForReuse];
return [c autorelease];
}
}
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 autorelease];
}
/**
* @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 autorelease];
}
- (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;
}
- (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];
}
- (void)setPullDownView:(TUIView *)p
{
[_pullDownView removeFromSuperview];
[p retain];
[_pullDownView release];
_pullDownView = p;
[self addSubview:_pullDownView];
_pullDownView.hidden = YES;
}
- (void)setHeaderView:(TUIView *)h
{
[_headerView removeFromSuperview];
[h retain];
[_headerView release];
_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] retain];
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 retain];
relativeOffset = _relativeOffsetForReload;
[_keepVisibleIndexPathForReload release];
_keepVisibleIndexPathForReload = nil;
}
}
[_sectionInfo release];
_sectionInfo = [[self _freshSectionInfo] retain]; // calculates new contentHeight here
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];
[savedIndexPath release];
}
}
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] autorelease];
[toRemove removeIndexes:newIndexes];
NSMutableIndexSet *toAdd = [[newIndexes mutableCopy] autorelease];
[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];
// check if this header needs to be pinned
if(CGRectGetMaxY(headerFrame) > CGRectGetMaxY(visible)) {
headerFrame.origin.y = CGRectGetMaxY(visible) - headerFrame.size.height;
pinnedHeader = section.headerView;
}
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;
}
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 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] autorelease];
[indexPathsToRemove removeObjectsInArray:newVisibleIndexPaths];
NSMutableArray *indexPathsToAdd = [[newVisibleIndexPaths mutableCopy] autorelease];
[indexPathsToAdd removeObjectsInArray:oldVisibleIndexPaths];
// remove offscreen cells
for(TUIFastIndexPath *i in indexPathsToRemove) {
TUITableViewCell *cell = [self cellForRowAtIndexPath:i];
[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 setNeedsLayout];
[cell prepareForDisplay];
if([i isEqual:_selectedIndexPath]) {
[cell setSelected:YES animated:NO];
} else {
[cell setSelected:NO animated:NO];
}
[self addSubview:cell];
if([_indexPathShouldBeFirstResponder isEqual:i]) {
[self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell withFutureRequestToken:_futureMakeFirstResponderToken];
[_indexPathShouldBeFirstResponder release];
_indexPathShouldBeFirstResponder = nil;
}
[_visibleItems setObject:cell forKey:i];
}
}
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;
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 release];
_keepVisibleIndexPathForReload = [indexPath retain];
_relativeOffsetForReload = relativeOffset;
[self reloadData];
}
- (void)reloadData
{
// 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];
}
[_visibleItems removeAllObjects];
[_sectionInfo release];
_sectionInfo = nil; // will be regenerated on next layout
[self layoutSubviews];
}
- (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)scrollToRowAtIndexPath:(TUIFastIndexPath *)indexPath atScrollPosition:(TUITableViewScrollPosition)scrollPosition animated:(BOOL)animated
{
CGRect v = [self visibleRect];
CGRect r = [self rectForRowAtIndexPath:indexPath];
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];
if(cell) {
[self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell];
} else {
[_indexPathShouldBeFirstResponder release];
_indexPathShouldBeFirstResponder = [indexPath retain];
_futureMakeFirstResponderToken = [self.nsWindow futureMakeFirstResponderRequestToken];
}
}
- (void)selectRowAtIndexPath:(TUIFastIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(TUITableViewScrollPosition)scrollPosition
{
if([indexPath isEqual:[self indexPathForSelectedRow]]) {
// just scroll to visible
} else {
[self deselectRowAtIndexPath:[self indexPathForSelectedRow] animated:animated];
TUITableViewCell *cell = [self cellForRowAtIndexPath:indexPath]; // may be nil
[cell setSelected:YES animated:animated];
[_selectedIndexPath release]; // should already be nil
_selectedIndexPath = [indexPath retain];
[cell setNeedsDisplay];
// only notify when the selection actually changes
if([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){
[self.delegate tableView:self didSelectRowAtIndexPath:indexPath];
}
}
[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 release];
_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]));;
switch([[event charactersIgnoringModifiers] characterAtIndex:0]) {
case NSUpArrowFunctionKey: {
TUIFastIndexPath *newIndexPath;
if(noCurrentSelection) {
newIndexPath = [self indexPathForLastVisibleRow];
} else {
NSUInteger section = _selectedIndexPath.section;
NSUInteger row = _selectedIndexPath.row;
if(row > 0) {
row--;
} else {
while(section > 0) {
section--;
NSUInteger rowsInSection = [self numberOfRowsInSection:section];
if(rowsInSection > 0) {
row = rowsInSection - 1;
break;
}
}
}
newIndexPath = [TUIFastIndexPath indexPathForRow:row inSection:section];
}
if(![_delegate respondsToSelector:@selector(tableView:shouldSelectRowAtIndexPath:forEvent:)] || [_delegate tableView:self shouldSelectRowAtIndexPath:newIndexPath forEvent:event]){
[self selectRowAtIndexPath:newIndexPath animated:self.animateSelectionChanges scrollPosition:TUITableViewScrollPositionToVisible];
}
return YES;
}
case NSDownArrowFunctionKey: {
TUIFastIndexPath *newIndexPath;
if(noCurrentSelection) {
newIndexPath = [self indexPathForFirstVisibleRow];
} else {
NSUInteger section = _selectedIndexPath.section;
NSUInteger row = _selectedIndexPath.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;
}
}
}
newIndexPath = [TUIFastIndexPath indexPathForRow:row inSection:section];
}
if(![_delegate respondsToSelector:@selector(tableView:shouldSelectRowAtIndexPath:forEvent:)] || [_delegate tableView:self shouldSelectRowAtIndexPath:newIndexPath forEvent:event]){
[self selectRowAtIndexPath:newIndexPath animated:self.animateSelectionChanges scrollPosition:TUITableViewScrollPositionToVisible];
}
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