app_list_view_controller.mm revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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/app_list_view_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "base/strings/string_util.h" 9#include "base/strings/sys_string_conversions.h" 10#include "skia/ext/skia_utils_mac.h" 11#include "ui/app_list/app_list_constants.h" 12#include "ui/app_list/app_list_model.h" 13#include "ui/app_list/app_list_view_delegate.h" 14#include "ui/app_list/app_list_view_delegate_observer.h" 15#include "ui/app_list/signin_delegate.h" 16#import "ui/app_list/cocoa/app_list_pager_view.h" 17#import "ui/app_list/cocoa/apps_grid_controller.h" 18#import "ui/app_list/cocoa/signin_view_controller.h" 19#import "ui/base/cocoa/flipped_view.h" 20#include "ui/app_list/search_box_model.h" 21#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" 22 23namespace { 24 25// The roundedness of the corners of the bubble. 26const CGFloat kBubbleCornerRadius = 3; 27 28// Height of the pager. 29const CGFloat kPagerPreferredHeight = 57; 30 31// Height of separator line drawn between the searchbox and grid view. 32const CGFloat kTopSeparatorSize = 1; 33 34// Height of the search input. 35const CGFloat kSearchInputHeight = 48; 36 37// Minimum margin on either side of the pager. If the pager grows beyond this, 38// the segment size is reduced. 39const CGFloat kMinPagerMargin = 40; 40// Maximum width of a single segment. 41const CGFloat kMaxSegmentWidth = 80; 42 43// Duration of the animation for sliding in and out search results. 44const NSTimeInterval kResultsAnimationDuration = 0.2; 45 46} // namespace 47 48@interface BackgroundView : FlippedView; 49@end 50 51@implementation BackgroundView 52 53- (void)drawRect:(NSRect)dirtyRect { 54 gfx::ScopedNSGraphicsContextSaveGState context; 55 NSRect boundsRect = [self bounds]; 56 NSRect searchAreaRect = NSMakeRect(0, 0, 57 NSWidth(boundsRect), kSearchInputHeight); 58 NSRect separatorRect = NSMakeRect(0, NSMaxY(searchAreaRect), 59 NSWidth(boundsRect), kTopSeparatorSize); 60 61 [[NSBezierPath bezierPathWithRoundedRect:boundsRect 62 xRadius:kBubbleCornerRadius 63 yRadius:kBubbleCornerRadius] addClip]; 64 65 [gfx::SkColorToSRGBNSColor(app_list::kContentsBackgroundColor) set]; 66 NSRectFill(boundsRect); 67 [gfx::SkColorToSRGBNSColor(app_list::kSearchBoxBackground) set]; 68 NSRectFill(searchAreaRect); 69 [gfx::SkColorToSRGBNSColor(app_list::kTopSeparatorColor) set]; 70 NSRectFill(separatorRect); 71} 72 73@end 74 75@interface AppListViewController () 76 77- (void)loadAndSetView; 78- (void)revealSearchResults:(BOOL)show; 79 80@end 81 82namespace app_list { 83 84class AppListModelObserverBridge : public AppListViewDelegateObserver { 85 public: 86 AppListModelObserverBridge(AppListViewController* parent); 87 virtual ~AppListModelObserverBridge(); 88 89 private: 90 // Overridden from app_list::AppListViewDelegateObserver: 91 virtual void OnProfilesChanged() OVERRIDE; 92 93 AppListViewController* parent_; // Weak. Owns us. 94 95 DISALLOW_COPY_AND_ASSIGN(AppListModelObserverBridge); 96}; 97 98AppListModelObserverBridge::AppListModelObserverBridge( 99 AppListViewController* parent) 100 : parent_(parent) { 101 [parent_ delegate]->AddObserver(this); 102} 103 104AppListModelObserverBridge::~AppListModelObserverBridge() { 105 [parent_ delegate]->RemoveObserver(this); 106} 107 108void AppListModelObserverBridge::OnProfilesChanged() { 109 [parent_ onProfilesChanged]; 110} 111 112} // namespace app_list 113 114@implementation AppListViewController 115 116- (id)init { 117 if ((self = [super init])) { 118 appsGridController_.reset([[AppsGridController alloc] init]); 119 [self loadAndSetView]; 120 121 [self totalPagesChanged]; 122 [self selectedPageChanged:0]; 123 [appsGridController_ setPaginationObserver:self]; 124 } 125 return self; 126} 127 128- (void)dealloc { 129 // Ensure that setDelegate(NULL) has been called before destruction, because 130 // dealloc can be called at odd times, and Objective C destruction order does 131 // not properly tear down these dependencies. 132 DCHECK(delegate_ == NULL); 133 [appsGridController_ setPaginationObserver:nil]; 134 [super dealloc]; 135} 136 137- (AppsSearchBoxController*)searchBoxController { 138 return appsSearchBoxController_; 139} 140 141- (BOOL)showingSearchResults { 142 return showingSearchResults_; 143} 144 145- (AppsGridController*)appsGridController { 146 return appsGridController_; 147} 148 149- (NSSegmentedControl*)pagerControl { 150 return pagerControl_; 151} 152 153- (NSView*)backgroundView { 154 return backgroundView_; 155} 156 157- (app_list::AppListViewDelegate*)delegate { 158 return delegate_.get(); 159} 160 161- (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate { 162 if (delegate_) { 163 // Ensure the search box is cleared when switching profiles. 164 if ([self searchBoxModel]) 165 [self searchBoxModel]->SetText(base::string16()); 166 167 // First clean up, in reverse order. 168 app_list_model_observer_bridge_.reset(); 169 [appsSearchResultsController_ setDelegate:nil]; 170 [appsSearchBoxController_ setDelegate:nil]; 171 [appsGridController_ setDelegate:nil]; 172 } 173 delegate_.reset(newDelegate.release()); 174 if (delegate_) { 175 [loadingIndicator_ stopAnimation:self]; 176 } else { 177 [loadingIndicator_ startAnimation:self]; 178 return; 179 } 180 181 [appsGridController_ setDelegate:delegate_.get()]; 182 [appsSearchBoxController_ setDelegate:self]; 183 [appsSearchResultsController_ setDelegate:self]; 184 app_list_model_observer_bridge_.reset( 185 new app_list::AppListModelObserverBridge(self)); 186 [self onProfilesChanged]; 187} 188 189-(void)loadAndSetView { 190 pagerControl_.reset([[AppListPagerView alloc] init]); 191 [pagerControl_ setTarget:appsGridController_]; 192 [pagerControl_ setAction:@selector(onPagerClicked:)]; 193 194 NSRect gridFrame = [[appsGridController_ view] frame]; 195 NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize, 196 NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight - 197 [AppsGridController scrollerPadding]); 198 199 contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]); 200 201 // The contents view contains animations both from an NSCollectionView and the 202 // app list's own transitive drag layers. Ensure the subviews have access to 203 // a compositing layer they can share. 204 [contentsView_ setWantsLayer:YES]; 205 206 backgroundView_.reset( 207 [[BackgroundView alloc] initWithFrame: 208 NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]); 209 appsSearchBoxController_.reset( 210 [[AppsSearchBoxController alloc] initWithFrame: 211 NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]); 212 appsSearchResultsController_.reset( 213 [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize: 214 [contentsView_ bounds].size]); 215 base::scoped_nsobject<NSView> containerView( 216 [[NSView alloc] initWithFrame:[backgroundView_ frame]]); 217 218 loadingIndicator_.reset( 219 [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]); 220 [loadingIndicator_ setStyle:NSProgressIndicatorSpinningStyle]; 221 [loadingIndicator_ sizeToFit]; 222 NSRect indicatorRect = [loadingIndicator_ frame]; 223 indicatorRect.origin.x = NSWidth(contentsRect) / 2 - NSMidX(indicatorRect); 224 indicatorRect.origin.y = NSHeight(contentsRect) / 2 - NSMidY(indicatorRect); 225 [loadingIndicator_ setFrame:indicatorRect]; 226 [loadingIndicator_ setDisplayedWhenStopped:NO]; 227 [loadingIndicator_ startAnimation:self]; 228 229 [contentsView_ addSubview:[appsGridController_ view]]; 230 [contentsView_ addSubview:pagerControl_]; 231 [contentsView_ addSubview:loadingIndicator_]; 232 [backgroundView_ addSubview:contentsView_]; 233 [backgroundView_ addSubview:[appsSearchResultsController_ view]]; 234 [backgroundView_ addSubview:[appsSearchBoxController_ view]]; 235 [containerView addSubview:backgroundView_]; 236 [self setView:containerView]; 237} 238 239- (void)revealSearchResults:(BOOL)show { 240 if (show == showingSearchResults_) 241 return; 242 243 showingSearchResults_ = show; 244 NSSize contentsSize = [contentsView_ frame].size; 245 NSRect resultsTargetRect = NSMakeRect( 246 0, kSearchInputHeight + kTopSeparatorSize, 247 contentsSize.width, contentsSize.height); 248 NSRect contentsTargetRect = resultsTargetRect; 249 250 // Shows results by sliding the grid and pager down to the bottom of the view. 251 // Hides results by collapsing the search results container to a height of 0. 252 if (show) 253 contentsTargetRect.origin.y += NSHeight(contentsTargetRect); 254 else 255 resultsTargetRect.size.height = 0; 256 257 [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration]; 258 [[contentsView_ animator] setFrame:contentsTargetRect]; 259 [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect]; 260} 261 262- (void)totalPagesChanged { 263 size_t pageCount = [appsGridController_ pageCount]; 264 [pagerControl_ setSegmentCount:pageCount]; 265 266 NSRect viewFrame = [[pagerControl_ superview] bounds]; 267 CGFloat segmentWidth = std::min( 268 kMaxSegmentWidth, 269 (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount); 270 271 for (size_t i = 0; i < pageCount; ++i) { 272 [pagerControl_ setWidth:segmentWidth 273 forSegment:i]; 274 [[pagerControl_ cell] setTag:i 275 forSegment:i]; 276 } 277 278 // Center in view. 279 [pagerControl_ sizeToFit]; 280 [pagerControl_ setFrame: 281 NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]), 282 viewFrame.size.height - kPagerPreferredHeight, 283 [pagerControl_ bounds].size.width, 284 kPagerPreferredHeight)]; 285} 286 287- (void)selectedPageChanged:(int)newSelected { 288 [pagerControl_ selectSegmentWithTag:newSelected]; 289} 290 291- (void)pageVisibilityChanged { 292 [pagerControl_ setNeedsDisplay:YES]; 293} 294 295- (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow { 296 return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow]; 297} 298 299- (app_list::SearchBoxModel*)searchBoxModel { 300 app_list::AppListModel* appListModel = [appsGridController_ model]; 301 return appListModel ? appListModel->search_box() : NULL; 302} 303 304- (app_list::AppListViewDelegate*)appListDelegate { 305 return [self delegate]; 306} 307 308- (BOOL)control:(NSControl*)control 309 textView:(NSTextView*)textView 310 doCommandBySelector:(SEL)command { 311 if (showingSearchResults_) 312 return [appsSearchResultsController_ handleCommandBySelector:command]; 313 314 // If anything has been written, let the search view handle it. 315 if ([[control stringValue] length] > 0) 316 return NO; 317 318 // Handle escape. 319 if (command == @selector(complete:) || 320 command == @selector(cancel:) || 321 command == @selector(cancelOperation:)) { 322 if (delegate_) 323 delegate_->Dismiss(); 324 return YES; 325 } 326 327 // Possibly handle grid navigation. 328 return [appsGridController_ handleCommandBySelector:command]; 329} 330 331- (void)modelTextDidChange { 332 app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel]; 333 if (!searchBoxModel || !delegate_) 334 return; 335 336 base::string16 query; 337 TrimWhitespace(searchBoxModel->text(), TRIM_ALL, &query); 338 BOOL shouldShowSearch = !query.empty(); 339 [self revealSearchResults:shouldShowSearch]; 340 if (shouldShowSearch) 341 delegate_->StartSearch(); 342 else 343 delegate_->StopSearch(); 344} 345 346- (app_list::AppListModel*)appListModel { 347 return [appsGridController_ model]; 348} 349 350- (void)openResult:(app_list::SearchResult*)result { 351 if (delegate_) { 352 delegate_->OpenSearchResult( 353 result, false /* auto_launch */, 0 /* event flags */); 354 } 355} 356 357- (void)redoSearch { 358 [self modelTextDidChange]; 359} 360 361- (void)onProfilesChanged { 362 [appsSearchBoxController_ rebuildMenu]; 363 app_list::SigninDelegate* signinDelegate = 364 delegate_ ? delegate_->GetSigninDelegate() : NULL; 365 BOOL showSigninView = signinDelegate && signinDelegate->NeedSignin(); 366 367 [[signinViewController_ view] removeFromSuperview]; 368 signinViewController_.reset(); 369 370 if (!showSigninView) { 371 [backgroundView_ setHidden:NO]; 372 return; 373 } 374 375 [backgroundView_ setHidden:YES]; 376 signinViewController_.reset( 377 [[SigninViewController alloc] initWithFrame:[backgroundView_ frame] 378 cornerRadius:kBubbleCornerRadius 379 delegate:signinDelegate]); 380 [[self view] addSubview:[signinViewController_ view]]; 381} 382 383@end 384