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