blob: f4de6a16e4113d25609a782eecd538bb7386d988 [file] [log] [blame]
// 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"
#import "ui/base/cocoa/flipped_view.h"
#include "ui/app_list/search_box_model.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<NSView> 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)redoSearch {
[self modelTextDidChange];
}
- (void)onProfilesChanged {
[appsSearchBoxController_ rebuildMenu];
}
@end