apps_grid_controller.mm revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ui/app_list/cocoa/apps_grid_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "ui/app_list/app_list_item_model.h" 9#include "ui/app_list/app_list_model.h" 10#include "ui/app_list/app_list_model_observer.h" 11#include "ui/app_list/app_list_view_delegate.h" 12#import "ui/app_list/cocoa/apps_collection_view_drag_manager.h" 13#import "ui/app_list/cocoa/apps_grid_view_item.h" 14#import "ui/app_list/cocoa/apps_pagination_model_observer.h" 15#include "ui/base/models/list_model_observer.h" 16 17namespace { 18 19// OSX app list has hardcoded rows and columns for now. 20const int kFixedRows = 4; 21const int kFixedColumns = 4; 22const int kItemsPerPage = kFixedRows * kFixedColumns; 23 24// Padding space in pixels for fixed layout. 25const CGFloat kGridTopPadding = 1; 26const CGFloat kLeftRightPadding = 21; 27const CGFloat kScrollerPadding = 16; 28 29// Preferred tile size when showing in fixed layout. These should be even 30// numbers to ensure that if they are grown 50% they remain integers. 31const CGFloat kPreferredTileWidth = 88; 32const CGFloat kPreferredTileHeight = 98; 33 34const CGFloat kViewWidth = 35 kFixedColumns * kPreferredTileWidth + 2 * kLeftRightPadding; 36const CGFloat kViewHeight = kFixedRows * kPreferredTileHeight; 37 38const NSTimeInterval kScrollWhileDraggingDelay = 1.0; 39NSTimeInterval g_scroll_duration = 0.18; 40 41} // namespace 42 43@interface AppsGridController () 44 45- (void)scrollToPageWithTimer:(size_t)targetPage; 46- (void)onTimer:(NSTimer*)theTimer; 47 48// Cancel a currently running scroll animation. 49- (void)cancelScrollAnimation; 50 51// Index of the page with the most content currently visible. 52- (size_t)nearestPageIndex; 53 54// Bootstrap the views this class controls. 55- (void)loadAndSetView; 56 57- (void)boundsDidChange:(NSNotification*)notification; 58 59// Action for buttons in the grid. 60- (void)onItemClicked:(id)sender; 61 62- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 63 indexInPage:(size_t)indexInPage; 64 65// Return the button of the selected item. 66- (NSButton*)selectedButton; 67 68// The scroll view holding the grid pages. 69- (NSScrollView*)gridScrollView; 70 71- (NSView*)pagesContainerView; 72 73// Create any new pages after updating |items_|. 74- (void)updatePages:(size_t)startItemIndex; 75 76- (void)updatePageContent:(size_t)pageIndex 77 resetModel:(BOOL)resetModel; 78 79// Bridged methods for ui::ListModelObserver. 80- (void)listItemsAdded:(size_t)start 81 count:(size_t)count; 82 83- (void)listItemsRemoved:(size_t)start 84 count:(size_t)count; 85 86- (void)listItemMovedFromIndex:(size_t)fromIndex 87 toModelIndex:(size_t)toIndex; 88 89// Moves the selection by |indexDelta| items. 90- (BOOL)moveSelectionByDelta:(int)indexDelta; 91 92@end 93 94namespace app_list { 95 96class AppsGridDelegateBridge : public ui::ListModelObserver { 97 public: 98 AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {} 99 100 private: 101 // Overridden from ui::ListModelObserver: 102 virtual void ListItemsAdded(size_t start, size_t count) OVERRIDE { 103 [parent_ listItemsAdded:start 104 count:count]; 105 } 106 virtual void ListItemsRemoved(size_t start, size_t count) OVERRIDE { 107 [parent_ listItemsRemoved:start 108 count:count]; 109 } 110 virtual void ListItemMoved(size_t index, size_t target_index) OVERRIDE { 111 [parent_ listItemMovedFromIndex:index 112 toModelIndex:target_index]; 113 } 114 virtual void ListItemsChanged(size_t start, size_t count) OVERRIDE { 115 NOTREACHED(); 116 } 117 118 AppsGridController* parent_; // Weak, owns us. 119 120 DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge); 121}; 122 123} // namespace app_list 124 125@interface PageContainerView : NSView; 126@end 127 128// The container view needs to flip coordinates so that it is laid out 129// correctly whether or not there is a horizontal scrollbar. 130@implementation PageContainerView 131 132- (BOOL)isFlipped { 133 return YES; 134} 135 136@end 137 138@implementation AppsGridController 139 140+ (void)setScrollAnimationDuration:(NSTimeInterval)duration { 141 g_scroll_duration = duration; 142} 143 144+ (CGFloat)scrollerPadding { 145 return kScrollerPadding; 146} 147 148@synthesize paginationObserver = paginationObserver_; 149 150- (id)init { 151 if ((self = [super init])) { 152 bridge_.reset(new app_list::AppsGridDelegateBridge(self)); 153 NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight); 154 dragManager_.reset( 155 [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize 156 rows:kFixedRows 157 columns:kFixedColumns 158 gridController:self]); 159 pages_.reset([[NSMutableArray alloc] init]); 160 items_.reset([[NSMutableArray alloc] init]); 161 [self loadAndSetView]; 162 [self updatePages:0]; 163 } 164 return self; 165} 166 167- (void)dealloc { 168 [[NSNotificationCenter defaultCenter] removeObserver:self]; 169 [self setModel:scoped_ptr<app_list::AppListModel>()]; 170 [super dealloc]; 171} 172 173- (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex { 174 return [pages_ objectAtIndex:pageIndex]; 175} 176 177- (size_t)pageIndexForCollectionView:(NSCollectionView*)page { 178 for (size_t pageIndex = 0; pageIndex < [pages_ count]; ++pageIndex) { 179 if (page == [self collectionViewAtPageIndex:pageIndex]) 180 return pageIndex; 181 } 182 return NSNotFound; 183} 184 185- (app_list::AppListModel*)model { 186 return model_.get(); 187} 188 189- (void)setModel:(scoped_ptr<app_list::AppListModel>)newModel { 190 if (model_) { 191 model_->apps()->RemoveObserver(bridge_.get()); 192 193 // Since the model is about to be deleted, and the AppKit objects might be 194 // sitting in an NSAutoreleasePool, ensure there are no references to the 195 // model. 196 for (size_t i = 0; i < [items_ count]; ++i) 197 [[self itemAtIndex:i] setModel:NULL]; 198 199 [items_ removeAllObjects]; 200 [self updatePages:0]; 201 [self scrollToPage:0]; 202 } 203 204 model_.reset(newModel.release()); 205 if (!model_) 206 return; 207 208 model_->apps()->AddObserver(bridge_.get()); 209 [self listItemsAdded:0 210 count:model_->apps()->item_count()]; 211} 212 213- (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate { 214 scoped_ptr<app_list::AppListModel> newModel(new app_list::AppListModel); 215 delegate_ = newDelegate; 216 if (delegate_) 217 delegate_->InitModel(newModel.get()); // Populates items. 218 [self setModel:newModel.Pass()]; 219} 220 221- (size_t)visiblePage { 222 return visiblePage_; 223} 224 225- (void)activateSelection { 226 [[self selectedButton] performClick:self]; 227} 228 229- (size_t)pageCount { 230 return [pages_ count]; 231} 232 233- (size_t)itemCount { 234 return [items_ count]; 235} 236 237- (void)scrollToPage:(size_t)pageIndex { 238 NSClipView* clipView = [[self gridScrollView] contentView]; 239 NSPoint newOrigin = [clipView bounds].origin; 240 241 // Scrolling outside of this range is edge elasticity, which animates 242 // automatically. 243 if ((pageIndex == 0 && (newOrigin.x <= 0)) || 244 (pageIndex + 1 == [self pageCount] && 245 newOrigin.x >= pageIndex * kViewWidth)) { 246 return; 247 } 248 249 // Clear any selection on the current page (unless it has been removed). 250 if (visiblePage_ < [pages_ count]) { 251 [[self collectionViewAtPageIndex:visiblePage_] 252 setSelectionIndexes:[NSIndexSet indexSet]]; 253 } 254 255 newOrigin.x = pageIndex * kViewWidth; 256 [NSAnimationContext beginGrouping]; 257 [[NSAnimationContext currentContext] setDuration:g_scroll_duration]; 258 [[clipView animator] setBoundsOrigin:newOrigin]; 259 [NSAnimationContext endGrouping]; 260 animatingScroll_ = YES; 261 targetScrollPage_ = pageIndex; 262 [self cancelScrollTimer]; 263} 264 265- (void)maybeChangePageForPoint:(NSPoint)locationInWindow { 266 NSPoint pointInView = [[self view] convertPoint:locationInWindow 267 fromView:nil]; 268 // Check if the point is outside the view on the left or right. 269 if (pointInView.x <= 0 || pointInView.x >= NSWidth([[self view] bounds])) { 270 size_t targetPage = visiblePage_; 271 if (pointInView.x <= 0) 272 targetPage -= targetPage != 0 ? 1 : 0; 273 else 274 targetPage += targetPage < [pages_ count] - 1 ? 1 : 0; 275 [self scrollToPageWithTimer:targetPage]; 276 return; 277 } 278 279 if (paginationObserver_) { 280 NSInteger segment = 281 [paginationObserver_ pagerSegmentAtLocation:locationInWindow]; 282 if (segment >= 0 && static_cast<size_t>(segment) != targetScrollPage_) { 283 [self scrollToPageWithTimer:segment]; 284 return; 285 } 286 } 287 288 // Otherwise the point may have moved back into the view. 289 [self cancelScrollTimer]; 290} 291 292- (void)cancelScrollTimer { 293 scheduledScrollPage_ = targetScrollPage_; 294 [scrollWhileDraggingTimer_ invalidate]; 295} 296 297- (void)scrollToPageWithTimer:(size_t)targetPage { 298 if (targetPage == targetScrollPage_) { 299 [self cancelScrollTimer]; 300 return; 301 } 302 303 if (targetPage == scheduledScrollPage_) 304 return; 305 306 scheduledScrollPage_ = targetPage; 307 [scrollWhileDraggingTimer_ invalidate]; 308 scrollWhileDraggingTimer_.reset( 309 [[NSTimer scheduledTimerWithTimeInterval:kScrollWhileDraggingDelay 310 target:self 311 selector:@selector(onTimer:) 312 userInfo:nil 313 repeats:NO] retain]); 314} 315 316- (void)onTimer:(NSTimer*)theTimer { 317 if (scheduledScrollPage_ == targetScrollPage_) 318 return; // Already animating scroll. 319 320 [self scrollToPage:scheduledScrollPage_]; 321} 322 323- (void)cancelScrollAnimation { 324 NSClipView* clipView = [[self gridScrollView] contentView]; 325 [NSAnimationContext beginGrouping]; 326 [[NSAnimationContext currentContext] setDuration:0]; 327 [[clipView animator] setBoundsOrigin:[clipView bounds].origin]; 328 [NSAnimationContext endGrouping]; 329 animatingScroll_ = NO; 330} 331 332- (size_t)nearestPageIndex { 333 return lround( 334 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth); 335} 336 337- (void)userScrolling:(BOOL)isScrolling { 338 if (isScrolling) { 339 if (animatingScroll_) 340 [self cancelScrollAnimation]; 341 } else { 342 [self scrollToPage:[self nearestPageIndex]]; 343 } 344} 345 346- (void)loadAndSetView { 347 base::scoped_nsobject<PageContainerView> pagesContainer( 348 [[PageContainerView alloc] initWithFrame:NSZeroRect]); 349 350 NSRect scrollFrame = NSMakeRect(0, kGridTopPadding, kViewWidth, 351 kViewHeight + kScrollerPadding); 352 base::scoped_nsobject<ScrollViewWithNoScrollbars> scrollView( 353 [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]); 354 [scrollView setBorderType:NSNoBorder]; 355 [scrollView setLineScroll:kViewWidth]; 356 [scrollView setPageScroll:kViewWidth]; 357 [scrollView setDelegate:self]; 358 [scrollView setDocumentView:pagesContainer]; 359 [scrollView setDrawsBackground:NO]; 360 361 [[NSNotificationCenter defaultCenter] 362 addObserver:self 363 selector:@selector(boundsDidChange:) 364 name:NSViewBoundsDidChangeNotification 365 object:[scrollView contentView]]; 366 367 [self setView:scrollView]; 368} 369 370- (void)boundsDidChange:(NSNotification*)notification { 371 size_t newPage = [self nearestPageIndex]; 372 if (newPage == visiblePage_) { 373 [paginationObserver_ pageVisibilityChanged]; 374 return; 375 } 376 377 visiblePage_ = newPage; 378 [paginationObserver_ selectedPageChanged:newPage]; 379 [paginationObserver_ pageVisibilityChanged]; 380} 381 382- (void)onItemClicked:(id)sender { 383 for (size_t i = 0; i < [items_ count]; ++i) { 384 AppsGridViewItem* item = [self itemAtIndex:i]; 385 if ([[item button] isEqual:sender]) 386 [item model]->Activate(0); 387 } 388} 389 390- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 391 indexInPage:(size_t)indexInPage { 392 return base::mac::ObjCCastStrict<AppsGridViewItem>( 393 [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]); 394} 395 396- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex { 397 const size_t pageIndex = itemIndex / kItemsPerPage; 398 return [self itemAtPageIndex:pageIndex 399 indexInPage:itemIndex - pageIndex * kItemsPerPage]; 400} 401 402- (NSUInteger)selectedItemIndex { 403 NSCollectionView* page = [self collectionViewAtPageIndex:visiblePage_]; 404 NSUInteger indexOnPage = [[page selectionIndexes] firstIndex]; 405 if (indexOnPage == NSNotFound) 406 return NSNotFound; 407 408 return indexOnPage + visiblePage_ * kItemsPerPage; 409} 410 411- (NSButton*)selectedButton { 412 NSUInteger index = [self selectedItemIndex]; 413 if (index == NSNotFound) 414 return nil; 415 416 return [[self itemAtIndex:index] button]; 417} 418 419- (NSScrollView*)gridScrollView { 420 return base::mac::ObjCCastStrict<NSScrollView>([self view]); 421} 422 423- (NSView*)pagesContainerView { 424 return [[self gridScrollView] documentView]; 425} 426 427- (void)updatePages:(size_t)startItemIndex { 428 // Note there is always at least one page. 429 size_t targetPages = 1; 430 if ([items_ count] != 0) 431 targetPages = ([items_ count] - 1) / kItemsPerPage + 1; 432 433 const size_t currentPages = [self pageCount]; 434 // First see if the number of pages have changed. 435 if (targetPages != currentPages) { 436 if (targetPages < currentPages) { 437 // Pages need to be removed. 438 [pages_ removeObjectsInRange:NSMakeRange(targetPages, 439 currentPages - targetPages)]; 440 } else { 441 // Pages need to be added. 442 for (size_t i = currentPages; i < targetPages; ++i) { 443 NSRect pageFrame = NSMakeRect( 444 kLeftRightPadding + kViewWidth * i, 0, 445 kViewWidth, kViewHeight); 446 [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]]; 447 } 448 } 449 450 [[self pagesContainerView] setSubviews:pages_]; 451 NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight); 452 [[self pagesContainerView] setFrameSize:pagesSize]; 453 [paginationObserver_ totalPagesChanged]; 454 } 455 456 const size_t startPage = startItemIndex / kItemsPerPage; 457 // All pages on or after |startPage| may need items added or removed. 458 for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) { 459 [self updatePageContent:pageIndex 460 resetModel:YES]; 461 } 462} 463 464- (void)updatePageContent:(size_t)pageIndex 465 resetModel:(BOOL)resetModel { 466 NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex]; 467 if (resetModel) { 468 // Clear the models first, otherwise removed items could be autoreleased at 469 // an unknown point in the future, when the model owner may have gone away. 470 for (size_t i = 0; i < [[pageView content] count]; ++i) { 471 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 472 [pageView itemAtIndex:i]); 473 [item setModel:NULL]; 474 } 475 } 476 477 NSRange inPageRange = NSIntersectionRange( 478 NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage), 479 NSMakeRange(0, [items_ count])); 480 NSArray* pageContent = [items_ subarrayWithRange:inPageRange]; 481 [pageView setContent:pageContent]; 482 if (!resetModel) 483 return; 484 485 for (size_t i = 0; i < [pageContent count]; ++i) { 486 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 487 [pageView itemAtIndex:i]); 488 [item setModel:static_cast<app_list::AppListItemModel*>( 489 [[pageContent objectAtIndex:i] pointerValue])]; 490 } 491} 492 493- (void)moveItemInView:(size_t)fromIndex 494 toItemIndex:(size_t)toIndex { 495 base::scoped_nsobject<NSValue> item( 496 [[items_ objectAtIndex:fromIndex] retain]); 497 [items_ removeObjectAtIndex:fromIndex]; 498 [items_ insertObject:item 499 atIndex:toIndex]; 500 501 size_t fromPageIndex = fromIndex / kItemsPerPage; 502 size_t toPageIndex = toIndex / kItemsPerPage; 503 if (fromPageIndex == toPageIndex) { 504 [self updatePageContent:fromPageIndex 505 resetModel:NO]; // Just reorder items. 506 return; 507 } 508 509 if (fromPageIndex > toPageIndex) 510 std::swap(fromPageIndex, toPageIndex); 511 512 for (size_t i = fromPageIndex; i <= toPageIndex; ++i) { 513 [self updatePageContent:i 514 resetModel:YES]; 515 } 516} 517 518// Compare with views implementation in AppsGridView::MoveItemInModel(). 519- (void)moveItemWithIndex:(size_t)itemIndex 520 toModelIndex:(size_t)modelIndex { 521 // Ingore no-op moves. Note that this is always the case when canceled. 522 if (itemIndex == modelIndex) 523 return; 524 525 model_->apps()->RemoveObserver(bridge_.get()); 526 model_->apps()->Move(itemIndex, modelIndex); 527 model_->apps()->AddObserver(bridge_.get()); 528} 529 530- (AppsCollectionViewDragManager*)dragManager { 531 return dragManager_; 532} 533 534- (size_t)scheduledScrollPage { 535 return scheduledScrollPage_; 536} 537 538- (void)listItemsAdded:(size_t)start 539 count:(size_t)count { 540 // Cancel any drag, to ensure the model stays consistent. 541 [dragManager_ cancelDrag]; 542 543 for (size_t i = start; i < start + count; ++i) { 544 app_list::AppListItemModel* itemModel = model_->apps()->GetItemAt(i); 545 [items_ insertObject:[NSValue valueWithPointer:itemModel] 546 atIndex:i]; 547 } 548 549 [self updatePages:start]; 550} 551 552- (void)listItemsRemoved:(size_t)start 553 count:(size_t)count { 554 [dragManager_ cancelDrag]; 555 556 // Clear the models explicitly to avoid surprises from autorelease. 557 for (size_t i = start; i < start + count; ++i) 558 [[self itemAtIndex:i] setModel:NULL]; 559 560 [items_ removeObjectsInRange:NSMakeRange(start, count)]; 561 [self updatePages:start]; 562} 563 564- (void)listItemMovedFromIndex:(size_t)fromIndex 565 toModelIndex:(size_t)toIndex { 566 [dragManager_ cancelDrag]; 567 [self moveItemInView:fromIndex 568 toItemIndex:toIndex]; 569} 570 571- (CGFloat)visiblePortionOfPage:(int)page { 572 CGFloat scrollOffsetOfPage = 573 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page; 574 if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0) 575 return 0.0; 576 577 if (scrollOffsetOfPage <= 0.0) 578 return scrollOffsetOfPage + 1.0; 579 580 return -1.0 + scrollOffsetOfPage; 581} 582 583- (void)onPagerClicked:(AppListPagerView*)sender { 584 int selectedSegment = [sender selectedSegment]; 585 if (selectedSegment < 0) 586 return; // No selection. 587 588 int pageIndex = [[sender cell] tagForSegment:selectedSegment]; 589 if (pageIndex >= 0) 590 [self scrollToPage:pageIndex]; 591} 592 593- (BOOL)moveSelectionByDelta:(int)indexDelta { 594 if (indexDelta == 0) 595 return NO; 596 597 NSUInteger oldIndex = [self selectedItemIndex]; 598 599 // If nothing is currently selected, select the first item on the page. 600 if (oldIndex == NSNotFound) { 601 [self selectItemAtIndex:visiblePage_ * kItemsPerPage]; 602 return YES; 603 } 604 605 // Can't select a negative index. 606 if (indexDelta < 0 && static_cast<NSUInteger>(-indexDelta) > oldIndex) 607 return NO; 608 609 // Can't select an index greater or equal to the number of items. 610 if (oldIndex + indexDelta >= [items_ count]) { 611 if (visiblePage_ == [pages_ count] - 1) 612 return NO; 613 614 // If we're not on the last page, then select the last item. 615 [self selectItemAtIndex:[items_ count] - 1]; 616 return YES; 617 } 618 619 [self selectItemAtIndex:oldIndex + indexDelta]; 620 return YES; 621} 622 623- (void)selectItemAtIndex:(NSUInteger)index { 624 if (index >= [items_ count]) 625 return; 626 627 if (index / kItemsPerPage != visiblePage_) 628 [self scrollToPage:index / kItemsPerPage]; 629 630 [[self itemAtIndex:index] setSelected:YES]; 631} 632 633- (BOOL)handleCommandBySelector:(SEL)command { 634 if (command == @selector(insertNewline:) || 635 command == @selector(insertLineBreak:)) { 636 [self activateSelection]; 637 return YES; 638 } 639 640 NSUInteger oldIndex = [self selectedItemIndex]; 641 // If nothing is currently selected, select the first item on the page. 642 if (oldIndex == NSNotFound) { 643 [self selectItemAtIndex:visiblePage_ * kItemsPerPage]; 644 return YES; 645 } 646 647 if (command == @selector(moveLeft:)) { 648 return oldIndex % kFixedColumns == 0 ? 649 [self moveSelectionByDelta:-kItemsPerPage + kFixedColumns - 1] : 650 [self moveSelectionByDelta:-1]; 651 } 652 653 if (command == @selector(moveRight:)) { 654 return oldIndex % kFixedColumns == kFixedColumns - 1 ? 655 [self moveSelectionByDelta:+kItemsPerPage - kFixedColumns + 1] : 656 [self moveSelectionByDelta:1]; 657 } 658 659 if (command == @selector(moveUp:)) { 660 return oldIndex / kFixedColumns % kFixedRows == 0 ? 661 NO : [self moveSelectionByDelta:-kFixedColumns]; 662 } 663 664 if (command == @selector(moveDown:)) { 665 return oldIndex / kFixedColumns % kFixedRows == kFixedRows - 1 ? 666 NO : [self moveSelectionByDelta:kFixedColumns]; 667 } 668 669 if (command == @selector(pageUp:) || 670 command == @selector(scrollPageUp:)) 671 return [self moveSelectionByDelta:-kItemsPerPage]; 672 673 if (command == @selector(pageDown:) || 674 command == @selector(scrollPageDown:)) 675 return [self moveSelectionByDelta:kItemsPerPage]; 676 677 return NO; 678} 679 680@end 681