// 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 #include #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 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(); }