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