mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +00:00
1249 lines
37 KiB
Objective-C
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
|