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