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