blob: 537389ed4e4fea89c8677ef639bd7961af2adef3 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/cocoa/extensions/extension_installed_bubble_controller.h"
#include <stddef.h>
#include "base/i18n/rtl.h"
#include "base/macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/bundle_installer.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/chrome_style.h"
#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
#include "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/bubble_sync_promo_controller.h"
#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
#include "chrome/browser/ui/cocoa/extensions/bundle_util.h"
#include "chrome/browser/ui/cocoa/hover_close_button.h"
#include "chrome/browser/ui/cocoa/info_bubble_view.h"
#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
#include "chrome/browser/ui/cocoa/new_tab_button.h"
#include "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
#include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
#include "chrome/browser/ui/extensions/extension_install_ui_factory.h"
#include "chrome/browser/ui/extensions/extension_installed_bubble.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/sync/sync_promo_ui.h"
#include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
#include "chrome/common/extensions/sync_helper.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/bubble/bubble_controller.h"
#include "components/bubble/bubble_ui.h"
#include "components/signin/core/browser/signin_metrics.h"
#include "extensions/browser/install/extension_install_ui.h"
#include "extensions/common/extension.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#include "third_party/skia/include/core/SkBitmap.h"
#import "ui/base/cocoa/controls/hyperlink_text_view.h"
#include "ui/base/l10n/l10n_util.h"
using content::BrowserThread;
using extensions::BundleInstaller;
using extensions::Extension;
@interface ExtensionInstalledBubbleController ()
- (const Extension*)extension;
- (void)windowWillClose:(NSNotification*)notification;
- (void)windowDidResignKey:(NSNotification*)notification;
- (void)removePageActionPreviewIfNecessary;
- (NSPoint)calculateArrowPoint;
- (NSWindow*)initializeWindow;
- (int)calculateWindowHeight;
- (NSInteger)addExtensionList:(NSTextField*)headingMsg
itemsView:(NSView*)itemsView
state:(BundleInstaller::Item::State)state;
- (void)setMessageFrames:(int)newWindowHeight;
- (void)updateAnchorPosition;
@end // ExtensionInstalledBubbleController ()
namespace {
class ExtensionInstalledBubbleBridge : public BubbleUi {
public:
explicit ExtensionInstalledBubbleBridge(
ExtensionInstalledBubbleController* controller);
~ExtensionInstalledBubbleBridge() override;
private:
// BubbleUi:
void Show(BubbleReference bubble_reference) override;
void Close() override;
void UpdateAnchorPosition() override;
// Weak reference to the controller. |controller_| will outlive the bridge.
ExtensionInstalledBubbleController* controller_;
DISALLOW_COPY_AND_ASSIGN(ExtensionInstalledBubbleBridge);
};
ExtensionInstalledBubbleBridge::ExtensionInstalledBubbleBridge(
ExtensionInstalledBubbleController* controller)
: controller_(controller) {
}
ExtensionInstalledBubbleBridge::~ExtensionInstalledBubbleBridge() {
}
void ExtensionInstalledBubbleBridge::Show(BubbleReference bubble_reference) {
[controller_ setBubbleReference:bubble_reference];
[controller_ showWindow:controller_];
}
void ExtensionInstalledBubbleBridge::Close() {
[controller_ doClose];
}
void ExtensionInstalledBubbleBridge::UpdateAnchorPosition() {
[controller_ updateAnchorPosition];
}
} // namespace
// Cocoa specific implementation.
bool ExtensionInstalledBubble::ShouldShow() {
return true;
}
// Implemented here to create the platform specific instance of the BubbleUi.
scoped_ptr<BubbleUi> ExtensionInstalledBubble::BuildBubbleUi() {
// |controller| is owned by the parent window.
ExtensionInstalledBubbleController* controller =
[[ExtensionInstalledBubbleController alloc]
initWithParentWindow:browser()->window()->GetNativeWindow()
extensionBubble:this];
// The bridge to the C++ object that performs shared logic across platforms.
// This tells the controller when to show the bubble.
return make_scoped_ptr(new ExtensionInstalledBubbleBridge(controller));
}
@implementation ExtensionInstalledBubbleController
@synthesize bundle = bundle_;
@synthesize installedBubble = installedBubble_;
// Exposed for unit tests.
@synthesize heading = heading_;
@synthesize closeButton = closeButton_;
@synthesize howToUse = howToUse_;
@synthesize howToManage = howToManage_;
@synthesize appInstalledShortcutLink = appInstalledShortcutLink_;
@synthesize manageShortcutLink = manageShortcutLink_;
@synthesize promoContainer = promoContainer_;
@synthesize iconImage = iconImage_;
@synthesize pageActionPreviewShowing = pageActionPreviewShowing_;
- (id)initWithParentWindow:(NSWindow*)parentWindow
extensionBubble:(ExtensionInstalledBubble*)extensionBubble {
if ((self = [super initWithWindowNibPath:@"ExtensionInstalledBubble"
parentWindow:parentWindow
anchoredAt:NSZeroPoint])) {
DCHECK(extensionBubble);
const extensions::Extension* extension = extensionBubble->extension();
browser_ = extensionBubble->browser();
DCHECK(browser_);
icon_.reset([skia::SkBitmapToNSImage(extensionBubble->icon()) retain]);
pageActionPreviewShowing_ = NO;
type_ = extension->is_app() ? extension_installed_bubble::kApp :
extension_installed_bubble::kExtension;
installedBubble_ = extensionBubble;
}
return self;
}
- (id)initWithParentWindow:(NSWindow*)parentWindow
bundle:(const BundleInstaller*)bundle
browser:(Browser*)browser {
if ((self = [super initWithWindowNibPath:@"ExtensionInstalledBubbleBundle"
parentWindow:parentWindow
anchoredAt:NSZeroPoint])) {
bundle_ = bundle;
DCHECK(browser);
browser_ = browser;
icon_.reset([skia::SkBitmapToNSImage(SkBitmap()) retain]);
pageActionPreviewShowing_ = NO;
type_ = extension_installed_bubble::kBundle;
[self showWindow:self];
}
return self;
}
- (const Extension*)extension {
if (type_ == extension_installed_bubble::kBundle || !installedBubble_)
return nullptr;
return installedBubble_->extension();
}
- (void)windowWillClose:(NSNotification*)notification {
// Turn off page action icon preview when the window closes, unless we
// already removed it when the window resigned key status.
[self removePageActionPreviewIfNecessary];
browser_ = nullptr;
[closeButton_ setTrackingEnabled:NO];
[super windowWillClose:notification];
}
// The controller is the delegate of the window, so it receives "did resign
// key" notifications. When key is resigned, close the window.
- (void)windowDidResignKey:(NSNotification*)notification {
// If the browser window is closing, we need to remove the page action
// immediately, otherwise the closing animation may overlap with
// browser destruction.
[self removePageActionPreviewIfNecessary];
[super windowDidResignKey:notification];
}
- (IBAction)closeWindow:(id)sender {
DCHECK([[self window] isVisible]);
DCHECK([self bubbleReference]);
bool didClose =
[self bubbleReference]->CloseBubble(BUBBLE_CLOSE_USER_DISMISSED);
DCHECK(didClose);
}
// Extracted to a function here so that it can be overridden for unit testing.
- (void)removePageActionPreviewIfNecessary {
if (![self extension] || !pageActionPreviewShowing_)
return;
ExtensionAction* page_action =
extensions::ExtensionActionManager::Get(browser_->profile())->
GetPageAction(*[self extension]);
if (!page_action)
return;
pageActionPreviewShowing_ = NO;
BrowserWindowCocoa* window =
static_cast<BrowserWindowCocoa*>(browser_->window());
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
locationBarView->SetPreviewEnabledPageAction(page_action,
false); // disables preview.
}
// The extension installed bubble points at the browser action icon or the
// page action icon (shown as a preview), depending on the extension type.
// We need to calculate the location of these icons and the size of the
// message itself (which varies with the title of the extension) in order
// to figure out the origin point for the extension installed bubble.
// TODO(mirandac): add framework to easily test extension UI components!
- (NSPoint)calculateArrowPoint {
BrowserWindowCocoa* window =
static_cast<BrowserWindowCocoa*>(browser_->window());
NSPoint arrowPoint = NSZeroPoint;
auto getAppMenuButtonAnchorPoint = [window]() {
// Point at the bottom of the app menu menu.
NSView* appMenuButton =
[[window->cocoa_controller() toolbarController] appMenuButton];
const NSRect bounds = [appMenuButton bounds];
NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
return [appMenuButton convertPoint:anchor toView:nil];
};
if (type_ == extension_installed_bubble::kApp) {
TabStripView* view = [window->cocoa_controller() tabStripView];
NewTabButton* button = [view getNewTabButton];
NSRect bounds = [button bounds];
NSPoint anchor = NSMakePoint(
NSMidX(bounds),
NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
arrowPoint = [button convertPoint:anchor toView:nil];
} else if (type_ == extension_installed_bubble::kBundle) {
arrowPoint = getAppMenuButtonAnchorPoint();
} else {
DCHECK(installedBubble_);
switch (installedBubble_->anchor_position()) {
case ExtensionInstalledBubble::ANCHOR_BROWSER_ACTION: {
BrowserActionsController* controller =
[[window->cocoa_controller() toolbarController]
browserActionsController];
arrowPoint = [controller popupPointForId:[self extension]->id()];
break;
}
case ExtensionInstalledBubble::ANCHOR_PAGE_ACTION: {
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
ExtensionAction* page_action =
extensions::ExtensionActionManager::Get(browser_->profile())->
GetPageAction(*[self extension]);
// Tell the location bar to show a preview of the page action icon,
// which would ordinarily only be displayed on a page of the appropriate
// type. We remove this preview when the extension installed bubble
// closes.
locationBarView->SetPreviewEnabledPageAction(page_action, true);
pageActionPreviewShowing_ = YES;
// Find the center of the bottom of the page action icon.
arrowPoint = locationBarView->GetPageActionBubblePoint(page_action);
break;
}
case ExtensionInstalledBubble::ANCHOR_OMNIBOX: {
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
arrowPoint = locationBarView->GetPageInfoBubblePoint();
break;
}
case ExtensionInstalledBubble::ANCHOR_APP_MENU: {
arrowPoint = getAppMenuButtonAnchorPoint();
break;
}
}
}
return arrowPoint;
}
// Override -[BaseBubbleController showWindow:] to tweak bubble location and
// set up UI elements.
- (void)showWindow:(id)sender {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Load nib and calculate height based on messages to be shown.
NSWindow* window = [self initializeWindow];
int newWindowHeight = [self calculateWindowHeight];
[self.bubble setFrameSize:NSMakeSize(
NSWidth([[window contentView] bounds]), newWindowHeight)];
NSSize windowDelta = NSMakeSize(
0, newWindowHeight - NSHeight([[window contentView] bounds]));
windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
NSRect newFrame = [window frame];
newFrame.size.height += windowDelta.height;
[window setFrame:newFrame display:NO];
// Now that we have resized the window, adjust y pos of the messages.
[self setMessageFrames:newWindowHeight];
// Find window origin, taking into account bubble size and arrow location.
[self updateAnchorPosition];
[super showWindow:sender];
}
// Finish nib loading, set arrow location and load icon into window. This
// function is exposed for unit testing.
- (NSWindow*)initializeWindow {
NSWindow* window = [self window]; // completes nib load
if (installedBubble_ &&
installedBubble_->anchor_position() ==
ExtensionInstalledBubble::ANCHOR_OMNIBOX) {
[self.bubble setArrowLocation:info_bubble::kTopLeft];
} else {
[self.bubble setArrowLocation:info_bubble::kTopRight];
}
if (type_ == extension_installed_bubble::kBundle)
return window;
// Set appropriate icon, resizing if necessary.
if ([icon_ size].width > extension_installed_bubble::kIconSize) {
[icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
extension_installed_bubble::kIconSize)];
}
[iconImage_ setImage:icon_];
[iconImage_ setNeedsDisplay:YES];
return window;
}
// Calculate the height of each install message, resizing messages in their
// frames to fit window width. Return the new window height, based on the
// total of all message heights.
- (int)calculateWindowHeight {
// Adjust the window height to reflect the sum height of all messages
// and vertical padding.
// If there's few enough messages, the icon area may be larger than the
// messages.
int contentColumnHeight =
2 * extension_installed_bubble::kOuterVerticalMargin;
int iconColumnHeight = 2 * extension_installed_bubble::kOuterVerticalMargin +
NSHeight([iconImage_ frame]);
// If type is bundle, list the extensions that were installed and those that
// failed.
if (type_ == extension_installed_bubble::kBundle) {
NSInteger installedListHeight =
[self addExtensionList:installedHeadingMsg_
itemsView:installedItemsView_
state:BundleInstaller::Item::STATE_INSTALLED];
NSInteger failedListHeight =
[self addExtensionList:failedHeadingMsg_
itemsView:failedItemsView_
state:BundleInstaller::Item::STATE_FAILED];
contentColumnHeight += installedListHeight + failedListHeight;
// Put some space between the lists if both are present.
if (installedListHeight > 0 && failedListHeight > 0)
contentColumnHeight += extension_installed_bubble::kInnerVerticalMargin;
return std::max(contentColumnHeight, iconColumnHeight);
}
CGFloat syncPromoHeight = 0;
if (installedBubble_->options() & ExtensionInstalledBubble::SIGN_IN_PROMO) {
signin_metrics::AccessPoint accessPoint =
signin_metrics::AccessPoint::ACCESS_POINT_EXTENSION_INSTALL_BUBBLE;
syncPromoController_.reset(
[[BubbleSyncPromoController alloc]
initWithBrowser:browser_
promoStringId:IDS_EXTENSION_INSTALLED_SYNC_PROMO_NEW
linkStringId:IDS_EXTENSION_INSTALLED_SYNC_PROMO_LINK_NEW
accessPoint:accessPoint]);
[promoContainer_ addSubview:[syncPromoController_ view]];
// Resize the sync promo and its placeholder.
NSRect syncPromoPlaceholderFrame = [promoContainer_ frame];
CGFloat windowWidth = NSWidth([[self bubble] frame]);
syncPromoPlaceholderFrame.size.width = windowWidth;
syncPromoHeight =
[syncPromoController_ preferredHeightForWidth:windowWidth];
syncPromoPlaceholderFrame.size.height = syncPromoHeight;
[promoContainer_ setFrame:syncPromoPlaceholderFrame];
[[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
} else {
[promoContainer_ setHidden:YES];
}
// First part of extension installed message, the heading.
base::string16 extension_name =
base::UTF8ToUTF16([self extension]->name().c_str());
base::i18n::AdjustStringForLocaleDirection(&extension_name);
[heading_ setStringValue:l10n_util::GetNSStringF(
IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:heading_];
contentColumnHeight += NSHeight([heading_ frame]);
if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_USE) {
[howToUse_ setStringValue:base::SysUTF16ToNSString(
installedBubble_->GetHowToUseDescription())];
[howToUse_ setHidden:NO];
[[howToUse_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:howToUse_];
contentColumnHeight += NSHeight([howToUse_ frame]) +
extension_installed_bubble::kInnerVerticalMargin;
}
// If type is app, hide howToManage_, and include a "show me" link in the
// bubble.
if (type_ == extension_installed_bubble::kApp) {
[howToManage_ setHidden:YES];
[appShortcutLink_ setHidden:NO];
contentColumnHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
contentColumnHeight += NSHeight([appShortcutLink_ frame]);
} else if (installedBubble_->options() &
ExtensionInstalledBubble::HOW_TO_MANAGE) {
// Second part of extension installed message.
[[howToManage_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:howToManage_];
contentColumnHeight += NSHeight([howToManage_ frame]) +
extension_installed_bubble::kInnerVerticalMargin;
} else {
[howToManage_ setHidden:YES];
}
// Sync sign-in promo, if any.
if (syncPromoHeight > 0) {
// The sync promo goes at the bottom of the window and includes its own
// bottom margin. Thus, we subtract off the one of the outer margins, and
// apply it to both the icon area and content area.
int syncPromoDelta = extension_installed_bubble::kInnerVerticalMargin +
syncPromoHeight -
extension_installed_bubble::kOuterVerticalMargin;
contentColumnHeight += syncPromoDelta;
iconColumnHeight += syncPromoDelta;
}
if (installedBubble_->options() & ExtensionInstalledBubble::SHOW_KEYBINDING) {
[manageShortcutLink_ setHidden:NO];
[[manageShortcutLink_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[[manageShortcutLink_ cell]
setTextColor:skia::SkColorToCalibratedNSColor(
chrome_style::GetLinkColor())];
[GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
contentColumnHeight += extension_installed_bubble::kInnerVerticalMargin;
contentColumnHeight += NSHeight([manageShortcutLink_ frame]);
}
return std::max(contentColumnHeight, iconColumnHeight);
}
- (NSInteger)addExtensionList:(NSTextField*)headingMsg
itemsView:(NSView*)itemsView
state:(BundleInstaller::Item::State)state {
base::string16 heading = bundle_->GetHeadingTextFor(state);
bool hidden = heading.empty();
[headingMsg setHidden:hidden];
[itemsView setHidden:hidden];
if (hidden)
return 0;
[headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
[GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
CGFloat height =
PopulateBundleItemsList(bundle_->GetItemsWithState(state), itemsView);
NSRect frame = [itemsView frame];
frame.size.height = height;
[itemsView setFrame:frame];
return NSHeight([headingMsg frame]) +
extension_installed_bubble::kInnerVerticalMargin +
NSHeight([itemsView frame]);
}
// Adjust y-position of messages to sit properly in new window height.
- (void)setMessageFrames:(int)newWindowHeight {
if (type_ == extension_installed_bubble::kBundle) {
// Layout the messages from the bottom up.
NSView* msgs[] = { failedItemsView_, failedHeadingMsg_,
installedItemsView_, installedHeadingMsg_ };
NSInteger offsetFromBottom = 0;
BOOL isFirstVisible = YES;
for (size_t i = 0; i < arraysize(msgs); ++i) {
if ([msgs[i] isHidden])
continue;
NSRect frame = [msgs[i] frame];
NSInteger margin = isFirstVisible ?
extension_installed_bubble::kOuterVerticalMargin :
extension_installed_bubble::kInnerVerticalMargin;
frame.origin.y = offsetFromBottom + margin;
[msgs[i] setFrame:frame];
offsetFromBottom += NSHeight(frame) + margin;
isFirstVisible = NO;
}
// Move the close button a bit to vertically align it with the heading.
NSInteger closeButtonFudge = 1;
NSRect frame = [closeButton_ frame];
frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
extension_installed_bubble::kOuterVerticalMargin);
[closeButton_ setFrame:frame];
return;
}
NSRect headingFrame = [heading_ frame];
headingFrame.origin.y = newWindowHeight - (
NSHeight(headingFrame) +
extension_installed_bubble::kOuterVerticalMargin);
[heading_ setFrame:headingFrame];
int nextY = NSMinY(headingFrame);
auto adjustView = [](NSView* view, int* nextY) {
DCHECK(nextY);
NSRect frame = [view frame];
frame.origin.y = *nextY -
(NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
[view setFrame:frame];
*nextY = NSMinY(frame);
};
if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_USE)
adjustView(howToUse_, &nextY);
if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_MANAGE)
adjustView(howToManage_, &nextY);
if (installedBubble_->options() & ExtensionInstalledBubble::SHOW_KEYBINDING)
adjustView(manageShortcutLink_, &nextY);
if (installedBubble_->options() & ExtensionInstalledBubble::SIGN_IN_PROMO) {
// The sync promo goes at the bottom of the bubble, but that might be
// different than directly below the previous content if the icon is larger
// than the messages. Workaround by just always setting nextY to be at the
// bottom.
nextY = NSHeight([promoContainer_ frame]) +
extension_installed_bubble::kInnerVerticalMargin;
adjustView(promoContainer_, &nextY);
}
}
- (void)updateAnchorPosition {
self.anchorPoint =
[self.parentWindow convertBaseToScreen:[self calculateArrowPoint]];
}
- (IBAction)onManageShortcutClicked:(id)sender {
DCHECK([self bubbleReference]);
bool didClose = [self bubbleReference]->CloseBubble(BUBBLE_CLOSE_ACCEPTED);
DCHECK(didClose);
std::string configure_url = chrome::kChromeUIExtensionsURL;
configure_url += chrome::kExtensionConfigureCommandsSubPage;
chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
browser_, GURL(configure_url)));
chrome::Navigate(&params);
}
- (IBAction)onAppShortcutClicked:(id)sender {
scoped_ptr<extensions::ExtensionInstallUI> install_ui(
extensions::CreateExtensionInstallUI(browser_->profile()));
install_ui->OpenAppInstalledUI([self extension]->id());
}
- (void)doClose {
installedBubble_ = nullptr;
[self close];
}
@end