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