1 // Copyright 2013 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 #include "chrome/browser/ui/views/extensions/extension_installed_bubble_view.h"
10 #include "base/i18n/rtl.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/extension_action.h"
15 #include "chrome/browser/extensions/extension_action_manager.h"
16 #include "chrome/browser/extensions/extension_install_ui.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/signin/signin_promo.h"
19 #include "chrome/browser/ui/browser.h"
20 #include "chrome/browser/ui/browser_window.h"
21 #include "chrome/browser/ui/singleton_tabs.h"
22 #include "chrome/browser/ui/sync/sync_promo_ui.h"
23 #include "chrome/browser/ui/views/frame/browser_view.h"
24 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
25 #include "chrome/browser/ui/views/tabs/tab_strip.h"
26 #include "chrome/browser/ui/views/toolbar/browser_action_view.h"
27 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
28 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
29 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
30 #include "chrome/common/extensions/sync_helper.h"
31 #include "chrome/common/url_constants.h"
32 #include "extensions/common/extension.h"
33 #include "grit/chromium_strings.h"
34 #include "grit/generated_resources.h"
35 #include "grit/ui_resources.h"
36 #include "ui/base/l10n/l10n_util.h"
37 #include "ui/base/resource/resource_bundle.h"
38 #include "ui/gfx/render_text.h"
39 #include "ui/gfx/text_elider.h"
40 #include "ui/views/controls/button/image_button.h"
41 #include "ui/views/controls/image_view.h"
42 #include "ui/views/controls/label.h"
43 #include "ui/views/controls/link.h"
44 #include "ui/views/controls/link_listener.h"
45 #include "ui/views/layout/fill_layout.h"
46 #include "ui/views/layout/layout_constants.h"
48 using extensions::Extension
;
52 const int kIconSize
= 43;
54 const int kRightColumnWidth
= 285;
56 // The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace
57 // around the content view. We compensate by reducing our outer borders by this
59 const int kOuterMarginInset
= 10;
60 const int kHorizOuterMargin
= views::kPanelHorizMargin
- kOuterMarginInset
;
61 const int kVertOuterMargin
= views::kPanelVertMargin
- kOuterMarginInset
;
63 // Interior vertical margin is 8px smaller than standard
64 const int kVertInnerMargin
= views::kPanelVertMargin
- 8;
66 // We want to shift the right column (which contains the header and text) up
67 // 4px to align with icon.
68 const int kRightcolumnVerticalShift
= -4;
74 void ShowExtensionInstalledBubble(const Extension
* extension
,
76 const SkBitmap
& icon
) {
77 ExtensionInstalledBubbleView::Show(extension
, browser
, icon
);
82 // InstalledBubbleContent is the content view which is placed in the
83 // ExtensionInstalledBubbleView. It displays the install icon and explanatory
84 // text about the installed extension.
85 class InstalledBubbleContent
: public views::View
,
86 public views::ButtonListener
,
87 public views::LinkListener
{
89 InstalledBubbleContent(Browser
* browser
,
90 const Extension
* extension
,
91 ExtensionInstalledBubble::BubbleType type
,
93 ExtensionInstalledBubbleView
* bubble
)
95 extension_id_(extension
->id()),
99 height_of_signin_promo_(0u),
103 manage_shortcut_(NULL
) {
104 // The Extension Installed bubble takes on various forms, depending on the
105 // type of extension installed. In general, though, they are all similar:
107 // -------------------------
111 // -------------------------
113 // Icon and Heading are always shown (as well as the close button).
114 // Info is shown for browser actions, page actions and Omnibox keyword
115 // extensions and might list keyboard shorcut for the former two types.
117 // ... for other types, either a description of how to manage the extension
118 // or a link to configure the keybinding shortcut (if one exists).
119 // Extra info can include a promo for signing into sync.
121 // First figure out the keybinding situation.
122 extensions::Command command
;
123 bool has_keybinding
= GetKeybinding(&command
);
124 base::string16 key
; // Keyboard shortcut or keyword to display in bubble.
126 if (extensions::sync_helper::IsSyncableExtension(extension
) &&
127 SyncPromoUI::ShouldShowSyncPromo(browser
->profile()))
128 flavors_
|= SIGN_IN_PROMO
;
130 // Determine the bubble flavor we want, based on the extension type.
132 case ExtensionInstalledBubble::BROWSER_ACTION
:
133 case ExtensionInstalledBubble::PAGE_ACTION
: {
134 flavors_
|= HOW_TO_USE
;
135 if (has_keybinding
) {
136 flavors_
|= SHOW_KEYBINDING
;
137 key
= command
.accelerator().GetShortcutText();
139 // The How-To-Use text makes the bubble seem a little crowded when the
140 // extension has a keybinding, so the How-To-Manage text is not shown
142 flavors_
|= HOW_TO_MANAGE
;
146 case ExtensionInstalledBubble::OMNIBOX_KEYWORD
: {
147 flavors_
|= HOW_TO_USE
| HOW_TO_MANAGE
;
148 key
= base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension
));
151 case ExtensionInstalledBubble::GENERIC
: {
155 // When adding a new bubble type, the flavor needs to be set.
156 COMPILE_ASSERT(ExtensionInstalledBubble::GENERIC
== 3,
157 kBubbleTypeEnumHasChangedButNotThisSwitchStatement
);
162 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
163 const gfx::FontList
& font_list
=
164 rb
.GetFontList(ui::ResourceBundle::BaseFont
);
166 // Add the icon (for all flavors).
167 // Scale down to 43x43, but allow smaller icons (don't scale up).
168 gfx::Size
size(icon
->width(), icon
->height());
169 if (size
.width() > kIconSize
|| size
.height() > kIconSize
)
170 size
= gfx::Size(kIconSize
, kIconSize
);
171 icon_
= new views::ImageView();
172 icon_
->SetImageSize(size
);
173 icon_
->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(*icon
));
176 // Add the heading (for all flavors).
177 base::string16 extension_name
= base::UTF8ToUTF16(extension
->name());
178 base::i18n::AdjustStringForLocaleDirection(&extension_name
);
179 heading_
= new views::Label(l10n_util::GetStringFUTF16(
180 IDS_EXTENSION_INSTALLED_HEADING
, extension_name
));
181 heading_
->SetFontList(rb
.GetFontList(ui::ResourceBundle::MediumFont
));
182 heading_
->SetMultiLine(true);
183 heading_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
184 AddChildView(heading_
);
186 if (flavors_
& HOW_TO_USE
) {
187 how_to_use_
= new views::Label(GetHowToUseDescription(key
));
188 how_to_use_
->SetFontList(font_list
);
189 how_to_use_
->SetMultiLine(true);
190 how_to_use_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
191 AddChildView(how_to_use_
);
194 if (flavors_
& SHOW_KEYBINDING
) {
195 manage_shortcut_
= new views::Link(
196 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS
));
197 manage_shortcut_
->set_listener(this);
198 AddChildView(manage_shortcut_
);
201 if (flavors_
& HOW_TO_MANAGE
) {
202 manage_
= new views::Label(l10n_util::GetStringUTF16(
203 #if defined(OS_CHROMEOS)
204 IDS_EXTENSION_INSTALLED_MANAGE_INFO_CHROMEOS
));
206 IDS_EXTENSION_INSTALLED_MANAGE_INFO
));
208 manage_
->SetFontList(font_list
);
209 manage_
->SetMultiLine(true);
210 manage_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
211 AddChildView(manage_
);
214 if (flavors_
& SIGN_IN_PROMO
) {
216 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO
);
218 signin_promo_link_text_
=
219 l10n_util::GetStringFUTF16(
220 IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK
,
221 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME
));
222 sign_in_link_
= new views::Link(signin_promo_link_text_
);
223 sign_in_link_
->SetFontList(font_list
);
224 sign_in_link_
->set_listener(this);
225 AddChildView(sign_in_link_
);
228 // Add the Close button (for all flavors).
229 close_button_
= new views::ImageButton(this);
230 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
231 rb
.GetImageSkiaNamed(IDR_CLOSE_2
));
232 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
233 rb
.GetImageSkiaNamed(IDR_CLOSE_2_H
));
234 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
235 rb
.GetImageSkiaNamed(IDR_CLOSE_2_P
));
236 AddChildView(close_button_
);
239 virtual void ButtonPressed(views::Button
* sender
,
240 const ui::Event
& event
) OVERRIDE
{
241 if (sender
== close_button_
)
242 bubble_
->StartFade(false);
244 NOTREACHED() << "Unknown view";
247 // Implements the views::LinkListener interface.
248 virtual void LinkClicked(views::Link
* source
, int event_flags
) OVERRIDE
{
249 GetWidget()->Close();
250 std::string configure_url
;
251 if (source
== manage_shortcut_
) {
252 configure_url
= chrome::kChromeUIExtensionsURL
;
253 configure_url
+= chrome::kExtensionConfigureCommandsSubPage
;
254 } else if (source
== sign_in_link_
) {
255 configure_url
= signin::GetPromoURL(
256 signin::SOURCE_EXTENSION_INSTALL_BUBBLE
, false).spec();
261 chrome::NavigateParams
params(
262 chrome::GetSingletonTabNavigateParams(
263 browser_
, GURL(configure_url
.c_str())));
264 chrome::Navigate(¶ms
);
271 HOW_TO_MANAGE
= 1 << 1,
272 SHOW_KEYBINDING
= 1 << 2,
273 SIGN_IN_PROMO
= 1 << 3,
276 bool GetKeybinding(extensions::Command
* command
) {
277 extensions::CommandService
* command_service
=
278 extensions::CommandService::Get(browser_
->profile());
279 if (type_
== ExtensionInstalledBubble::BROWSER_ACTION
) {
280 return command_service
->GetBrowserActionCommand(
282 extensions::CommandService::ACTIVE_ONLY
,
285 } else if (type_
== ExtensionInstalledBubble::PAGE_ACTION
) {
286 return command_service
->GetPageActionCommand(
288 extensions::CommandService::ACTIVE_ONLY
,
296 base::string16
GetHowToUseDescription(const base::string16
& key
) {
298 case ExtensionInstalledBubble::BROWSER_ACTION
:
300 return l10n_util::GetStringFUTF16(
301 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT
, key
);
303 return l10n_util::GetStringUTF16(
304 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO
);
307 case ExtensionInstalledBubble::PAGE_ACTION
:
309 return l10n_util::GetStringFUTF16(
310 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT
, key
);
312 return l10n_util::GetStringUTF16(
313 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO
);
316 case ExtensionInstalledBubble::OMNIBOX_KEYWORD
:
317 return l10n_util::GetStringFUTF16(
318 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO
, key
);
324 return base::string16();
327 // Layout the signin promo at coordinates |offset_x| and |offset_y|. Returns
328 // the height (in pixels) of the promo UI.
329 int LayoutSigninPromo(int offset_x
, int offset_y
) {
330 sign_in_promo_lines_
.clear();
332 gfx::Rect contents_area
= GetContentsBounds();
333 if (contents_area
.IsEmpty())
335 contents_area
.set_width(kRightColumnWidth
);
337 base::string16 full_text
= signin_promo_link_text_
+ signin_promo_text_
;
339 // The link is the first item in the text.
340 const gfx::Size link_size
= sign_in_link_
->GetPreferredSize();
341 sign_in_link_
->SetBounds(
342 offset_x
, offset_y
, link_size
.width(), link_size
.height());
344 // Word-wrap the full label text.
345 const gfx::FontList font_list
;
346 std::vector
<base::string16
> lines
;
347 gfx::ElideRectangleText(full_text
, font_list
, contents_area
.width(),
348 contents_area
.height(), gfx::ELIDE_LONG_WORDS
,
351 gfx::Point position
= gfx::Point(
352 contents_area
.origin().x() + offset_x
,
353 contents_area
.origin().y() + offset_y
+ 1);
354 if (base::i18n::IsRTL()) {
355 position
-= gfx::Vector2d(
356 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
, 0);
359 // Loop through the lines, creating a renderer for each.
360 for (std::vector
<base::string16
>::const_iterator it
= lines
.begin();
361 it
!= lines
.end(); ++it
) {
362 gfx::RenderText
* line
= gfx::RenderText::CreateInstance();
363 line
->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI
);
365 const gfx::Size
size(contents_area
.width(),
366 line
->GetStringSize().height());
367 line
->SetDisplayRect(gfx::Rect(position
, size
));
368 position
.set_y(position
.y() + size
.height());
369 sign_in_promo_lines_
.push_back(line
);
370 height
+= size
.height();
373 // The link is drawn separately; make it transparent here to only draw once.
374 // The link always leads other text and is assumed to fit on the first line.
375 sign_in_promo_lines_
.front()->ApplyColor(SK_ColorTRANSPARENT
,
376 gfx::Range(0, signin_promo_link_text_
.size()));
381 virtual gfx::Size
GetPreferredSize() OVERRIDE
{
382 int width
= kHorizOuterMargin
;
384 width
+= views::kPanelHorizMargin
;
385 width
+= kRightColumnWidth
;
386 width
+= 2 * views::kPanelHorizMargin
;
387 width
+= kHorizOuterMargin
;
389 int height
= kVertOuterMargin
;
390 height
+= heading_
->GetHeightForWidth(kRightColumnWidth
);
391 height
+= kVertInnerMargin
;
393 if (flavors_
& HOW_TO_USE
) {
394 height
+= how_to_use_
->GetHeightForWidth(kRightColumnWidth
);
395 height
+= kVertInnerMargin
;
398 if (flavors_
& HOW_TO_MANAGE
) {
399 height
+= manage_
->GetHeightForWidth(kRightColumnWidth
);
400 height
+= kVertInnerMargin
;
403 if (flavors_
& SIGN_IN_PROMO
&& height_of_signin_promo_
> 0u) {
404 height
+= height_of_signin_promo_
;
405 height
+= kVertInnerMargin
;
408 if (flavors_
& SHOW_KEYBINDING
) {
409 height
+= manage_shortcut_
->GetHeightForWidth(kRightColumnWidth
);
410 height
+= kVertInnerMargin
;
413 return gfx::Size(width
, std::max(height
, kIconSize
+ 2 * kVertOuterMargin
));
416 virtual void Layout() OVERRIDE
{
417 int x
= kHorizOuterMargin
;
418 int y
= kVertOuterMargin
;
420 icon_
->SetBounds(x
, y
, kIconSize
, kIconSize
);
422 x
+= views::kPanelHorizMargin
;
424 y
+= kRightcolumnVerticalShift
;
425 heading_
->SizeToFit(kRightColumnWidth
);
428 y
+= heading_
->height();
429 y
+= kVertInnerMargin
;
431 if (flavors_
& HOW_TO_USE
) {
432 how_to_use_
->SizeToFit(kRightColumnWidth
);
433 how_to_use_
->SetX(x
);
434 how_to_use_
->SetY(y
);
435 y
+= how_to_use_
->height();
436 y
+= kVertInnerMargin
;
439 if (flavors_
& HOW_TO_MANAGE
) {
440 manage_
->SizeToFit(kRightColumnWidth
);
443 y
+= manage_
->height();
444 y
+= kVertInnerMargin
;
447 if (flavors_
& SIGN_IN_PROMO
) {
448 height_of_signin_promo_
= LayoutSigninPromo(x
, y
);
449 y
+= height_of_signin_promo_
;
450 y
+= kVertInnerMargin
;
453 if (flavors_
& SHOW_KEYBINDING
) {
454 gfx::Size sz
= manage_shortcut_
->GetPreferredSize();
455 manage_shortcut_
->SetBounds(width() - 2 * kHorizOuterMargin
- sz
.width(),
459 y
+= manage_shortcut_
->height();
460 y
+= kVertInnerMargin
;
464 x
+= kRightColumnWidth
+ 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
-
465 close_button_
->GetPreferredSize().width();
466 y
= kVertOuterMargin
;
467 sz
= close_button_
->GetPreferredSize();
468 // x-1 & y-1 is just slop to get the close button visually aligned with the
469 // title text and bubble arrow.
470 close_button_
->SetBounds(x
- 1, y
- 1, sz
.width(), sz
.height());
473 virtual void OnPaint(gfx::Canvas
* canvas
) OVERRIDE
{
474 for (ScopedVector
<gfx::RenderText
>::const_iterator it
=
475 sign_in_promo_lines_
.begin();
476 it
!= sign_in_promo_lines_
.end(); ++it
)
479 views::View::OnPaint(canvas
);
482 // The browser we're associated with.
485 // The id of the extension just installed.
486 const std::string extension_id_
;
488 // The ExtensionInstalledBubbleView showing us.
489 ExtensionInstalledBubbleView
* bubble_
;
491 // The string that contains the link text at the beginning of the sign-in
493 base::string16 signin_promo_link_text_
;
494 // The remaining text of the sign-in promo text.
495 base::string16 signin_promo_text_
;
497 // A vector of RenderText objects representing the full sign-in promo
498 // paragraph as layed out within the bubble, but has the text of the link
499 // whited out so the link can be drawn in its place.
500 ScopedVector
<gfx::RenderText
> sign_in_promo_lines_
;
502 // The type of the bubble to show (Browser Action, Omnibox keyword, etc).
503 ExtensionInstalledBubble::BubbleType type_
;
505 // A bitmask containing the various flavors of bubble sections to show.
508 // The height, in pixels, of the sign-in promo.
509 size_t height_of_signin_promo_
;
511 views::ImageView
* icon_
;
512 views::Label
* heading_
;
513 views::Label
* how_to_use_
;
514 views::Link
* sign_in_link_
;
515 views::Label
* manage_
;
516 views::Link
* manage_shortcut_
;
517 views::ImageButton
* close_button_
;
519 DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent
);
522 void ExtensionInstalledBubbleView::Show(const Extension
* extension
,
524 const SkBitmap
& icon
) {
525 new ExtensionInstalledBubbleView(extension
, browser
, icon
);
528 ExtensionInstalledBubbleView::ExtensionInstalledBubbleView(
529 const Extension
* extension
, Browser
*browser
, const SkBitmap
& icon
)
530 : bubble_(this, extension
, browser
, icon
) {
533 ExtensionInstalledBubbleView::~ExtensionInstalledBubbleView() {}
535 bool ExtensionInstalledBubbleView::MaybeShowNow() {
536 BrowserView
* browser_view
=
537 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
538 extensions::ExtensionActionManager
* extension_action_manager
=
539 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile());
541 views::View
* reference_view
= NULL
;
542 if (bubble_
.type() == bubble_
.BROWSER_ACTION
) {
543 BrowserActionsContainer
* container
=
544 browser_view
->GetToolbarView()->browser_actions();
545 if (container
->animating())
548 reference_view
= container
->GetBrowserActionView(
549 extension_action_manager
->GetBrowserAction(*bubble_
.extension()));
550 // If the view is not visible then it is in the chevron, so point the
551 // install bubble to the chevron instead. If this is an incognito window,
552 // both could be invisible.
553 if (!reference_view
|| !reference_view
->visible()) {
554 reference_view
= container
->chevron();
555 if (!reference_view
|| !reference_view
->visible())
556 reference_view
= NULL
; // fall back to app menu below.
558 } else if (bubble_
.type() == bubble_
.PAGE_ACTION
) {
559 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
560 ExtensionAction
* page_action
=
561 extension_action_manager
->GetPageAction(*bubble_
.extension());
562 location_bar_view
->SetPreviewEnabledPageAction(page_action
,
563 true); // preview_enabled
564 reference_view
= location_bar_view
->GetPageActionView(page_action
);
565 DCHECK(reference_view
);
566 } else if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
567 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
568 reference_view
= location_bar_view
;
569 DCHECK(reference_view
);
573 if (reference_view
== NULL
)
574 reference_view
= browser_view
->GetToolbarView()->app_menu();
575 SetAnchorView(reference_view
);
577 set_arrow(bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
?
578 views::BubbleBorder::TOP_LEFT
:
579 views::BubbleBorder::TOP_RIGHT
);
580 SetLayoutManager(new views::FillLayout());
581 AddChildView(new InstalledBubbleContent(
582 bubble_
.browser(), bubble_
.extension(), bubble_
.type(),
583 &bubble_
.icon(), this));
585 views::BubbleDelegateView::CreateBubble(this);
587 // The bubble widget is now the parent and owner of |this| and takes care of
588 // deletion when the bubble or browser go away.
589 bubble_
.IgnoreBrowserClosing();
595 gfx::Rect
ExtensionInstalledBubbleView::GetAnchorRect() {
596 // For omnibox keyword bubbles, move the arrow to point to the left edge
597 // of the omnibox, just to the right of the icon.
598 if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
599 LocationBarView
* location_bar_view
=
600 BrowserView::GetBrowserViewForBrowser(bubble_
.browser())->
601 GetLocationBarView();
602 return gfx::Rect(location_bar_view
->GetOmniboxViewOrigin(),
603 gfx::Size(0, location_bar_view
->omnibox_view()->height()));
605 return views::BubbleDelegateView::GetAnchorRect();
608 void ExtensionInstalledBubbleView::WindowClosing() {
609 if (bubble_
.extension() && bubble_
.type() == bubble_
.PAGE_ACTION
) {
610 BrowserView
* browser_view
=
611 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
612 browser_view
->GetLocationBarView()->SetPreviewEnabledPageAction(
613 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile())->
614 GetPageAction(*bubble_
.extension()),
615 false); // preview_enabled