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 #import "ui/app_list/cocoa/app_list_pager_view.h"
17 #import "ui/app_list/cocoa/apps_grid_controller.h"
18 #include "ui/app_list/search_box_model.h"
19 #import "ui/base/cocoa/flipped_view.h"
20 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
24 // The roundedness of the corners of the bubble.
25 const CGFloat kBubbleCornerRadius = 3;
27 // Height of the pager.
28 const CGFloat kPagerPreferredHeight = 57;
30 // Height of separator line drawn between the searchbox and grid view.
31 const CGFloat kTopSeparatorSize = 1;
33 // Height of the search input.
34 const CGFloat kSearchInputHeight = 48;
36 // Minimum margin on either side of the pager. If the pager grows beyond this,
37 // the segment size is reduced.
38 const CGFloat kMinPagerMargin = 40;
39 // Maximum width of a single segment.
40 const CGFloat kMaxSegmentWidth = 80;
42 // Duration of the animation for sliding in and out search results.
43 const NSTimeInterval kResultsAnimationDuration = 0.2;
47 @interface BackgroundView : FlippedView;
50 @implementation BackgroundView
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);
60 [[NSBezierPath bezierPathWithRoundedRect:boundsRect
61 xRadius:kBubbleCornerRadius
62 yRadius:kBubbleCornerRadius] addClip];
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);
74 @interface AppListViewController ()
76 - (void)loadAndSetView;
77 - (void)revealSearchResults:(BOOL)show;
83 class AppListModelObserverBridge : public AppListViewDelegateObserver {
85 AppListModelObserverBridge(AppListViewController* parent);
86 ~AppListModelObserverBridge() override;
89 // Overridden from app_list::AppListViewDelegateObserver:
90 void OnProfilesChanged() override;
91 void OnShutdown() 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 void AppListModelObserverBridge::OnShutdown() {
113 [parent_ setDelegate:nil];
116 } // namespace app_list
118 @implementation AppListViewController
121 if ((self = [super init])) {
122 appsGridController_.reset([[AppsGridController alloc] init]);
123 [self loadAndSetView];
125 [self totalPagesChanged];
126 [self selectedPageChanged:0];
127 [appsGridController_ setPaginationObserver:self];
133 // Ensure that setDelegate(NULL) has been called before destruction, because
134 // dealloc can be called at odd times, and Objective C destruction order does
135 // not properly tear down these dependencies.
136 DCHECK(delegate_ == NULL);
137 [appsGridController_ setPaginationObserver:nil];
141 - (AppsSearchBoxController*)searchBoxController {
142 return appsSearchBoxController_;
145 - (BOOL)showingSearchResults {
146 return showingSearchResults_;
149 - (AppsGridController*)appsGridController {
150 return appsGridController_;
153 - (NSSegmentedControl*)pagerControl {
154 return pagerControl_;
157 - (NSView*)backgroundView {
158 return backgroundView_;
161 - (app_list::AppListViewDelegate*)delegate {
165 - (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate {
167 // Ensure the search box is cleared when switching profiles.
168 if ([self searchBoxModel])
169 [self searchBoxModel]->SetText(base::string16());
171 // First clean up, in reverse order.
172 app_list_model_observer_bridge_.reset();
173 [appsSearchResultsController_ setDelegate:nil];
174 [appsSearchBoxController_ setDelegate:nil];
175 [appsGridController_ setDelegate:nil];
177 delegate_ = newDelegate;
179 [loadingIndicator_ stopAnimation:self];
181 [loadingIndicator_ startAnimation:self];
185 [appsGridController_ setDelegate:delegate_];
186 [appsSearchBoxController_ setDelegate:self];
187 [appsSearchResultsController_ setDelegate:self];
188 app_list_model_observer_bridge_.reset(
189 new app_list::AppListModelObserverBridge(self));
190 [self onProfilesChanged];
193 -(void)loadAndSetView {
194 pagerControl_.reset([[AppListPagerView alloc] init]);
195 [pagerControl_ setTarget:appsGridController_];
196 [pagerControl_ setAction:@selector(onPagerClicked:)];
198 NSRect gridFrame = [[appsGridController_ view] frame];
199 NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
200 NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
201 [AppsGridController scrollerPadding]);
203 contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
205 // The contents view contains animations both from an NSCollectionView and the
206 // app list's own transitive drag layers. On Mavericks, the subviews need to
207 // have access to a compositing layer they can share. Otherwise the compositor
208 // makes tearing artifacts. However, doing this on Mountain Lion or earler
209 // results in flickering whilst an item is installing.
210 if (base::mac::IsOSMavericksOrLater())
211 [contentsView_ setWantsLayer:YES];
213 backgroundView_.reset(
214 [[BackgroundView alloc] initWithFrame:
215 NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]);
216 appsSearchBoxController_.reset(
217 [[AppsSearchBoxController alloc] initWithFrame:
218 NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]);
219 appsSearchResultsController_.reset(
220 [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
221 [contentsView_ bounds].size]);
222 base::scoped_nsobject<NSView> containerView(
223 [[NSView alloc] initWithFrame:[backgroundView_ frame]]);
225 loadingIndicator_.reset(
226 [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]);
227 [loadingIndicator_ setStyle:NSProgressIndicatorSpinningStyle];
228 [loadingIndicator_ sizeToFit];
229 NSRect indicatorRect = [loadingIndicator_ frame];
230 indicatorRect.origin.x = NSWidth(contentsRect) / 2 - NSMidX(indicatorRect);
231 indicatorRect.origin.y = NSHeight(contentsRect) / 2 - NSMidY(indicatorRect);
232 [loadingIndicator_ setFrame:indicatorRect];
233 [loadingIndicator_ setDisplayedWhenStopped:NO];
234 [loadingIndicator_ startAnimation:self];
236 [contentsView_ addSubview:[appsGridController_ view]];
237 [contentsView_ addSubview:pagerControl_];
238 [contentsView_ addSubview:loadingIndicator_];
239 [backgroundView_ addSubview:contentsView_];
240 [backgroundView_ addSubview:[appsSearchResultsController_ view]];
241 [backgroundView_ addSubview:[appsSearchBoxController_ view]];
242 [containerView addSubview:backgroundView_];
243 [self setView:containerView];
246 - (void)revealSearchResults:(BOOL)show {
247 if (show == showingSearchResults_)
250 showingSearchResults_ = show;
251 NSSize contentsSize = [contentsView_ frame].size;
252 NSRect resultsTargetRect = NSMakeRect(
253 0, kSearchInputHeight + kTopSeparatorSize,
254 contentsSize.width, contentsSize.height);
255 NSRect contentsTargetRect = resultsTargetRect;
257 // Shows results by sliding the grid and pager down to the bottom of the view.
258 // Hides results by collapsing the search results container to a height of 0.
260 contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
262 resultsTargetRect.size.height = 0;
264 [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
265 [[contentsView_ animator] setFrame:contentsTargetRect];
266 [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
269 - (void)totalPagesChanged {
270 size_t pageCount = [appsGridController_ pageCount];
271 [pagerControl_ setSegmentCount:pageCount];
273 NSRect viewFrame = [[pagerControl_ superview] bounds];
274 CGFloat segmentWidth = std::min(
276 (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
278 for (size_t i = 0; i < pageCount; ++i) {
279 [pagerControl_ setWidth:segmentWidth
281 [[pagerControl_ cell] setTag:i
286 [pagerControl_ sizeToFit];
287 [pagerControl_ setFrame:
288 NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]),
289 viewFrame.size.height - kPagerPreferredHeight,
290 [pagerControl_ bounds].size.width,
291 kPagerPreferredHeight)];
294 - (void)selectedPageChanged:(int)newSelected {
295 [pagerControl_ selectSegmentWithTag:newSelected];
298 - (void)pageVisibilityChanged {
299 [pagerControl_ setNeedsDisplay:YES];
302 - (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
303 return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
306 - (app_list::SearchBoxModel*)searchBoxModel {
307 app_list::AppListModel* appListModel = [appsGridController_ model];
308 return appListModel ? appListModel->search_box() : NULL;
311 - (app_list::AppListViewDelegate*)appListDelegate {
312 return [self delegate];
315 - (BOOL)control:(NSControl*)control
316 textView:(NSTextView*)textView
317 doCommandBySelector:(SEL)command {
318 if (showingSearchResults_)
319 return [appsSearchResultsController_ handleCommandBySelector:command];
321 // If anything has been written, let the search view handle it.
322 if ([[control stringValue] length] > 0)
326 if (command == @selector(complete:) ||
327 command == @selector(cancel:) ||
328 command == @selector(cancelOperation:)) {
330 delegate_->Dismiss();
334 // Possibly handle grid navigation.
335 return [appsGridController_ handleCommandBySelector:command];
338 - (void)modelTextDidChange {
339 app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
340 if (!searchBoxModel || !delegate_)
343 base::string16 query;
344 base::TrimWhitespace(searchBoxModel->text(), base::TRIM_ALL, &query);
345 BOOL shouldShowSearch = !query.empty();
346 [self revealSearchResults:shouldShowSearch];
347 if (shouldShowSearch)
348 delegate_->StartSearch();
350 delegate_->StopSearch();
353 - (app_list::AppListModel*)appListModel {
354 return [appsGridController_ model];
357 - (void)openResult:(app_list::SearchResult*)result {
359 delegate_->OpenSearchResult(
360 result, false /* auto_launch */, 0 /* event flags */);
364 - (void)onProfilesChanged {
365 [appsSearchBoxController_ rebuildMenu];