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