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/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"
25 // The roundedness of the corners of the bubble.
26 const CGFloat kBubbleCornerRadius = 3;
28 // Height of the pager.
29 const CGFloat kPagerPreferredHeight = 57;
31 // Height of separator line drawn between the searchbox and grid view.
32 const CGFloat kTopSeparatorSize = 1;
34 // Height of the search input.
35 const CGFloat kSearchInputHeight = 48;
37 // Minimum margin on either side of the pager. If the pager grows beyond this,
38 // the segment size is reduced.
39 const CGFloat kMinPagerMargin = 40;
40 // Maximum width of a single segment.
41 const CGFloat kMaxSegmentWidth = 80;
43 // Duration of the animation for sliding in and out search results.
44 const NSTimeInterval kResultsAnimationDuration = 0.2;
48 @interface BackgroundView : FlippedView;
51 @implementation BackgroundView
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);
61 [[NSBezierPath bezierPathWithRoundedRect:boundsRect
62 xRadius:kBubbleCornerRadius
63 yRadius:kBubbleCornerRadius] addClip];
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);
75 @interface AppListViewController ()
77 - (void)loadAndSetView;
78 - (void)revealSearchResults:(BOOL)show;
84 class AppListModelObserverBridge : public AppListViewDelegateObserver {
86 AppListModelObserverBridge(AppListViewController* parent);
87 virtual ~AppListModelObserverBridge();
90 // Overridden from app_list::AppListViewDelegateObserver:
91 virtual void OnProfilesChanged() OVERRIDE;
93 AppListViewController* parent_; // Weak. Owns us.
95 DISALLOW_COPY_AND_ASSIGN(AppListModelObserverBridge);
98 AppListModelObserverBridge::AppListModelObserverBridge(
99 AppListViewController* parent)
101 [parent_ delegate]->AddObserver(this);
104 AppListModelObserverBridge::~AppListModelObserverBridge() {
105 [parent_ delegate]->RemoveObserver(this);
108 void AppListModelObserverBridge::OnProfilesChanged() {
109 [parent_ onProfilesChanged];
112 } // namespace app_list
114 @implementation AppListViewController
117 if ((self = [super init])) {
118 appsGridController_.reset([[AppsGridController alloc] init]);
119 [self loadAndSetView];
121 [self totalPagesChanged];
122 [self selectedPageChanged:0];
123 [appsGridController_ setPaginationObserver:self];
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];
137 - (AppsSearchBoxController*)searchBoxController {
138 return appsSearchBoxController_;
141 - (BOOL)showingSearchResults {
142 return showingSearchResults_;
145 - (AppsGridController*)appsGridController {
146 return appsGridController_;
149 - (NSSegmentedControl*)pagerControl {
150 return pagerControl_;
153 - (NSView*)backgroundView {
154 return backgroundView_;
157 - (app_list::AppListViewDelegate*)delegate {
158 return delegate_.get();
161 - (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate {
163 // Ensure the search box is cleared when switching profiles.
164 if ([self searchBoxModel])
165 [self searchBoxModel]->SetText(base::string16());
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];
173 delegate_.reset(newDelegate.release());
175 [loadingIndicator_ stopAnimation:self];
177 [loadingIndicator_ startAnimation:self];
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];
189 -(void)loadAndSetView {
190 pagerControl_.reset([[AppListPagerView alloc] init]);
191 [pagerControl_ setTarget:appsGridController_];
192 [pagerControl_ setAction:@selector(onPagerClicked:)];
194 NSRect gridFrame = [[appsGridController_ view] frame];
195 NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
196 NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
197 [AppsGridController scrollerPadding]);
199 contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
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];
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]]);
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];
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];
239 - (void)revealSearchResults:(BOOL)show {
240 if (show == showingSearchResults_)
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;
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.
253 contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
255 resultsTargetRect.size.height = 0;
257 [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
258 [[contentsView_ animator] setFrame:contentsTargetRect];
259 [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
262 - (void)totalPagesChanged {
263 size_t pageCount = [appsGridController_ pageCount];
264 [pagerControl_ setSegmentCount:pageCount];
266 NSRect viewFrame = [[pagerControl_ superview] bounds];
267 CGFloat segmentWidth = std::min(
269 (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
271 for (size_t i = 0; i < pageCount; ++i) {
272 [pagerControl_ setWidth:segmentWidth
274 [[pagerControl_ cell] setTag:i
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)];
287 - (void)selectedPageChanged:(int)newSelected {
288 [pagerControl_ selectSegmentWithTag:newSelected];
291 - (void)pageVisibilityChanged {
292 [pagerControl_ setNeedsDisplay:YES];
295 - (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
296 return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
299 - (app_list::SearchBoxModel*)searchBoxModel {
300 app_list::AppListModel* appListModel = [appsGridController_ model];
301 return appListModel ? appListModel->search_box() : NULL;
304 - (app_list::AppListViewDelegate*)appListDelegate {
305 return [self delegate];
308 - (BOOL)control:(NSControl*)control
309 textView:(NSTextView*)textView
310 doCommandBySelector:(SEL)command {
311 if (showingSearchResults_)
312 return [appsSearchResultsController_ handleCommandBySelector:command];
314 // If anything has been written, let the search view handle it.
315 if ([[control stringValue] length] > 0)
319 if (command == @selector(complete:) ||
320 command == @selector(cancel:) ||
321 command == @selector(cancelOperation:)) {
323 delegate_->Dismiss();
327 // Possibly handle grid navigation.
328 return [appsGridController_ handleCommandBySelector:command];
331 - (void)modelTextDidChange {
332 app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
333 if (!searchBoxModel || !delegate_)
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();
343 delegate_->StopSearch();
346 - (app_list::AppListModel*)appListModel {
347 return [appsGridController_ model];
350 - (void)openResult:(app_list::SearchResult*)result {
352 delegate_->OpenSearchResult(
353 result, false /* auto_launch */, 0 /* event flags */);
358 [self modelTextDidChange];
361 - (void)onProfilesChanged {
362 [appsSearchBoxController_ rebuildMenu];
363 app_list::SigninDelegate* signinDelegate =
364 delegate_ ? delegate_->GetSigninDelegate() : NULL;
365 BOOL showSigninView = signinDelegate && signinDelegate->NeedSignin();
367 [[signinViewController_ view] removeFromSuperview];
368 signinViewController_.reset();
370 if (!showSigninView) {
371 [backgroundView_ setHidden:NO];
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]];