app_list_view_controller.mm revision f2477e01787aa58f445919b809d89e252beef54f
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_model_observer.h"
14#include "ui/app_list/app_list_view_delegate.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 AppListModelObserver {
85 public:
86  AppListModelObserverBridge(AppListViewController* parent);
87  virtual ~AppListModelObserverBridge();
88
89 private:
90  // Overridden from app_list::AppListModelObserver:
91  virtual void OnAppListModelSigninStatusChanged() 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_ appsGridController] model]->AddObserver(this);
102}
103
104AppListModelObserverBridge::~AppListModelObserverBridge() {
105  [[parent_ appsGridController] model]->RemoveObserver(this);
106}
107
108void AppListModelObserverBridge::OnAppListModelSigninStatusChanged() {
109  [parent_ onSigninStatusChanged];
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- (AppsGridController*)appsGridController {
138  return appsGridController_;
139}
140
141- (NSSegmentedControl*)pagerControl {
142  return pagerControl_;
143}
144
145- (NSView*)backgroundView {
146  return backgroundView_;
147}
148
149- (app_list::AppListViewDelegate*)delegate {
150  return delegate_.get();
151}
152
153- (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate {
154  if (delegate_) {
155    // First clean up, in reverse order.
156    app_list_model_observer_bridge_.reset();
157    [appsSearchResultsController_ setDelegate:nil];
158    [appsSearchBoxController_ setDelegate:nil];
159    [appsGridController_ setDelegate:nil];
160  }
161  delegate_.reset(newDelegate.release());
162  if (!delegate_)
163    return;
164  [appsGridController_ setDelegate:delegate_.get()];
165  [appsSearchBoxController_ setDelegate:self];
166  [appsSearchResultsController_ setDelegate:self];
167  app_list_model_observer_bridge_.reset(
168      new app_list::AppListModelObserverBridge(self));
169  [self onSigninStatusChanged];
170}
171
172-(void)loadAndSetView {
173  pagerControl_.reset([[AppListPagerView alloc] init]);
174  [pagerControl_ setTarget:appsGridController_];
175  [pagerControl_ setAction:@selector(onPagerClicked:)];
176
177  NSRect gridFrame = [[appsGridController_ view] frame];
178  NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
179      NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
180          [AppsGridController scrollerPadding]);
181
182  contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
183  backgroundView_.reset(
184      [[BackgroundView alloc] initWithFrame:
185              NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]);
186  appsSearchBoxController_.reset(
187      [[AppsSearchBoxController alloc] initWithFrame:
188          NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]);
189  appsSearchResultsController_.reset(
190      [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
191          [contentsView_ bounds].size]);
192  base::scoped_nsobject<NSView> containerView(
193      [[NSView alloc] initWithFrame:[backgroundView_ frame]]);
194
195  [contentsView_ addSubview:[appsGridController_ view]];
196  [contentsView_ addSubview:pagerControl_];
197  [backgroundView_ addSubview:contentsView_];
198  [backgroundView_ addSubview:[appsSearchResultsController_ view]];
199  [backgroundView_ addSubview:[appsSearchBoxController_ view]];
200  [containerView addSubview:backgroundView_];
201  [self setView:containerView];
202}
203
204- (void)revealSearchResults:(BOOL)show {
205  if (show == showingSearchResults_)
206    return;
207
208  showingSearchResults_ = show;
209  NSSize contentsSize = [contentsView_ frame].size;
210  NSRect resultsTargetRect = NSMakeRect(
211      0, kSearchInputHeight + kTopSeparatorSize,
212      contentsSize.width, contentsSize.height);
213  NSRect contentsTargetRect = resultsTargetRect;
214
215  // Shows results by sliding the grid and pager down to the bottom of the view.
216  // Hides results by collapsing the search results container to a height of 0.
217  if (show)
218    contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
219  else
220    resultsTargetRect.size.height = 0;
221
222  [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
223  [[contentsView_ animator] setFrame:contentsTargetRect];
224  [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
225}
226
227- (void)totalPagesChanged {
228  size_t pageCount = [appsGridController_ pageCount];
229  [pagerControl_ setSegmentCount:pageCount];
230
231  NSRect viewFrame = [[pagerControl_ superview] bounds];
232  CGFloat segmentWidth = std::min(
233      kMaxSegmentWidth,
234      (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
235
236  for (size_t i = 0; i < pageCount; ++i) {
237    [pagerControl_ setWidth:segmentWidth
238                 forSegment:i];
239    [[pagerControl_ cell] setTag:i
240                      forSegment:i];
241  }
242
243  // Center in view.
244  [pagerControl_ sizeToFit];
245  [pagerControl_ setFrame:
246      NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]),
247                 viewFrame.size.height - kPagerPreferredHeight,
248                 [pagerControl_ bounds].size.width,
249                 kPagerPreferredHeight)];
250}
251
252- (void)selectedPageChanged:(int)newSelected {
253  [pagerControl_ selectSegmentWithTag:newSelected];
254}
255
256- (void)pageVisibilityChanged {
257  [pagerControl_ setNeedsDisplay:YES];
258}
259
260- (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
261  return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
262}
263
264- (app_list::SearchBoxModel*)searchBoxModel {
265  app_list::AppListModel* appListModel = [appsGridController_ model];
266  return appListModel ? appListModel->search_box() : NULL;
267}
268
269- (app_list::AppListViewDelegate*)appListDelegate {
270  return [self delegate];
271}
272
273- (BOOL)control:(NSControl*)control
274               textView:(NSTextView*)textView
275    doCommandBySelector:(SEL)command {
276  if (showingSearchResults_)
277    return [appsSearchResultsController_ handleCommandBySelector:command];
278
279  // If anything has been written, let the search view handle it.
280  if ([[control stringValue] length] > 0)
281    return NO;
282
283  // Handle escape.
284  if (command == @selector(complete:) ||
285      command == @selector(cancel:) ||
286      command == @selector(cancelOperation:)) {
287    if (delegate_)
288      delegate_->Dismiss();
289    return YES;
290  }
291
292  // Possibly handle grid navigation.
293  return [appsGridController_ handleCommandBySelector:command];
294}
295
296- (void)modelTextDidChange {
297  app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
298  if (!searchBoxModel || !delegate_)
299    return;
300
301  base::string16 query;
302  TrimWhitespace(searchBoxModel->text(), TRIM_ALL, &query);
303  BOOL shouldShowSearch = !query.empty();
304  [self revealSearchResults:shouldShowSearch];
305  if (shouldShowSearch)
306    delegate_->StartSearch();
307  else
308    delegate_->StopSearch();
309}
310
311- (app_list::AppListModel*)appListModel {
312  return [appsGridController_ model];
313}
314
315- (void)openResult:(app_list::SearchResult*)result {
316  if (delegate_)
317    delegate_->OpenSearchResult(result, 0 /* event flags */);
318
319  [appsSearchBoxController_ clearSearch];
320}
321
322- (void)redoSearch {
323  [self modelTextDidChange];
324}
325
326- (void)onSigninStatusChanged {
327  [appsSearchBoxController_ rebuildMenu];
328  app_list::SigninDelegate* signinDelegate =
329      delegate_ ? delegate_->GetSigninDelegate() : NULL;
330  BOOL show_signin_view =
331      signinDelegate && ![appsGridController_ model]->signed_in();
332
333  [[signinViewController_ view] removeFromSuperview];
334  signinViewController_.reset();
335
336  if (!show_signin_view) {
337    [backgroundView_ setHidden:NO];
338    return;
339  }
340
341  [backgroundView_ setHidden:YES];
342  signinViewController_.reset(
343      [[SigninViewController alloc] initWithFrame:[backgroundView_ frame]
344                                     cornerRadius:kBubbleCornerRadius
345                                         delegate:signinDelegate]);
346  [[self view] addSubview:[signinViewController_ view]];
347}
348
349@end
350