app_list_view_controller.mm revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
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
92  AppListViewController* parent_;  // Weak. Owns us.
93
94  DISALLOW_COPY_AND_ASSIGN(AppListModelObserverBridge);
95};
96
97AppListModelObserverBridge::AppListModelObserverBridge(
98    AppListViewController* parent)
99    : parent_(parent) {
100  [parent_ delegate]->AddObserver(this);
101}
102
103AppListModelObserverBridge::~AppListModelObserverBridge() {
104  [parent_ delegate]->RemoveObserver(this);
105}
106
107void AppListModelObserverBridge::OnProfilesChanged() {
108  [parent_ onProfilesChanged];
109}
110
111}  // namespace app_list
112
113@implementation AppListViewController
114
115- (id)init {
116  if ((self = [super init])) {
117    appsGridController_.reset([[AppsGridController alloc] init]);
118    [self loadAndSetView];
119
120    [self totalPagesChanged];
121    [self selectedPageChanged:0];
122    [appsGridController_ setPaginationObserver:self];
123  }
124  return self;
125}
126
127- (void)dealloc {
128  // Ensure that setDelegate(NULL) has been called before destruction, because
129  // dealloc can be called at odd times, and Objective C destruction order does
130  // not properly tear down these dependencies.
131  DCHECK(delegate_ == NULL);
132  [appsGridController_ setPaginationObserver:nil];
133  [super dealloc];
134}
135
136- (AppsSearchBoxController*)searchBoxController {
137  return appsSearchBoxController_;
138}
139
140- (BOOL)showingSearchResults {
141  return showingSearchResults_;
142}
143
144- (AppsGridController*)appsGridController {
145  return appsGridController_;
146}
147
148- (NSSegmentedControl*)pagerControl {
149  return pagerControl_;
150}
151
152- (NSView*)backgroundView {
153  return backgroundView_;
154}
155
156- (app_list::AppListViewDelegate*)delegate {
157  return delegate_.get();
158}
159
160- (void)setDelegate:(scoped_ptr<app_list::AppListViewDelegate>)newDelegate {
161  if (delegate_) {
162    // Ensure the search box is cleared when switching profiles.
163    if ([self searchBoxModel])
164      [self searchBoxModel]->SetText(base::string16());
165
166    // First clean up, in reverse order.
167    app_list_model_observer_bridge_.reset();
168    [appsSearchResultsController_ setDelegate:nil];
169    [appsSearchBoxController_ setDelegate:nil];
170    [appsGridController_ setDelegate:nil];
171  }
172  delegate_.reset(newDelegate.release());
173  if (delegate_) {
174    [loadingIndicator_ stopAnimation:self];
175  } else {
176    [loadingIndicator_ startAnimation:self];
177    return;
178  }
179
180  [appsGridController_ setDelegate:delegate_.get()];
181  [appsSearchBoxController_ setDelegate:self];
182  [appsSearchResultsController_ setDelegate:self];
183  app_list_model_observer_bridge_.reset(
184      new app_list::AppListModelObserverBridge(self));
185  [self onProfilesChanged];
186}
187
188-(void)loadAndSetView {
189  pagerControl_.reset([[AppListPagerView alloc] init]);
190  [pagerControl_ setTarget:appsGridController_];
191  [pagerControl_ setAction:@selector(onPagerClicked:)];
192
193  NSRect gridFrame = [[appsGridController_ view] frame];
194  NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize,
195      NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight -
196          [AppsGridController scrollerPadding]);
197
198  contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]);
199
200  // The contents view contains animations both from an NSCollectionView and the
201  // app list's own transitive drag layers. On Mavericks, the subviews need to
202  // have access to a compositing layer they can share. Otherwise the compositor
203  // makes tearing artifacts. However, doing this on Mountain Lion or earler
204  // results in flickering whilst an item is installing.
205  if (base::mac::IsOSMavericksOrLater())
206    [contentsView_ setWantsLayer:YES];
207
208  backgroundView_.reset(
209      [[BackgroundView alloc] initWithFrame:
210              NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]);
211  appsSearchBoxController_.reset(
212      [[AppsSearchBoxController alloc] initWithFrame:
213          NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]);
214  appsSearchResultsController_.reset(
215      [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
216          [contentsView_ bounds].size]);
217  base::scoped_nsobject<NSView> containerView(
218      [[NSView alloc] initWithFrame:[backgroundView_ frame]]);
219
220  loadingIndicator_.reset(
221      [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]);
222  [loadingIndicator_ setStyle:NSProgressIndicatorSpinningStyle];
223  [loadingIndicator_ sizeToFit];
224  NSRect indicatorRect = [loadingIndicator_ frame];
225  indicatorRect.origin.x = NSWidth(contentsRect) / 2 - NSMidX(indicatorRect);
226  indicatorRect.origin.y = NSHeight(contentsRect) / 2 - NSMidY(indicatorRect);
227  [loadingIndicator_ setFrame:indicatorRect];
228  [loadingIndicator_ setDisplayedWhenStopped:NO];
229  [loadingIndicator_ startAnimation:self];
230
231  [contentsView_ addSubview:[appsGridController_ view]];
232  [contentsView_ addSubview:pagerControl_];
233  [contentsView_ addSubview:loadingIndicator_];
234  [backgroundView_ addSubview:contentsView_];
235  [backgroundView_ addSubview:[appsSearchResultsController_ view]];
236  [backgroundView_ addSubview:[appsSearchBoxController_ view]];
237  [containerView addSubview:backgroundView_];
238  [self setView:containerView];
239}
240
241- (void)revealSearchResults:(BOOL)show {
242  if (show == showingSearchResults_)
243    return;
244
245  showingSearchResults_ = show;
246  NSSize contentsSize = [contentsView_ frame].size;
247  NSRect resultsTargetRect = NSMakeRect(
248      0, kSearchInputHeight + kTopSeparatorSize,
249      contentsSize.width, contentsSize.height);
250  NSRect contentsTargetRect = resultsTargetRect;
251
252  // Shows results by sliding the grid and pager down to the bottom of the view.
253  // Hides results by collapsing the search results container to a height of 0.
254  if (show)
255    contentsTargetRect.origin.y += NSHeight(contentsTargetRect);
256  else
257    resultsTargetRect.size.height = 0;
258
259  [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration];
260  [[contentsView_ animator] setFrame:contentsTargetRect];
261  [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect];
262}
263
264- (void)totalPagesChanged {
265  size_t pageCount = [appsGridController_ pageCount];
266  [pagerControl_ setSegmentCount:pageCount];
267
268  NSRect viewFrame = [[pagerControl_ superview] bounds];
269  CGFloat segmentWidth = std::min(
270      kMaxSegmentWidth,
271      (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount);
272
273  for (size_t i = 0; i < pageCount; ++i) {
274    [pagerControl_ setWidth:segmentWidth
275                 forSegment:i];
276    [[pagerControl_ cell] setTag:i
277                      forSegment:i];
278  }
279
280  // Center in view.
281  [pagerControl_ sizeToFit];
282  [pagerControl_ setFrame:
283      NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]),
284                 viewFrame.size.height - kPagerPreferredHeight,
285                 [pagerControl_ bounds].size.width,
286                 kPagerPreferredHeight)];
287}
288
289- (void)selectedPageChanged:(int)newSelected {
290  [pagerControl_ selectSegmentWithTag:newSelected];
291}
292
293- (void)pageVisibilityChanged {
294  [pagerControl_ setNeedsDisplay:YES];
295}
296
297- (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow {
298  return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow];
299}
300
301- (app_list::SearchBoxModel*)searchBoxModel {
302  app_list::AppListModel* appListModel = [appsGridController_ model];
303  return appListModel ? appListModel->search_box() : NULL;
304}
305
306- (app_list::AppListViewDelegate*)appListDelegate {
307  return [self delegate];
308}
309
310- (BOOL)control:(NSControl*)control
311               textView:(NSTextView*)textView
312    doCommandBySelector:(SEL)command {
313  if (showingSearchResults_)
314    return [appsSearchResultsController_ handleCommandBySelector:command];
315
316  // If anything has been written, let the search view handle it.
317  if ([[control stringValue] length] > 0)
318    return NO;
319
320  // Handle escape.
321  if (command == @selector(complete:) ||
322      command == @selector(cancel:) ||
323      command == @selector(cancelOperation:)) {
324    if (delegate_)
325      delegate_->Dismiss();
326    return YES;
327  }
328
329  // Possibly handle grid navigation.
330  return [appsGridController_ handleCommandBySelector:command];
331}
332
333- (void)modelTextDidChange {
334  app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel];
335  if (!searchBoxModel || !delegate_)
336    return;
337
338  base::string16 query;
339  base::TrimWhitespace(searchBoxModel->text(), base::TRIM_ALL, &query);
340  BOOL shouldShowSearch = !query.empty();
341  [self revealSearchResults:shouldShowSearch];
342  if (shouldShowSearch)
343    delegate_->StartSearch();
344  else
345    delegate_->StopSearch();
346}
347
348- (app_list::AppListModel*)appListModel {
349  return [appsGridController_ model];
350}
351
352- (void)openResult:(app_list::SearchResult*)result {
353  if (delegate_) {
354    delegate_->OpenSearchResult(
355        result, false /* auto_launch */, 0 /* event flags */);
356  }
357}
358
359- (void)redoSearch {
360  [self modelTextDidChange];
361}
362
363- (void)onProfilesChanged {
364  [appsSearchBoxController_ rebuildMenu];
365}
366
367@end
368