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