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 {
55 explicit ExtensionInstalledBubbleBridge(
56 ExtensionInstalledBubbleController* controller);
57 ~ExtensionInstalledBubbleBridge() override;
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_];
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
93 NSString* nibName = bundle ? @"ExtensionInstalledBubbleBundle" :
94 @"ExtensionInstalledBubble";
95 if ((self = [super initWithWindowNibPath:nibName
96 parentWindow:parentWindow
97 anchoredAt:NSZeroPoint])) {
101 icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
102 pageActionPreviewShowing_ = NO;
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;
116 type_ = extension_installed_bubble::kGeneric;
119 if (type_ == extension_installed_bubble::kBundle) {
120 [self showWindow:self];
122 // Start showing window only after extension has fully loaded.
123 installedBubbleBridge_.reset(new ExtensionInstalledBubbleBridge(self));
124 installedBubble_.reset(new ExtensionInstalledBubble(
125 installedBubbleBridge_.get(),
129 installedBubble_->IgnoreBrowserClosing();
135 - (const Extension*)extension {
136 if (type_ == extension_installed_bubble::kBundle)
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
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)
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];
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]);
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);
197 // Extracted to a function here so that it can be overridden for unit testing.
198 - (void)removePageActionPreviewIfNecessary {
199 if (![self extension] || !pageActionPreviewShowing_)
201 ExtensionAction* page_action =
202 extensions::ExtensionActionManager::Get(browser_->profile())->
203 GetPageAction(*[self extension]);
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(
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()->
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
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();
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];
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.
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];
312 [self.bubble setArrowLocation:info_bubble::kTopRight];
315 if (type_ == extension_installed_bubble::kBundle)
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)];
323 [iconImage_ setImage:icon_];
324 [iconImage_ setNeedsDisplay:YES];
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
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;
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])
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]);
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];
404 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
405 [GTMUILocalizerAndLayoutTweaker
406 sizeToFitFixedWidthTextField:howToUse_];
407 newWindowHeight += NSHeight([howToUse_ frame]) +
408 extension_installed_bubble::kInnerVerticalMargin;
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];
417 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
418 [GTMUILocalizerAndLayoutTweaker
419 sizeToFitFixedWidthTextField:howToUse_];
420 newWindowHeight += NSHeight([howToUse_ frame]) +
421 extension_installed_bubble::kInnerVerticalMargin;
424 // If type is app, hide howToManage_, and include a "show me" link in the
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]);
432 // Second part of extension installed message.
434 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
435 [GTMUILocalizerAndLayoutTweaker
436 sizeToFitFixedWidthTextField:howToManage_];
437 newWindowHeight += NSHeight([howToManage_ frame]);
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;
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]);
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];
475 [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
476 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];
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])
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;
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];
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);
546 howToManageFrame.origin.y = NSMinY(headingFrame) - (
547 NSHeight(howToManageFrame) +
548 extension_installed_bubble::kInnerVerticalMargin);
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];
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];
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 {
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(¶ms);
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 {
611 [self initializeLabel];