Add/resurrect support for bundles of WebStore items.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_installed_bubble_controller.mm
blobd42ba95a4e611bbf2479c252834b6e32f1ba0f1f
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/memory/scoped_ptr.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/extensions/bundle_installer.h"
12 #include "chrome/browser/extensions/extension_action.h"
13 #include "chrome/browser/extensions/extension_action_manager.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/browser_navigator.h"
16 #include "chrome/browser/ui/browser_window.h"
17 #include "chrome/browser/ui/chrome_pages.h"
18 #include "chrome/browser/ui/chrome_style.h"
19 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
20 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
21 #include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
22 #include "chrome/browser/ui/cocoa/hover_close_button.h"
23 #include "chrome/browser/ui/cocoa/info_bubble_view.h"
24 #include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
25 #include "chrome/browser/ui/cocoa/new_tab_button.h"
26 #include "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
27 #include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
28 #include "chrome/browser/ui/extensions/extension_install_ui_factory.h"
29 #include "chrome/browser/ui/extensions/extension_installed_bubble.h"
30 #include "chrome/browser/ui/singleton_tabs.h"
31 #include "chrome/browser/ui/sync/sync_promo_ui.h"
32 #include "chrome/common/extensions/api/extension_action/action_info.h"
33 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
34 #include "chrome/common/extensions/sync_helper.h"
35 #include "chrome/common/url_constants.h"
36 #include "chrome/grit/chromium_strings.h"
37 #include "chrome/grit/generated_resources.h"
38 #include "components/signin/core/browser/signin_metrics.h"
39 #include "extensions/browser/install/extension_install_ui.h"
40 #include "extensions/common/extension.h"
41 #include "extensions/common/feature_switch.h"
42 #import "skia/ext/skia_utils_mac.h"
43 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
44 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
45 #include "ui/base/l10n/l10n_util.h"
47 using content::BrowserThread;
48 using extensions::BundleInstaller;
49 using extensions::Extension;
51 class ExtensionInstalledBubbleBridge
52     : public ExtensionInstalledBubble::Delegate {
53  public:
54   explicit ExtensionInstalledBubbleBridge(
55       ExtensionInstalledBubbleController* controller);
56   ~ExtensionInstalledBubbleBridge() override;
58  private:
59   // ExtensionInstalledBubble::Delegate:
60   bool MaybeShowNow() override;
62   // The (owning) installed bubble controller.
63   ExtensionInstalledBubbleController* controller_;
65   DISALLOW_COPY_AND_ASSIGN(ExtensionInstalledBubbleBridge);
68 ExtensionInstalledBubbleBridge::ExtensionInstalledBubbleBridge(
69     ExtensionInstalledBubbleController* controller)
70     : controller_(controller) {
73 ExtensionInstalledBubbleBridge::~ExtensionInstalledBubbleBridge() {
76 bool ExtensionInstalledBubbleBridge::MaybeShowNow() {
77   [controller_ showWindow:controller_];
78   return true;
81 @implementation ExtensionInstalledBubbleController
83 @synthesize bundle = bundle_;
84 // Exposed for unit test.
85 @synthesize pageActionPreviewShowing = pageActionPreviewShowing_;
87 - (id)initWithParentWindow:(NSWindow*)parentWindow
88                  extension:(const Extension*)extension
89                     bundle:(const BundleInstaller*)bundle
90                    browser:(Browser*)browser
91                       icon:(SkBitmap)icon {
92   NSString* nibName = bundle ? @"ExtensionInstalledBubbleBundle" :
93                                @"ExtensionInstalledBubble";
94   if ((self = [super initWithWindowNibPath:nibName
95                               parentWindow:parentWindow
96                                 anchoredAt:NSZeroPoint])) {
97     bundle_ = bundle;
98     DCHECK(browser);
99     browser_ = browser;
100     icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
101     pageActionPreviewShowing_ = NO;
103     if (bundle_) {
104       type_ = extension_installed_bubble::kBundle;
105     } else if (extension->is_app()) {
106       type_ = extension_installed_bubble::kApp;
107     } else if (!extensions::OmniboxInfo::GetKeyword(extension).empty()) {
108       type_ = extension_installed_bubble::kOmniboxKeyword;
109     } else if (extensions::ActionInfo::GetBrowserActionInfo(extension)) {
110       type_ = extension_installed_bubble::kBrowserAction;
111     } else if (extensions::ActionInfo::GetPageActionInfo(extension) &&
112                extensions::ActionInfo::IsVerboseInstallMessage(extension)) {
113       type_ = extension_installed_bubble::kPageAction;
114     } else {
115       type_ = extension_installed_bubble::kGeneric;
116     }
118     if (type_ == extension_installed_bubble::kBundle) {
119       [self showWindow:self];
120     } else {
121       // Start showing window only after extension has fully loaded.
122       installedBubbleBridge_.reset(new ExtensionInstalledBubbleBridge(self));
123       installedBubble_.reset(new ExtensionInstalledBubble(
124           installedBubbleBridge_.get(),
125           extension,
126           browser,
127           icon));
128       installedBubble_->IgnoreBrowserClosing();
129     }
130   }
131   return self;
134 - (const Extension*)extension {
135   if (type_ == extension_installed_bubble::kBundle)
136     return nullptr;
137   return installedBubble_->extension();
140 // Sets |promo_| based on |promoPlaceholder_|, sets |promoPlaceholder_| to nil.
141 - (void)initializeLabel {
142  // Replace the promo placeholder NSTextField with the real label NSTextView.
143  // The former doesn't show links in a nice way, but the latter can't be added
144  // in IB without a containing scroll view, so create the NSTextView
145  // programmatically.
146  promo_.reset([[HyperlinkTextView alloc]
147      initWithFrame:[promoPlaceholder_ frame]]);
148  [promo_.get() setAutoresizingMask:[promoPlaceholder_ autoresizingMask]];
149  [[promoPlaceholder_ superview]
150      replaceSubview:promoPlaceholder_ with:promo_.get()];
151  promoPlaceholder_ = nil;  // Now released.
152  [promo_.get() setDelegate:self];
155 // Returns YES if the sync promo should be shown in the bubble.
156 - (BOOL)showSyncPromo {
157   if (type_ == extension_installed_bubble::kBundle)
158     return false;
159   return extensions::sync_helper::IsSyncableExtension([self extension]) &&
160       SyncPromoUI::ShouldShowSyncPromo(browser_->profile());
163 - (void)windowWillClose:(NSNotification*)notification {
164   // Turn off page action icon preview when the window closes, unless we
165   // already removed it when the window resigned key status.
166   [self removePageActionPreviewIfNecessary];
167   browser_ = NULL;
168   [closeButton_ setTrackingEnabled:NO];
169   [super windowWillClose:notification];
172 // The controller is the delegate of the window, so it receives "did resign
173 // key" notifications.  When key is resigned, close the window.
174 - (void)windowDidResignKey:(NSNotification*)notification {
175   // If the browser window is closing, we need to remove the page action
176   // immediately, otherwise the closing animation may overlap with
177   // browser destruction.
178   [self removePageActionPreviewIfNecessary];
179   [super windowDidResignKey:notification];
182 - (IBAction)closeWindow:(id)sender {
183   DCHECK([[self window] isVisible]);
184   [self close];
187 - (BOOL)textView:(NSTextView*)aTextView
188    clickedOnLink:(id)link
189          atIndex:(NSUInteger)charIndex {
190   DCHECK_EQ(promo_.get(), aTextView);
191   chrome::ShowBrowserSignin(browser_,
192                             signin_metrics::SOURCE_EXTENSION_INSTALL_BUBBLE);
193   return YES;
196 // Extracted to a function here so that it can be overridden for unit testing.
197 - (void)removePageActionPreviewIfNecessary {
198   if (![self extension] || !pageActionPreviewShowing_)
199     return;
200   ExtensionAction* page_action =
201       extensions::ExtensionActionManager::Get(browser_->profile())->
202       GetPageAction(*[self extension]);
203   if (!page_action)
204     return;
205   pageActionPreviewShowing_ = NO;
207   BrowserWindowCocoa* window =
208       static_cast<BrowserWindowCocoa*>(browser_->window());
209   LocationBarViewMac* locationBarView =
210       [window->cocoa_controller() locationBarBridge];
211   locationBarView->SetPreviewEnabledPageAction(page_action,
212                                                false);  // disables preview.
215 // The extension installed bubble points at the browser action icon or the
216 // page action icon (shown as a preview), depending on the extension type.
217 // We need to calculate the location of these icons and the size of the
218 // message itself (which varies with the title of the extension) in order
219 // to figure out the origin point for the extension installed bubble.
220 // TODO(mirandac): add framework to easily test extension UI components!
221 - (NSPoint)calculateArrowPoint {
222   BrowserWindowCocoa* window =
223       static_cast<BrowserWindowCocoa*>(browser_->window());
224   NSPoint arrowPoint = NSZeroPoint;
226   if (type_ == extension_installed_bubble::kApp) {
227     TabStripView* view = [window->cocoa_controller() tabStripView];
228     NewTabButton* button = [view getNewTabButton];
229     NSRect bounds = [button bounds];
230     NSPoint anchor = NSMakePoint(
231         NSMidX(bounds),
232         NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
233     arrowPoint = [button convertPoint:anchor toView:nil];
234   } else if (type_ == extension_installed_bubble::kBrowserAction ||
235              extensions::FeatureSwitch::extension_action_redesign()->
236                  IsEnabled()) {
237     // If the toolbar redesign is enabled, all bubbles for extensions point to
238     // their toolbar action.
239     BrowserActionsController* controller =
240         [[window->cocoa_controller() toolbarController]
241             browserActionsController];
242     arrowPoint = [controller popupPointForId:[self extension]->id()];
243   } else if (type_ == extension_installed_bubble::kPageAction) {
244     LocationBarViewMac* locationBarView =
245         [window->cocoa_controller() locationBarBridge];
247     ExtensionAction* page_action =
248         extensions::ExtensionActionManager::Get(browser_->profile())->
249         GetPageAction(*[self extension]);
251     // Tell the location bar to show a preview of the page action icon,
252     // which would ordinarily only be displayed on a page of the appropriate
253     // type. We remove this preview when the extension installed bubble
254     // closes.
255     locationBarView->SetPreviewEnabledPageAction(page_action, true);
256     pageActionPreviewShowing_ = YES;
258     // Find the center of the bottom of the page action icon.
259     arrowPoint = locationBarView->GetPageActionBubblePoint(page_action);
260   } else if (type_ == extension_installed_bubble::kOmniboxKeyword) {
261     LocationBarViewMac* locationBarView =
262         [window->cocoa_controller() locationBarBridge];
263     arrowPoint = locationBarView->GetPageInfoBubblePoint();
264   } else {
265     DCHECK(type_ == extension_installed_bubble::kBundle ||
266            type_ == extension_installed_bubble::kGeneric);
267     // Point at the bottom of the wrench menu.
268     NSView* wrenchButton =
269         [[window->cocoa_controller() toolbarController] wrenchButton];
270     const NSRect bounds = [wrenchButton bounds];
271     NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
272     arrowPoint = [wrenchButton convertPoint:anchor toView:nil];
273   }
274   return arrowPoint;
277 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
278 // set up UI elements.
279 - (void)showWindow:(id)sender {
280   DCHECK_CURRENTLY_ON(BrowserThread::UI);
282   // Load nib and calculate height based on messages to be shown.
283   NSWindow* window = [self initializeWindow];
284   int newWindowHeight = [self calculateWindowHeight];
285   [self.bubble setFrameSize:NSMakeSize(
286       NSWidth([[window contentView] bounds]), newWindowHeight)];
287   NSSize windowDelta = NSMakeSize(
288       0, newWindowHeight - NSHeight([[window contentView] bounds]));
289   windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
290   NSRect newFrame = [window frame];
291   newFrame.size.height += windowDelta.height;
292   [window setFrame:newFrame display:NO];
294   // Now that we have resized the window, adjust y pos of the messages.
295   [self setMessageFrames:newWindowHeight];
297   // Find window origin, taking into account bubble size and arrow location.
298   self.anchorPoint =
299       [self.parentWindow convertBaseToScreen:[self calculateArrowPoint]];
300   [super showWindow:sender];
303 // Finish nib loading, set arrow location and load icon into window.  This
304 // function is exposed for unit testing.
305 - (NSWindow*)initializeWindow {
306   NSWindow* window = [self window];  // completes nib load
308   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
309     [self.bubble setArrowLocation:info_bubble::kTopLeft];
310   } else {
311     [self.bubble setArrowLocation:info_bubble::kTopRight];
312   }
314   if (type_ == extension_installed_bubble::kBundle)
315     return window;
317   // Set appropriate icon, resizing if necessary.
318   if ([icon_ size].width > extension_installed_bubble::kIconSize) {
319     [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
320                               extension_installed_bubble::kIconSize)];
321   }
322   [iconImage_ setImage:icon_];
323   [iconImage_ setNeedsDisplay:YES];
324   return window;
327 // Calculate the height of each install message, resizing messages in their
328 // frames to fit window width.  Return the new window height, based on the
329 // total of all message heights.
330 - (int)calculateWindowHeight {
331   // Adjust the window height to reflect the sum height of all messages
332   // and vertical padding.
333   int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
335   // If type is bundle, list the extensions that were installed and those that
336   // failed.
337   if (type_ == extension_installed_bubble::kBundle) {
338     NSInteger installedListHeight =
339         [self addExtensionList:installedHeadingMsg_
340                       itemsMsg:installedItemsMsg_
341                          state:BundleInstaller::Item::STATE_INSTALLED];
343     NSInteger failedListHeight =
344         [self addExtensionList:failedHeadingMsg_
345                       itemsMsg:failedItemsMsg_
346                          state:BundleInstaller::Item::STATE_FAILED];
348     newWindowHeight += installedListHeight + failedListHeight;
350     // Put some space between the lists if both are present.
351     if (installedListHeight > 0 && failedListHeight > 0)
352       newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
354     return newWindowHeight;
355   }
357   int sync_promo_height = 0;
358   if ([self showSyncPromo]) {
359     // First calculate the height of the sign-in promo.
360     NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
362     NSString* link(l10n_util::GetNSStringWithFixup(
363         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK));
364     NSString* message(l10n_util::GetNSStringWithFixup(
365         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO));
366     message = [link stringByAppendingString:message];
368     HyperlinkTextView* view = promo_.get();
369     [view setMessage:message withFont:font messageColor:[NSColor blackColor]];
370     [view addLinkRange:NSMakeRange(0, [link length])
371               withName:@""
372              linkColor:gfx::SkColorToCalibratedNSColor(
373                            chrome_style::GetLinkColor())];
375     // HACK! The TextView does not report correct height even after you stuff
376     // it with text (it tells you it is single-line even if it is multiline), so
377     // here the hidden howToUse_ TextField is temporarily repurposed to
378     // calculate the correct height for the TextView.
379     [[howToUse_ cell] setAttributedStringValue:[promo_ attributedString]];
380     [GTMUILocalizerAndLayoutTweaker
381           sizeToFitFixedWidthTextField:howToUse_];
382     sync_promo_height = NSHeight([howToUse_ frame]);
383   }
385   // First part of extension installed message, the heading.
386   base::string16 extension_name =
387       base::UTF8ToUTF16([self extension]->name().c_str());
388   base::i18n::AdjustStringForLocaleDirection(&extension_name);
389   [heading_ setStringValue:l10n_util::GetNSStringF(
390       IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
391   [GTMUILocalizerAndLayoutTweaker
392       sizeToFitFixedWidthTextField:heading_];
393   newWindowHeight += NSHeight([heading_ frame]) +
394       extension_installed_bubble::kInnerVerticalMargin;
396   // If type is browser/page action, include a special message about them.
397   if (type_ == extension_installed_bubble::kBrowserAction ||
398       type_ == extension_installed_bubble::kPageAction) {
399     [howToUse_ setStringValue:base::SysUTF16ToNSString(
400          installedBubble_->GetHowToUseDescription())];
401     [howToUse_ setHidden:NO];
402     [[howToUse_ cell]
403         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
404     [GTMUILocalizerAndLayoutTweaker
405         sizeToFitFixedWidthTextField:howToUse_];
406     newWindowHeight += NSHeight([howToUse_ frame]) +
407         extension_installed_bubble::kInnerVerticalMargin;
408   }
410   // If type is omnibox keyword, include a special message about the keyword.
411   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
412     [howToUse_ setStringValue:base::SysUTF16ToNSString(
413          installedBubble_->GetHowToUseDescription())];
414     [howToUse_ setHidden:NO];
415     [[howToUse_ cell]
416         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
417     [GTMUILocalizerAndLayoutTweaker
418         sizeToFitFixedWidthTextField:howToUse_];
419     newWindowHeight += NSHeight([howToUse_ frame]) +
420         extension_installed_bubble::kInnerVerticalMargin;
421   }
423   // If type is app, hide howToManage_, and include a "show me" link in the
424   // bubble.
425   if (type_ == extension_installed_bubble::kApp) {
426     [howToManage_ setHidden:YES];
427     [appShortcutLink_ setHidden:NO];
428     newWindowHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
429     newWindowHeight += NSHeight([appShortcutLink_ frame]);
430   } else {
431     // Second part of extension installed message.
432     [[howToManage_ cell]
433         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
434     [GTMUILocalizerAndLayoutTweaker
435         sizeToFitFixedWidthTextField:howToManage_];
436     newWindowHeight += NSHeight([howToManage_ frame]);
437   }
439   // Sync sign-in promo, if any.
440   if (sync_promo_height > 0) {
441     NSRect promo_frame = [promo_.get() frame];
442     promo_frame.size.height = sync_promo_height;
443     [promo_.get() setFrame:promo_frame];
444     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
445     newWindowHeight += sync_promo_height;
446   }
448   if (type_ != extension_installed_bubble::kBundle &&
449       installedBubble_->has_command_keybinding()) {
450     [manageShortcutLink_ setHidden:NO];
451     [[manageShortcutLink_ cell]
452         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
453     [[manageShortcutLink_ cell]
454         setTextColor:gfx::SkColorToCalibratedNSColor(
455             chrome_style::GetLinkColor())];
456     [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
457     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
458     newWindowHeight += NSHeight([manageShortcutLink_ frame]);
459   }
461   return newWindowHeight;
464 - (NSInteger)addExtensionList:(NSTextField*)headingMsg
465                      itemsMsg:(NSTextField*)itemsMsg
466                         state:(BundleInstaller::Item::State)state {
467   base::string16 heading = bundle_->GetHeadingTextFor(state);
468   bool hidden = heading.empty();
469   [headingMsg setHidden:hidden];
470   [itemsMsg setHidden:hidden];
471   if (hidden)
472     return 0;
474   [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
475   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
477   NSMutableString* joinedItems = [NSMutableString string];
478   BundleInstaller::ItemList items = bundle_->GetItemsWithState(state);
479   for (size_t i = 0; i < items.size(); ++i) {
480     if (i > 0)
481       [joinedItems appendString:@"\n"];
482     [joinedItems appendString:base::SysUTF16ToNSString(
483         items[i].GetNameForDisplay())];
484   }
486   [itemsMsg setStringValue:joinedItems];
487   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:itemsMsg];
489   return NSHeight([headingMsg frame]) +
490       extension_installed_bubble::kInnerVerticalMargin +
491       NSHeight([itemsMsg frame]);
494 // Adjust y-position of messages to sit properly in new window height.
495 - (void)setMessageFrames:(int)newWindowHeight {
496   if (type_ == extension_installed_bubble::kBundle) {
497     // Layout the messages from the bottom up.
498     NSTextField* msgs[] = { failedItemsMsg_, failedHeadingMsg_,
499                             installedItemsMsg_, installedHeadingMsg_ };
500     NSInteger offsetFromBottom = 0;
501     BOOL isFirstVisible = YES;
502     for (size_t i = 0; i < arraysize(msgs); ++i) {
503       if ([msgs[i] isHidden])
504         continue;
506       NSRect frame = [msgs[i] frame];
507       NSInteger margin = isFirstVisible ?
508           extension_installed_bubble::kOuterVerticalMargin :
509           extension_installed_bubble::kInnerVerticalMargin;
511       frame.origin.y = offsetFromBottom + margin;
512       [msgs[i] setFrame:frame];
513       offsetFromBottom += NSHeight(frame) + margin;
515       isFirstVisible = NO;
516     }
518     // Move the close button a bit to vertically align it with the heading.
519     NSInteger closeButtonFudge = 1;
520     NSRect frame = [closeButton_ frame];
521     frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
522          extension_installed_bubble::kOuterVerticalMargin);
523     [closeButton_ setFrame:frame];
525     return;
526   }
528   NSRect headingFrame = [heading_ frame];
529   headingFrame.origin.y = newWindowHeight - (
530       NSHeight(headingFrame) +
531       extension_installed_bubble::kOuterVerticalMargin);
532   [heading_ setFrame:headingFrame];
534   NSRect howToManageFrame = [howToManage_ frame];
535   if (!extensions::OmniboxInfo::GetKeyword([self extension]).empty() ||
536       extensions::ActionInfo::GetBrowserActionInfo([self extension]) ||
537       extensions::ActionInfo::IsVerboseInstallMessage([self extension])) {
538     // For browser actions, page actions and omnibox keyword show the
539     // 'how to use' message before the 'how to manage' message.
540     NSRect howToUseFrame = [howToUse_ frame];
541     howToUseFrame.origin.y = headingFrame.origin.y - (
542         NSHeight(howToUseFrame) +
543         extension_installed_bubble::kInnerVerticalMargin);
544     [howToUse_ setFrame:howToUseFrame];
546     howToManageFrame.origin.y = howToUseFrame.origin.y - (
547         NSHeight(howToManageFrame) +
548         extension_installed_bubble::kInnerVerticalMargin);
549   } else {
550     howToManageFrame.origin.y = NSMinY(headingFrame) - (
551         NSHeight(howToManageFrame) +
552         extension_installed_bubble::kInnerVerticalMargin);
553   }
554   [howToManage_ setFrame:howToManageFrame];
556   NSRect frame = howToManageFrame;
557   if ([self showSyncPromo]) {
558     frame = [promo_.get() frame];
559     frame.origin.y = NSMinY(howToManageFrame) -
560         (NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
561     [promo_.get() setFrame:frame];
562   }
564   if (![manageShortcutLink_ isHidden]) {
565     NSRect manageShortcutFrame = [manageShortcutLink_ frame];
566     manageShortcutFrame.origin.y = NSMinY(frame) - (
567         NSHeight(manageShortcutFrame) +
568         extension_installed_bubble::kInnerVerticalMargin);
569     // Right-align the link.
570     manageShortcutFrame.origin.x = NSMaxX(frame) -
571                                    NSWidth(manageShortcutFrame);
572     [manageShortcutLink_ setFrame:manageShortcutFrame];
573   }
576 // Exposed for unit testing.
577 - (NSRect)headingFrame {
578   return [heading_ frame];
581 - (NSRect)frameOfHowToUse {
582   return [howToUse_ frame];
585 - (NSRect)frameOfHowToManage {
586   return [howToManage_ frame];
589 - (NSRect)frameOfSigninPromo {
590   return [promo_ frame];
593 - (NSButton*)appInstalledShortcutLink {
594   return appShortcutLink_;
597 - (IBAction)onManageShortcutClicked:(id)sender {
598   [self close];
599   std::string configure_url = chrome::kChromeUIExtensionsURL;
600   configure_url += chrome::kExtensionConfigureCommandsSubPage;
601   chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
602       browser_, GURL(configure_url)));
603   chrome::Navigate(&params);
606 - (IBAction)onAppShortcutClicked:(id)sender {
607   scoped_ptr<extensions::ExtensionInstallUI> install_ui(
608       extensions::CreateExtensionInstallUI(browser_->profile()));
609   install_ui->OpenAppInstalledUI([self extension]->id());
612 - (void)awakeFromNib {
613   if (bundle_)
614     return;
615   [self initializeLabel];
618 @end