blob: adb92ca8e3a1ac30e4ed8cd229f890354f379fdf [file] [log] [blame]
// Copyright (c) 2010 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 "extension_installed_bubble_controller.h"
#include "app/l10n_util.h"
#include "base/mac_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/cocoa/browser_window_cocoa.h"
#include "chrome/browser/cocoa/browser_window_controller.h"
#include "chrome/browser/cocoa/extensions/browser_actions_controller.h"
#include "chrome/browser/cocoa/hover_close_button.h"
#include "chrome/browser/cocoa/info_bubble_view.h"
#include "chrome/browser/cocoa/location_bar/location_bar_view_mac.h"
#include "chrome/browser/cocoa/toolbar_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_action.h"
#include "chrome/common/notification_registrar.h"
#include "chrome/common/notification_service.h"
#include "grit/generated_resources.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
// C++ class that receives EXTENSION_LOADED notifications and proxies them back
// to |controller|.
class ExtensionLoadedNotificationObserver : public NotificationObserver {
public:
ExtensionLoadedNotificationObserver(
ExtensionInstalledBubbleController* controller, Profile* profile)
: controller_(controller) {
registrar_.Add(this, NotificationType::EXTENSION_LOADED,
Source<Profile>(profile));
registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
Source<Profile>(profile));
}
private:
// NotificationObserver implementation. Tells the controller to start showing
// its window on the main thread when the extension has finished loading.
void Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
if (type == NotificationType::EXTENSION_LOADED) {
const Extension* extension = Details<const Extension>(details).ptr();
if (extension == [controller_ extension]) {
[controller_ performSelectorOnMainThread:@selector(showWindow:)
withObject:controller_
waitUntilDone:NO];
}
} else if (type == NotificationType::EXTENSION_UNLOADED) {
const Extension* extension = Details<const Extension>(details).ptr();
if (extension == [controller_ extension]) {
[controller_ performSelectorOnMainThread:@selector(extensionUnloaded:)
withObject:controller_
waitUntilDone:NO];
}
} else {
NOTREACHED() << "Received unexpected notification.";
}
}
NotificationRegistrar registrar_;
ExtensionInstalledBubbleController* controller_; // weak, owns us
};
@implementation ExtensionInstalledBubbleController
@synthesize extension = extension_;
@synthesize pageActionRemoved = pageActionRemoved_; // Exposed for unit test.
- (id)initWithParentWindow:(NSWindow*)parentWindow
extension:(const Extension*)extension
browser:(Browser*)browser
icon:(SkBitmap)icon {
NSString* nibPath =
[mac_util::MainAppBundle() pathForResource:@"ExtensionInstalledBubble"
ofType:@"nib"];
if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
DCHECK(parentWindow);
parentWindow_ = parentWindow;
DCHECK(extension);
extension_ = extension;
DCHECK(browser);
browser_ = browser;
icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
pageActionRemoved_ = NO;
if (!extension->omnibox_keyword().empty()) {
type_ = extension_installed_bubble::kOmniboxKeyword;
} else if (extension->browser_action()) {
type_ = extension_installed_bubble::kBrowserAction;
} else if (extension->page_action() &&
!extension->page_action()->default_icon_path().empty()) {
type_ = extension_installed_bubble::kPageAction;
} else {
NOTREACHED(); // kGeneric installs handled in the extension_install_ui.
}
// Start showing window only after extension has fully loaded.
extensionObserver_.reset(new ExtensionLoadedNotificationObserver(
self, browser->profile()));
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)close {
[parentWindow_ removeChildWindow:[self window]];
[super close];
}
- (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];
extension_ = NULL;
browser_ = NULL;
parentWindow_ = nil;
// We caught a close so we don't need to watch for the parent closing.
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self autorelease];
}
// 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 {
NSWindow* window = [self window];
DCHECK_EQ([notification object], window);
DCHECK([window isVisible]);
// 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];
[self close];
}
- (IBAction)closeWindow:(id)sender {
DCHECK([[self window] isVisible]);
[self close];
}
// Extracted to a function here so that it can be overwritten for unit
// testing.
- (void)removePageActionPreviewIfNecessary {
if (!extension_ || !extension_->page_action() || pageActionRemoved_)
return;
pageActionRemoved_ = YES;
BrowserWindowCocoa* window =
static_cast<BrowserWindowCocoa*>(browser_->window());
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
locationBarView->SetPreviewEnabledPageAction(extension_->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;
switch(type_) {
case extension_installed_bubble::kOmniboxKeyword: {
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
arrowPoint = locationBarView->GetPageInfoBubblePoint();
break;
}
case extension_installed_bubble::kBrowserAction: {
BrowserActionsController* controller =
[[window->cocoa_controller() toolbarController]
browserActionsController];
arrowPoint = [controller popupPointForBrowserAction:extension_];
break;
}
case extension_installed_bubble::kPageAction: {
LocationBarViewMac* locationBarView =
[window->cocoa_controller() locationBarBridge];
// 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(extension_->page_action(),
true);
// Find the center of the bottom of the page action icon.
arrowPoint =
locationBarView->GetPageActionBubblePoint(extension_->page_action());
break;
}
default: {
NOTREACHED() << "Generic extension type not allowed in install bubble.";
}
}
return arrowPoint;
}
// We want this to be a child of a browser window. addChildWindow:
// (called from this function) will bring the window on-screen;
// unfortunately, [NSWindowController showWindow:] will also bring it
// on-screen (but will cause unexpected changes to the window's
// position). We cannot have an addChildWindow: and a subsequent
// showWindow:. Thus, we have our own version.
- (void)showWindow:(id)sender {
// Generic extensions get an infobar rather than a bubble.
DCHECK(type_ != extension_installed_bubble::kGeneric);
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Load nib and calculate height based on messages to be shown.
NSWindow* window = [self initializeWindow];
int newWindowHeight = [self calculateWindowHeight];
[infoBubbleView_ 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.
NSPoint origin =
[parentWindow_ convertBaseToScreen:[self calculateArrowPoint]];
NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
info_bubble::kBubbleArrowWidth / 2.0, 0);
offsets = [[window contentView] convertSize:offsets toView:nil];
if ([infoBubbleView_ arrowLocation] == info_bubble::kTopRight)
origin.x -= NSWidth([window frame]) - offsets.width;
origin.y -= NSHeight([window frame]);
[window setFrameOrigin:origin];
[parentWindow_ addChildWindow:window
ordered:NSWindowAbove];
[window makeKeyAndOrderFront:self];
}
// 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 (type_ == extension_installed_bubble::kOmniboxKeyword) {
[infoBubbleView_ setArrowLocation:info_bubble::kTopLeft];
} else {
[infoBubbleView_ setArrowLocation:info_bubble::kTopRight];
}
// 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.
int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
// First part of extension installed message.
[extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF(
IDS_EXTENSION_INSTALLED_HEADING, UTF8ToUTF16(extension_->name()))];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:extensionInstalledMsg_];
newWindowHeight += [extensionInstalledMsg_ frame].size.height +
extension_installed_bubble::kInnerVerticalMargin;
// If type is page action, include a special message about page actions.
if (type_ == extension_installed_bubble::kPageAction) {
[extraInfoMsg_ setHidden:NO];
[[extraInfoMsg_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:extraInfoMsg_];
newWindowHeight += [extraInfoMsg_ frame].size.height +
extension_installed_bubble::kInnerVerticalMargin;
}
// If type is omnibox keyword, include a special message about the keyword.
if (type_ == extension_installed_bubble::kOmniboxKeyword) {
[extraInfoMsg_ setStringValue:l10n_util::GetNSStringF(
IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
UTF8ToUTF16(extension_->omnibox_keyword()))];
[extraInfoMsg_ setHidden:NO];
[[extraInfoMsg_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:extraInfoMsg_];
newWindowHeight += [extraInfoMsg_ frame].size.height +
extension_installed_bubble::kInnerVerticalMargin;
}
// Second part of extension installed message.
[[extensionInstalledInfoMsg_ cell]
setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_];
newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height;
return newWindowHeight;
}
// Adjust y-position of messages to sit properly in new window height.
- (void)setMessageFrames:(int)newWindowHeight {
// The extension messages will always be shown.
NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame];
NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame];
extensionMessageFrame1.origin.y = newWindowHeight - (
extensionMessageFrame1.size.height +
extension_installed_bubble::kOuterVerticalMargin);
[extensionInstalledMsg_ setFrame:extensionMessageFrame1];
if (type_ == extension_installed_bubble::kPageAction ||
type_ == extension_installed_bubble::kOmniboxKeyword) {
// The extra message is only shown when appropriate.
NSRect extraMessageFrame = [extraInfoMsg_ frame];
extraMessageFrame.origin.y = extensionMessageFrame1.origin.y - (
extraMessageFrame.size.height +
extension_installed_bubble::kInnerVerticalMargin);
[extraInfoMsg_ setFrame:extraMessageFrame];
extensionMessageFrame2.origin.y = extraMessageFrame.origin.y - (
extensionMessageFrame2.size.height +
extension_installed_bubble::kInnerVerticalMargin);
} else {
extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - (
extensionMessageFrame2.size.height +
extension_installed_bubble::kInnerVerticalMargin);
}
[extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2];
}
// Exposed for unit testing.
- (NSRect)getExtensionInstalledMsgFrame {
return [extensionInstalledMsg_ frame];
}
- (NSRect)getExtraInfoMsgFrame {
return [extraInfoMsg_ frame];
}
- (NSRect)getExtensionInstalledInfoMsgFrame {
return [extensionInstalledInfoMsg_ frame];
}
- (void)extensionUnloaded:(id)sender {
extension_ = NULL;
}
@end