| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/extensions/extension_installed_bubble_view.h" |
| |
| #include <algorithm> |
| #include <string> |
| |
| #include "base/macros.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "chrome/browser/extensions/extension_action_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/singleton_tabs.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/location_bar/location_bar_view.h" |
| #include "chrome/browser/ui/views/location_bar/page_action_with_badge_view.h" |
| #include "chrome/browser/ui/views/sync/bubble_sync_promo_view.h" |
| #include "chrome/browser/ui/views/toolbar/app_menu_button.h" |
| #include "chrome/browser/ui/views/toolbar/browser_actions_container.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/grit/chromium_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/bubble/bubble_controller.h" |
| #include "components/bubble/bubble_ui.h" |
| #include "content/public/browser/user_metrics.h" |
| #include "extensions/common/extension.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/link.h" |
| #include "ui/views/controls/link_listener.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/layout/layout_constants.h" |
| |
| using extensions::Extension; |
| |
| namespace { |
| |
| const int kIconSize = 43; |
| |
| const int kRightColumnWidth = 285; |
| |
| views::Label* CreateLabel(const base::string16& text, |
| const gfx::FontList& font) { |
| views::Label* label = new views::Label(text, font); |
| label->SetMultiLine(true); |
| label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| return label; |
| } |
| |
| class HeadingAndCloseButtonView : public views::View { |
| public: |
| HeadingAndCloseButtonView(views::Label* heading, views::LabelButton* close) |
| : heading_(heading), close_(close) { |
| AddChildView(heading_); |
| AddChildView(close_); |
| } |
| ~HeadingAndCloseButtonView() override {} |
| |
| void Layout() override { |
| gfx::Size close_size = close_->GetPreferredSize(); |
| gfx::Size view_size = size(); |
| |
| // Close button is in the upper right and always gets its full desired size. |
| close_->SetBounds(view_size.width() - close_size.width(), |
| 0, |
| close_size.width(), |
| close_size.height()); |
| // The heading takes up the remaining room (modulo padding). |
| heading_->SetBounds( |
| 0, |
| 0, |
| view_size.width() - close_size.width() - |
| views::kUnrelatedControlHorizontalSpacing, |
| view_size.height()); |
| } |
| |
| gfx::Size GetPreferredSize() const override { |
| gfx::Size heading_size = heading_->GetPreferredSize(); |
| gfx::Size close_size = close_->GetPreferredSize(); |
| return gfx::Size(kRightColumnWidth, |
| std::max(heading_size.height(), close_size.height())); |
| } |
| |
| int GetHeightForWidth(int width) const override { |
| gfx::Size close_size = close_->GetPreferredSize(); |
| int heading_width = width - views::kUnrelatedControlHorizontalSpacing - |
| close_size.width(); |
| return std::max(heading_->GetHeightForWidth(heading_width), |
| close_size.height()); |
| } |
| |
| private: |
| views::Label* heading_; |
| views::LabelButton* close_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HeadingAndCloseButtonView); |
| }; |
| |
| } // namespace |
| |
| ExtensionInstalledBubbleView::ExtensionInstalledBubbleView( |
| ExtensionInstalledBubble* bubble, |
| BubbleReference bubble_reference) |
| : bubble_(bubble), |
| bubble_reference_(bubble_reference), |
| extension_(bubble->extension()), |
| browser_(bubble->browser()), |
| type_(bubble->type()), |
| anchor_position_(bubble->anchor_position()), |
| close_(nullptr), |
| manage_shortcut_(nullptr) {} |
| |
| ExtensionInstalledBubbleView::~ExtensionInstalledBubbleView() {} |
| |
| void ExtensionInstalledBubbleView::UpdateAnchorView() { |
| BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); |
| |
| views::View* reference_view = nullptr; |
| switch (anchor_position_) { |
| case ExtensionInstalledBubble::ANCHOR_BROWSER_ACTION: { |
| BrowserActionsContainer* container = |
| browser_view->GetToolbarView()->browser_actions(); |
| // Hitting this DCHECK means |ShouldShow| failed. |
| DCHECK(!container->animating()); |
| |
| reference_view = container->GetViewForId(extension_->id()); |
| // If the view is not visible then it is in the chevron, so point the |
| // install bubble to the chevron instead. If this is an incognito window, |
| // both could be invisible. |
| if (!reference_view || !reference_view->visible()) { |
| reference_view = container->chevron(); |
| if (!reference_view || !reference_view->visible()) |
| reference_view = nullptr; // fall back to app menu below. |
| } |
| break; |
| } |
| case ExtensionInstalledBubble::ANCHOR_PAGE_ACTION: { |
| LocationBarView* location_bar_view = browser_view->GetLocationBarView(); |
| ExtensionAction* page_action = |
| extensions::ExtensionActionManager::Get(browser_->profile()) |
| ->GetPageAction(*extension_); |
| location_bar_view->SetPreviewEnabledPageAction(page_action, |
| true); // preview_enabled |
| reference_view = location_bar_view->GetPageActionView(page_action); |
| DCHECK(reference_view); |
| break; |
| } |
| case ExtensionInstalledBubble::ANCHOR_OMNIBOX: { |
| LocationBarView* location_bar_view = browser_view->GetLocationBarView(); |
| reference_view = location_bar_view; |
| DCHECK(reference_view); |
| break; |
| } |
| case ExtensionInstalledBubble::ANCHOR_APP_MENU: |
| // Will be caught below. |
| break; |
| } |
| |
| // Default case. |
| if (!reference_view) |
| reference_view = browser_view->GetToolbarView()->app_menu_button(); |
| SetAnchorView(reference_view); |
| } |
| |
| views::View* ExtensionInstalledBubbleView::CreateFootnoteView() { |
| if (!(bubble_->options() & ExtensionInstalledBubble::SIGN_IN_PROMO)) |
| return nullptr; |
| |
| return new BubbleSyncPromoView(this, |
| IDS_EXTENSION_INSTALLED_SYNC_PROMO_LINK_NEW, |
| IDS_EXTENSION_INSTALLED_SYNC_PROMO_NEW); |
| } |
| |
| void ExtensionInstalledBubbleView::WindowClosing() { |
| if (anchor_position_ == ExtensionInstalledBubble::ANCHOR_PAGE_ACTION) { |
| BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); |
| browser_view->GetLocationBarView()->SetPreviewEnabledPageAction( |
| extensions::ExtensionActionManager::Get(browser_->profile()) |
| ->GetPageAction(*extension_), |
| false); // preview_enabled |
| } |
| } |
| |
| gfx::Rect ExtensionInstalledBubbleView::GetAnchorRect() const { |
| // For omnibox keyword bubbles, move the arrow to point to the left edge |
| // of the omnibox, just to the right of the icon. |
| if (type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) { |
| const LocationBarView* location_bar_view = |
| BrowserView::GetBrowserViewForBrowser(browser_)->GetLocationBarView(); |
| return gfx::Rect(location_bar_view->GetOmniboxViewOrigin(), |
| gfx::Size(0, location_bar_view->omnibox_view()->height())); |
| } |
| return views::BubbleDelegateView::GetAnchorRect(); |
| } |
| |
| void ExtensionInstalledBubbleView::OnWidgetClosing(views::Widget* widget) { |
| if (bubble_reference_) { |
| DCHECK_EQ(widget, GetWidget()); |
| // A more specific close reason should already be recorded. |
| // This is the catch-all close reason for this bubble. |
| bubble_reference_->CloseBubble(BUBBLE_CLOSE_FOCUS_LOST); |
| } |
| } |
| |
| void ExtensionInstalledBubbleView::OnWidgetActivationChanged( |
| views::Widget* widget, |
| bool active) { |
| if (!active && bubble_reference_ && widget == GetWidget()) |
| bubble_reference_->CloseBubble(BUBBLE_CLOSE_FOCUS_LOST); |
| } |
| |
| bool ExtensionInstalledBubbleView::AcceleratorPressed( |
| const ui::Accelerator& accelerator) { |
| if (!close_on_esc() || accelerator.key_code() != ui::VKEY_ESCAPE) |
| return false; |
| DCHECK(bubble_reference_); |
| bool did_close = bubble_reference_->CloseBubble(BUBBLE_CLOSE_USER_DISMISSED); |
| DCHECK(did_close); |
| return true; |
| } |
| |
| void ExtensionInstalledBubbleView::OnSignInLinkClicked() { |
| GetWidget()->Close(); |
| chrome::ShowBrowserSignin( |
| browser_, |
| signin_metrics::AccessPoint::ACCESS_POINT_EXTENSION_INSTALL_BUBBLE); |
| } |
| |
| void ExtensionInstalledBubbleView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| DCHECK_EQ(sender, close_); |
| GetWidget()->Close(); |
| } |
| |
| void ExtensionInstalledBubbleView::LinkClicked(views::Link* source, |
| int event_flags) { |
| DCHECK_EQ(manage_shortcut_, source); |
| GetWidget()->Close(); |
| |
| std::string configure_url = chrome::kChromeUIExtensionsURL; |
| configure_url += chrome::kExtensionConfigureCommandsSubPage; |
| chrome::NavigateParams params( |
| chrome::GetSingletonTabNavigateParams(browser_, GURL(configure_url))); |
| chrome::Navigate(¶ms); |
| } |
| |
| void ExtensionInstalledBubbleView::InitLayout() { |
| // The Extension Installed bubble takes on various forms, depending on the |
| // type of extension installed. In general, though, they are all similar: |
| // |
| // ------------------------- |
| // | | Heading [X] | |
| // | Icon | Info | |
| // | | Extra info | |
| // ------------------------- |
| // |
| // Icon and Heading are always shown (as well as the close button). |
| // Info is shown for browser actions, page actions and Omnibox keyword |
| // extensions and might list keyboard shorcut for the former two types. |
| // Extra info is... |
| // ... for other types, either a description of how to manage the extension |
| // or a link to configure the keybinding shortcut (if one exists). |
| // Extra info can include a promo for signing into sync. |
| |
| const ExtensionInstalledBubble& bubble = *bubble_; |
| // The number of rows in the content section of the bubble. |
| int main_content_row_count = 1; |
| if (bubble.options() & ExtensionInstalledBubble::HOW_TO_USE) |
| ++main_content_row_count; |
| if (bubble.options() & ExtensionInstalledBubble::SHOW_KEYBINDING) |
| ++main_content_row_count; |
| if (bubble.options() & ExtensionInstalledBubble::HOW_TO_MANAGE) |
| ++main_content_row_count; |
| |
| views::GridLayout* layout = new views::GridLayout(this); |
| SetLayoutManager(layout); |
| |
| const int cs_id = 0; |
| |
| views::ColumnSet* main_cs = layout->AddColumnSet(cs_id); |
| // Icon column. |
| main_cs->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING, 0, |
| views::GridLayout::USE_PREF, 0, 0); |
| main_cs->AddPaddingColumn(0, views::kUnrelatedControlHorizontalSpacing); |
| // Heading column. |
| main_cs->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, |
| views::GridLayout::FIXED, kRightColumnWidth, 0); |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| const gfx::FontList& font_list = rb.GetFontList(ui::ResourceBundle::BaseFont); |
| |
| const SkBitmap& bitmap = bubble.icon(); |
| // Add the icon (for all options). |
| // Scale down to 43x43, but allow smaller icons (don't scale up). |
| gfx::Size size(bitmap.width(), bitmap.height()); |
| if (size.width() > kIconSize || size.height() > kIconSize) |
| size = gfx::Size(kIconSize, kIconSize); |
| views::ImageView* icon = new views::ImageView(); |
| icon->SetImageSize(size); |
| icon->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| layout->StartRow(0, cs_id); |
| layout->AddView(icon, 1, main_content_row_count); |
| |
| // Add the heading (for all options). |
| base::string16 extension_name = base::UTF8ToUTF16(extension_->name()); |
| base::i18n::AdjustStringForLocaleDirection(&extension_name); |
| views::Label* heading = |
| CreateLabel(l10n_util::GetStringFUTF16(IDS_EXTENSION_INSTALLED_HEADING, |
| extension_name), |
| rb.GetFontList(ui::ResourceBundle::MediumFont)); |
| |
| close_ = views::BubbleFrameView::CreateCloseButton(this); |
| |
| HeadingAndCloseButtonView* heading_and_close = |
| new HeadingAndCloseButtonView(heading, close_); |
| |
| layout->AddView(heading_and_close); |
| layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); |
| |
| auto add_content_view = [&layout, &cs_id](views::View* view) { |
| layout->StartRow(0, cs_id); |
| // Skip the icon column. |
| layout->SkipColumns(1); |
| layout->AddView(view); |
| layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); |
| }; |
| |
| if (bubble.options() & ExtensionInstalledBubble::HOW_TO_USE) { |
| add_content_view(CreateLabel(bubble.GetHowToUseDescription(), font_list)); |
| } |
| |
| if (bubble.options() & ExtensionInstalledBubble::SHOW_KEYBINDING) { |
| manage_shortcut_ = new views::Link( |
| l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS)); |
| manage_shortcut_->set_listener(this); |
| manage_shortcut_->SetUnderline(false); |
| add_content_view(manage_shortcut_); |
| } |
| |
| if (bubble.options() & ExtensionInstalledBubble::HOW_TO_MANAGE) { |
| add_content_view(CreateLabel( |
| l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_INFO), |
| font_list)); |
| } |
| } |
| |
| // Views specific implementation. |
| bool ExtensionInstalledBubble::ShouldShow() { |
| if (anchor_position() == ANCHOR_BROWSER_ACTION) { |
| BrowserActionsContainer* container = |
| BrowserView::GetBrowserViewForBrowser(browser()) |
| ->GetToolbarView() |
| ->browser_actions(); |
| return !container->animating(); |
| } |
| return true; |
| } |
| |
| class ExtensionInstalledBubbleUi : public BubbleUi { |
| public: |
| explicit ExtensionInstalledBubbleUi(ExtensionInstalledBubble* bubble); |
| ~ExtensionInstalledBubbleUi() override; |
| |
| private: |
| // BubbleUi: |
| void Show(BubbleReference bubble_reference) override; |
| void Close() override; |
| void UpdateAnchorPosition() override; |
| |
| ExtensionInstalledBubble* bubble_; |
| ExtensionInstalledBubbleView* delegate_view_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInstalledBubbleUi); |
| }; |
| |
| // Implemented here to create the platform specific instance of the BubbleUi. |
| scoped_ptr<BubbleUi> ExtensionInstalledBubble::BuildBubbleUi() { |
| return make_scoped_ptr(new ExtensionInstalledBubbleUi(this)); |
| } |
| |
| ExtensionInstalledBubbleUi::ExtensionInstalledBubbleUi( |
| ExtensionInstalledBubble* bubble) |
| : bubble_(bubble), delegate_view_(nullptr) { |
| DCHECK(bubble_); |
| } |
| |
| ExtensionInstalledBubbleUi::~ExtensionInstalledBubbleUi() {} |
| |
| void ExtensionInstalledBubbleUi::Show(BubbleReference bubble_reference) { |
| // Owned by widget. |
| delegate_view_ = new ExtensionInstalledBubbleView(bubble_, bubble_reference); |
| delegate_view_->UpdateAnchorView(); |
| |
| delegate_view_->set_arrow(bubble_->type() == bubble_->OMNIBOX_KEYWORD |
| ? views::BubbleBorder::TOP_LEFT |
| : views::BubbleBorder::TOP_RIGHT); |
| |
| delegate_view_->InitLayout(); |
| |
| views::BubbleDelegateView::CreateBubble(delegate_view_)->Show(); |
| content::RecordAction( |
| base::UserMetricsAction("Signin_Impression_FromExtensionInstallBubble")); |
| } |
| |
| void ExtensionInstalledBubbleUi::Close() { |
| if (delegate_view_) { |
| delegate_view_->GetWidget()->Close(); |
| delegate_view_ = nullptr; |
| } |
| } |
| |
| void ExtensionInstalledBubbleUi::UpdateAnchorPosition() { |
| DCHECK(delegate_view_); |
| delegate_view_->UpdateAnchorView(); |
| } |