Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / views / extensions / extension_installed_bubble_view.cc
blob9e3886a18bcd3d72c40dc7a6e6b1a6062d69c06c
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"
7 #include <algorithm>
8 #include <string>
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;
50 namespace {
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
58 // amount + 4px.
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;
70 } // namespace
72 namespace chrome {
74 void ShowExtensionInstalledBubble(const Extension* extension,
75 Browser* browser,
76 const SkBitmap& icon) {
77 ExtensionInstalledBubbleView::Show(extension, browser, icon);
80 } // namespace chrome
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 {
88 public:
89 InstalledBubbleContent(const ExtensionInstalledBubble& bubble,
90 Browser* browser);
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;
98 private:
99 enum Flavors {
100 NONE = 0,
101 HOW_TO_USE = 1 << 0,
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.
117 Browser* browser_;
119 // The string that contains the link text at the beginning of the sign-in
120 // promo text.
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.
131 int flavors_;
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,
149 Browser* browser)
150 : browser_(browser),
151 flavors_(NONE),
152 height_of_signin_promo_(0u),
153 how_to_use_(NULL),
154 sign_in_link_(NULL),
155 manage_(NULL),
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 // -------------------------
161 // | | Heading [X] |
162 // | Icon | Info |
163 // | | Extra info |
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.
169 // Extra info is...
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;
186 } else {
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
189 // in those cases.
190 flavors_ |= HOW_TO_MANAGE;
192 break;
193 case ExtensionInstalledBubble::OMNIBOX_KEYWORD:
194 flavors_ |= HOW_TO_USE | HOW_TO_MANAGE;
195 break;
196 case ExtensionInstalledBubble::GENERIC:
197 break;
198 default:
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 "
202 "be updateed");
203 break;
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));
219 AddChildView(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) {
256 signin_promo_text_ =
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);
290 return;
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(&params);
302 int InstalledBubbleContent::LayoutSigninPromo(int offset_x, int offset_y) {
303 sign_in_promo_lines_.clear();
304 int height = 0;
305 gfx::Rect contents_area = GetContentsBounds();
306 if (contents_area.IsEmpty())
307 return height;
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,
322 &lines);
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);
337 line->SetText(*it);
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()));
351 return height;
354 gfx::Size InstalledBubbleContent::GetPreferredSize() const {
355 int width = kHorizOuterMargin;
356 width += kIconSize;
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);
394 x += kIconSize;
395 x += views::kPanelHorizMargin;
397 y += kRightcolumnVerticalShift;
398 heading_->SizeToFit(kRightColumnWidth);
399 heading_->SetX(x);
400 heading_->SetY(y);
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);
414 manage_->SetX(x);
415 manage_->SetY(y);
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(),
430 sz.width(),
431 sz.height());
432 y += manage_shortcut_->height();
433 y += kVertInnerMargin;
436 gfx::Size sz;
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)
450 (*it)->Draw(canvas);
452 views::View::OnPaint(canvas);
455 void ExtensionInstalledBubbleView::Show(const Extension* extension,
456 Browser* browser,
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())
478 return false;
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);
504 // Default case.
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();
521 return true;
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