Extensions cleanup: Merge IsSyncableApp+Extension, ShouldSyncApp+Extension
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_installed_bubble_controller.mm
blob6ae5a7f8db848461389b35daf9f47e78f32c95f7
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   [super windowWillClose:notification];
173 // The controller is the delegate of the window, so it receives "did resign
174 // key" notifications.  When key is resigned, close the window.
175 - (void)windowDidResignKey:(NSNotification*)notification {
176   // If the browser window is closing, we need to remove the page action
177   // immediately, otherwise the closing animation may overlap with
178   // browser destruction.
179   [self removePageActionPreviewIfNecessary];
180   [super windowDidResignKey:notification];
183 - (IBAction)closeWindow:(id)sender {
184   DCHECK([[self window] isVisible]);
185   [self close];
188 - (BOOL)textView:(NSTextView*)aTextView
189    clickedOnLink:(id)link
190          atIndex:(NSUInteger)charIndex {
191   DCHECK_EQ(promo_.get(), aTextView);
192   chrome::ShowBrowserSignin(browser_,
193                             signin_metrics::SOURCE_EXTENSION_INSTALL_BUBBLE);
194   return YES;
197 // Extracted to a function here so that it can be overridden for unit testing.
198 - (void)removePageActionPreviewIfNecessary {
199   if (![self extension] || !pageActionPreviewShowing_)
200     return;
201   ExtensionAction* page_action =
202       extensions::ExtensionActionManager::Get(browser_->profile())->
203       GetPageAction(*[self extension]);
204   if (!page_action)
205     return;
206   pageActionPreviewShowing_ = NO;
208   BrowserWindowCocoa* window =
209       static_cast<BrowserWindowCocoa*>(browser_->window());
210   LocationBarViewMac* locationBarView =
211       [window->cocoa_controller() locationBarBridge];
212   locationBarView->SetPreviewEnabledPageAction(page_action,
213                                                false);  // disables preview.
216 // The extension installed bubble points at the browser action icon or the
217 // page action icon (shown as a preview), depending on the extension type.
218 // We need to calculate the location of these icons and the size of the
219 // message itself (which varies with the title of the extension) in order
220 // to figure out the origin point for the extension installed bubble.
221 // TODO(mirandac): add framework to easily test extension UI components!
222 - (NSPoint)calculateArrowPoint {
223   BrowserWindowCocoa* window =
224       static_cast<BrowserWindowCocoa*>(browser_->window());
225   NSPoint arrowPoint = NSZeroPoint;
227   if (type_ == extension_installed_bubble::kApp) {
228     TabStripView* view = [window->cocoa_controller() tabStripView];
229     NewTabButton* button = [view getNewTabButton];
230     NSRect bounds = [button bounds];
231     NSPoint anchor = NSMakePoint(
232         NSMidX(bounds),
233         NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
234     arrowPoint = [button convertPoint:anchor toView:nil];
235   } else if (type_ == extension_installed_bubble::kBrowserAction ||
236              extensions::FeatureSwitch::extension_action_redesign()->
237                  IsEnabled()) {
238     // If the toolbar redesign is enabled, all bubbles for extensions point to
239     // their toolbar action.
240     BrowserActionsController* controller =
241         [[window->cocoa_controller() toolbarController]
242             browserActionsController];
243     arrowPoint = [controller popupPointForId:[self extension]->id()];
244   } else if (type_ == extension_installed_bubble::kPageAction) {
245     LocationBarViewMac* locationBarView =
246         [window->cocoa_controller() locationBarBridge];
248     ExtensionAction* page_action =
249         extensions::ExtensionActionManager::Get(browser_->profile())->
250         GetPageAction(*[self extension]);
252     // Tell the location bar to show a preview of the page action icon,
253     // which would ordinarily only be displayed on a page of the appropriate
254     // type. We remove this preview when the extension installed bubble
255     // closes.
256     locationBarView->SetPreviewEnabledPageAction(page_action, true);
257     pageActionPreviewShowing_ = YES;
259     // Find the center of the bottom of the page action icon.
260     arrowPoint = locationBarView->GetPageActionBubblePoint(page_action);
261   } else if (type_ == extension_installed_bubble::kOmniboxKeyword) {
262     LocationBarViewMac* locationBarView =
263         [window->cocoa_controller() locationBarBridge];
264     arrowPoint = locationBarView->GetPageInfoBubblePoint();
265   } else {
266     DCHECK(type_ == extension_installed_bubble::kBundle ||
267            type_ == extension_installed_bubble::kGeneric);
268     // Point at the bottom of the wrench menu.
269     NSView* wrenchButton =
270         [[window->cocoa_controller() toolbarController] wrenchButton];
271     const NSRect bounds = [wrenchButton bounds];
272     NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
273     arrowPoint = [wrenchButton convertPoint:anchor toView:nil];
274   }
275   return arrowPoint;
278 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
279 // set up UI elements.
280 - (void)showWindow:(id)sender {
281   DCHECK_CURRENTLY_ON(BrowserThread::UI);
283   // Load nib and calculate height based on messages to be shown.
284   NSWindow* window = [self initializeWindow];
285   int newWindowHeight = [self calculateWindowHeight];
286   [self.bubble setFrameSize:NSMakeSize(
287       NSWidth([[window contentView] bounds]), newWindowHeight)];
288   NSSize windowDelta = NSMakeSize(
289       0, newWindowHeight - NSHeight([[window contentView] bounds]));
290   windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
291   NSRect newFrame = [window frame];
292   newFrame.size.height += windowDelta.height;
293   [window setFrame:newFrame display:NO];
295   // Now that we have resized the window, adjust y pos of the messages.
296   [self setMessageFrames:newWindowHeight];
298   // Find window origin, taking into account bubble size and arrow location.
299   self.anchorPoint =
300       [self.parentWindow convertBaseToScreen:[self calculateArrowPoint]];
301   [super showWindow:sender];
304 // Finish nib loading, set arrow location and load icon into window.  This
305 // function is exposed for unit testing.
306 - (NSWindow*)initializeWindow {
307   NSWindow* window = [self window];  // completes nib load
309   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
310     [self.bubble setArrowLocation:info_bubble::kTopLeft];
311   } else {
312     [self.bubble setArrowLocation:info_bubble::kTopRight];
313   }
315   if (type_ == extension_installed_bubble::kBundle)
316     return window;
318   // Set appropriate icon, resizing if necessary.
319   if ([icon_ size].width > extension_installed_bubble::kIconSize) {
320     [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
321                               extension_installed_bubble::kIconSize)];
322   }
323   [iconImage_ setImage:icon_];
324   [iconImage_ setNeedsDisplay:YES];
325   return window;
328 // Calculate the height of each install message, resizing messages in their
329 // frames to fit window width.  Return the new window height, based on the
330 // total of all message heights.
331 - (int)calculateWindowHeight {
332   // Adjust the window height to reflect the sum height of all messages
333   // and vertical padding.
334   int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
336   // If type is bundle, list the extensions that were installed and those that
337   // failed.
338   if (type_ == extension_installed_bubble::kBundle) {
339     NSInteger installedListHeight =
340         [self addExtensionList:installedHeadingMsg_
341                      itemsView:installedItemsView_
342                          state:BundleInstaller::Item::STATE_INSTALLED];
344     NSInteger failedListHeight =
345         [self addExtensionList:failedHeadingMsg_
346                      itemsView:failedItemsView_
347                          state:BundleInstaller::Item::STATE_FAILED];
349     newWindowHeight += installedListHeight + failedListHeight;
351     // Put some space between the lists if both are present.
352     if (installedListHeight > 0 && failedListHeight > 0)
353       newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
355     return newWindowHeight;
356   }
358   int sync_promo_height = 0;
359   if ([self showSyncPromo]) {
360     // First calculate the height of the sign-in promo.
361     NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
363     NSString* link(l10n_util::GetNSStringWithFixup(
364         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK));
365     NSString* message(l10n_util::GetNSStringWithFixup(
366         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO));
367     message = [link stringByAppendingString:message];
369     HyperlinkTextView* view = promo_.get();
370     [view setMessage:message withFont:font messageColor:[NSColor blackColor]];
371     [view addLinkRange:NSMakeRange(0, [link length])
372               withName:@""
373              linkColor:gfx::SkColorToCalibratedNSColor(
374                            chrome_style::GetLinkColor())];
376     // HACK! The TextView does not report correct height even after you stuff
377     // it with text (it tells you it is single-line even if it is multiline), so
378     // here the hidden howToUse_ TextField is temporarily repurposed to
379     // calculate the correct height for the TextView.
380     [[howToUse_ cell] setAttributedStringValue:[promo_ attributedString]];
381     [GTMUILocalizerAndLayoutTweaker
382           sizeToFitFixedWidthTextField:howToUse_];
383     sync_promo_height = NSHeight([howToUse_ frame]);
384   }
386   // First part of extension installed message, the heading.
387   base::string16 extension_name =
388       base::UTF8ToUTF16([self extension]->name().c_str());
389   base::i18n::AdjustStringForLocaleDirection(&extension_name);
390   [heading_ setStringValue:l10n_util::GetNSStringF(
391       IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
392   [GTMUILocalizerAndLayoutTweaker
393       sizeToFitFixedWidthTextField:heading_];
394   newWindowHeight += NSHeight([heading_ frame]) +
395       extension_installed_bubble::kInnerVerticalMargin;
397   // If type is browser/page action, include a special message about them.
398   if (type_ == extension_installed_bubble::kBrowserAction ||
399       type_ == extension_installed_bubble::kPageAction) {
400     [howToUse_ setStringValue:base::SysUTF16ToNSString(
401          installedBubble_->GetHowToUseDescription())];
402     [howToUse_ setHidden:NO];
403     [[howToUse_ cell]
404         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
405     [GTMUILocalizerAndLayoutTweaker
406         sizeToFitFixedWidthTextField:howToUse_];
407     newWindowHeight += NSHeight([howToUse_ frame]) +
408         extension_installed_bubble::kInnerVerticalMargin;
409   }
411   // If type is omnibox keyword, include a special message about the keyword.
412   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
413     [howToUse_ setStringValue:base::SysUTF16ToNSString(
414          installedBubble_->GetHowToUseDescription())];
415     [howToUse_ setHidden:NO];
416     [[howToUse_ cell]
417         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
418     [GTMUILocalizerAndLayoutTweaker
419         sizeToFitFixedWidthTextField:howToUse_];
420     newWindowHeight += NSHeight([howToUse_ frame]) +
421         extension_installed_bubble::kInnerVerticalMargin;
422   }
424   // If type is app, hide howToManage_, and include a "show me" link in the
425   // bubble.
426   if (type_ == extension_installed_bubble::kApp) {
427     [howToManage_ setHidden:YES];
428     [appShortcutLink_ setHidden:NO];
429     newWindowHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
430     newWindowHeight += NSHeight([appShortcutLink_ frame]);
431   } else {
432     // Second part of extension installed message.
433     [[howToManage_ cell]
434         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
435     [GTMUILocalizerAndLayoutTweaker
436         sizeToFitFixedWidthTextField:howToManage_];
437     newWindowHeight += NSHeight([howToManage_ frame]);
438   }
440   // Sync sign-in promo, if any.
441   if (sync_promo_height > 0) {
442     NSRect promo_frame = [promo_.get() frame];
443     promo_frame.size.height = sync_promo_height;
444     [promo_.get() setFrame:promo_frame];
445     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
446     newWindowHeight += sync_promo_height;
447   }
449   if (type_ != extension_installed_bubble::kBundle &&
450       installedBubble_->has_command_keybinding()) {
451     [manageShortcutLink_ setHidden:NO];
452     [[manageShortcutLink_ cell]
453         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
454     [[manageShortcutLink_ cell]
455         setTextColor:gfx::SkColorToCalibratedNSColor(
456             chrome_style::GetLinkColor())];
457     [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
458     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
459     newWindowHeight += NSHeight([manageShortcutLink_ frame]);
460   }
462   return newWindowHeight;
465 - (NSInteger)addExtensionList:(NSTextField*)headingMsg
466                     itemsView:(NSView*)itemsView
467                         state:(BundleInstaller::Item::State)state {
468   base::string16 heading = bundle_->GetHeadingTextFor(state);
469   bool hidden = heading.empty();
470   [headingMsg setHidden:hidden];
471   [itemsView setHidden:hidden];
472   if (hidden)
473     return 0;
475   [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
476   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
478   CGFloat height =
479       PopulateBundleItemsList(bundle_->GetItemsWithState(state), itemsView);
481   NSRect frame = [itemsView frame];
482   frame.size.height = height;
483   [itemsView setFrame:frame];
485   return NSHeight([headingMsg frame]) +
486       extension_installed_bubble::kInnerVerticalMargin +
487       NSHeight([itemsView frame]);
490 // Adjust y-position of messages to sit properly in new window height.
491 - (void)setMessageFrames:(int)newWindowHeight {
492   if (type_ == extension_installed_bubble::kBundle) {
493     // Layout the messages from the bottom up.
494     NSView* msgs[] = { failedItemsView_, failedHeadingMsg_,
495                        installedItemsView_, installedHeadingMsg_ };
496     NSInteger offsetFromBottom = 0;
497     BOOL isFirstVisible = YES;
498     for (size_t i = 0; i < arraysize(msgs); ++i) {
499       if ([msgs[i] isHidden])
500         continue;
502       NSRect frame = [msgs[i] frame];
503       NSInteger margin = isFirstVisible ?
504           extension_installed_bubble::kOuterVerticalMargin :
505           extension_installed_bubble::kInnerVerticalMargin;
507       frame.origin.y = offsetFromBottom + margin;
508       [msgs[i] setFrame:frame];
509       offsetFromBottom += NSHeight(frame) + margin;
511       isFirstVisible = NO;
512     }
514     // Move the close button a bit to vertically align it with the heading.
515     NSInteger closeButtonFudge = 1;
516     NSRect frame = [closeButton_ frame];
517     frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
518          extension_installed_bubble::kOuterVerticalMargin);
519     [closeButton_ setFrame:frame];
521     return;
522   }
524   NSRect headingFrame = [heading_ frame];
525   headingFrame.origin.y = newWindowHeight - (
526       NSHeight(headingFrame) +
527       extension_installed_bubble::kOuterVerticalMargin);
528   [heading_ setFrame:headingFrame];
530   NSRect howToManageFrame = [howToManage_ frame];
531   if (!extensions::OmniboxInfo::GetKeyword([self extension]).empty() ||
532       extensions::ActionInfo::GetBrowserActionInfo([self extension]) ||
533       extensions::ActionInfo::IsVerboseInstallMessage([self extension])) {
534     // For browser actions, page actions and omnibox keyword show the
535     // 'how to use' message before the 'how to manage' message.
536     NSRect howToUseFrame = [howToUse_ frame];
537     howToUseFrame.origin.y = headingFrame.origin.y - (
538         NSHeight(howToUseFrame) +
539         extension_installed_bubble::kInnerVerticalMargin);
540     [howToUse_ setFrame:howToUseFrame];
542     howToManageFrame.origin.y = howToUseFrame.origin.y - (
543         NSHeight(howToManageFrame) +
544         extension_installed_bubble::kInnerVerticalMargin);
545   } else {
546     howToManageFrame.origin.y = NSMinY(headingFrame) - (
547         NSHeight(howToManageFrame) +
548         extension_installed_bubble::kInnerVerticalMargin);
549   }
550   [howToManage_ setFrame:howToManageFrame];
552   NSRect frame = howToManageFrame;
553   if ([self showSyncPromo]) {
554     frame = [promo_.get() frame];
555     frame.origin.y = NSMinY(howToManageFrame) -
556         (NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
557     [promo_.get() setFrame:frame];
558   }
560   if (![manageShortcutLink_ isHidden]) {
561     NSRect manageShortcutFrame = [manageShortcutLink_ frame];
562     manageShortcutFrame.origin.y = NSMinY(frame) - (
563         NSHeight(manageShortcutFrame) +
564         extension_installed_bubble::kInnerVerticalMargin);
565     // Right-align the link.
566     manageShortcutFrame.origin.x = NSMaxX(frame) -
567                                    NSWidth(manageShortcutFrame);
568     [manageShortcutLink_ setFrame:manageShortcutFrame];
569   }
572 // Exposed for unit testing.
573 - (NSRect)headingFrame {
574   return [heading_ frame];
577 - (NSRect)frameOfHowToUse {
578   return [howToUse_ frame];
581 - (NSRect)frameOfHowToManage {
582   return [howToManage_ frame];
585 - (NSRect)frameOfSigninPromo {
586   return [promo_ frame];
589 - (NSButton*)appInstalledShortcutLink {
590   return appShortcutLink_;
593 - (IBAction)onManageShortcutClicked:(id)sender {
594   [self close];
595   std::string configure_url = chrome::kChromeUIExtensionsURL;
596   configure_url += chrome::kExtensionConfigureCommandsSubPage;
597   chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
598       browser_, GURL(configure_url)));
599   chrome::Navigate(&params);
602 - (IBAction)onAppShortcutClicked:(id)sender {
603   scoped_ptr<extensions::ExtensionInstallUI> install_ui(
604       extensions::CreateExtensionInstallUI(browser_->profile()));
605   install_ui->OpenAppInstalledUI([self extension]->id());
608 - (void)awakeFromNib {
609   if (bundle_)
610     return;
611   [self initializeLabel];
614 @end