1 // Copyright (c) 2012 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.
5 #import "chrome/browser/ui/cocoa/extensions/extension_installed_bubble_controller.h"
7 #include "base/i18n/rtl.h"
8 #include "base/mac/bundle_locations.h"
9 #include "base/mac/mac_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/extensions/api/commands/command_service.h"
14 #include "chrome/browser/extensions/bundle_installer.h"
15 #include "chrome/browser/extensions/extension_action.h"
16 #include "chrome/browser/extensions/extension_action_manager.h"
17 #include "chrome/browser/extensions/extension_install_ui.h"
18 #include "chrome/browser/signin/signin_promo.h"
19 #include "chrome/browser/ui/browser.h"
20 #include "chrome/browser/ui/browser_navigator.h"
21 #include "chrome/browser/ui/browser_window.h"
22 #include "chrome/browser/ui/chrome_style.h"
23 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
24 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
25 #include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
26 #include "chrome/browser/ui/cocoa/hover_close_button.h"
27 #import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
28 #include "chrome/browser/ui/cocoa/info_bubble_view.h"
29 #include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
30 #include "chrome/browser/ui/cocoa/new_tab_button.h"
31 #include "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
32 #include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
33 #include "chrome/browser/ui/singleton_tabs.h"
34 #include "chrome/browser/ui/sync/sync_promo_ui.h"
35 #include "chrome/common/extensions/api/commands/commands_handler.h"
36 #include "chrome/common/extensions/api/extension_action/action_info.h"
37 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
38 #include "chrome/common/extensions/sync_helper.h"
39 #include "chrome/common/url_constants.h"
40 #include "content/public/browser/notification_details.h"
41 #include "content/public/browser/notification_registrar.h"
42 #include "content/public/browser/notification_source.h"
43 #include "extensions/common/extension.h"
44 #include "grit/chromium_strings.h"
45 #include "grit/generated_resources.h"
46 #import "skia/ext/skia_utils_mac.h"
47 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
48 #include "ui/base/l10n/l10n_util.h"
50 using content::BrowserThread;
51 using extensions::BundleInstaller;
52 using extensions::Extension;
53 using extensions::UnloadedExtensionInfo;
55 // C++ class that receives EXTENSION_LOADED notifications and proxies them back
57 class ExtensionLoadedNotificationObserver
58 : public content::NotificationObserver {
60 ExtensionLoadedNotificationObserver(
61 ExtensionInstalledBubbleController* controller, Profile* profile)
62 : controller_(controller) {
63 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
64 content::Source<Profile>(profile));
65 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED,
66 content::Source<Profile>(profile));
70 // NotificationObserver implementation. Tells the controller to start showing
71 // its window on the main thread when the extension has finished loading.
74 const content::NotificationSource& source,
75 const content::NotificationDetails& details) OVERRIDE {
76 if (type == chrome::NOTIFICATION_EXTENSION_LOADED) {
77 const Extension* extension =
78 content::Details<const Extension>(details).ptr();
79 if (extension == [controller_ extension]) {
80 [controller_ performSelectorOnMainThread:@selector(showWindow:)
81 withObject:controller_
84 } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED) {
85 const Extension* extension =
86 content::Details<const UnloadedExtensionInfo>(details)->extension;
87 if (extension == [controller_ extension]) {
88 [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:)
89 withObject:controller_
93 NOTREACHED() << "Received unexpected notification.";
97 content::NotificationRegistrar registrar_;
98 ExtensionInstalledBubbleController* controller_; // weak, owns us
101 @implementation ExtensionInstalledBubbleController
103 @synthesize extension = extension_;
104 @synthesize bundle = bundle_;
105 // Exposed for unit test.
106 @synthesize pageActionPreviewShowing = pageActionPreviewShowing_;
108 - (id)initWithParentWindow:(NSWindow*)parentWindow
109 extension:(const Extension*)extension
110 bundle:(const BundleInstaller*)bundle
111 browser:(Browser*)browser
112 icon:(SkBitmap)icon {
113 NSString* nibName = bundle ? @"ExtensionInstalledBubbleBundle" :
114 @"ExtensionInstalledBubble";
115 if ((self = [super initWithWindowNibPath:nibName
116 parentWindow:parentWindow
117 anchoredAt:NSZeroPoint])) {
118 extension_ = extension;
122 icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
123 pageActionPreviewShowing_ = NO;
126 type_ = extension_installed_bubble::kBundle;
127 } else if (extension->is_app()) {
128 type_ = extension_installed_bubble::kApp;
129 } else if (!extensions::OmniboxInfo::GetKeyword(extension).empty()) {
130 type_ = extension_installed_bubble::kOmniboxKeyword;
131 } else if (extensions::ActionInfo::GetBrowserActionInfo(extension)) {
132 type_ = extension_installed_bubble::kBrowserAction;
133 } else if (extensions::ActionInfo::GetPageActionInfo(extension) &&
134 extensions::ActionInfo::IsVerboseInstallMessage(extension)) {
135 type_ = extension_installed_bubble::kPageAction;
137 type_ = extension_installed_bubble::kGeneric;
140 if (type_ == extension_installed_bubble::kBundle) {
141 [self showWindow:self];
143 // Start showing window only after extension has fully loaded.
144 extensionObserver_.reset(new ExtensionLoadedNotificationObserver(
145 self, browser->profile()));
151 // Sets |promo_| based on |promoPlaceholder_|, sets |promoPlaceholder_| to nil.
152 - (void)initializeLabel {
153 // Replace the promo placeholder NSTextField with the real label NSTextView.
154 // The former doesn't show links in a nice way, but the latter can't be added
155 // in IB without a containing scroll view, so create the NSTextView
157 promo_.reset([[HyperlinkTextView alloc]
158 initWithFrame:[promoPlaceholder_ frame]]);
159 [promo_.get() setAutoresizingMask:[promoPlaceholder_ autoresizingMask]];
160 [[promoPlaceholder_ superview]
161 replaceSubview:promoPlaceholder_ with:promo_.get()];
162 promoPlaceholder_ = nil; // Now released.
163 [promo_.get() setDelegate:self];
166 // Returns YES if the sync promo should be shown in the bubble.
167 - (BOOL)showSyncPromo {
168 return extensions::sync_helper::IsSyncableExtension(extension_) &&
169 SyncPromoUI::ShouldShowSyncPromo(browser_->profile());
172 - (void)windowWillClose:(NSNotification*)notification {
173 // Turn off page action icon preview when the window closes, unless we
174 // already removed it when the window resigned key status.
175 [self removePageActionPreviewIfNecessary];
179 [super windowWillClose:notification];
182 // The controller is the delegate of the window, so it receives "did resign
183 // key" notifications. When key is resigned, close the window.
184 - (void)windowDidResignKey:(NSNotification*)notification {
185 // If the browser window is closing, we need to remove the page action
186 // immediately, otherwise the closing animation may overlap with
187 // browser destruction.
188 [self removePageActionPreviewIfNecessary];
189 [super windowDidResignKey:notification];
192 - (IBAction)closeWindow:(id)sender {
193 DCHECK([[self window] isVisible]);
197 - (BOOL)textView:(NSTextView*)aTextView
198 clickedOnLink:(id)link
199 atIndex:(NSUInteger)charIndex {
200 DCHECK_EQ(promo_.get(), aTextView);
202 signin::GetPromoURL(signin::SOURCE_EXTENSION_INSTALL_BUBBLE, false);
203 chrome::NavigateParams params(
204 chrome::GetSingletonTabNavigateParams(browser_, promo_url));
205 chrome::Navigate(¶ms);
209 // Extracted to a function here so that it can be overridden for unit testing.
210 - (void)removePageActionPreviewIfNecessary {
211 if (!extension_ || !pageActionPreviewShowing_)
213 ExtensionAction* page_action =
214 extensions::ExtensionActionManager::Get(browser_->profile())->
215 GetPageAction(*extension_);
218 pageActionPreviewShowing_ = NO;
220 BrowserWindowCocoa* window =
221 static_cast<BrowserWindowCocoa*>(browser_->window());
222 LocationBarViewMac* locationBarView =
223 [window->cocoa_controller() locationBarBridge];
224 locationBarView->SetPreviewEnabledPageAction(page_action,
225 false); // disables preview.
228 // The extension installed bubble points at the browser action icon or the
229 // page action icon (shown as a preview), depending on the extension type.
230 // We need to calculate the location of these icons and the size of the
231 // message itself (which varies with the title of the extension) in order
232 // to figure out the origin point for the extension installed bubble.
233 // TODO(mirandac): add framework to easily test extension UI components!
234 - (NSPoint)calculateArrowPoint {
235 BrowserWindowCocoa* window =
236 static_cast<BrowserWindowCocoa*>(browser_->window());
237 NSPoint arrowPoint = NSZeroPoint;
240 case extension_installed_bubble::kApp: {
241 TabStripView* view = [window->cocoa_controller() tabStripView];
242 NewTabButton* button = [view getNewTabButton];
243 NSRect bounds = [button bounds];
244 NSPoint anchor = NSMakePoint(
246 NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
247 arrowPoint = [button convertPoint:anchor toView:nil];
250 case extension_installed_bubble::kOmniboxKeyword: {
251 LocationBarViewMac* locationBarView =
252 [window->cocoa_controller() locationBarBridge];
253 arrowPoint = locationBarView->GetPageInfoBubblePoint();
256 case extension_installed_bubble::kBrowserAction: {
257 BrowserActionsController* controller =
258 [[window->cocoa_controller() toolbarController]
259 browserActionsController];
260 arrowPoint = [controller popupPointForBrowserAction:extension_];
263 case extension_installed_bubble::kPageAction: {
264 LocationBarViewMac* locationBarView =
265 [window->cocoa_controller() locationBarBridge];
267 ExtensionAction* page_action =
268 extensions::ExtensionActionManager::Get(browser_->profile())->
269 GetPageAction(*extension_);
271 // Tell the location bar to show a preview of the page action icon, which
272 // would ordinarily only be displayed on a page of the appropriate type.
273 // We remove this preview when the extension installed bubble closes.
274 locationBarView->SetPreviewEnabledPageAction(page_action, true);
275 pageActionPreviewShowing_ = YES;
277 // Find the center of the bottom of the page action icon.
279 locationBarView->GetPageActionBubblePoint(page_action);
282 case extension_installed_bubble::kBundle:
283 case extension_installed_bubble::kGeneric: {
284 // Point at the bottom of the wrench menu.
285 NSView* wrenchButton =
286 [[window->cocoa_controller() toolbarController] wrenchButton];
287 const NSRect bounds = [wrenchButton bounds];
288 NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
289 arrowPoint = [wrenchButton convertPoint:anchor toView:nil];
299 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
300 // set up UI elements.
301 - (void)showWindow:(id)sender {
302 DCHECK_CURRENTLY_ON(BrowserThread::UI);
304 // Load nib and calculate height based on messages to be shown.
305 NSWindow* window = [self initializeWindow];
306 int newWindowHeight = [self calculateWindowHeight];
307 [self.bubble setFrameSize:NSMakeSize(
308 NSWidth([[window contentView] bounds]), newWindowHeight)];
309 NSSize windowDelta = NSMakeSize(
310 0, newWindowHeight - NSHeight([[window contentView] bounds]));
311 windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
312 NSRect newFrame = [window frame];
313 newFrame.size.height += windowDelta.height;
314 [window setFrame:newFrame display:NO];
316 // Now that we have resized the window, adjust y pos of the messages.
317 [self setMessageFrames:newWindowHeight];
319 // Find window origin, taking into account bubble size and arrow location.
321 [self.parentWindow convertBaseToScreen:[self calculateArrowPoint]];
322 [super showWindow:sender];
325 // Finish nib loading, set arrow location and load icon into window. This
326 // function is exposed for unit testing.
327 - (NSWindow*)initializeWindow {
328 NSWindow* window = [self window]; // completes nib load
330 if (type_ == extension_installed_bubble::kOmniboxKeyword) {
331 [self.bubble setArrowLocation:info_bubble::kTopLeft];
333 [self.bubble setArrowLocation:info_bubble::kTopRight];
336 if (type_ == extension_installed_bubble::kBundle)
339 // Set appropriate icon, resizing if necessary.
340 if ([icon_ size].width > extension_installed_bubble::kIconSize) {
341 [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
342 extension_installed_bubble::kIconSize)];
344 [iconImage_ setImage:icon_];
345 [iconImage_ setNeedsDisplay:YES];
349 - (bool)hasActivePageAction:(extensions::Command*)command {
350 extensions::CommandService* command_service =
351 extensions::CommandService::Get(browser_->profile());
352 if (type_ == extension_installed_bubble::kPageAction) {
353 if (extensions::CommandsInfo::GetPageActionCommand(extension_) &&
354 command_service->GetPageActionCommand(
356 extensions::CommandService::ACTIVE_ONLY,
366 - (bool)hasActiveBrowserAction:(extensions::Command*)command {
367 extensions::CommandService* command_service =
368 extensions::CommandService::Get(browser_->profile());
369 if (type_ == extension_installed_bubble::kBrowserAction) {
370 if (extensions::CommandsInfo::GetBrowserActionCommand(extension_) &&
371 command_service->GetBrowserActionCommand(
373 extensions::CommandService::ACTIVE_ONLY,
383 - (NSString*)installMessageForCurrentExtensionAction {
384 if (type_ == extension_installed_bubble::kPageAction) {
385 extensions::Command page_action_command;
386 if ([self hasActivePageAction:&page_action_command]) {
387 return l10n_util::GetNSStringF(
388 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT,
389 page_action_command.accelerator().GetShortcutText());
391 return l10n_util::GetNSString(
392 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO);
395 CHECK_EQ(extension_installed_bubble::kBrowserAction, type_);
396 extensions::Command browser_action_command;
397 if ([self hasActiveBrowserAction:&browser_action_command]) {
398 return l10n_util::GetNSStringF(
399 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT,
400 browser_action_command.accelerator().GetShortcutText());
402 return l10n_util::GetNSString(
403 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO);
408 // Calculate the height of each install message, resizing messages in their
409 // frames to fit window width. Return the new window height, based on the
410 // total of all message heights.
411 - (int)calculateWindowHeight {
412 // Adjust the window height to reflect the sum height of all messages
413 // and vertical padding.
414 int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
416 // If type is bundle, list the extensions that were installed and those that
418 if (type_ == extension_installed_bubble::kBundle) {
419 NSInteger installedListHeight =
420 [self addExtensionList:installedHeadingMsg_
421 itemsMsg:installedItemsMsg_
422 state:BundleInstaller::Item::STATE_INSTALLED];
424 NSInteger failedListHeight =
425 [self addExtensionList:failedHeadingMsg_
426 itemsMsg:failedItemsMsg_
427 state:BundleInstaller::Item::STATE_FAILED];
429 newWindowHeight += installedListHeight + failedListHeight;
431 // Put some space between the lists if both are present.
432 if (installedListHeight > 0 && failedListHeight > 0)
433 newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
435 return newWindowHeight;
438 int sync_promo_height = 0;
439 if ([self showSyncPromo]) {
440 // First calculate the height of the sign-in promo.
441 NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
443 NSString* link(l10n_util::GetNSStringWithFixup(
444 IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK));
445 NSString* message(l10n_util::GetNSStringWithFixup(
446 IDS_EXTENSION_INSTALLED_SIGNIN_PROMO));
448 HyperlinkTextView* view = promo_.get();
449 [view setMessageAndLink:message
453 messageColor:[NSColor blackColor]
454 linkColor:gfx::SkColorToCalibratedNSColor(
455 chrome_style::GetLinkColor())];
457 // HACK! The TextView does not report correct height even after you stuff
458 // it with text (it tells you it is single-line even if it is multiline), so
459 // here the hidden howToUse_ TextField is temporarily repurposed to
460 // calculate the correct height for the TextView.
461 [[howToUse_ cell] setAttributedStringValue:[promo_ attributedString]];
462 [GTMUILocalizerAndLayoutTweaker
463 sizeToFitFixedWidthTextField:howToUse_];
464 sync_promo_height = NSHeight([howToUse_ frame]);
467 // First part of extension installed message, the heading.
468 base::string16 extension_name = base::UTF8ToUTF16(extension_->name().c_str());
469 base::i18n::AdjustStringForLocaleDirection(&extension_name);
470 [heading_ setStringValue:l10n_util::GetNSStringF(
471 IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
472 [GTMUILocalizerAndLayoutTweaker
473 sizeToFitFixedWidthTextField:heading_];
474 newWindowHeight += NSHeight([heading_ frame]) +
475 extension_installed_bubble::kInnerVerticalMargin;
477 // If type is browser/page action, include a special message about them.
478 if (type_ == extension_installed_bubble::kBrowserAction ||
479 type_ == extension_installed_bubble::kPageAction) {
480 [howToUse_ setStringValue:[self
481 installMessageForCurrentExtensionAction]];
482 [howToUse_ setHidden:NO];
484 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
485 [GTMUILocalizerAndLayoutTweaker
486 sizeToFitFixedWidthTextField:howToUse_];
487 newWindowHeight += NSHeight([howToUse_ frame]) +
488 extension_installed_bubble::kInnerVerticalMargin;
491 // If type is omnibox keyword, include a special message about the keyword.
492 if (type_ == extension_installed_bubble::kOmniboxKeyword) {
493 [howToUse_ setStringValue:l10n_util::GetNSStringF(
494 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
495 base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension_)))];
496 [howToUse_ setHidden:NO];
498 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
499 [GTMUILocalizerAndLayoutTweaker
500 sizeToFitFixedWidthTextField:howToUse_];
501 newWindowHeight += NSHeight([howToUse_ frame]) +
502 extension_installed_bubble::kInnerVerticalMargin;
505 // If type is app, hide howToManage_, and include a "show me" link in the
507 if (type_ == extension_installed_bubble::kApp) {
508 [howToManage_ setHidden:YES];
509 [appShortcutLink_ setHidden:NO];
510 newWindowHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
511 newWindowHeight += NSHeight([appShortcutLink_ frame]);
513 // Second part of extension installed message.
515 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
516 [GTMUILocalizerAndLayoutTweaker
517 sizeToFitFixedWidthTextField:howToManage_];
518 newWindowHeight += NSHeight([howToManage_ frame]);
521 // Sync sign-in promo, if any.
522 if (sync_promo_height > 0) {
523 NSRect promo_frame = [promo_.get() frame];
524 promo_frame.size.height = sync_promo_height;
525 [promo_.get() setFrame:promo_frame];
526 newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
527 newWindowHeight += sync_promo_height;
530 extensions::Command command;
531 if ([self hasActivePageAction:&command] ||
532 [self hasActiveBrowserAction:&command]) {
533 [manageShortcutLink_ setHidden:NO];
534 [[manageShortcutLink_ cell]
535 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
536 [[manageShortcutLink_ cell]
537 setTextColor:gfx::SkColorToCalibratedNSColor(
538 chrome_style::GetLinkColor())];
539 [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
540 newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
541 newWindowHeight += NSHeight([manageShortcutLink_ frame]);
544 return newWindowHeight;
547 - (NSInteger)addExtensionList:(NSTextField*)headingMsg
548 itemsMsg:(NSTextField*)itemsMsg
549 state:(BundleInstaller::Item::State)state {
550 base::string16 heading = bundle_->GetHeadingTextFor(state);
551 bool hidden = heading.empty();
552 [headingMsg setHidden:hidden];
553 [itemsMsg setHidden:hidden];
557 [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
558 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
560 NSMutableString* joinedItems = [NSMutableString string];
561 BundleInstaller::ItemList items = bundle_->GetItemsWithState(state);
562 for (size_t i = 0; i < items.size(); ++i) {
564 [joinedItems appendString:@"\n"];
565 [joinedItems appendString:base::SysUTF16ToNSString(
566 items[i].GetNameForDisplay())];
569 [itemsMsg setStringValue:joinedItems];
570 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:itemsMsg];
572 return NSHeight([headingMsg frame]) +
573 extension_installed_bubble::kInnerVerticalMargin +
574 NSHeight([itemsMsg frame]);
577 // Adjust y-position of messages to sit properly in new window height.
578 - (void)setMessageFrames:(int)newWindowHeight {
579 if (type_ == extension_installed_bubble::kBundle) {
580 // Layout the messages from the bottom up.
581 NSTextField* msgs[] = { failedItemsMsg_, failedHeadingMsg_,
582 installedItemsMsg_, installedHeadingMsg_ };
583 NSInteger offsetFromBottom = 0;
584 BOOL isFirstVisible = YES;
585 for (size_t i = 0; i < arraysize(msgs); ++i) {
586 if ([msgs[i] isHidden])
589 NSRect frame = [msgs[i] frame];
590 NSInteger margin = isFirstVisible ?
591 extension_installed_bubble::kOuterVerticalMargin :
592 extension_installed_bubble::kInnerVerticalMargin;
594 frame.origin.y = offsetFromBottom + margin;
595 [msgs[i] setFrame:frame];
596 offsetFromBottom += NSHeight(frame) + margin;
601 // Move the close button a bit to vertically align it with the heading.
602 NSInteger closeButtonFudge = 1;
603 NSRect frame = [closeButton_ frame];
604 frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
605 extension_installed_bubble::kOuterVerticalMargin);
606 [closeButton_ setFrame:frame];
611 NSRect headingFrame = [heading_ frame];
612 headingFrame.origin.y = newWindowHeight - (
613 NSHeight(headingFrame) +
614 extension_installed_bubble::kOuterVerticalMargin);
615 [heading_ setFrame:headingFrame];
617 NSRect howToManageFrame = [howToManage_ frame];
618 if (!extensions::OmniboxInfo::GetKeyword(extension_).empty() ||
619 extensions::ActionInfo::GetBrowserActionInfo(extension_) ||
620 extensions::ActionInfo::IsVerboseInstallMessage(extension_)) {
621 // For browser actions, page actions and omnibox keyword show the
622 // 'how to use' message before the 'how to manage' message.
623 NSRect howToUseFrame = [howToUse_ frame];
624 howToUseFrame.origin.y = headingFrame.origin.y - (
625 NSHeight(howToUseFrame) +
626 extension_installed_bubble::kInnerVerticalMargin);
627 [howToUse_ setFrame:howToUseFrame];
629 howToManageFrame.origin.y = howToUseFrame.origin.y - (
630 NSHeight(howToManageFrame) +
631 extension_installed_bubble::kInnerVerticalMargin);
633 howToManageFrame.origin.y = NSMinY(headingFrame) - (
634 NSHeight(howToManageFrame) +
635 extension_installed_bubble::kInnerVerticalMargin);
637 [howToManage_ setFrame:howToManageFrame];
639 NSRect frame = howToManageFrame;
640 if ([self showSyncPromo]) {
641 frame = [promo_.get() frame];
642 frame.origin.y = NSMinY(howToManageFrame) -
643 (NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
644 [promo_.get() setFrame:frame];
647 extensions::Command command;
648 if (![manageShortcutLink_ isHidden]) {
649 NSRect manageShortcutFrame = [manageShortcutLink_ frame];
650 manageShortcutFrame.origin.y = NSMinY(frame) - (
651 NSHeight(manageShortcutFrame) +
652 extension_installed_bubble::kInnerVerticalMargin);
653 // Right-align the link.
654 manageShortcutFrame.origin.x = NSMaxX(frame) -
655 NSWidth(manageShortcutFrame);
656 [manageShortcutLink_ setFrame:manageShortcutFrame];
660 // Exposed for unit testing.
661 - (NSRect)headingFrame {
662 return [heading_ frame];
665 - (NSRect)frameOfHowToUse {
666 return [howToUse_ frame];
669 - (NSRect)frameOfHowToManage {
670 return [howToManage_ frame];
673 - (NSRect)frameOfSigninPromo {
674 return [promo_ frame];
677 - (NSButton*)appInstalledShortcutLink {
678 return appShortcutLink_;
681 - (void)extensionUnloaded:(id)sender {
685 - (IBAction)onManageShortcutClicked:(id)sender {
687 std::string configure_url = chrome::kChromeUIExtensionsURL;
688 configure_url += chrome::kExtensionConfigureCommandsSubPage;
689 chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
690 browser_, GURL(configure_url)));
691 chrome::Navigate(¶ms);
694 - (IBAction)onAppShortcutClicked:(id)sender {
695 ExtensionInstallUI::OpenAppInstalledUI(browser_->profile(), extension_->id());
698 - (void)awakeFromNib {
701 [self initializeLabel];