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/extensions/extension_action.h"
13 #include "chrome/browser/extensions/extension_action_manager.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/signin/signin_promo.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/browser_window.h"
18 #include "chrome/browser/ui/chrome_pages.h"
19 #include "chrome/browser/ui/singleton_tabs.h"
20 #include "chrome/browser/ui/sync/sync_promo_ui.h"
21 #include "chrome/browser/ui/views/frame/browser_view.h"
22 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
23 #include "chrome/browser/ui/views/location_bar/page_action_with_badge_view.h"
24 #include "chrome/browser/ui/views/tabs/tab_strip.h"
25 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
26 #include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"
27 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
28 #include "chrome/browser/ui/views/toolbar/wrench_toolbar_button.h"
29 #include "chrome/common/extensions/sync_helper.h"
30 #include "chrome/common/url_constants.h"
31 #include "chrome/grit/chromium_strings.h"
32 #include "chrome/grit/generated_resources.h"
33 #include "extensions/common/extension.h"
34 #include "extensions/common/feature_switch.h"
35 #include "ui/base/l10n/l10n_util.h"
36 #include "ui/base/resource/resource_bundle.h"
37 #include "ui/gfx/render_text.h"
38 #include "ui/gfx/text_elider.h"
39 #include "ui/resources/grit/ui_resources.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(const ExtensionInstalledBubble
& bubble
,
92 // Overridden from views::ButtonListener.
93 void ButtonPressed(views::Button
* sender
, const ui::Event
& event
) override
;
95 // Overriden from views::LinkListener.
96 void LinkClicked(views::Link
* source
, int event_flags
) override
;
102 HOW_TO_MANAGE
= 1 << 1,
103 SHOW_KEYBINDING
= 1 << 2,
104 SIGN_IN_PROMO
= 1 << 3,
107 // Layout the signin promo at coordinates |offset_x| and |offset_y|. Returns
108 // the height (in pixels) of the promo UI.
109 int LayoutSigninPromo(int offset_x
, int offset_y
);
111 // Overriden from views::View.
112 gfx::Size
GetPreferredSize() const override
;
113 void Layout() override
;
114 void OnPaint(gfx::Canvas
* canvas
) override
;
116 // The browser we're associated with.
119 // The string that contains the link text at the beginning of the sign-in
121 base::string16 signin_promo_link_text_
;
122 // The remaining text of the sign-in promo text.
123 base::string16 signin_promo_text_
;
125 // A vector of RenderText objects representing the full sign-in promo
126 // paragraph as layed out within the bubble, but has the text of the link
127 // whited out so the link can be drawn in its place.
128 ScopedVector
<gfx::RenderText
> sign_in_promo_lines_
;
130 // A bitmask containing the various flavors of bubble sections to show.
133 // The height, in pixels, of the sign-in promo.
134 size_t height_of_signin_promo_
;
136 views::ImageView
* icon_
;
137 views::Label
* heading_
;
138 views::Label
* how_to_use_
;
139 views::Link
* sign_in_link_
;
140 views::Label
* manage_
;
141 views::Link
* manage_shortcut_
;
142 views::ImageButton
* close_button_
;
144 DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent
);
147 InstalledBubbleContent::InstalledBubbleContent(
148 const ExtensionInstalledBubble
& bubble
,
152 height_of_signin_promo_(0u),
156 manage_shortcut_(NULL
) {
157 // The Extension Installed bubble takes on various forms, depending on the
158 // type of extension installed. In general, though, they are all similar:
160 // -------------------------
164 // -------------------------
166 // Icon and Heading are always shown (as well as the close button).
167 // Info is shown for browser actions, page actions and Omnibox keyword
168 // extensions and might list keyboard shorcut for the former two types.
170 // ... for other types, either a description of how to manage the extension
171 // or a link to configure the keybinding shortcut (if one exists).
172 // Extra info can include a promo for signing into sync.
174 const Extension
* extension
= bubble
.extension();
175 if (extensions::sync_helper::IsSyncable(extension
) &&
176 SyncPromoUI::ShouldShowSyncPromo(browser
->profile()))
177 flavors_
|= SIGN_IN_PROMO
;
179 // Determine the bubble flavor we want, based on the extension type.
180 switch (bubble
.type()) {
181 case ExtensionInstalledBubble::BROWSER_ACTION
:
182 case ExtensionInstalledBubble::PAGE_ACTION
:
183 flavors_
|= HOW_TO_USE
;
184 if (bubble
.has_command_keybinding()) {
185 flavors_
|= SHOW_KEYBINDING
;
187 // The How-To-Use text makes the bubble seem a little crowded when the
188 // extension has a keybinding, so the How-To-Manage text is not shown
190 flavors_
|= HOW_TO_MANAGE
;
193 case ExtensionInstalledBubble::OMNIBOX_KEYWORD
:
194 flavors_
|= HOW_TO_USE
| HOW_TO_MANAGE
;
196 case ExtensionInstalledBubble::GENERIC
:
199 // When adding a new bubble type, the flavor needs to be set.
200 static_assert(ExtensionInstalledBubble::GENERIC
== 3,
201 "kBubbleType enum has changed, this switch statement must "
206 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
207 const gfx::FontList
& font_list
=
208 rb
.GetFontList(ui::ResourceBundle::BaseFont
);
210 const SkBitmap
& icon
= bubble
.icon();
211 // Add the icon (for all flavors).
212 // Scale down to 43x43, but allow smaller icons (don't scale up).
213 gfx::Size
size(icon
.width(), icon
.height());
214 if (size
.width() > kIconSize
|| size
.height() > kIconSize
)
215 size
= gfx::Size(kIconSize
, kIconSize
);
216 icon_
= new views::ImageView();
217 icon_
->SetImageSize(size
);
218 icon_
->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(icon
));
221 // Add the heading (for all flavors).
222 base::string16 extension_name
= base::UTF8ToUTF16(extension
->name());
223 base::i18n::AdjustStringForLocaleDirection(&extension_name
);
224 heading_
= new views::Label(l10n_util::GetStringFUTF16(
225 IDS_EXTENSION_INSTALLED_HEADING
, extension_name
));
226 heading_
->SetFontList(rb
.GetFontList(ui::ResourceBundle::MediumFont
));
227 heading_
->SetMultiLine(true);
228 heading_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
229 AddChildView(heading_
);
231 if (flavors_
& HOW_TO_USE
) {
232 how_to_use_
= new views::Label(bubble
.GetHowToUseDescription());
233 how_to_use_
->SetFontList(font_list
);
234 how_to_use_
->SetMultiLine(true);
235 how_to_use_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
236 AddChildView(how_to_use_
);
239 if (flavors_
& SHOW_KEYBINDING
) {
240 manage_shortcut_
= new views::Link(
241 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS
));
242 manage_shortcut_
->set_listener(this);
243 AddChildView(manage_shortcut_
);
246 if (flavors_
& HOW_TO_MANAGE
) {
247 manage_
= new views::Label(l10n_util::GetStringUTF16(
248 IDS_EXTENSION_INSTALLED_MANAGE_INFO
));
249 manage_
->SetFontList(font_list
);
250 manage_
->SetMultiLine(true);
251 manage_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
252 AddChildView(manage_
);
255 if (flavors_
& SIGN_IN_PROMO
) {
257 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO
);
259 signin_promo_link_text_
=
260 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK
);
261 sign_in_link_
= new views::Link(signin_promo_link_text_
);
262 sign_in_link_
->SetFontList(font_list
);
263 sign_in_link_
->set_listener(this);
264 AddChildView(sign_in_link_
);
267 // Add the Close button (for all flavors).
268 close_button_
= new views::ImageButton(this);
269 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
270 rb
.GetImageSkiaNamed(IDR_CLOSE_2
));
271 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
272 rb
.GetImageSkiaNamed(IDR_CLOSE_2_H
));
273 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
274 rb
.GetImageSkiaNamed(IDR_CLOSE_2_P
));
275 AddChildView(close_button_
);
278 void InstalledBubbleContent::ButtonPressed(views::Button
* sender
,
279 const ui::Event
& event
) {
280 DCHECK_EQ(sender
, close_button_
);
281 GetWidget()->Close();
284 void InstalledBubbleContent::LinkClicked(views::Link
* source
, int event_flags
) {
285 GetWidget()->Close();
287 if (source
== sign_in_link_
) {
288 chrome::ShowBrowserSignin(
289 browser_
, signin_metrics::SOURCE_EXTENSION_INSTALL_BUBBLE
);
293 DCHECK_EQ(manage_shortcut_
, source
);
295 std::string configure_url
= chrome::kChromeUIExtensionsURL
;
296 configure_url
+= chrome::kExtensionConfigureCommandsSubPage
;
297 chrome::NavigateParams
params(chrome::GetSingletonTabNavigateParams(
298 browser_
, GURL(configure_url
)));
299 chrome::Navigate(¶ms
);
302 int InstalledBubbleContent::LayoutSigninPromo(int offset_x
, int offset_y
) {
303 sign_in_promo_lines_
.clear();
305 gfx::Rect contents_area
= GetContentsBounds();
306 if (contents_area
.IsEmpty())
308 contents_area
.set_width(kRightColumnWidth
);
310 base::string16 full_text
= signin_promo_link_text_
+ signin_promo_text_
;
312 // The link is the first item in the text.
313 const gfx::Size link_size
= sign_in_link_
->GetPreferredSize();
314 sign_in_link_
->SetBounds(
315 offset_x
, offset_y
, link_size
.width(), link_size
.height());
317 // Word-wrap the full label text.
318 const gfx::FontList font_list
;
319 std::vector
<base::string16
> lines
;
320 gfx::ElideRectangleText(full_text
, font_list
, contents_area
.width(),
321 contents_area
.height(), gfx::ELIDE_LONG_WORDS
,
324 gfx::Point position
= gfx::Point(
325 contents_area
.origin().x() + offset_x
,
326 contents_area
.origin().y() + offset_y
+ 1);
327 if (base::i18n::IsRTL()) {
328 position
-= gfx::Vector2d(
329 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
, 0);
332 // Loop through the lines, creating a renderer for each.
333 for (std::vector
<base::string16
>::const_iterator it
= lines
.begin();
334 it
!= lines
.end(); ++it
) {
335 gfx::RenderText
* line
= gfx::RenderText::CreateInstance();
336 line
->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI
);
338 const gfx::Size
size(contents_area
.width(),
339 line
->GetStringSize().height());
340 line
->SetDisplayRect(gfx::Rect(position
, size
));
341 position
.set_y(position
.y() + size
.height());
342 sign_in_promo_lines_
.push_back(line
);
343 height
+= size
.height();
346 // The link is drawn separately; make it transparent here to only draw once.
347 // The link always leads other text and is assumed to fit on the first line.
348 sign_in_promo_lines_
.front()->ApplyColor(SK_ColorTRANSPARENT
,
349 gfx::Range(0, signin_promo_link_text_
.size()));
354 gfx::Size
InstalledBubbleContent::GetPreferredSize() const {
355 int width
= kHorizOuterMargin
;
357 width
+= views::kPanelHorizMargin
;
358 width
+= kRightColumnWidth
;
359 width
+= 2 * views::kPanelHorizMargin
;
360 width
+= kHorizOuterMargin
;
362 int height
= kVertOuterMargin
;
363 height
+= heading_
->GetHeightForWidth(kRightColumnWidth
);
364 height
+= kVertInnerMargin
;
366 if (flavors_
& HOW_TO_USE
) {
367 height
+= how_to_use_
->GetHeightForWidth(kRightColumnWidth
);
368 height
+= kVertInnerMargin
;
371 if (flavors_
& HOW_TO_MANAGE
) {
372 height
+= manage_
->GetHeightForWidth(kRightColumnWidth
);
373 height
+= kVertInnerMargin
;
376 if (flavors_
& SIGN_IN_PROMO
&& height_of_signin_promo_
> 0u) {
377 height
+= height_of_signin_promo_
;
378 height
+= kVertInnerMargin
;
381 if (flavors_
& SHOW_KEYBINDING
) {
382 height
+= manage_shortcut_
->GetHeightForWidth(kRightColumnWidth
);
383 height
+= kVertInnerMargin
;
386 return gfx::Size(width
, std::max(height
, kIconSize
+ 2 * kVertOuterMargin
));
389 void InstalledBubbleContent::Layout() {
390 int x
= kHorizOuterMargin
;
391 int y
= kVertOuterMargin
;
393 icon_
->SetBounds(x
, y
, kIconSize
, kIconSize
);
395 x
+= views::kPanelHorizMargin
;
397 y
+= kRightcolumnVerticalShift
;
398 heading_
->SizeToFit(kRightColumnWidth
);
401 y
+= heading_
->height();
402 y
+= kVertInnerMargin
;
404 if (flavors_
& HOW_TO_USE
) {
405 how_to_use_
->SizeToFit(kRightColumnWidth
);
406 how_to_use_
->SetX(x
);
407 how_to_use_
->SetY(y
);
408 y
+= how_to_use_
->height();
409 y
+= kVertInnerMargin
;
412 if (flavors_
& HOW_TO_MANAGE
) {
413 manage_
->SizeToFit(kRightColumnWidth
);
416 y
+= manage_
->height();
417 y
+= kVertInnerMargin
;
420 if (flavors_
& SIGN_IN_PROMO
) {
421 height_of_signin_promo_
= LayoutSigninPromo(x
, y
);
422 y
+= height_of_signin_promo_
;
423 y
+= kVertInnerMargin
;
426 if (flavors_
& SHOW_KEYBINDING
) {
427 gfx::Size sz
= manage_shortcut_
->GetPreferredSize();
428 manage_shortcut_
->SetBounds(width() - 2 * kHorizOuterMargin
- sz
.width(),
432 y
+= manage_shortcut_
->height();
433 y
+= kVertInnerMargin
;
437 x
+= kRightColumnWidth
+ 2 * views::kPanelHorizMargin
+ kHorizOuterMargin
-
438 close_button_
->GetPreferredSize().width();
439 y
= kVertOuterMargin
;
440 sz
= close_button_
->GetPreferredSize();
441 // x-1 & y-1 is just slop to get the close button visually aligned with the
442 // title text and bubble arrow.
443 close_button_
->SetBounds(x
- 1, y
- 1, sz
.width(), sz
.height());
446 void InstalledBubbleContent::OnPaint(gfx::Canvas
* canvas
) {
447 for (ScopedVector
<gfx::RenderText
>::const_iterator it
=
448 sign_in_promo_lines_
.begin();
449 it
!= sign_in_promo_lines_
.end(); ++it
)
452 views::View::OnPaint(canvas
);
455 void ExtensionInstalledBubbleView::Show(const Extension
* extension
,
457 const SkBitmap
& icon
) {
458 new ExtensionInstalledBubbleView(extension
, browser
, icon
);
461 ExtensionInstalledBubbleView::ExtensionInstalledBubbleView(
462 const Extension
* extension
, Browser
*browser
, const SkBitmap
& icon
)
463 : bubble_(this, extension
, browser
, icon
) {
466 ExtensionInstalledBubbleView::~ExtensionInstalledBubbleView() {}
468 bool ExtensionInstalledBubbleView::MaybeShowNow() {
469 BrowserView
* browser_view
=
470 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
472 views::View
* reference_view
= NULL
;
473 if (bubble_
.type() == bubble_
.BROWSER_ACTION
||
474 extensions::FeatureSwitch::extension_action_redesign()->IsEnabled()) {
475 BrowserActionsContainer
* container
=
476 browser_view
->GetToolbarView()->browser_actions();
477 if (container
->animating())
480 reference_view
= container
->GetViewForId(bubble_
.extension()->id());
481 // If the view is not visible then it is in the chevron, so point the
482 // install bubble to the chevron instead. If this is an incognito window,
483 // both could be invisible.
484 if (!reference_view
|| !reference_view
->visible()) {
485 reference_view
= container
->chevron();
486 if (!reference_view
|| !reference_view
->visible())
487 reference_view
= NULL
; // fall back to app menu below.
489 } else if (bubble_
.type() == bubble_
.PAGE_ACTION
) {
490 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
491 ExtensionAction
* page_action
=
492 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile())->
493 GetPageAction(*bubble_
.extension());
494 location_bar_view
->SetPreviewEnabledPageAction(page_action
,
495 true); // preview_enabled
496 reference_view
= location_bar_view
->GetPageActionView(page_action
);
497 DCHECK(reference_view
);
498 } else if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
499 LocationBarView
* location_bar_view
= browser_view
->GetLocationBarView();
500 reference_view
= location_bar_view
;
501 DCHECK(reference_view
);
505 if (reference_view
== NULL
)
506 reference_view
= browser_view
->GetToolbarView()->app_menu();
507 SetAnchorView(reference_view
);
509 set_arrow(bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
?
510 views::BubbleBorder::TOP_LEFT
:
511 views::BubbleBorder::TOP_RIGHT
);
512 SetLayoutManager(new views::FillLayout());
513 AddChildView(new InstalledBubbleContent(bubble_
, bubble_
.browser()));
515 views::BubbleDelegateView::CreateBubble(this)->Show();
517 // The bubble widget is now the parent and owner of |this| and takes care of
518 // deletion when the bubble or browser go away.
519 bubble_
.IgnoreBrowserClosing();
524 gfx::Rect
ExtensionInstalledBubbleView::GetAnchorRect() const {
525 // For omnibox keyword bubbles, move the arrow to point to the left edge
526 // of the omnibox, just to the right of the icon.
527 if (bubble_
.type() == bubble_
.OMNIBOX_KEYWORD
) {
528 const LocationBarView
* location_bar_view
=
529 BrowserView::GetBrowserViewForBrowser(bubble_
.browser())->
530 GetLocationBarView();
531 return gfx::Rect(location_bar_view
->GetOmniboxViewOrigin(),
532 gfx::Size(0, location_bar_view
->omnibox_view()->height()));
534 return views::BubbleDelegateView::GetAnchorRect();
537 void ExtensionInstalledBubbleView::WindowClosing() {
538 if (bubble_
.extension() && bubble_
.type() == bubble_
.PAGE_ACTION
&&
539 !extensions::FeatureSwitch::extension_action_redesign()->IsEnabled()) {
540 BrowserView
* browser_view
=
541 BrowserView::GetBrowserViewForBrowser(bubble_
.browser());
542 browser_view
->GetLocationBarView()->SetPreviewEnabledPageAction(
543 extensions::ExtensionActionManager::Get(bubble_
.browser()->profile())->
544 GetPageAction(*bubble_
.extension()),
545 false); // preview_enabled