[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_installed_bubble_controller.mm
blob01f53d49590c1f8b0324de02d8ab136ce076186e
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
56 // to |controller|.
57 class ExtensionLoadedNotificationObserver
58     : public content::NotificationObserver {
59  public:
60   ExtensionLoadedNotificationObserver(
61       ExtensionInstalledBubbleController* controller, Profile* profile)
62           : controller_(controller) {
63     registrar_.Add(this,
64                    chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
65                    content::Source<Profile>(profile));
66     registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED,
67         content::Source<Profile>(profile));
68   }
70  private:
71   // NotificationObserver implementation. Tells the controller to start showing
72   // its window on the main thread when the extension has finished loading.
73   virtual void Observe(
74       int type,
75       const content::NotificationSource& source,
76       const content::NotificationDetails& details) OVERRIDE {
77     if (type == chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED) {
78       const Extension* extension =
79           content::Details<const Extension>(details).ptr();
80       if (extension == [controller_ extension]) {
81         [controller_ performSelectorOnMainThread:@selector(showWindow:)
82                                       withObject:controller_
83                                    waitUntilDone:NO];
84       }
85     } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED) {
86       const Extension* extension =
87           content::Details<const UnloadedExtensionInfo>(details)->extension;
88       if (extension == [controller_ extension]) {
89         [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:)
90                                       withObject:controller_
91                                    waitUntilDone:NO];
92       }
93     } else {
94       NOTREACHED() << "Received unexpected notification.";
95     }
96   }
98   content::NotificationRegistrar registrar_;
99   ExtensionInstalledBubbleController* controller_;  // weak, owns us
102 @implementation ExtensionInstalledBubbleController
104 @synthesize extension = extension_;
105 @synthesize bundle = bundle_;
106 // Exposed for unit test.
107 @synthesize pageActionPreviewShowing = pageActionPreviewShowing_;
109 - (id)initWithParentWindow:(NSWindow*)parentWindow
110                  extension:(const Extension*)extension
111                     bundle:(const BundleInstaller*)bundle
112                    browser:(Browser*)browser
113                       icon:(SkBitmap)icon {
114   NSString* nibName = bundle ? @"ExtensionInstalledBubbleBundle" :
115                                @"ExtensionInstalledBubble";
116   if ((self = [super initWithWindowNibPath:nibName
117                               parentWindow:parentWindow
118                                 anchoredAt:NSZeroPoint])) {
119     extension_ = extension;
120     bundle_ = bundle;
121     DCHECK(browser);
122     browser_ = browser;
123     icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
124     pageActionPreviewShowing_ = NO;
126     if (bundle_) {
127       type_ = extension_installed_bubble::kBundle;
128     } else if (extension->is_app()) {
129       type_ = extension_installed_bubble::kApp;
130     } else if (!extensions::OmniboxInfo::GetKeyword(extension).empty()) {
131       type_ = extension_installed_bubble::kOmniboxKeyword;
132     } else if (extensions::ActionInfo::GetBrowserActionInfo(extension)) {
133       type_ = extension_installed_bubble::kBrowserAction;
134     } else if (extensions::ActionInfo::GetPageActionInfo(extension) &&
135                extensions::ActionInfo::IsVerboseInstallMessage(extension)) {
136       type_ = extension_installed_bubble::kPageAction;
137     } else {
138       type_ = extension_installed_bubble::kGeneric;
139     }
141     if (type_ == extension_installed_bubble::kBundle) {
142       [self showWindow:self];
143     } else {
144       // Start showing window only after extension has fully loaded.
145       extensionObserver_.reset(new ExtensionLoadedNotificationObserver(
146           self, browser->profile()));
147     }
148   }
149   return self;
152 // Sets |promo_| based on |promoPlaceholder_|, sets |promoPlaceholder_| to nil.
153 - (void)initializeLabel {
154  // Replace the promo placeholder NSTextField with the real label NSTextView.
155  // The former doesn't show links in a nice way, but the latter can't be added
156  // in IB without a containing scroll view, so create the NSTextView
157  // programmatically.
158  promo_.reset([[HyperlinkTextView alloc]
159      initWithFrame:[promoPlaceholder_ frame]]);
160  [promo_.get() setAutoresizingMask:[promoPlaceholder_ autoresizingMask]];
161  [[promoPlaceholder_ superview]
162      replaceSubview:promoPlaceholder_ with:promo_.get()];
163  promoPlaceholder_ = nil;  // Now released.
164  [promo_.get() setDelegate:self];
167 // Returns YES if the sync promo should be shown in the bubble.
168 - (BOOL)showSyncPromo {
169   return extensions::sync_helper::IsSyncableExtension(extension_) &&
170          SyncPromoUI::ShouldShowSyncPromo(browser_->profile());
173 - (void)windowWillClose:(NSNotification*)notification {
174   // Turn off page action icon preview when the window closes, unless we
175   // already removed it when the window resigned key status.
176   [self removePageActionPreviewIfNecessary];
177   extension_ = NULL;
178   browser_ = NULL;
180   [super windowWillClose:notification];
183 // The controller is the delegate of the window, so it receives "did resign
184 // key" notifications.  When key is resigned, close the window.
185 - (void)windowDidResignKey:(NSNotification*)notification {
186   // If the browser window is closing, we need to remove the page action
187   // immediately, otherwise the closing animation may overlap with
188   // browser destruction.
189   [self removePageActionPreviewIfNecessary];
190   [super windowDidResignKey:notification];
193 - (IBAction)closeWindow:(id)sender {
194   DCHECK([[self window] isVisible]);
195   [self close];
198 - (BOOL)textView:(NSTextView*)aTextView
199    clickedOnLink:(id)link
200          atIndex:(NSUInteger)charIndex {
201   DCHECK_EQ(promo_.get(), aTextView);
202   GURL promo_url =
203       signin::GetPromoURL(signin::SOURCE_EXTENSION_INSTALL_BUBBLE, false);
204   chrome::NavigateParams params(
205       chrome::GetSingletonTabNavigateParams(browser_, promo_url));
206   chrome::Navigate(&params);
207   return YES;
210 // Extracted to a function here so that it can be overridden for unit testing.
211 - (void)removePageActionPreviewIfNecessary {
212   if (!extension_ || !pageActionPreviewShowing_)
213     return;
214   ExtensionAction* page_action =
215       extensions::ExtensionActionManager::Get(browser_->profile())->
216       GetPageAction(*extension_);
217   if (!page_action)
218     return;
219   pageActionPreviewShowing_ = NO;
221   BrowserWindowCocoa* window =
222       static_cast<BrowserWindowCocoa*>(browser_->window());
223   LocationBarViewMac* locationBarView =
224       [window->cocoa_controller() locationBarBridge];
225   locationBarView->SetPreviewEnabledPageAction(page_action,
226                                                false);  // disables preview.
229 // The extension installed bubble points at the browser action icon or the
230 // page action icon (shown as a preview), depending on the extension type.
231 // We need to calculate the location of these icons and the size of the
232 // message itself (which varies with the title of the extension) in order
233 // to figure out the origin point for the extension installed bubble.
234 // TODO(mirandac): add framework to easily test extension UI components!
235 - (NSPoint)calculateArrowPoint {
236   BrowserWindowCocoa* window =
237       static_cast<BrowserWindowCocoa*>(browser_->window());
238   NSPoint arrowPoint = NSZeroPoint;
240   switch(type_) {
241     case extension_installed_bubble::kApp: {
242       TabStripView* view = [window->cocoa_controller() tabStripView];
243       NewTabButton* button = [view getNewTabButton];
244       NSRect bounds = [button bounds];
245       NSPoint anchor = NSMakePoint(
246           NSMidX(bounds),
247           NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
248       arrowPoint = [button convertPoint:anchor toView:nil];
249       break;
250     }
251     case extension_installed_bubble::kOmniboxKeyword: {
252       LocationBarViewMac* locationBarView =
253           [window->cocoa_controller() locationBarBridge];
254       arrowPoint = locationBarView->GetPageInfoBubblePoint();
255       break;
256     }
257     case extension_installed_bubble::kBrowserAction: {
258       BrowserActionsController* controller =
259           [[window->cocoa_controller() toolbarController]
260               browserActionsController];
261       arrowPoint = [controller popupPointForBrowserAction:extension_];
262       break;
263     }
264     case extension_installed_bubble::kPageAction: {
265       LocationBarViewMac* locationBarView =
266           [window->cocoa_controller() locationBarBridge];
268       ExtensionAction* page_action =
269           extensions::ExtensionActionManager::Get(browser_->profile())->
270           GetPageAction(*extension_);
272       // Tell the location bar to show a preview of the page action icon, which
273       // would ordinarily only be displayed on a page of the appropriate type.
274       // We remove this preview when the extension installed bubble closes.
275       locationBarView->SetPreviewEnabledPageAction(page_action, true);
276       pageActionPreviewShowing_ = YES;
278       // Find the center of the bottom of the page action icon.
279       arrowPoint =
280           locationBarView->GetPageActionBubblePoint(page_action);
281       break;
282     }
283     case extension_installed_bubble::kBundle:
284     case extension_installed_bubble::kGeneric: {
285       // Point at the bottom of the wrench menu.
286       NSView* wrenchButton =
287           [[window->cocoa_controller() toolbarController] wrenchButton];
288       const NSRect bounds = [wrenchButton bounds];
289       NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
290       arrowPoint = [wrenchButton convertPoint:anchor toView:nil];
291       break;
292     }
293     default: {
294       NOTREACHED();
295     }
296   }
297   return arrowPoint;
300 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
301 // set up UI elements.
302 - (void)showWindow:(id)sender {
303   DCHECK_CURRENTLY_ON(BrowserThread::UI);
305   // Load nib and calculate height based on messages to be shown.
306   NSWindow* window = [self initializeWindow];
307   int newWindowHeight = [self calculateWindowHeight];
308   [self.bubble setFrameSize:NSMakeSize(
309       NSWidth([[window contentView] bounds]), newWindowHeight)];
310   NSSize windowDelta = NSMakeSize(
311       0, newWindowHeight - NSHeight([[window contentView] bounds]));
312   windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
313   NSRect newFrame = [window frame];
314   newFrame.size.height += windowDelta.height;
315   [window setFrame:newFrame display:NO];
317   // Now that we have resized the window, adjust y pos of the messages.
318   [self setMessageFrames:newWindowHeight];
320   // Find window origin, taking into account bubble size and arrow location.
321   self.anchorPoint =
322       [self.parentWindow convertBaseToScreen:[self calculateArrowPoint]];
323   [super showWindow:sender];
326 // Finish nib loading, set arrow location and load icon into window.  This
327 // function is exposed for unit testing.
328 - (NSWindow*)initializeWindow {
329   NSWindow* window = [self window];  // completes nib load
331   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
332     [self.bubble setArrowLocation:info_bubble::kTopLeft];
333   } else {
334     [self.bubble setArrowLocation:info_bubble::kTopRight];
335   }
337   if (type_ == extension_installed_bubble::kBundle)
338     return window;
340   // Set appropriate icon, resizing if necessary.
341   if ([icon_ size].width > extension_installed_bubble::kIconSize) {
342     [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
343                               extension_installed_bubble::kIconSize)];
344   }
345   [iconImage_ setImage:icon_];
346   [iconImage_ setNeedsDisplay:YES];
347   return window;
350 - (bool)hasActivePageAction:(extensions::Command*)command {
351   extensions::CommandService* command_service =
352       extensions::CommandService::Get(browser_->profile());
353   if (type_ == extension_installed_bubble::kPageAction) {
354     if (extensions::CommandsInfo::GetPageActionCommand(extension_) &&
355         command_service->GetPageActionCommand(
356             extension_->id(),
357             extensions::CommandService::ACTIVE_ONLY,
358             command,
359             NULL)) {
360       return true;
361     }
362   }
364   return false;
367 - (bool)hasActiveBrowserAction:(extensions::Command*)command {
368   extensions::CommandService* command_service =
369       extensions::CommandService::Get(browser_->profile());
370   if (type_ == extension_installed_bubble::kBrowserAction) {
371     if (extensions::CommandsInfo::GetBrowserActionCommand(extension_) &&
372         command_service->GetBrowserActionCommand(
373             extension_->id(),
374             extensions::CommandService::ACTIVE_ONLY,
375             command,
376             NULL)) {
377       return true;
378     }
379   }
381   return false;
384 - (NSString*)installMessageForCurrentExtensionAction {
385   if (type_ == extension_installed_bubble::kPageAction) {
386     extensions::Command page_action_command;
387     if ([self hasActivePageAction:&page_action_command]) {
388       return l10n_util::GetNSStringF(
389           IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT,
390           page_action_command.accelerator().GetShortcutText());
391     } else {
392       return l10n_util::GetNSString(
393           IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO);
394     }
395   } else {
396     CHECK_EQ(extension_installed_bubble::kBrowserAction, type_);
397     extensions::Command browser_action_command;
398     if ([self hasActiveBrowserAction:&browser_action_command]) {
399       return l10n_util::GetNSStringF(
400           IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT,
401           browser_action_command.accelerator().GetShortcutText());
402     } else {
403       return l10n_util::GetNSString(
404           IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO);
405     }
406   }
409 // Calculate the height of each install message, resizing messages in their
410 // frames to fit window width.  Return the new window height, based on the
411 // total of all message heights.
412 - (int)calculateWindowHeight {
413   // Adjust the window height to reflect the sum height of all messages
414   // and vertical padding.
415   int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
417   // If type is bundle, list the extensions that were installed and those that
418   // failed.
419   if (type_ == extension_installed_bubble::kBundle) {
420     NSInteger installedListHeight =
421         [self addExtensionList:installedHeadingMsg_
422                       itemsMsg:installedItemsMsg_
423                          state:BundleInstaller::Item::STATE_INSTALLED];
425     NSInteger failedListHeight =
426         [self addExtensionList:failedHeadingMsg_
427                       itemsMsg:failedItemsMsg_
428                          state:BundleInstaller::Item::STATE_FAILED];
430     newWindowHeight += installedListHeight + failedListHeight;
432     // Put some space between the lists if both are present.
433     if (installedListHeight > 0 && failedListHeight > 0)
434       newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
436     return newWindowHeight;
437   }
439   int sync_promo_height = 0;
440   if ([self showSyncPromo]) {
441     // First calculate the height of the sign-in promo.
442     NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
444     NSString* link(l10n_util::GetNSStringWithFixup(
445         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK));
446     NSString* message(l10n_util::GetNSStringWithFixup(
447         IDS_EXTENSION_INSTALLED_SIGNIN_PROMO));
449     HyperlinkTextView* view = promo_.get();
450     [view setMessageAndLink:message
451                    withLink:link
452                    atOffset:0
453                        font:font
454                messageColor:[NSColor blackColor]
455                   linkColor:gfx::SkColorToCalibratedNSColor(
456                                 chrome_style::GetLinkColor())];
458     // HACK! The TextView does not report correct height even after you stuff
459     // it with text (it tells you it is single-line even if it is multiline), so
460     // here the hidden howToUse_ TextField is temporarily repurposed to
461     // calculate the correct height for the TextView.
462     [[howToUse_ cell] setAttributedStringValue:[promo_ attributedString]];
463     [GTMUILocalizerAndLayoutTweaker
464           sizeToFitFixedWidthTextField:howToUse_];
465     sync_promo_height = NSHeight([howToUse_ frame]);
466   }
468   // First part of extension installed message, the heading.
469   base::string16 extension_name = base::UTF8ToUTF16(extension_->name().c_str());
470   base::i18n::AdjustStringForLocaleDirection(&extension_name);
471   [heading_ setStringValue:l10n_util::GetNSStringF(
472       IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
473   [GTMUILocalizerAndLayoutTweaker
474       sizeToFitFixedWidthTextField:heading_];
475   newWindowHeight += NSHeight([heading_ frame]) +
476       extension_installed_bubble::kInnerVerticalMargin;
478   // If type is browser/page action, include a special message about them.
479   if (type_ == extension_installed_bubble::kBrowserAction ||
480       type_ == extension_installed_bubble::kPageAction) {
481     [howToUse_ setStringValue:[self
482         installMessageForCurrentExtensionAction]];
483     [howToUse_ setHidden:NO];
484     [[howToUse_ cell]
485         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
486     [GTMUILocalizerAndLayoutTweaker
487         sizeToFitFixedWidthTextField:howToUse_];
488     newWindowHeight += NSHeight([howToUse_ frame]) +
489         extension_installed_bubble::kInnerVerticalMargin;
490   }
492   // If type is omnibox keyword, include a special message about the keyword.
493   if (type_ == extension_installed_bubble::kOmniboxKeyword) {
494     [howToUse_ setStringValue:l10n_util::GetNSStringF(
495         IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
496         base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension_)))];
497     [howToUse_ setHidden:NO];
498     [[howToUse_ cell]
499         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
500     [GTMUILocalizerAndLayoutTweaker
501         sizeToFitFixedWidthTextField:howToUse_];
502     newWindowHeight += NSHeight([howToUse_ frame]) +
503         extension_installed_bubble::kInnerVerticalMargin;
504   }
506   // If type is app, hide howToManage_, and include a "show me" link in the
507   // bubble.
508   if (type_ == extension_installed_bubble::kApp) {
509     [howToManage_ setHidden:YES];
510     [appShortcutLink_ setHidden:NO];
511     newWindowHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
512     newWindowHeight += NSHeight([appShortcutLink_ frame]);
513   } else {
514     // Second part of extension installed message.
515     [[howToManage_ cell]
516         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
517     [GTMUILocalizerAndLayoutTweaker
518         sizeToFitFixedWidthTextField:howToManage_];
519     newWindowHeight += NSHeight([howToManage_ frame]);
520   }
522   // Sync sign-in promo, if any.
523   if (sync_promo_height > 0) {
524     NSRect promo_frame = [promo_.get() frame];
525     promo_frame.size.height = sync_promo_height;
526     [promo_.get() setFrame:promo_frame];
527     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
528     newWindowHeight += sync_promo_height;
529   }
531   extensions::Command command;
532   if ([self hasActivePageAction:&command] ||
533       [self hasActiveBrowserAction:&command]) {
534     [manageShortcutLink_ setHidden:NO];
535     [[manageShortcutLink_ cell]
536         setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
537     [[manageShortcutLink_ cell]
538         setTextColor:gfx::SkColorToCalibratedNSColor(
539             chrome_style::GetLinkColor())];
540     [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
541     newWindowHeight += extension_installed_bubble::kInnerVerticalMargin;
542     newWindowHeight += NSHeight([manageShortcutLink_ frame]);
543   }
545   return newWindowHeight;
548 - (NSInteger)addExtensionList:(NSTextField*)headingMsg
549                      itemsMsg:(NSTextField*)itemsMsg
550                         state:(BundleInstaller::Item::State)state {
551   base::string16 heading = bundle_->GetHeadingTextFor(state);
552   bool hidden = heading.empty();
553   [headingMsg setHidden:hidden];
554   [itemsMsg setHidden:hidden];
555   if (hidden)
556     return 0;
558   [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
559   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
561   NSMutableString* joinedItems = [NSMutableString string];
562   BundleInstaller::ItemList items = bundle_->GetItemsWithState(state);
563   for (size_t i = 0; i < items.size(); ++i) {
564     if (i > 0)
565       [joinedItems appendString:@"\n"];
566     [joinedItems appendString:base::SysUTF16ToNSString(
567         items[i].GetNameForDisplay())];
568   }
570   [itemsMsg setStringValue:joinedItems];
571   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:itemsMsg];
573   return NSHeight([headingMsg frame]) +
574       extension_installed_bubble::kInnerVerticalMargin +
575       NSHeight([itemsMsg frame]);
578 // Adjust y-position of messages to sit properly in new window height.
579 - (void)setMessageFrames:(int)newWindowHeight {
580   if (type_ == extension_installed_bubble::kBundle) {
581     // Layout the messages from the bottom up.
582     NSTextField* msgs[] = { failedItemsMsg_, failedHeadingMsg_,
583                             installedItemsMsg_, installedHeadingMsg_ };
584     NSInteger offsetFromBottom = 0;
585     BOOL isFirstVisible = YES;
586     for (size_t i = 0; i < arraysize(msgs); ++i) {
587       if ([msgs[i] isHidden])
588         continue;
590       NSRect frame = [msgs[i] frame];
591       NSInteger margin = isFirstVisible ?
592           extension_installed_bubble::kOuterVerticalMargin :
593           extension_installed_bubble::kInnerVerticalMargin;
595       frame.origin.y = offsetFromBottom + margin;
596       [msgs[i] setFrame:frame];
597       offsetFromBottom += NSHeight(frame) + margin;
599       isFirstVisible = NO;
600     }
602     // Move the close button a bit to vertically align it with the heading.
603     NSInteger closeButtonFudge = 1;
604     NSRect frame = [closeButton_ frame];
605     frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
606          extension_installed_bubble::kOuterVerticalMargin);
607     [closeButton_ setFrame:frame];
609     return;
610   }
612   NSRect headingFrame = [heading_ frame];
613   headingFrame.origin.y = newWindowHeight - (
614       NSHeight(headingFrame) +
615       extension_installed_bubble::kOuterVerticalMargin);
616   [heading_ setFrame:headingFrame];
618   NSRect howToManageFrame = [howToManage_ frame];
619   if (!extensions::OmniboxInfo::GetKeyword(extension_).empty() ||
620       extensions::ActionInfo::GetBrowserActionInfo(extension_) ||
621       extensions::ActionInfo::IsVerboseInstallMessage(extension_)) {
622     // For browser actions, page actions and omnibox keyword show the
623     // 'how to use' message before the 'how to manage' message.
624     NSRect howToUseFrame = [howToUse_ frame];
625     howToUseFrame.origin.y = headingFrame.origin.y - (
626         NSHeight(howToUseFrame) +
627         extension_installed_bubble::kInnerVerticalMargin);
628     [howToUse_ setFrame:howToUseFrame];
630     howToManageFrame.origin.y = howToUseFrame.origin.y - (
631         NSHeight(howToManageFrame) +
632         extension_installed_bubble::kInnerVerticalMargin);
633   } else {
634     howToManageFrame.origin.y = NSMinY(headingFrame) - (
635         NSHeight(howToManageFrame) +
636         extension_installed_bubble::kInnerVerticalMargin);
637   }
638   [howToManage_ setFrame:howToManageFrame];
640   NSRect frame = howToManageFrame;
641   if ([self showSyncPromo]) {
642     frame = [promo_.get() frame];
643     frame.origin.y = NSMinY(howToManageFrame) -
644         (NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
645     [promo_.get() setFrame:frame];
646   }
648   extensions::Command command;
649   if (![manageShortcutLink_ isHidden]) {
650     NSRect manageShortcutFrame = [manageShortcutLink_ frame];
651     manageShortcutFrame.origin.y = NSMinY(frame) - (
652         NSHeight(manageShortcutFrame) +
653         extension_installed_bubble::kInnerVerticalMargin);
654     // Right-align the link.
655     manageShortcutFrame.origin.x = NSMaxX(frame) -
656                                    NSWidth(manageShortcutFrame);
657     [manageShortcutLink_ setFrame:manageShortcutFrame];
658   }
661 // Exposed for unit testing.
662 - (NSRect)headingFrame {
663   return [heading_ frame];
666 - (NSRect)frameOfHowToUse {
667   return [howToUse_ frame];
670 - (NSRect)frameOfHowToManage {
671   return [howToManage_ frame];
674 - (NSRect)frameOfSigninPromo {
675   return [promo_ frame];
678 - (NSButton*)appInstalledShortcutLink {
679   return appShortcutLink_;
682 - (void)extensionUnloaded:(id)sender {
683   extension_ = NULL;
686 - (IBAction)onManageShortcutClicked:(id)sender {
687   [self close];
688   std::string configure_url = chrome::kChromeUIExtensionsURL;
689   configure_url += chrome::kExtensionConfigureCommandsSubPage;
690   chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
691       browser_, GURL(configure_url)));
692   chrome::Navigate(&params);
695 - (IBAction)onAppShortcutClicked:(id)sender {
696   ExtensionInstallUI::OpenAppInstalledUI(browser_->profile(), extension_->id());
699 - (void)awakeFromNib {
700   if (bundle_)
701     return;
702   [self initializeLabel];
705 @end