app_list_view_controller.mm revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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  virtual void OnShutdown() 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
112void AppListModelObserverBridge::OnShutdown() {
113  [parent_ setDelegate:nil];
114}
115
116}  // namespace app_list
117
118@implementation AppListViewController
119
120- (id)init {
121  if ((self = [super init])) {
122    appsGridController_.reset([[AppsGridController alloc] init]);
123    [self loadAndSetView];
124
125    [self totalPagesChanged];
126    [self selectedPageChanged:0];
127    [appsGridController_ setPaginationObserver:self];
128  }
129  return self;
130}
131
132- (void)dealloc {
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];
138  [super dealloc];
139}
140
141- (AppsSearchBoxController*)searchBoxController {
142  return appsSearchBoxController_;
143}
144
145- (BOOL)showingSearchResults {
146  return showingSearchResults_;
147}
148
149- (AppsGridController*)appsGridController {
150  return appsGridController_;
151}
152
153- (NSSegmentedControl*)pagerControl {
154  return pagerControl_;
155}
156
157- (NSView*)backgroundView {
158  return backgroundView_;
159}
160
161- (app_list::AppListViewDelegate*)delegate {
162  return delegate_;
163}
164
165- (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate {
166  if (delegate_) {
167    // Ensure the search box is cleared when switching profiles.
168    if ([self searchBoxModel])
169      [self searchBoxModel]->SetText(base::string16());
170
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];
176  }
177  delegate_ = newDelegate;
178  if (delegate_) {
179    [loadingIndicator_ stopAnimation:self];
180  } else {
181    [loadingIndicator_ startAnimation:self];
182    return;
183  }
184
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];
191}
192
193-(void)loadAndSetView {
194  pagerControl_.reset([[AppListPagerView alloc] init]);
195  [pagerControl_ setTarget:appsGridController_];
196  [pagerControl_ setAction:@selector(onPagerClicked:)];
197
198  NSRect gridFrame = [[appsGridController_ view] frame];
199  NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
200      NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
201          [AppsGridController scrollerPadding]);
202
203  contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
204
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];
212
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]]);
224
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];
235
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];
244}
245
246- (void)revealSearchResults:(BOOL)show {
247  if (show == showingSearchResults_)
248    return;
249
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;
256
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.
259  if (show)
260    contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
261  else
262    resultsTargetRect.size.height = 0;
263
264  [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
265  [[contentsView_ animator] setFrame:contentsTargetRect];
266  [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
267}
268
269- (void)totalPagesChanged {
270  size_t pageCount = [appsGridController_ pageCount];
271  [pagerControl_ setSegmentCount:pageCount];
272
273  NSRect viewFrame = [[pagerControl_ superview] bounds];
274  CGFloat segmentWidth = std::min(
275      kMaxSegmentWidth,
276      (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
277
278  for (size_t i = 0; i < pageCount; ++i) {
279    [pagerControl_ setWidth:segmentWidth
280                 forSegment:i];
281    [[pagerControl_ cell] setTag:i
282                      forSegment:i];
283  }
284
285  // Center in view.
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)];
292}
293
294- (void)selectedPageChanged:(int)newSelected {
295  [pagerControl_ selectSegmentWithTag:newSelected];
296}
297
298- (void)pageVisibilityChanged {
299  [pagerControl_ setNeedsDisplay:YES];
300}
301
302- (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
303  return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
304}
305
306- (app_list::SearchBoxModel*)searchBoxModel {
307  app_list::AppListModel* appListModel = [appsGridController_ model];
308  return appListModel ? appListModel->search_box() : NULL;
309}
310
311- (app_list::AppListViewDelegate*)appListDelegate {
312  return [self delegate];
313}
314
315- (BOOL)control:(NSControl*)control
316               textView:(NSTextView*)textView
317    doCommandBySelector:(SEL)command {
318  if (showingSearchResults_)
319    return [appsSearchResultsController_ handleCommandBySelector:command];
320
321  // If anything has been written, let the search view handle it.
322  if ([[control stringValue] length] > 0)
323    return NO;
324
325  // Handle escape.
326  if (command == @selector(complete:) ||
327      command == @selector(cancel:) ||
328      command == @selector(cancelOperation:)) {
329    if (delegate_)
330      delegate_->Dismiss();
331    return YES;
332  }
333
334  // Possibly handle grid navigation.
335  return [appsGridController_ handleCommandBySelector:command];
336}
337
338- (void)modelTextDidChange {
339  app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
340  if (!searchBoxModel || !delegate_)
341    return;
342
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();
349  else
350    delegate_->StopSearch();
351}
352
353- (app_list::AppListModel*)appListModel {
354  return [appsGridController_ model];
355}
356
357- (void)openResult:(app_list::SearchResult*)result {
358  if (delegate_) {
359    delegate_->OpenSearchResult(
360        result, false /* auto_launch */, 0 /* event flags */);
361  }
362}
363
364- (void)redoSearch {
365  [self modelTextDidChange];
366}
367
368- (void)onProfilesChanged {
369  [appsSearchBoxController_ rebuildMenu];
370}
371
372@end
373