apps_grid_controller.mm revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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.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 78// Bridged methods for AppListItemListObserver. 79- (void)listItemAdded:(size_t)index 80 item:(app_list::AppListItem*)item; 81 82- (void)listItemRemoved:(size_t)index; 83 84- (void)listItemMovedFromIndex:(size_t)fromIndex 85 toModelIndex:(size_t)toIndex; 86 87// Moves the selection by |indexDelta| items. 88- (BOOL)moveSelectionByDelta:(int)indexDelta; 89 90// -[NSCollectionView frameForItemAtIndex:] misreports the frame origin of an 91// item when the method is called during a scroll animation provided by the 92// NSScrollView. This returns the correct value. 93- (NSRect)trueFrameForItemAtIndex:(size_t)itemIndex; 94 95@end 96 97namespace app_list { 98 99class AppsGridDelegateBridge : public AppListItemListObserver { 100 public: 101 AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {} 102 103 private: 104 // Overridden from AppListItemListObserver: 105 virtual void OnListItemAdded(size_t index, AppListItem* item) OVERRIDE { 106 [parent_ listItemAdded:index 107 item:item]; 108 } 109 virtual void OnListItemRemoved(size_t index, AppListItem* item) OVERRIDE { 110 [parent_ listItemRemoved:index]; 111 } 112 virtual void OnListItemMoved(size_t from_index, 113 size_t to_index, 114 AppListItem* item) OVERRIDE { 115 [parent_ listItemMovedFromIndex:from_index 116 toModelIndex:to_index]; 117 } 118 119 AppsGridController* parent_; // Weak, owns us. 120 121 DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge); 122}; 123 124} // namespace app_list 125 126@interface PageContainerView : NSView; 127@end 128 129// The container view needs to flip coordinates so that it is laid out 130// correctly whether or not there is a horizontal scrollbar. 131@implementation PageContainerView 132 133- (BOOL)isFlipped { 134 return YES; 135} 136 137@end 138 139@implementation AppsGridController 140 141+ (void)setScrollAnimationDuration:(NSTimeInterval)duration { 142 g_scroll_duration = duration; 143} 144 145+ (CGFloat)scrollerPadding { 146 return kScrollerPadding; 147} 148 149@synthesize paginationObserver = paginationObserver_; 150 151- (id)init { 152 if ((self = [super init])) { 153 bridge_.reset(new app_list::AppsGridDelegateBridge(self)); 154 NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight); 155 dragManager_.reset( 156 [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize 157 rows:kFixedRows 158 columns:kFixedColumns 159 gridController:self]); 160 pages_.reset([[NSMutableArray alloc] init]); 161 items_.reset([[NSMutableArray alloc] init]); 162 [self loadAndSetView]; 163 [self updatePages:0]; 164 } 165 return self; 166} 167 168- (void)dealloc { 169 [[NSNotificationCenter defaultCenter] removeObserver:self]; 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 delegate_ ? delegate_->GetModel() : NULL; 187} 188 189- (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate { 190 if (delegate_) { 191 app_list::AppListModel* oldModel = delegate_->GetModel(); 192 if (oldModel) 193 oldModel->top_level_item_list()->RemoveObserver(bridge_.get()); 194 } 195 196 // Since the old model may be getting deleted, and the AppKit objects might 197 // be sitting in an NSAutoreleasePool, ensure there are no references to 198 // the model. 199 for (size_t i = 0; i < [items_ count]; ++i) 200 [[self itemAtIndex:i] setModel:NULL]; 201 202 [items_ removeAllObjects]; 203 [self updatePages:0]; 204 [self scrollToPage:0]; 205 206 delegate_ = newDelegate; 207 if (!delegate_) 208 return; 209 210 app_list::AppListModel* newModel = delegate_->GetModel(); 211 if (!newModel) 212 return; 213 214 newModel->top_level_item_list()->AddObserver(bridge_.get()); 215 for (size_t i = 0; i < newModel->top_level_item_list()->item_count(); ++i) { 216 app_list::AppListItem* itemModel = 217 newModel->top_level_item_list()->item_at(i); 218 [items_ insertObject:[NSValue valueWithPointer:itemModel] 219 atIndex:i]; 220 } 221 [self updatePages:0]; 222} 223 224- (size_t)visiblePage { 225 return visiblePage_; 226} 227 228- (void)activateSelection { 229 [[self selectedButton] performClick:self]; 230} 231 232- (size_t)pageCount { 233 return [pages_ count]; 234} 235 236- (size_t)itemCount { 237 return [items_ count]; 238} 239 240- (void)scrollToPage:(size_t)pageIndex { 241 NSClipView* clipView = [[self gridScrollView] contentView]; 242 NSPoint newOrigin = [clipView bounds].origin; 243 244 // Scrolling outside of this range is edge elasticity, which animates 245 // automatically. 246 if ((pageIndex == 0 && (newOrigin.x <= 0)) || 247 (pageIndex + 1 == [self pageCount] && 248 newOrigin.x >= pageIndex * kViewWidth)) { 249 return; 250 } 251 252 // Clear any selection on the current page (unless it has been removed). 253 if (visiblePage_ < [pages_ count]) { 254 [[self collectionViewAtPageIndex:visiblePage_] 255 setSelectionIndexes:[NSIndexSet indexSet]]; 256 } 257 258 newOrigin.x = pageIndex * kViewWidth; 259 [NSAnimationContext beginGrouping]; 260 [[NSAnimationContext currentContext] setDuration:g_scroll_duration]; 261 [[clipView animator] setBoundsOrigin:newOrigin]; 262 [NSAnimationContext endGrouping]; 263 animatingScroll_ = YES; 264 targetScrollPage_ = pageIndex; 265 [self cancelScrollTimer]; 266} 267 268- (void)maybeChangePageForPoint:(NSPoint)locationInWindow { 269 NSPoint pointInView = [[self view] convertPoint:locationInWindow 270 fromView:nil]; 271 // Check if the point is outside the view on the left or right. 272 if (pointInView.x <= 0 || pointInView.x >= NSWidth([[self view] bounds])) { 273 size_t targetPage = visiblePage_; 274 if (pointInView.x <= 0) 275 targetPage -= targetPage != 0 ? 1 : 0; 276 else 277 targetPage += targetPage < [pages_ count] - 1 ? 1 : 0; 278 [self scrollToPageWithTimer:targetPage]; 279 return; 280 } 281 282 if (paginationObserver_) { 283 NSInteger segment = 284 [paginationObserver_ pagerSegmentAtLocation:locationInWindow]; 285 if (segment >= 0 && static_cast<size_t>(segment) != targetScrollPage_) { 286 [self scrollToPageWithTimer:segment]; 287 return; 288 } 289 } 290 291 // Otherwise the point may have moved back into the view. 292 [self cancelScrollTimer]; 293} 294 295- (void)cancelScrollTimer { 296 scheduledScrollPage_ = targetScrollPage_; 297 [scrollWhileDraggingTimer_ invalidate]; 298} 299 300- (void)scrollToPageWithTimer:(size_t)targetPage { 301 if (targetPage == targetScrollPage_) { 302 [self cancelScrollTimer]; 303 return; 304 } 305 306 if (targetPage == scheduledScrollPage_) 307 return; 308 309 scheduledScrollPage_ = targetPage; 310 [scrollWhileDraggingTimer_ invalidate]; 311 scrollWhileDraggingTimer_.reset( 312 [[NSTimer scheduledTimerWithTimeInterval:kScrollWhileDraggingDelay 313 target:self 314 selector:@selector(onTimer:) 315 userInfo:nil 316 repeats:NO] retain]); 317} 318 319- (void)onTimer:(NSTimer*)theTimer { 320 if (scheduledScrollPage_ == targetScrollPage_) 321 return; // Already animating scroll. 322 323 [self scrollToPage:scheduledScrollPage_]; 324} 325 326- (void)cancelScrollAnimation { 327 NSClipView* clipView = [[self gridScrollView] contentView]; 328 [NSAnimationContext beginGrouping]; 329 [[NSAnimationContext currentContext] setDuration:0]; 330 [[clipView animator] setBoundsOrigin:[clipView bounds].origin]; 331 [NSAnimationContext endGrouping]; 332 animatingScroll_ = NO; 333} 334 335- (size_t)nearestPageIndex { 336 return lround( 337 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth); 338} 339 340- (void)userScrolling:(BOOL)isScrolling { 341 if (isScrolling) { 342 if (animatingScroll_) 343 [self cancelScrollAnimation]; 344 } else { 345 [self scrollToPage:[self nearestPageIndex]]; 346 } 347} 348 349- (void)loadAndSetView { 350 base::scoped_nsobject<PageContainerView> pagesContainer( 351 [[PageContainerView alloc] initWithFrame:NSZeroRect]); 352 353 NSRect scrollFrame = NSMakeRect(0, kGridTopPadding, kViewWidth, 354 kViewHeight + kScrollerPadding); 355 base::scoped_nsobject<ScrollViewWithNoScrollbars> scrollView( 356 [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]); 357 [scrollView setBorderType:NSNoBorder]; 358 [scrollView setLineScroll:kViewWidth]; 359 [scrollView setPageScroll:kViewWidth]; 360 [scrollView setDelegate:self]; 361 [scrollView setDocumentView:pagesContainer]; 362 [scrollView setDrawsBackground:NO]; 363 364 [[NSNotificationCenter defaultCenter] 365 addObserver:self 366 selector:@selector(boundsDidChange:) 367 name:NSViewBoundsDidChangeNotification 368 object:[scrollView contentView]]; 369 370 [self setView:scrollView]; 371} 372 373- (void)boundsDidChange:(NSNotification*)notification { 374 size_t newPage = [self nearestPageIndex]; 375 if (newPage == visiblePage_) { 376 [paginationObserver_ pageVisibilityChanged]; 377 return; 378 } 379 380 visiblePage_ = newPage; 381 [paginationObserver_ selectedPageChanged:newPage]; 382 [paginationObserver_ pageVisibilityChanged]; 383} 384 385- (void)onItemClicked:(id)sender { 386 for (size_t i = 0; i < [items_ count]; ++i) { 387 AppsGridViewItem* gridItem = [self itemAtIndex:i]; 388 if ([[gridItem button] isEqual:sender]) 389 [gridItem model]->Activate(0); 390 } 391} 392 393- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 394 indexInPage:(size_t)indexInPage { 395 return base::mac::ObjCCastStrict<AppsGridViewItem>( 396 [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]); 397} 398 399- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex { 400 const size_t pageIndex = itemIndex / kItemsPerPage; 401 return [self itemAtPageIndex:pageIndex 402 indexInPage:itemIndex - pageIndex * kItemsPerPage]; 403} 404 405- (NSUInteger)selectedItemIndex { 406 NSCollectionView* page = [self collectionViewAtPageIndex:visiblePage_]; 407 NSUInteger indexOnPage = [[page selectionIndexes] firstIndex]; 408 if (indexOnPage == NSNotFound) 409 return NSNotFound; 410 411 return indexOnPage + visiblePage_ * kItemsPerPage; 412} 413 414- (NSButton*)selectedButton { 415 NSUInteger index = [self selectedItemIndex]; 416 if (index == NSNotFound) 417 return nil; 418 419 return [[self itemAtIndex:index] button]; 420} 421 422- (NSScrollView*)gridScrollView { 423 return base::mac::ObjCCastStrict<NSScrollView>([self view]); 424} 425 426- (NSView*)pagesContainerView { 427 return [[self gridScrollView] documentView]; 428} 429 430- (void)updatePages:(size_t)startItemIndex { 431 // Note there is always at least one page. 432 size_t targetPages = 1; 433 if ([items_ count] != 0) 434 targetPages = ([items_ count] - 1) / kItemsPerPage + 1; 435 436 const size_t currentPages = [self pageCount]; 437 // First see if the number of pages have changed. 438 if (targetPages != currentPages) { 439 if (targetPages < currentPages) { 440 // Pages need to be removed. 441 [pages_ removeObjectsInRange:NSMakeRange(targetPages, 442 currentPages - targetPages)]; 443 } else { 444 // Pages need to be added. 445 for (size_t i = currentPages; i < targetPages; ++i) { 446 NSRect pageFrame = NSMakeRect( 447 kLeftRightPadding + kViewWidth * i, 0, 448 kViewWidth, kViewHeight); 449 [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]]; 450 } 451 } 452 453 [[self pagesContainerView] setSubviews:pages_]; 454 NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight); 455 [[self pagesContainerView] setFrameSize:pagesSize]; 456 [paginationObserver_ totalPagesChanged]; 457 } 458 459 const size_t startPage = startItemIndex / kItemsPerPage; 460 // All pages on or after |startPage| may need items added or removed. 461 for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) 462 [self updatePageContent:pageIndex]; 463} 464 465- (void)updatePageContent:(size_t)pageIndex { 466 NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex]; 467 // Clear the models first, otherwise removed items could be autoreleased at 468 // an unknown point in the future, when the model owner may have gone away. 469 for (size_t i = 0; i < [[pageView content] count]; ++i) { 470 AppsGridViewItem* gridItem = 471 base::mac::ObjCCastStrict<AppsGridViewItem>([pageView itemAtIndex:i]); 472 [gridItem setModel:NULL]; 473 } 474 475 NSRange inPageRange = NSIntersectionRange( 476 NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage), 477 NSMakeRange(0, [items_ count])); 478 NSArray* pageContent = [items_ subarrayWithRange:inPageRange]; 479 [pageView setContent:pageContent]; 480 481 for (size_t i = 0; i < [pageContent count]; ++i) { 482 AppsGridViewItem* gridItem = base::mac::ObjCCastStrict<AppsGridViewItem>( 483 [pageView itemAtIndex:i]); 484 [gridItem setModel:static_cast<app_list::AppListItem*>( 485 [[pageContent objectAtIndex:i] pointerValue])]; 486 [gridItem setInitialFrameRect:[self trueFrameForItemAtIndex:i]]; 487 } 488} 489 490- (void)moveItemInView:(size_t)fromIndex 491 toItemIndex:(size_t)toIndex { 492 base::scoped_nsobject<NSValue> item( 493 [[items_ objectAtIndex:fromIndex] retain]); 494 [items_ removeObjectAtIndex:fromIndex]; 495 [items_ insertObject:item 496 atIndex:toIndex]; 497 498 size_t fromPageIndex = fromIndex / kItemsPerPage; 499 size_t toPageIndex = toIndex / kItemsPerPage; 500 if (fromPageIndex == toPageIndex) { 501 [self updatePageContent:fromPageIndex]; 502 return; 503 } 504 505 if (fromPageIndex > toPageIndex) 506 std::swap(fromPageIndex, toPageIndex); 507 508 for (size_t i = fromPageIndex; i <= toPageIndex; ++i) { 509 [self updatePageContent:i]; 510 } 511} 512 513// Compare with views implementation in AppsGridView::MoveItemInModel(). 514- (void)moveItemWithIndex:(size_t)itemIndex 515 toModelIndex:(size_t)modelIndex { 516 // Ingore no-op moves. Note that this is always the case when canceled. 517 if (itemIndex == modelIndex) 518 return; 519 520 app_list::AppListItemList* itemList = [self model]->top_level_item_list(); 521 itemList->RemoveObserver(bridge_.get()); 522 itemList->MoveItem(itemIndex, modelIndex); 523 itemList->AddObserver(bridge_.get()); 524} 525 526- (AppsCollectionViewDragManager*)dragManager { 527 return dragManager_; 528} 529 530- (size_t)scheduledScrollPage { 531 return scheduledScrollPage_; 532} 533 534- (void)listItemAdded:(size_t)index 535 item:(app_list::AppListItem*)itemModel { 536 // Cancel any drag, to ensure the model stays consistent. 537 [dragManager_ cancelDrag]; 538 539 [items_ insertObject:[NSValue valueWithPointer:itemModel] 540 atIndex:index]; 541 542 [self updatePages:index]; 543} 544 545- (void)listItemRemoved:(size_t)index { 546 [dragManager_ cancelDrag]; 547 548 // Clear the models explicitly to avoid surprises from autorelease. 549 [[self itemAtIndex:index] setModel:NULL]; 550 551 [items_ removeObjectsInRange:NSMakeRange(index, 1)]; 552 [self updatePages:index]; 553} 554 555- (void)listItemMovedFromIndex:(size_t)fromIndex 556 toModelIndex:(size_t)toIndex { 557 [dragManager_ cancelDrag]; 558 [self moveItemInView:fromIndex 559 toItemIndex:toIndex]; 560} 561 562- (CGFloat)visiblePortionOfPage:(int)page { 563 CGFloat scrollOffsetOfPage = 564 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page; 565 if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0) 566 return 0.0; 567 568 if (scrollOffsetOfPage <= 0.0) 569 return scrollOffsetOfPage + 1.0; 570 571 return -1.0 + scrollOffsetOfPage; 572} 573 574- (void)onPagerClicked:(AppListPagerView*)sender { 575 int selectedSegment = [sender selectedSegment]; 576 if (selectedSegment < 0) 577 return; // No selection. 578 579 int pageIndex = [[sender cell] tagForSegment:selectedSegment]; 580 if (pageIndex >= 0) 581 [self scrollToPage:pageIndex]; 582} 583 584- (BOOL)moveSelectionByDelta:(int)indexDelta { 585 if (indexDelta == 0) 586 return NO; 587 588 NSUInteger oldIndex = [self selectedItemIndex]; 589 590 // If nothing is currently selected, select the first item on the page. 591 if (oldIndex == NSNotFound) { 592 [self selectItemAtIndex:visiblePage_ * kItemsPerPage]; 593 return YES; 594 } 595 596 // Can't select a negative index. 597 if (indexDelta < 0 && static_cast<NSUInteger>(-indexDelta) > oldIndex) 598 return NO; 599 600 // Can't select an index greater or equal to the number of items. 601 if (oldIndex + indexDelta >= [items_ count]) { 602 if (visiblePage_ == [pages_ count] - 1) 603 return NO; 604 605 // If we're not on the last page, then select the last item. 606 [self selectItemAtIndex:[items_ count] - 1]; 607 return YES; 608 } 609 610 [self selectItemAtIndex:oldIndex + indexDelta]; 611 return YES; 612} 613 614- (NSRect)trueFrameForItemAtIndex:(size_t)itemIndex { 615 size_t column = itemIndex % kFixedColumns; 616 size_t row = itemIndex % kItemsPerPage / kFixedColumns; 617 return NSMakeRect(column * kPreferredTileWidth, 618 row * kPreferredTileHeight, 619 kPreferredTileWidth, 620 kPreferredTileHeight); 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