// Copyright (c) 2012 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.h" #include #include #include "base/bind.h" #include "base/i18n/rtl.h" #include "base/message_loop.h" #include "base/utf_string_conversions.h" #include "chrome/browser/extensions/extension_install_ui.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/views/browser_actions_container.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/tabs/tab_strip.h" #include "chrome/browser/ui/views/toolbar_view.h" #include "chrome/browser/ui/views/window.h" #include "chrome/common/chrome_notification_types.h" #include "chrome/common/extensions/extension.h" #include "chrome/common/extensions/extension_action.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_source.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" #include "grit/ui_resources_standard.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.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/fill_layout.h" #include "ui/views/layout/layout_constants.h" namespace { const int kIconSize = 43; const int kRightColumnWidth = 285; // The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace // around the content view. We compensate by reducing our outer borders by this // amount + 4px. const int kOuterMarginInset = 10; const int kHorizOuterMargin = views::kPanelHorizMargin - kOuterMarginInset; const int kVertOuterMargin = views::kPanelVertMargin - kOuterMarginInset; // Interior vertical margin is 8px smaller than standard const int kVertInnerMargin = views::kPanelVertMargin - 8; // The image we use for the close button has three pixels of whitespace padding. const int kCloseButtonPadding = 3; // We want to shift the right column (which contains the header and text) up // 4px to align with icon. const int kRightcolumnVerticalShift = -4; // How long to wait for browser action animations to complete before retrying. const int kAnimationWaitTime = 50; // How often we retry when waiting for browser action animation to end. const int kAnimationWaitMaxRetry = 10; } // namespace namespace browser { void ShowExtensionInstalledBubble( const Extension* extension, Browser* browser, const SkBitmap& icon, Profile* profile) { ExtensionInstalledBubble::Show(extension, browser, icon); } } // namespace browser // InstalledBubbleContent is the content view which is placed in the // ExtensionInstalledBubble. It displays the install icon and explanatory // text about the installed extension. class InstalledBubbleContent : public views::View, public views::ButtonListener, public views::LinkListener { public: InstalledBubbleContent(Browser* browser, const Extension* extension, ExtensionInstalledBubble::BubbleType type, SkBitmap* icon, ExtensionInstalledBubble* bubble) : browser_(browser), extension_id_(extension->id()), bubble_(bubble), type_(type), info_(NULL) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); const gfx::Font& font = rb.GetFont(ResourceBundle::BaseFont); // Scale down to 43x43, but allow smaller icons (don't scale up). gfx::Size size(icon->width(), icon->height()); if (size.width() > kIconSize || size.height() > kIconSize) size = gfx::Size(kIconSize, kIconSize); icon_ = new views::ImageView(); icon_->SetImageSize(size); icon_->SetImage(*icon); AddChildView(icon_); string16 extension_name = UTF8ToUTF16(extension->name()); base::i18n::AdjustStringForLocaleDirection(&extension_name); heading_ = new views::Label(l10n_util::GetStringFUTF16( IDS_EXTENSION_INSTALLED_HEADING, extension_name)); heading_->SetFont(rb.GetFont(ResourceBundle::MediumFont)); heading_->SetMultiLine(true); heading_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(heading_); switch (type_) { case ExtensionInstalledBubble::PAGE_ACTION: { info_ = new views::Label(l10n_util::GetStringUTF16( IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO)); info_->SetFont(font); info_->SetMultiLine(true); info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(info_); break; } case ExtensionInstalledBubble::OMNIBOX_KEYWORD: { info_ = new views::Label(l10n_util::GetStringFUTF16( IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, UTF8ToUTF16(extension->omnibox_keyword()))); info_->SetFont(font); info_->SetMultiLine(true); info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(info_); break; } case ExtensionInstalledBubble::APP: { views::Link* link = new views::Link( l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_APP_INFO)); link->set_listener(this); manage_ = link; manage_->SetFont(font); manage_->SetMultiLine(true); manage_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(manage_); break; } default: break; } if (type_ != ExtensionInstalledBubble::APP) { manage_ = new views::Label( l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_INFO)); manage_->SetFont(font); manage_->SetMultiLine(true); manage_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(manage_); } close_button_ = new views::ImageButton(this); close_button_->SetImage(views::CustomButton::BS_NORMAL, rb.GetBitmapNamed(IDR_CLOSE_BAR)); close_button_->SetImage(views::CustomButton::BS_HOT, rb.GetBitmapNamed(IDR_CLOSE_BAR_H)); close_button_->SetImage(views::CustomButton::BS_PUSHED, rb.GetBitmapNamed(IDR_CLOSE_BAR_P)); AddChildView(close_button_); } virtual void ButtonPressed(views::Button* sender, const views::Event& event) { if (sender == close_button_) bubble_->StartFade(false); else NOTREACHED() << "Unknown view"; } // Implements the views::LinkListener interface. virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE { GetWidget()->Close(); ExtensionInstallUI::OpenAppInstalledNTP(browser_, extension_id_); } private: virtual gfx::Size GetPreferredSize() { int width = kHorizOuterMargin; width += kIconSize; width += views::kPanelHorizMargin; width += kRightColumnWidth; width += 2 * views::kPanelHorizMargin; width += kHorizOuterMargin; int height = kVertOuterMargin; height += heading_->GetHeightForWidth(kRightColumnWidth); height += kVertInnerMargin; if (info_) { height += info_->GetHeightForWidth(kRightColumnWidth); height += kVertInnerMargin; } height += manage_->GetHeightForWidth(kRightColumnWidth); height += kVertOuterMargin; return gfx::Size(width, std::max(height, kIconSize + 2 * kVertOuterMargin)); } virtual void Layout() { int x = kHorizOuterMargin; int y = kVertOuterMargin; icon_->SetBounds(x, y, kIconSize, kIconSize); x += kIconSize; x += views::kPanelHorizMargin; y += kRightcolumnVerticalShift; heading_->SizeToFit(kRightColumnWidth); heading_->SetX(x); heading_->SetY(y); y += heading_->height(); y += kVertInnerMargin; if (info_) { info_->SizeToFit(kRightColumnWidth); info_->SetX(x); info_->SetY(y); y += info_->height(); y += kVertInnerMargin; } manage_->SizeToFit(kRightColumnWidth); manage_->SetX(x); manage_->SetY(y); y += manage_->height(); y += kVertInnerMargin; gfx::Size sz; x += kRightColumnWidth + 2 * views::kPanelHorizMargin + kHorizOuterMargin - close_button_->GetPreferredSize().width(); y = kVertOuterMargin; sz = close_button_->GetPreferredSize(); // x-1 & y-1 is just slop to get the close button visually aligned with the // title text and bubble arrow. close_button_->SetBounds(x - 1, y - 1, sz.width(), sz.height()); } // The browser we're associated with. Browser* browser_; // The id of the extension just installed. const std::string extension_id_; // The ExtensionInstalledBubble showing us. ExtensionInstalledBubble* bubble_; ExtensionInstalledBubble::BubbleType type_; views::ImageView* icon_; views::Label* heading_; views::Label* info_; views::Label* manage_; views::ImageButton* close_button_; DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent); }; void ExtensionInstalledBubble::Show(const Extension* extension, Browser *browser, const SkBitmap& icon) { new ExtensionInstalledBubble(extension, browser, icon); } ExtensionInstalledBubble::ExtensionInstalledBubble(const Extension* extension, Browser *browser, const SkBitmap& icon) : extension_(extension), browser_(browser), icon_(icon), animation_wait_retries_(0) { if (extension->is_app()) { type_ = APP; } else if (!extension_->omnibox_keyword().empty()) { type_ = OMNIBOX_KEYWORD; } else if (extension_->browser_action()) { type_ = BROWSER_ACTION; } else if (extension->page_action() && !extension->page_action()->default_icon_path().empty()) { type_ = PAGE_ACTION; } else { type_ = GENERIC; } // |extension| has been initialized but not loaded at this point. We need // to wait on showing the Bubble until not only the EXTENSION_LOADED gets // fired, but all of the EXTENSION_LOADED Observers have run. Only then can we // be sure that a BrowserAction or PageAction has had views created which we // can inspect for the purpose of previewing of pointing to them. registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, content::Source(browser->profile())); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, content::Source(browser->profile())); } ExtensionInstalledBubble::~ExtensionInstalledBubble() {} void ExtensionInstalledBubble::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { const Extension* extension = content::Details(details).ptr(); if (extension == extension_) { animation_wait_retries_ = 0; // PostTask to ourself to allow all EXTENSION_LOADED Observers to run. MessageLoopForUI::current()->PostTask( FROM_HERE, base::Bind(&ExtensionInstalledBubble::ShowInternal, base::Unretained(this))); } } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { const Extension* extension = content::Details(details)->extension; if (extension == extension_) extension_ = NULL; } else { NOTREACHED() << L"Received unexpected notification"; } } void ExtensionInstalledBubble::ShowInternal() { BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); views::View* reference_view = NULL; if (type_ == APP) { if (browser_view->IsTabStripVisible()) { AbstractTabStripView* tabstrip = browser_view->tabstrip(); views::View* ntp_button = tabstrip->GetNewTabButton(); if (ntp_button && ntp_button->IsDrawn()) { reference_view = ntp_button; } else { // Just have the bubble point at the tab strip. reference_view = tabstrip; } } } else if (type_ == BROWSER_ACTION) { BrowserActionsContainer* container = browser_view->GetToolbarView()->browser_actions(); if (container->animating() && animation_wait_retries_++ < kAnimationWaitMaxRetry) { // We don't know where the view will be until the container has stopped // animating, so check back in a little while. MessageLoopForUI::current()->PostDelayedTask( FROM_HERE, base::Bind(&ExtensionInstalledBubble::ShowInternal, base::Unretained(this)), kAnimationWaitTime); return; } reference_view = container->GetBrowserActionView( extension_->browser_action()); // 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 = NULL; // fall back to app menu below. } } else if (type_ == PAGE_ACTION) { LocationBarView* location_bar_view = browser_view->GetLocationBarView(); location_bar_view->SetPreviewEnabledPageAction(extension_->page_action(), true); // preview_enabled reference_view = location_bar_view->GetPageActionView( extension_->page_action()); DCHECK(reference_view); } else if (type_ == OMNIBOX_KEYWORD) { LocationBarView* location_bar_view = browser_view->GetLocationBarView(); reference_view = location_bar_view; DCHECK(reference_view); } // Default case. if (reference_view == NULL) reference_view = browser_view->GetToolbarView()->app_menu(); set_anchor_view(reference_view); set_arrow_location(type_ == OMNIBOX_KEYWORD ? views::BubbleBorder::TOP_LEFT : views::BubbleBorder::TOP_RIGHT); SetLayoutManager(new views::FillLayout()); AddChildView( new InstalledBubbleContent(browser_, extension_, type_, &icon_, this)); browser::CreateViewsBubble(this); StartFade(true); } gfx::Rect ExtensionInstalledBubble::GetAnchorRect() { // 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_ == OMNIBOX_KEYWORD) { LocationBarView* location_bar_view = BrowserView::GetBrowserViewForBrowser(browser_)->GetLocationBarView(); return gfx::Rect(location_bar_view->GetLocationEntryOrigin(), gfx::Size(0, location_bar_view->location_entry_view()->height())); } return views::BubbleDelegateView::GetAnchorRect(); } void ExtensionInstalledBubble::WindowClosing() { if (extension_ && type_ == PAGE_ACTION) { BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); browser_view->GetLocationBarView()->SetPreviewEnabledPageAction( extension_->page_action(), false); // preview_enabled } }