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