// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "ui/app_list/cocoa/app_list_view_controller.h" #include "base/mac/foundation_util.h" #include "base/mac/mac_util.h" #include "base/strings/string_util.h" #include "base/strings/sys_string_conversions.h" #include "skia/ext/skia_utils_mac.h" #include "ui/app_list/app_list_constants.h" #include "ui/app_list/app_list_model.h" #include "ui/app_list/app_list_view_delegate.h" #include "ui/app_list/app_list_view_delegate_observer.h" #import "ui/app_list/cocoa/app_list_pager_view.h" #import "ui/app_list/cocoa/apps_grid_controller.h" #include "ui/app_list/search_box_model.h" #import "ui/base/cocoa/flipped_view.h" #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" namespace { // The roundedness of the corners of the bubble. const CGFloat kBubbleCornerRadius = 3; // Height of the pager. const CGFloat kPagerPreferredHeight = 57; // Height of separator line drawn between the searchbox and grid view. const CGFloat kTopSeparatorSize = 1; // Height of the search input. const CGFloat kSearchInputHeight = 48; // Minimum margin on either side of the pager. If the pager grows beyond this, // the segment size is reduced. const CGFloat kMinPagerMargin = 40; // Maximum width of a single segment. const CGFloat kMaxSegmentWidth = 80; // Duration of the animation for sliding in and out search results. const NSTimeInterval kResultsAnimationDuration = 0.2; } // namespace @interface BackgroundView : FlippedView; @end @implementation BackgroundView - (void)drawRect:(NSRect)dirtyRect { gfx::ScopedNSGraphicsContextSaveGState context; NSRect boundsRect = [self bounds]; NSRect searchAreaRect = NSMakeRect(0, 0, NSWidth(boundsRect), kSearchInputHeight); NSRect separatorRect = NSMakeRect(0, NSMaxY(searchAreaRect), NSWidth(boundsRect), kTopSeparatorSize); [[NSBezierPath bezierPathWithRoundedRect:boundsRect xRadius:kBubbleCornerRadius yRadius:kBubbleCornerRadius] addClip]; [gfx::SkColorToSRGBNSColor(app_list::kContentsBackgroundColor) set]; NSRectFill(boundsRect); [gfx::SkColorToSRGBNSColor(app_list::kSearchBoxBackground) set]; NSRectFill(searchAreaRect); [gfx::SkColorToSRGBNSColor(app_list::kTopSeparatorColor) set]; NSRectFill(separatorRect); } @end @interface AppListViewController () - (void)loadAndSetView; - (void)revealSearchResults:(BOOL)show; @end namespace app_list { class AppListModelObserverBridge : public AppListViewDelegateObserver { public: AppListModelObserverBridge(AppListViewController* parent); ~AppListModelObserverBridge() override; private: // Overridden from app_list::AppListViewDelegateObserver: void OnProfilesChanged() override; void OnShutdown() override; AppListViewController* parent_; // Weak. Owns us. DISALLOW_COPY_AND_ASSIGN(AppListModelObserverBridge); }; AppListModelObserverBridge::AppListModelObserverBridge( AppListViewController* parent) : parent_(parent) { [parent_ delegate]->AddObserver(this); } AppListModelObserverBridge::~AppListModelObserverBridge() { [parent_ delegate]->RemoveObserver(this); } void AppListModelObserverBridge::OnProfilesChanged() { [parent_ onProfilesChanged]; } void AppListModelObserverBridge::OnShutdown() { [parent_ setDelegate:nil]; } } // namespace app_list @implementation AppListViewController - (id)init { if ((self = [super init])) { appsGridController_.reset([[AppsGridController alloc] init]); [self loadAndSetView]; [self totalPagesChanged]; [self selectedPageChanged:0]; [appsGridController_ setPaginationObserver:self]; } return self; } - (void)dealloc { // Ensure that setDelegate(NULL) has been called before destruction, because // dealloc can be called at odd times, and Objective C destruction order does // not properly tear down these dependencies. DCHECK(delegate_ == NULL); [appsGridController_ setPaginationObserver:nil]; [super dealloc]; } - (AppsSearchBoxController*)searchBoxController { return appsSearchBoxController_; } - (BOOL)showingSearchResults { return showingSearchResults_; } - (AppsGridController*)appsGridController { return appsGridController_; } - (NSSegmentedControl*)pagerControl { return pagerControl_; } - (NSView*)backgroundView { return backgroundView_; } - (app_list::AppListViewDelegate*)delegate { return delegate_; } - (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate { if (delegate_) { // Ensure the search box is cleared when switching profiles. if ([self searchBoxModel]) [self searchBoxModel]->SetText(base::string16()); // First clean up, in reverse order. app_list_model_observer_bridge_.reset(); [appsSearchResultsController_ setDelegate:nil]; [appsSearchBoxController_ setDelegate:nil]; [appsGridController_ setDelegate:nil]; } delegate_ = newDelegate; if (delegate_) { [loadingIndicator_ stopAnimation:self]; } else { [loadingIndicator_ startAnimation:self]; return; } [appsGridController_ setDelegate:delegate_]; [appsSearchBoxController_ setDelegate:self]; [appsSearchResultsController_ setDelegate:self]; app_list_model_observer_bridge_.reset( new app_list::AppListModelObserverBridge(self)); [self onProfilesChanged]; } -(void)loadAndSetView { pagerControl_.reset([[AppListPagerView alloc] init]); [pagerControl_ setTarget:appsGridController_]; [pagerControl_ setAction:@selector(onPagerClicked:)]; NSRect gridFrame = [[appsGridController_ view] frame]; NSRect contentsRect = NSMakeRect(0, kSearchInputHeight + kTopSeparatorSize, NSWidth(gridFrame), NSHeight(gridFrame) + kPagerPreferredHeight - [AppsGridController scrollerPadding]); contentsView_.reset([[FlippedView alloc] initWithFrame:contentsRect]); // The contents view contains animations both from an NSCollectionView and the // app list's own transitive drag layers. On Mavericks, the subviews need to // have access to a compositing layer they can share. Otherwise the compositor // makes tearing artifacts. However, doing this on Mountain Lion or earler // results in flickering whilst an item is installing. if (base::mac::IsOSMavericksOrLater()) [contentsView_ setWantsLayer:YES]; backgroundView_.reset( [[BackgroundView alloc] initWithFrame: NSMakeRect(0, 0, NSMaxX(contentsRect), NSMaxY(contentsRect))]); appsSearchBoxController_.reset( [[AppsSearchBoxController alloc] initWithFrame: NSMakeRect(0, 0, NSWidth(contentsRect), kSearchInputHeight)]); appsSearchResultsController_.reset( [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize: [contentsView_ bounds].size]); base::scoped_nsobject containerView( [[NSView alloc] initWithFrame:[backgroundView_ frame]]); loadingIndicator_.reset( [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]); [loadingIndicator_ setStyle:NSProgressIndicatorSpinningStyle]; [loadingIndicator_ sizeToFit]; NSRect indicatorRect = [loadingIndicator_ frame]; indicatorRect.origin.x = NSWidth(contentsRect) / 2 - NSMidX(indicatorRect); indicatorRect.origin.y = NSHeight(contentsRect) / 2 - NSMidY(indicatorRect); [loadingIndicator_ setFrame:indicatorRect]; [loadingIndicator_ setDisplayedWhenStopped:NO]; [loadingIndicator_ startAnimation:self]; [contentsView_ addSubview:[appsGridController_ view]]; [contentsView_ addSubview:pagerControl_]; [contentsView_ addSubview:loadingIndicator_]; [backgroundView_ addSubview:contentsView_]; [backgroundView_ addSubview:[appsSearchResultsController_ view]]; [backgroundView_ addSubview:[appsSearchBoxController_ view]]; [containerView addSubview:backgroundView_]; [self setView:containerView]; } - (void)revealSearchResults:(BOOL)show { if (show == showingSearchResults_) return; showingSearchResults_ = show; NSSize contentsSize = [contentsView_ frame].size; NSRect resultsTargetRect = NSMakeRect( 0, kSearchInputHeight + kTopSeparatorSize, contentsSize.width, contentsSize.height); NSRect contentsTargetRect = resultsTargetRect; // Shows results by sliding the grid and pager down to the bottom of the view. // Hides results by collapsing the search results container to a height of 0. if (show) contentsTargetRect.origin.y += NSHeight(contentsTargetRect); else resultsTargetRect.size.height = 0; [[NSAnimationContext currentContext] setDuration:kResultsAnimationDuration]; [[contentsView_ animator] setFrame:contentsTargetRect]; [[[appsSearchResultsController_ view] animator] setFrame:resultsTargetRect]; } - (void)totalPagesChanged { size_t pageCount = [appsGridController_ pageCount]; [pagerControl_ setSegmentCount:pageCount]; NSRect viewFrame = [[pagerControl_ superview] bounds]; CGFloat segmentWidth = std::min( kMaxSegmentWidth, (viewFrame.size.width - 2 * kMinPagerMargin) / pageCount); for (size_t i = 0; i < pageCount; ++i) { [pagerControl_ setWidth:segmentWidth forSegment:i]; [[pagerControl_ cell] setTag:i forSegment:i]; } // Center in view. [pagerControl_ sizeToFit]; [pagerControl_ setFrame: NSMakeRect(NSMidX(viewFrame) - NSMidX([pagerControl_ bounds]), viewFrame.size.height - kPagerPreferredHeight, [pagerControl_ bounds].size.width, kPagerPreferredHeight)]; } - (void)selectedPageChanged:(int)newSelected { [pagerControl_ selectSegmentWithTag:newSelected]; } - (void)pageVisibilityChanged { [pagerControl_ setNeedsDisplay:YES]; } - (NSInteger)pagerSegmentAtLocation:(NSPoint)locationInWindow { return [pagerControl_ findAndHighlightSegmentAtLocation:locationInWindow]; } - (app_list::SearchBoxModel*)searchBoxModel { app_list::AppListModel* appListModel = [appsGridController_ model]; return appListModel ? appListModel->search_box() : NULL; } - (app_list::AppListViewDelegate*)appListDelegate { return [self delegate]; } - (BOOL)control:(NSControl*)control textView:(NSTextView*)textView doCommandBySelector:(SEL)command { if (showingSearchResults_) return [appsSearchResultsController_ handleCommandBySelector:command]; // If anything has been written, let the search view handle it. if ([[control stringValue] length] > 0) return NO; // Handle escape. if (command == @selector(complete:) || command == @selector(cancel:) || command == @selector(cancelOperation:)) { if (delegate_) delegate_->Dismiss(); return YES; } // Possibly handle grid navigation. return [appsGridController_ handleCommandBySelector:command]; } - (void)modelTextDidChange { app_list::SearchBoxModel* searchBoxModel = [self searchBoxModel]; if (!searchBoxModel || !delegate_) return; base::string16 query; base::TrimWhitespace(searchBoxModel->text(), base::TRIM_ALL, &query); BOOL shouldShowSearch = !query.empty(); [self revealSearchResults:shouldShowSearch]; if (shouldShowSearch) delegate_->StartSearch(); else delegate_->StopSearch(); } - (app_list::AppListModel*)appListModel { return [appsGridController_ model]; } - (void)openResult:(app_list::SearchResult*)result { if (delegate_) { delegate_->OpenSearchResult( result, false /* auto_launch */, 0 /* event flags */); } } - (void)onProfilesChanged { [appsSearchBoxController_ rebuildMenu]; } @end