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