app_list_view_controller.mm revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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/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"
22
23namespace {
24
25// The roundedness of the corners of the bubble.
26const CGFloat kBubbleCornerRadius = 3;
27
28// Height of the pager.
29const CGFloat kPagerPreferredHeight = 57;
30
31// Height of separator line drawn between the searchbox and grid view.
32const CGFloat kTopSeparatorSize = 1;
33
34// Height of the search input.
35const CGFloat kSearchInputHeight = 48;
36
37// Minimum margin on either side of the pager. If the pager grows beyond this,
38// the segment size is reduced.
39const CGFloat kMinPagerMargin = 40;
40// Maximum width of a single segment.
41const CGFloat kMaxSegmentWidth = 80;
42
43// Duration of the animation for sliding in and out search results.
44const NSTimeInterval kResultsAnimationDuration = 0.2;
45
46}  // namespace
47
48@interface BackgroundView : FlippedView;
49@end
50
51@implementation BackgroundView
52
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);
60
61  [[NSBezierPath bezierPathWithRoundedRect:boundsRect
62                                   xRadius:kBubbleCornerRadius
63                                   yRadius:kBubbleCornerRadius] addClip];
64
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);
71}
72
73@end
74
75@interface AppListViewController ()
76
77- (void)loadAndSetView;
78- (void)revealSearchResults:(BOOL)show;
79
80@end
81
82namespace app_list {
83
84class AppListModelObserverBridge : public AppListViewDelegateObserver {
85 public:
86  AppListModelObserverBridge(AppListViewController* parent);
87  virtual ~AppListModelObserverBridge();
88
89 private:
90  // Overridden from app_list::AppListViewDelegateObserver:
91  virtual void OnProfilesChanged() 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
112}  // namespace app_list
113
114@implementation AppListViewController
115
116- (id)init {
117  if ((self = [super init])) {
118    appsGridController_.reset([[AppsGridController alloc] init]);
119    [self loadAndSetView];
120
121    [self totalPagesChanged];
122    [self selectedPageChanged:0];
123    [appsGridController_ setPaginationObserver:self];
124  }
125  return self;
126}
127
128- (void)dealloc {
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];
134  [super dealloc];
135}
136
137- (AppsSearchBoxController*)searchBoxController {
138  return appsSearchBoxController_;
139}
140
141- (BOOL)showingSearchResults {
142  return showingSearchResults_;
143}
144
145- (AppsGridController*)appsGridController {
146  return appsGridController_;
147}
148
149- (NSSegmentedControl*)pagerControl {
150  return pagerControl_;
151}
152
153- (NSView*)backgroundView {
154  return backgroundView_;
155}
156
157- (app_list::AppListViewDelegate*)delegate {
158  return delegate_.get();
159}
160
161- (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate {
162  if (delegate_) {
163    // Ensure the search box is cleared when switching profiles.
164    if ([self searchBoxModel])
165      [self searchBoxModel]->SetText(base::string16());
166
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];
172  }
173  delegate_.reset(newDelegate.release());
174  if (delegate_) {
175    [loadingIndicator_ stopAnimation:self];
176  } else {
177    [loadingIndicator_ startAnimation:self];
178    return;
179  }
180
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];
187}
188
189-(void)loadAndSetView {
190  pagerControl_.reset([[AppListPagerView alloc] init]);
191  [pagerControl_ setTarget:appsGridController_];
192  [pagerControl_ setAction:@selector(onPagerClicked:)];
193
194  NSRect gridFrame = [[appsGridController_ view] frame];
195  NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
196      NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
197          [AppsGridController scrollerPadding]);
198
199  contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
200
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];
205
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]]);
217
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];
228
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];
237}
238
239- (void)revealSearchResults:(BOOL)show {
240  if (show == showingSearchResults_)
241    return;
242
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;
249
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.
252  if (show)
253    contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
254  else
255    resultsTargetRect.size.height = 0;
256
257  [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
258  [[contentsView_ animator] setFrame:contentsTargetRect];
259  [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
260}
261
262- (void)totalPagesChanged {
263  size_t pageCount = [appsGridController_ pageCount];
264  [pagerControl_ setSegmentCount:pageCount];
265
266  NSRect viewFrame = [[pagerControl_ superview] bounds];
267  CGFloat segmentWidth = std::min(
268      kMaxSegmentWidth,
269      (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
270
271  for (size_t i = 0; i < pageCount; ++i) {
272    [pagerControl_ setWidth:segmentWidth
273                 forSegment:i];
274    [[pagerControl_ cell] setTag:i
275                      forSegment:i];
276  }
277
278  // Center in view.
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)];
285}
286
287- (void)selectedPageChanged:(int)newSelected {
288  [pagerControl_ selectSegmentWithTag:newSelected];
289}
290
291- (void)pageVisibilityChanged {
292  [pagerControl_ setNeedsDisplay:YES];
293}
294
295- (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
296  return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
297}
298
299- (app_list::SearchBoxModel*)searchBoxModel {
300  app_list::AppListModel* appListModel = [appsGridController_ model];
301  return appListModel ? appListModel->search_box() : NULL;
302}
303
304- (app_list::AppListViewDelegate*)appListDelegate {
305  return [self delegate];
306}
307
308- (BOOL)control:(NSControl*)control
309               textView:(NSTextView*)textView
310    doCommandBySelector:(SEL)command {
311  if (showingSearchResults_)
312    return [appsSearchResultsController_ handleCommandBySelector:command];
313
314  // If anything has been written, let the search view handle it.
315  if ([[control stringValue] length] > 0)
316    return NO;
317
318  // Handle escape.
319  if (command == @selector(complete:) ||
320      command == @selector(cancel:) ||
321      command == @selector(cancelOperation:)) {
322    if (delegate_)
323      delegate_->Dismiss();
324    return YES;
325  }
326
327  // Possibly handle grid navigation.
328  return [appsGridController_ handleCommandBySelector:command];
329}
330
331- (void)modelTextDidChange {
332  app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
333  if (!searchBoxModel || !delegate_)
334    return;
335
336  base::string16 query;
337  base::TrimWhitespace(searchBoxModel->text(), base::TRIM_ALL, &query);
338  BOOL shouldShowSearch = !query.empty();
339  [self revealSearchResults:shouldShowSearch];
340  if (shouldShowSearch)
341    delegate_->StartSearch();
342  else
343    delegate_->StopSearch();
344}
345
346- (app_list::AppListModel*)appListModel {
347  return [appsGridController_ model];
348}
349
350- (void)openResult:(app_list::SearchResult*)result {
351  if (delegate_) {
352    delegate_->OpenSearchResult(
353        result, false /* auto_launch */, 0 /* event flags */);
354  }
355}
356
357- (void)redoSearch {
358  [self modelTextDidChange];
359}
360
361- (void)onProfilesChanged {
362  [appsSearchBoxController_ rebuildMenu];
363  app_list::SigninDelegate* signinDelegate =
364      delegate_ ? delegate_->GetSigninDelegate() : NULL;
365  BOOL showSigninView = signinDelegate && signinDelegate->NeedSignin();
366
367  [[signinViewController_ view] removeFromSuperview];
368  signinViewController_.reset();
369
370  if (!showSigninView) {
371    [backgroundView_ setHidden:NO];
372    return;
373  }
374
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]];
381}
382
383@end
384