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.
5 #import "ui/app_list/cocoa/app_list_view_controller.h"
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 #include "ui/app_list/signin_delegate.h"
17 #import "ui/app_list/cocoa/app_list_pager_view.h"
18 #import "ui/app_list/cocoa/apps_grid_controller.h"
19 #import "ui/app_list/cocoa/signin_view_controller.h"
20 #import "ui/base/cocoa/flipped_view.h"
21 #include "ui/app_list/search_box_model.h"
22 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
26 // The roundedness of the corners of the bubble.
27 const CGFloat kBubbleCornerRadius = 3;
29 // Height of the pager.
30 const CGFloat kPagerPreferredHeight = 57;
32 // Height of separator line drawn between the searchbox and grid view.
33 const CGFloat kTopSeparatorSize = 1;
35 // Height of the search input.
36 const CGFloat kSearchInputHeight = 48;
38 // Minimum margin on either side of the pager. If the pager grows beyond this,
39 // the segment size is reduced.
40 const CGFloat kMinPagerMargin = 40;
41 // Maximum width of a single segment.
42 const CGFloat kMaxSegmentWidth = 80;
44 // Duration of the animation for sliding in and out search results.
45 const NSTimeInterval kResultsAnimationDuration = 0.2;
49 @interface BackgroundView : FlippedView;
52 @implementation BackgroundView
54 - (void)drawRect:(NSRect)dirtyRect {
55 gfx::ScopedNSGraphicsContextSaveGState context;
56 NSRect boundsRect = [self bounds];
57 NSRect searchAreaRect = NSMakeRect(0, 0,
58 NSWidth(boundsRect), kSearchInputHeight);
59 NSRect separatorRect = NSMakeRect(0, NSMaxY(searchAreaRect),
60 NSWidth(boundsRect), kTopSeparatorSize);
62 [[NSBezierPath bezierPathWithRoundedRect:boundsRect
63 xRadius:kBubbleCornerRadius
64 yRadius:kBubbleCornerRadius] addClip];
66 [gfx::SkColorToSRGBNSColor(app_list::kContentsBackgroundColor) set];
67 NSRectFill(boundsRect);
68 [gfx::SkColorToSRGBNSColor(app_list::kSearchBoxBackground) set];
69 NSRectFill(searchAreaRect);
70 [gfx::SkColorToSRGBNSColor(app_list::kTopSeparatorColor) set];
71 NSRectFill(separatorRect);
76 @interface AppListViewController ()
78 - (void)loadAndSetView;
79 - (void)revealSearchResults:(BOOL)show;
85 class AppListModelObserverBridge : public AppListViewDelegateObserver {
87 AppListModelObserverBridge(AppListViewController* parent);
88 virtual ~AppListModelObserverBridge();
91 // Overridden from app_list::AppListViewDelegateObserver:
92 virtual void OnProfilesChanged() OVERRIDE;
94 AppListViewController* parent_; // Weak. Owns us.
96 DISALLOW_COPY_AND_ASSIGN(AppListModelObserverBridge);
99 AppListModelObserverBridge::AppListModelObserverBridge(
100 AppListViewController* parent)
102 [parent_ delegate]->AddObserver(this);
105 AppListModelObserverBridge::~AppListModelObserverBridge() {
106 [parent_ delegate]->RemoveObserver(this);
109 void AppListModelObserverBridge::OnProfilesChanged() {
110 [parent_ onProfilesChanged];
113 } // namespace app_list
115 @implementation AppListViewController
118 if ((self = [super init])) {
119 appsGridController_.reset([[AppsGridController alloc] init]);
120 [self loadAndSetView];
122 [self totalPagesChanged];
123 [self selectedPageChanged:0];
124 [appsGridController_ setPaginationObserver:self];
130 // Ensure that setDelegate(NULL) has been called before destruction, because
131 // dealloc can be called at odd times, and Objective C destruction order does
132 // not properly tear down these dependencies.
133 DCHECK(delegate_ == NULL);
134 [appsGridController_ setPaginationObserver:nil];
138 - (AppsSearchBoxController*)searchBoxController {
139 return appsSearchBoxController_;
142 - (BOOL)showingSearchResults {
143 return showingSearchResults_;
146 - (AppsGridController*)appsGridController {
147 return appsGridController_;
150 - (NSSegmentedControl*)pagerControl {
151 return pagerControl_;
154 - (NSView*)backgroundView {
155 return backgroundView_;
158 - (app_list::AppListViewDelegate*)delegate {
159 return delegate_.get();
162 - (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate {
164 // Ensure the search box is cleared when switching profiles.
165 if ([self searchBoxModel])
166 [self searchBoxModel]->SetText(base::string16());
168 // First clean up, in reverse order.
169 app_list_model_observer_bridge_.reset();
170 [appsSearchResultsController_ setDelegate:nil];
171 [appsSearchBoxController_ setDelegate:nil];
172 [appsGridController_ setDelegate:nil];
174 delegate_.reset(newDelegate.release());
176 [loadingIndicator_ stopAnimation:self];
178 [loadingIndicator_ startAnimation:self];
182 [appsGridController_ setDelegate:delegate_.get()];
183 [appsSearchBoxController_ setDelegate:self];
184 [appsSearchResultsController_ setDelegate:self];
185 app_list_model_observer_bridge_.reset(
186 new app_list::AppListModelObserverBridge(self));
187 [self onProfilesChanged];
190 -(void)loadAndSetView {
191 pagerControl_.reset([[AppListPagerView alloc] init]);
192 [pagerControl_ setTarget:appsGridController_];
193 [pagerControl_ setAction:@selector(onPagerClicked:)];
195 NSRect gridFrame = [[appsGridController_ view] frame];
196 NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
197 NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
198 [AppsGridController scrollerPadding]);
200 contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
202 // The contents view contains animations both from an NSCollectionView and the
203 // app list's own transitive drag layers. On Mavericks, the subviews need to
204 // have access to a compositing layer they can share. Otherwise the compositor
205 // makes tearing artifacts. However, doing this on Mountain Lion or earler
206 // results in flickering whilst an item is installing.
207 if (base::mac::IsOSMavericksOrLater())
208 [contentsView_ setWantsLayer:YES];
210 backgroundView_.reset(
211 [[BackgroundView alloc] initWithFrame:
212 NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]);
213 appsSearchBoxController_.reset(
214 [[AppsSearchBoxController alloc] initWithFrame:
215 NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]);
216 appsSearchResultsController_.reset(
217 [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
218 [contentsView_ bounds].size]);
219 base::scoped_nsobject<NSView> containerView(
220 [[NSView alloc] initWithFrame:[backgroundView_ frame]]);
222 loadingIndicator_.reset(
223 [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]);
224 [loadingIndicator_ setStyle:NSProgressIndicatorSpinningStyle];
225 [loadingIndicator_ sizeToFit];
226 NSRect indicatorRect = [loadingIndicator_ frame];
227 indicatorRect.origin.x = NSWidth(contentsRect) / 2 - NSMidX(indicatorRect);
228 indicatorRect.origin.y = NSHeight(contentsRect) / 2 - NSMidY(indicatorRect);
229 [loadingIndicator_ setFrame:indicatorRect];
230 [loadingIndicator_ setDisplayedWhenStopped:NO];
231 [loadingIndicator_ startAnimation:self];
233 [contentsView_ addSubview:[appsGridController_ view]];
234 [contentsView_ addSubview:pagerControl_];
235 [contentsView_ addSubview:loadingIndicator_];
236 [backgroundView_ addSubview:contentsView_];
237 [backgroundView_ addSubview:[appsSearchResultsController_ view]];
238 [backgroundView_ addSubview:[appsSearchBoxController_ view]];
239 [containerView addSubview:backgroundView_];
240 [self setView:containerView];
243 - (void)revealSearchResults:(BOOL)show {
244 if (show == showingSearchResults_)
247 showingSearchResults_ = show;
248 NSSize contentsSize = [contentsView_ frame].size;
249 NSRect resultsTargetRect = NSMakeRect(
250 0, kSearchInputHeight + kTopSeparatorSize,
251 contentsSize.width, contentsSize.height);
252 NSRect contentsTargetRect = resultsTargetRect;
254 // Shows results by sliding the grid and pager down to the bottom of the view.
255 // Hides results by collapsing the search results container to a height of 0.
257 contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
259 resultsTargetRect.size.height = 0;
261 [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
262 [[contentsView_ animator] setFrame:contentsTargetRect];
263 [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
266 - (void)totalPagesChanged {
267 size_t pageCount = [appsGridController_ pageCount];
268 [pagerControl_ setSegmentCount:pageCount];
270 NSRect viewFrame = [[pagerControl_ superview] bounds];
271 CGFloat segmentWidth = std::min(
273 (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
275 for (size_t i = 0; i < pageCount; ++i) {
276 [pagerControl_ setWidth:segmentWidth
278 [[pagerControl_ cell] setTag:i
283 [pagerControl_ sizeToFit];
284 [pagerControl_ setFrame:
285 NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]),
286 viewFrame.size.height - kPagerPreferredHeight,
287 [pagerControl_ bounds].size.width,
288 kPagerPreferredHeight)];
291 - (void)selectedPageChanged:(int)newSelected {
292 [pagerControl_ selectSegmentWithTag:newSelected];
295 - (void)pageVisibilityChanged {
296 [pagerControl_ setNeedsDisplay:YES];
299 - (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
300 return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
303 - (app_list::SearchBoxModel*)searchBoxModel {
304 app_list::AppListModel* appListModel = [appsGridController_ model];
305 return appListModel ? appListModel->search_box() : NULL;
308 - (app_list::AppListViewDelegate*)appListDelegate {
309 return [self delegate];
312 - (BOOL)control:(NSControl*)control
313 textView:(NSTextView*)textView
314 doCommandBySelector:(SEL)command {
315 if (showingSearchResults_)
316 return [appsSearchResultsController_ handleCommandBySelector:command];
318 // If anything has been written, let the search view handle it.
319 if ([[control stringValue] length] > 0)
323 if (command == @selector(complete:) ||
324 command == @selector(cancel:) ||
325 command == @selector(cancelOperation:)) {
327 delegate_->Dismiss();
331 // Possibly handle grid navigation.
332 return [appsGridController_ handleCommandBySelector:command];
335 - (void)modelTextDidChange {
336 app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
337 if (!searchBoxModel || !delegate_)
340 base::string16 query;
341 base::TrimWhitespace(searchBoxModel->text(), base::TRIM_ALL, &query);
342 BOOL shouldShowSearch = !query.empty();
343 [self revealSearchResults:shouldShowSearch];
344 if (shouldShowSearch)
345 delegate_->StartSearch();
347 delegate_->StopSearch();
350 - (app_list::AppListModel*)appListModel {
351 return [appsGridController_ model];
354 - (void)openResult:(app_list::SearchResult*)result {
356 delegate_->OpenSearchResult(
357 result, false /* auto_launch */, 0 /* event flags */);
362 [self modelTextDidChange];
365 - (void)onProfilesChanged {
366 [appsSearchBoxController_ rebuildMenu];
367 app_list::SigninDelegate* signinDelegate =
368 delegate_ ? delegate_->GetSigninDelegate() : NULL;
369 BOOL showSigninView = signinDelegate && signinDelegate->NeedSignin();
371 [[signinViewController_ view] removeFromSuperview];
372 signinViewController_.reset();
374 if (!showSigninView) {
375 [backgroundView_ setHidden:NO];
379 [backgroundView_ setHidden:YES];
380 signinViewController_.reset(
381 [[SigninViewController alloc] initWithFrame:[backgroundView_ frame]
382 cornerRadius:kBubbleCornerRadius
383 delegate:signinDelegate]);
384 [[self view] addSubview:[signinViewController_ view]];