// Copyright (c) 2011 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 <algorithm> #include "base/i18n/rtl.h" #include "base/message_loop.h" #include "base/utf_string_conversions.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/toolbar_view.h" #include "chrome/common/extensions/extension.h" #include "chrome/common/extensions/extension_action.h" #include "content/common/notification_details.h" #include "content/common/notification_source.h" #include "content/common/notification_type.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "views/controls/button/image_button.h" #include "views/controls/image_view.h" #include "views/controls/label.h" #include "views/layout/layout_constants.h" #include "views/view.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: InstalledBubbleContent(const Extension* extension, ExtensionInstalledBubble::BubbleType type, SkBitmap* icon) : bubble_(NULL), 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(UTF16ToWide( 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_); if (type_ == ExtensionInstalledBubble::PAGE_ACTION) { info_ = new views::Label(UTF16ToWide(l10n_util::GetStringUTF16( IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO))); info_->SetFont(font); info_->SetMultiLine(true); info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(info_); } if (type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) { info_ = new views::Label(UTF16ToWide(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_); } manage_ = new views::Label(UTF16ToWide( 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_); } void set_bubble(Bubble* bubble) { bubble_ = bubble; } virtual void ButtonPressed( views::Button* sender, const views::Event& event) { if (sender == close_button_) { bubble_->set_fade_away_on_close(true); GetWidget()->Close(); } else { NOTREACHED() << "Unknown view"; } } 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 (type_ == ExtensionInstalledBubble::PAGE_ACTION || type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) { 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 (type_ == ExtensionInstalledBubble::PAGE_ACTION || type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) { 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 Bubble showing us. Bubble* 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) { AddRef(); // Balanced in BubbleClosing. 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, NotificationType::EXTENSION_LOADED, Source<Profile>(browser->profile())); registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, Source<Profile>(browser->profile())); } ExtensionInstalledBubble::~ExtensionInstalledBubble() {} void ExtensionInstalledBubble::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { if (type == NotificationType::EXTENSION_LOADED) { const Extension* extension = Details<const Extension>(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, NewRunnableMethod(this, &ExtensionInstalledBubble::ShowInternal)); } } else if (type == NotificationType::EXTENSION_UNLOADED) { const Extension* extension = Details<UnloadedExtensionInfo>(details)->extension; if (extension == extension_) extension_ = NULL; } else { NOTREACHED() << L"Received unexpected notification"; } } void ExtensionInstalledBubble::ShowInternal() { BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow( browser_->window()->GetNativeHandle()); const views::View* reference_view = NULL; 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, NewRunnableMethod(this, &ExtensionInstalledBubble::ShowInternal), 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->IsVisible()) { reference_view = container->chevron(); if (!reference_view || !reference_view->IsVisible()) 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(); gfx::Point origin; views::View::ConvertPointToScreen(reference_view, &origin); gfx::Rect bounds = reference_view->bounds(); bounds.set_origin(origin); BubbleBorder::ArrowLocation arrow_location = BubbleBorder::TOP_RIGHT; // 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) { bounds.set_origin( browser_view->GetLocationBarView()->GetLocationEntryOrigin()); bounds.set_width(0); arrow_location = BubbleBorder::TOP_LEFT; } bubble_content_ = new InstalledBubbleContent(extension_, type_, &icon_); Bubble* bubble = Bubble::Show(browser_view->GetWidget(), bounds, arrow_location, bubble_content_, this); bubble_content_->set_bubble(bubble); } // BubbleDelegate void ExtensionInstalledBubble::BubbleClosing(Bubble* bubble, bool closed_by_escape) { if (extension_ && type_ == PAGE_ACTION) { BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow( browser_->window()->GetNativeHandle()); browser_view->GetLocationBarView()->SetPreviewEnabledPageAction( extension_->page_action(), false); // preview_enabled } Release(); // Balanced in ctor. } bool ExtensionInstalledBubble::CloseOnEscape() { return true; } bool ExtensionInstalledBubble::FadeInOnShow() { return true; }