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/location_bar/page_action_with_badge_view.h"
26 #include "chrome/browser/ui/views/tabs/tab_strip.h"
27 #include "chrome/browser/ui/views/toolbar/browser_action_view.h"
28 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
29 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
30 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
31 #include "chrome/common/extensions/sync_helper.h"
32 #include "chrome/common/url_constants.h"
33 #include "extensions/common/extension.h"
34 #include "grit/chromium_strings.h"
35 #include "grit/generated_resources.h"
36 #include "grit/ui_resources.h"
37 #include "ui/base/l10n/l10n_util.h"
38 #include "ui/base/resource/resource_bundle.h"
39 #include "ui/gfx/render_text.h"
40 #include "ui/gfx/text_elider.h"
41 #include "ui/views/controls/button/image_button.h"
42 #include "ui/views/controls/image_view.h"
43 #include "ui/views/controls/label.h"
44 #include "ui/views/controls/link.h"
45 #include "ui/views/controls/link_listener.h"
46 #include "ui/views/layout/fill_layout.h"
47 #include "ui/views/layout/layout_constants.h"
49 using extensions::Extension
;
53 const int kIconSize
= 43;
55 const int kRightColumnWidth
= 285;
57 // The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace
58 // around the content view. We compensate by reducing our outer borders by this
60 const int kOuterMarginInset
= 10;
61 const int kHorizOuterMargin
= views::kPanelHorizMargin
- kOuterMarginInset
;
62 const int kVertOuterMargin
= views::kPanelVertMargin
- kOuterMarginInset
;
64 // Interior vertical margin is 8px smaller than standard
65 const int kVertInnerMargin
= views::kPanelVertMargin
- 8;
67 // We want to shift the right column (which contains the header and text) up
68 // 4px to align with icon.
69 const int kRightcolumnVerticalShift
= -4;
75 void ShowExtensionInstalledBubble(const Extension
* extension
,
77 const SkBitmap
& icon
) {
78 ExtensionInstalledBubbleView::Show(extension
, browser
, icon
);
83 // InstalledBubbleContent is the content view which is placed in the
84 // ExtensionInstalledBubbleView. It displays the install icon and explanatory
85 // text about the installed extension.
86 class InstalledBubbleContent
: public views::View
,
87 public views::ButtonListener
,
88 public views::LinkListener
{
90 InstalledBubbleContent(Browser
* browser
,
91 const Extension
* extension
,
92 ExtensionInstalledBubble::BubbleType type
,
95 extension_id_(extension
->id()),
98 height_of_signin_promo_(0u),
102 manage_shortcut_(NULL
) {
103 // The Extension Installed bubble takes on various forms, depending on the
104 // type of extension installed. In general, though, they are all similar:
106 // -------------------------
110 // -------------------------
112 // Icon and Heading are always shown (as well as the close button).
113 // Info is shown for browser actions, page actions and Omnibox keyword
114 // extensions and might list keyboard shorcut for the former two types.
116 // ... for other types, either a description of how to manage the extension
117 // or a link to configure the keybinding shortcut (if one exists).
118 // Extra info can include a promo for signing into sync.
120 // First figure out the keybinding situation.
121 extensions::Command command
;
122 bool has_keybinding
= GetKeybinding(&command
);
123 base::string16 key
; // Keyboard shortcut or keyword to display in bubble.
125 if (extensions::sync_helper::IsSyncableExtension(extension
) &&
126 SyncPromoUI::ShouldShowSyncPromo(browser
->profile()))
127 flavors_
|= SIGN_IN_PROMO
;
129 // Determine the bubble flavor we want, based on the extension type.
131 case ExtensionInstalledBubble::BROWSER_ACTION
:
132 case ExtensionInstalledBubble::PAGE_ACTION
: {
133 flavors_
|= HOW_TO_USE
;
134 if (has_keybinding
) {
135 flavors_
|= SHOW_KEYBINDING
;
136 key
= command
.accelerator().GetShortcutText();
138 // The How-To-Use text makes the bubble seem a little crowded when the
139 // extension has a keybinding, so the How-To-Manage text is not shown
141 flavors_
|= HOW_TO_MANAGE
;
145 case ExtensionInstalledBubble::OMNIBOX_KEYWORD
: {
146 flavors_
|= HOW_TO_USE
| HOW_TO_MANAGE
;
147 key
= base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension
));
150 case ExtensionInstalledBubble::GENERIC
: {
154 // When adding a new bubble type, the flavor needs to be set.
155 COMPILE_ASSERT(ExtensionInstalledBubble::GENERIC
== 3,
156 kBubbleTypeEnumHasChangedButNotThisSwitchStatement
);
161 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
162 const gfx::FontList
& font_list
=
163 rb
.GetFontList(ui::ResourceBundle::BaseFont
);
165 // Add the icon (for all flavors).
166 // Scale down to 43x43, but allow smaller icons (don't scale up).
167 gfx::Size
size(icon
->width(), icon
->height());
168 if (size
.width() > kIconSize
|| size
.height() > kIconSize
)
169 size
= gfx::Size(kIconSize
, kIconSize
);
170 icon_
= new views::ImageView();
171 icon_
->SetImageSize(size
);
172 icon_
->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(*icon
));
175 // Add the heading (for all flavors).
176 base::string16 extension_name
= base::UTF8ToUTF16(extension
->name());
177 base::i18n::AdjustStringForLocaleDirection(&extension_name
);
178 heading_
= new views::Label(l10n_util::GetStringFUTF16(
179 IDS_EXTENSION_INSTALLED_HEADING
, extension_name
));
180 heading_
->SetFontList(rb
.GetFontList(ui::ResourceBundle::MediumFont
));
181 heading_
->SetMultiLine(true);
182 heading_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
183 AddChildView(heading_
);
185 if (flavors_
& HOW_TO_USE
) {
186 how_to_use_
= new views::Label(GetHowToUseDescription(key
));
187 how_to_use_
->SetFontList(font_list
);
188 how_to_use_
->SetMultiLine(true);
189 how_to_use_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
190 AddChildView(how_to_use_
);
193 if (flavors_
& SHOW_KEYBINDING
) {
194 manage_shortcut_
= new views::Link(
195 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS
));
196 manage_shortcut_
->set_listener(this);
197 AddChildView(manage_shortcut_
);
200 if (flavors_
& HOW_TO_MANAGE
) {
201 manage_
= new views::Label(l10n_util::GetStringUTF16(
202 #if defined(OS_CHROMEOS)
203 IDS_EXTENSION_INSTALLED_MANAGE_INFO_CHROMEOS
));
205 IDS_EXTENSION_INSTALLED_MANAGE_INFO
));
207 manage_
->SetFontList(font_list
);
208 manage_
->SetMultiLine(true);
209 manage_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
210 AddChildView(manage_
);
213 if (flavors_
& SIGN_IN_PROMO
) {
215 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO
);
217 signin_promo_link_text_
=
218 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK
);
219 sign_in_link_
= new views::Link(signin_promo_link_text_
);
220 sign_in_link_
->SetFontList(font_list
);
221 sign_in_link_
->set_listener(this);
222 AddChildView(sign_in_link_
);
225 // Add the Close button (for all flavors).
226 close_button_
= new views::ImageButton(this);
227 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
228 rb
.GetImageSkiaNamed(IDR_CLOSE_2
));
229 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
230 rb
.GetImageSkiaNamed(IDR_CLOSE_2_H
));
231 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
232 rb
.GetImageSkiaNamed(IDR_CLOSE_2_P
));
233 AddChildView(close_button_
);
236 virtual void ButtonPressed(views::Button
* sender
,
237 const ui::Event
& event
) OVERRIDE
{
238 DCHECK_EQ(sender
, close_button_
);
239 GetWidget()->Close();
242 // Implements the views::LinkListener interface.
243 virtual void LinkClicked(views::Link
* source
, int event_flags
) OVERRIDE
{
244 GetWidget()->Close();
245 std::string configure_url
;
246 if (source
== manage_shortcut_
) {
247 configure_url
= chrome::kChromeUIExtensionsURL
;
248 configure_url
+= chrome::kExtensionConfigureCommandsSubPage
;
249 } else if (source
== sign_in_link_
) {
250 configure_url
= signin::GetPromoURL(
251 signin::SOURCE_EXTENSION_INSTALL_BUBBLE
, false).spec();
256 chrome::NavigateParams
params(
257 chrome::GetSingletonTabNavigateParams(
258 browser_
, GURL(configure_url
.c_str())));
259 chrome::Navigate(¶ms
);
266 HOW_TO_MANAGE
= 1 << 1,
267 SHOW_KEYBINDING
= 1 << 2,
268 SIGN_IN_PROMO
= 1 << 3,
271 bool GetKeybinding(extensions::Command
* command
) {
272 extensions::CommandService
* command_service
=
273 extensions::CommandService::Get(browser_
->profile());
274 if (type_
== ExtensionInstalledBubble::BROWSER_ACTION
) {
275 return command_service
->GetBrowserActionCommand(
277 extensions::CommandService::ACTIVE_ONLY
,
280 } else if (type_
== ExtensionInstalledBubble::PAGE_ACTION
) {
281 return command_service
->GetPageActionCommand(
283 extensions::CommandService::ACTIVE_ONLY
,
291 base::string16
GetHowToUseDescription(const base::string16
& key
) {
293 case ExtensionInstalledBubble::BROWSER_ACTION
:
295 return l10n_util::GetStringFUTF16(
296 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT
, key
);
298 return l10n_util::GetStringUTF16(
299 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO
);
302 case ExtensionInstalledBubble::PAGE_ACTION
:
304 return l10n_util::GetStringFUTF16(
305 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT
, key
);
307 return l10n_util::GetStringUTF16(
308 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO
);
311 case ExtensionInstalledBubble::OMNIBOX_KEYWORD
:
312 return l10n_util::GetStringFUTF16(
313 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO
, key
);
319 return base::string16();
322 // Layout the signin promo at coordinates |offset_x| and |offset_y|. Returns
323 // the height (in pixels) of the promo UI.
324 int LayoutSigninPromo(int offset_x
, int offset_y
) {
325 sign_in_promo_lines_
.clear();
327 gfx::Rect contents_area
= GetContentsBounds();
328 if (contents_area
.IsEmpty())
330 contents_area
.set_width(kRightColumnWidth
);
332 base::string16 full_text
= signin_promo_link_text_
+ signin_promo_text_
;
334 // The link is the first item in the text.
335 const gfx::Size link_size
= sign_in_link_
->GetPreferredSize();
336 sign_in_link_
->SetBounds(
337 offset_x
, offset_y
, link_size
.width(), link_size
.height());
339 // Word-wrap the full label text.
340 const gfx::FontList font_list
;
341 std::vector
<base::string16
> lines
;
342 gfx::ElideRectangleText(full_text
, font_list
, contents_area
.width(),
343 contents_area
.height(), gfx::ELIDE_LONG_WORDS
,
346 gfx::Point position
= gfx::Point(
347 contents_area
.origin().x() + offset_x
,
348 contents_area
.origin().y() + offset_y
+ 1);
349 if (base::i18n::IsRTL()) {
350 position
-= gfx::Vector2d(
351 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
, 0);
354 // Loop through the lines, creating a renderer for each.
355 for (std::vector
<base::string16
>::const_iterator it
= lines
.begin();
356 it
!= lines
.end(); ++it
) {
357 gfx::RenderText
* line
= gfx::RenderText::CreateInstance();
358 line
->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI
);
360 const gfx::Size
size(contents_area
.width(),
361 line
->GetStringSize().height());
362 line
->SetDisplayRect(gfx::Rect(position
, size
));
363 position
.set_y(position
.y() + size
.height());
364 sign_in_promo_lines_
.push_back(line
);
365 height
+= size
.height();
368 // The link is drawn separately; make it transparent here to only draw once.
369 // The link always leads other text and is assumed to fit on the first line.
370 sign_in_promo_lines_
.front()->ApplyColor(SK_ColorTRANSPARENT
,
371 gfx::Range(0, signin_promo_link_text_
.size()));
376 virtual gfx::Size
GetPreferredSize() const OVERRIDE
{
377 int width
= kHorizOuterMargin
;
379 width
+= views::kPanelHorizMargin
;
380 width
+= kRightColumnWidth
;
381 width
+= 2 * views::kPanelHorizMargin
;
382 width
+= kHorizOuterMargin
;
384 int height
= kVertOuterMargin
;
385 height
+= heading_
->GetHeightForWidth(kRightColumnWidth
);
386 height
+= kVertInnerMargin
;
388 if (flavors_
& HOW_TO_USE
) {
389 height
+= how_to_use_
->GetHeightForWidth(kRightColumnWidth
);
390 height
+= kVertInnerMargin
;
393 if (flavors_
& HOW_TO_MANAGE
) {
394 height
+= manage_
->GetHeightForWidth(kRightColumnWidth
);
395 height
+= kVertInnerMargin
;
398 if (flavors_
& SIGN_IN_PROMO
&& height_of_signin_promo_
> 0u) {
399 height
+= height_of_signin_promo_
;
400 height
+= kVertInnerMargin
;
403 if (flavors_
& SHOW_KEYBINDING
) {
404 height
+= manage_shortcut_
->GetHeightForWidth(kRightColumnWidth
);
405 height
+= kVertInnerMargin
;
408 return gfx::Size(width
, std::max(height
, kIconSize
+ 2 * kVertOuterMargin
));
411 virtual void Layout() OVERRIDE
{
412 int x
= kHorizOuterMargin
;
413 int y
= kVertOuterMargin
;
415 icon_
->SetBounds(x
, y
, kIconSize
, kIconSize
);
417 x
+= views::kPanelHorizMargin
;
419 y
+= kRightcolumnVerticalShift
;
420 heading_
->SizeToFit(kRightColumnWidth
);
423 y
+= heading_
->height();
424 y
+= kVertInnerMargin
;
426 if (flavors_
& HOW_TO_USE
) {
427 how_to_use_
->SizeToFit(kRightColumnWidth
);
428 how_to_use_
->SetX(x
);
429 how_to_use_
->SetY(y
);
430 y
+= how_to_use_
->height();
431 y
+= kVertInnerMargin
;
434 if (flavors_
& HOW_TO_MANAGE
) {
435 manage_
->SizeToFit(kRightColumnWidth
);
438 y
+= manage_
->height();
439 y
+= kVertInnerMargin
;
442 if (flavors_
& SIGN_IN_PROMO
) {
443 height_of_signin_promo_
= LayoutSigninPromo(x
, y
);
444 y
+= height_of_signin_promo_
;
445 y
+= kVertInnerMargin
;
448 if (flavors_
& SHOW_KEYBINDING
) {
449 gfx::Size sz
= manage_shortcut_
->GetPreferredSize();
450 manage_shortcut_
->SetBounds(width() - 2 * kHorizOuterMargin
- sz
.width(),
454 y
+= manage_shortcut_
->height();
455 y
+= kVertInnerMargin
;
459 x
+= kRightColumnWidth
+ 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
-
460 close_button_
->GetPreferredSize().width();
461 y
= kVertOuterMargin
;
462 sz
= close_button_
->GetPreferredSize();
463 // x-1 & y-1 is just slop to get the close button visually aligned with the
464 // title text and bubble arrow.
465 close_button_
->SetBounds(x
- 1, y
- 1, sz
.width(), sz
.height());
468 virtual void OnPaint(gfx::Canvas
* canvas
) OVERRIDE
{
469 for (ScopedVector
<gfx::RenderText
>::const_iterator it
=
470 sign_in_promo_lines_
.begin();
471 it
!= sign_in_promo_lines_
.end(); ++it
)
474 views::View::OnPaint(canvas
);
477 // The browser we're associated with.
480 // The id of the extension just installed.
481 const std::string extension_id_
;
483 // The string that contains the link text at the beginning of the sign-in
485 base::string16 signin_promo_link_text_
;
486 // The remaining text of the sign-in promo text.
487 base::string16 signin_promo_text_
;
489 // A vector of RenderText objects representing the full sign-in promo
490 // paragraph as layed out within the bubble, but has the text of the link
491 // whited out so the link can be drawn in its place.
492 ScopedVector
<gfx::RenderText
> sign_in_promo_lines_
;
494 // The type of the bubble to show (Browser Action, Omnibox keyword, etc).
495 ExtensionInstalledBubble::BubbleType type_
;
497 // A bitmask containing the various flavors of bubble sections to show.
500 // The height, in pixels, of the sign-in promo.
501 size_t height_of_signin_promo_
;
503 views::ImageView
* icon_
;
504 views::Label
* heading_
;
505 views::Label
* how_to_use_
;
506 views::Link
* sign_in_link_
;
507 views::Label
* manage_
;
508 views::Link
* manage_shortcut_
;
509 views::ImageButton
* close_button_
;
511 DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent
);
514 void ExtensionInstalledBubbleView::Show(const Extension
* extension
,
516 const SkBitmap
& icon
) {
517 new ExtensionInstalledBubbleView(extension
, browser
, icon
);
520 ExtensionInstalledBubbleView::ExtensionInstalledBubbleView(
521 const Extension
* extension
, Browser
*browser
, const SkBitmap
& icon
)
522 : bubble_(this, extension
, browser
, icon
) {
525 ExtensionInstalledBubbleView::~ExtensionInstalledBubbleView() {}
527 bool ExtensionInstalledBubbleView::MaybeShowNow() {
528 BrowserView
* browser_view
=
529 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
530 extensions::ExtensionActionManager
* extension_action_manager
=
531 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile());
533 views::View
* reference_view
= NULL
;
534 if (bubble_
.type() == bubble_
.BROWSER_ACTION
) {
535 BrowserActionsContainer
* container
=
536 browser_view
->GetToolbarView()->browser_actions();
537 if (container
->animating())
540 reference_view
= container
->GetBrowserActionView(
541 extension_action_manager
->GetBrowserAction(*bubble_
.extension()));
542 // If the view is not visible then it is in the chevron, so point the
543 // install bubble to the chevron instead. If this is an incognito window,
544 // both could be invisible.
545 if (!reference_view
|| !reference_view
->visible()) {
546 reference_view
= container
->chevron();
547 if (!reference_view
|| !reference_view
->visible())
548 reference_view
= NULL
; // fall back to app menu below.
550 } else if (bubble_
.type() == bubble_
.PAGE_ACTION
) {
551 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
552 ExtensionAction
* page_action
=
553 extension_action_manager
->GetPageAction(*bubble_
.extension());
554 location_bar_view
->SetPreviewEnabledPageAction(page_action
,
555 true); // preview_enabled
556 reference_view
= location_bar_view
->GetPageActionView(page_action
);
557 DCHECK(reference_view
);
558 } else if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
559 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
560 reference_view
= location_bar_view
;
561 DCHECK(reference_view
);
565 if (reference_view
== NULL
)
566 reference_view
= browser_view
->GetToolbarView()->app_menu();
567 SetAnchorView(reference_view
);
569 set_arrow(bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
?
570 views::BubbleBorder::TOP_LEFT
:
571 views::BubbleBorder::TOP_RIGHT
);
572 SetLayoutManager(new views::FillLayout());
573 AddChildView(new InstalledBubbleContent(
574 bubble_
.browser(), bubble_
.extension(), bubble_
.type(),
577 views::BubbleDelegateView::CreateBubble(this)->Show();
579 // The bubble widget is now the parent and owner of |this| and takes care of
580 // deletion when the bubble or browser go away.
581 bubble_
.IgnoreBrowserClosing();
586 gfx::Rect
ExtensionInstalledBubbleView::GetAnchorRect() const {
587 // For omnibox keyword bubbles, move the arrow to point to the left edge
588 // of the omnibox, just to the right of the icon.
589 if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
590 const LocationBarView
* location_bar_view
=
591 BrowserView::GetBrowserViewForBrowser(bubble_
.browser())->
592 GetLocationBarView();
593 return gfx::Rect(location_bar_view
->GetOmniboxViewOrigin(),
594 gfx::Size(0, location_bar_view
->omnibox_view()->height()));
596 return views::BubbleDelegateView::GetAnchorRect();
599 void ExtensionInstalledBubbleView::WindowClosing() {
600 if (bubble_
.extension() && bubble_
.type() == bubble_
.PAGE_ACTION
) {
601 BrowserView
* browser_view
=
602 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
603 browser_view
->GetLocationBarView()->SetPreviewEnabledPageAction(
604 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile())->
605 GetPageAction(*bubble_
.extension()),
606 false); // preview_enabled