Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2011 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.
      4 
      5 #include "chrome/browser/ui/views/extensions/extension_installed_bubble.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "base/i18n/rtl.h"
     10 #include "base/message_loop.h"
     11 #include "base/utf_string_conversions.h"
     12 #include "chrome/browser/profiles/profile.h"
     13 #include "chrome/browser/ui/browser.h"
     14 #include "chrome/browser/ui/browser_window.h"
     15 #include "chrome/browser/ui/views/browser_actions_container.h"
     16 #include "chrome/browser/ui/views/frame/browser_view.h"
     17 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
     18 #include "chrome/browser/ui/views/toolbar_view.h"
     19 #include "chrome/common/extensions/extension.h"
     20 #include "chrome/common/extensions/extension_action.h"
     21 #include "content/common/notification_details.h"
     22 #include "content/common/notification_source.h"
     23 #include "content/common/notification_type.h"
     24 #include "grit/generated_resources.h"
     25 #include "grit/theme_resources.h"
     26 #include "ui/base/l10n/l10n_util.h"
     27 #include "ui/base/resource/resource_bundle.h"
     28 #include "views/controls/button/image_button.h"
     29 #include "views/controls/image_view.h"
     30 #include "views/controls/label.h"
     31 #include "views/layout/layout_constants.h"
     32 #include "views/view.h"
     33 
     34 namespace {
     35 
     36 const int kIconSize = 43;
     37 
     38 const int kRightColumnWidth = 285;
     39 
     40 // The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace
     41 // around the content view. We compensate by reducing our outer borders by this
     42 // amount + 4px.
     43 const int kOuterMarginInset = 10;
     44 const int kHorizOuterMargin = views::kPanelHorizMargin - kOuterMarginInset;
     45 const int kVertOuterMargin = views::kPanelVertMargin - kOuterMarginInset;
     46 
     47 // Interior vertical margin is 8px smaller than standard
     48 const int kVertInnerMargin = views::kPanelVertMargin - 8;
     49 
     50 // The image we use for the close button has three pixels of whitespace padding.
     51 const int kCloseButtonPadding = 3;
     52 
     53 // We want to shift the right column (which contains the header and text) up
     54 // 4px to align with icon.
     55 const int kRightcolumnVerticalShift = -4;
     56 
     57 // How long to wait for browser action animations to complete before retrying.
     58 const int kAnimationWaitTime = 50;
     59 
     60 // How often we retry when waiting for browser action animation to end.
     61 const int kAnimationWaitMaxRetry = 10;
     62 
     63 }  // namespace
     64 
     65 namespace browser {
     66 
     67 void ShowExtensionInstalledBubble(
     68     const Extension* extension,
     69     Browser* browser,
     70     const SkBitmap& icon,
     71     Profile* profile) {
     72   ExtensionInstalledBubble::Show(extension, browser, icon);
     73 }
     74 
     75 } // namespace browser
     76 
     77 // InstalledBubbleContent is the content view which is placed in the
     78 // ExtensionInstalledBubble. It displays the install icon and explanatory
     79 // text about the installed extension.
     80 class InstalledBubbleContent : public views::View,
     81                                public views::ButtonListener {
     82  public:
     83   InstalledBubbleContent(const Extension* extension,
     84                          ExtensionInstalledBubble::BubbleType type,
     85                          SkBitmap* icon)
     86       : bubble_(NULL),
     87         type_(type),
     88         info_(NULL) {
     89     ResourceBundle& rb = ResourceBundle::GetSharedInstance();
     90     const gfx::Font& font = rb.GetFont(ResourceBundle::BaseFont);
     91 
     92     // Scale down to 43x43, but allow smaller icons (don't scale up).
     93     gfx::Size size(icon->width(), icon->height());
     94     if (size.width() > kIconSize || size.height() > kIconSize)
     95       size = gfx::Size(kIconSize, kIconSize);
     96     icon_ = new views::ImageView();
     97     icon_->SetImageSize(size);
     98     icon_->SetImage(*icon);
     99     AddChildView(icon_);
    100 
    101     string16 extension_name = UTF8ToUTF16(extension->name());
    102     base::i18n::AdjustStringForLocaleDirection(&extension_name);
    103     heading_ = new views::Label(UTF16ToWide(
    104         l10n_util::GetStringFUTF16(IDS_EXTENSION_INSTALLED_HEADING,
    105                                    extension_name)));
    106     heading_->SetFont(rb.GetFont(ResourceBundle::MediumFont));
    107     heading_->SetMultiLine(true);
    108     heading_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
    109     AddChildView(heading_);
    110 
    111     if (type_ == ExtensionInstalledBubble::PAGE_ACTION) {
    112       info_ = new views::Label(UTF16ToWide(l10n_util::GetStringUTF16(
    113           IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO)));
    114       info_->SetFont(font);
    115       info_->SetMultiLine(true);
    116       info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
    117       AddChildView(info_);
    118     }
    119 
    120     if (type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
    121       info_ = new views::Label(UTF16ToWide(l10n_util::GetStringFUTF16(
    122           IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
    123           UTF8ToUTF16(extension->omnibox_keyword()))));
    124       info_->SetFont(font);
    125       info_->SetMultiLine(true);
    126       info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
    127       AddChildView(info_);
    128     }
    129 
    130     manage_ = new views::Label(UTF16ToWide(
    131         l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_INFO)));
    132     manage_->SetFont(font);
    133     manage_->SetMultiLine(true);
    134     manage_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
    135     AddChildView(manage_);
    136 
    137     close_button_ = new views::ImageButton(this);
    138     close_button_->SetImage(views::CustomButton::BS_NORMAL,
    139         rb.GetBitmapNamed(IDR_CLOSE_BAR));
    140     close_button_->SetImage(views::CustomButton::BS_HOT,
    141         rb.GetBitmapNamed(IDR_CLOSE_BAR_H));
    142     close_button_->SetImage(views::CustomButton::BS_PUSHED,
    143         rb.GetBitmapNamed(IDR_CLOSE_BAR_P));
    144     AddChildView(close_button_);
    145   }
    146 
    147   void set_bubble(Bubble* bubble) { bubble_ = bubble; }
    148 
    149   virtual void ButtonPressed(
    150       views::Button* sender,
    151       const views::Event& event) {
    152     if (sender == close_button_) {
    153       bubble_->set_fade_away_on_close(true);
    154       GetWidget()->Close();
    155     } else {
    156       NOTREACHED() << "Unknown view";
    157     }
    158   }
    159 
    160  private:
    161   virtual gfx::Size GetPreferredSize() {
    162     int width = kHorizOuterMargin;
    163     width += kIconSize;
    164     width += views::kPanelHorizMargin;
    165     width += kRightColumnWidth;
    166     width += 2 * views::kPanelHorizMargin;
    167     width += kHorizOuterMargin;
    168 
    169     int height = kVertOuterMargin;
    170     height += heading_->GetHeightForWidth(kRightColumnWidth);
    171     height += kVertInnerMargin;
    172     if (type_ == ExtensionInstalledBubble::PAGE_ACTION ||
    173         type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
    174       height += info_->GetHeightForWidth(kRightColumnWidth);
    175       height += kVertInnerMargin;
    176     }
    177     height += manage_->GetHeightForWidth(kRightColumnWidth);
    178     height += kVertOuterMargin;
    179 
    180     return gfx::Size(width, std::max(height, kIconSize + 2 * kVertOuterMargin));
    181   }
    182 
    183   virtual void Layout() {
    184     int x = kHorizOuterMargin;
    185     int y = kVertOuterMargin;
    186 
    187     icon_->SetBounds(x, y, kIconSize, kIconSize);
    188     x += kIconSize;
    189     x += views::kPanelHorizMargin;
    190 
    191     y += kRightcolumnVerticalShift;
    192     heading_->SizeToFit(kRightColumnWidth);
    193     heading_->SetX(x);
    194     heading_->SetY(y);
    195     y += heading_->height();
    196     y += kVertInnerMargin;
    197 
    198     if (type_ == ExtensionInstalledBubble::PAGE_ACTION ||
    199         type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
    200       info_->SizeToFit(kRightColumnWidth);
    201       info_->SetX(x);
    202       info_->SetY(y);
    203       y += info_->height();
    204       y += kVertInnerMargin;
    205     }
    206 
    207     manage_->SizeToFit(kRightColumnWidth);
    208     manage_->SetX(x);
    209     manage_->SetY(y);
    210     y += manage_->height();
    211     y += kVertInnerMargin;
    212 
    213     gfx::Size sz;
    214     x += kRightColumnWidth + 2 * views::kPanelHorizMargin + kHorizOuterMargin -
    215         close_button_->GetPreferredSize().width();
    216     y = kVertOuterMargin;
    217     sz = close_button_->GetPreferredSize();
    218     // x-1 & y-1 is just slop to get the close button visually aligned with the
    219     // title text and bubble arrow.
    220     close_button_->SetBounds(x - 1, y - 1, sz.width(), sz.height());
    221   }
    222 
    223   // The Bubble showing us.
    224   Bubble* bubble_;
    225 
    226   ExtensionInstalledBubble::BubbleType type_;
    227   views::ImageView* icon_;
    228   views::Label* heading_;
    229   views::Label* info_;
    230   views::Label* manage_;
    231   views::ImageButton* close_button_;
    232 
    233   DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent);
    234 };
    235 
    236 void ExtensionInstalledBubble::Show(const Extension* extension,
    237                                     Browser *browser,
    238                                     const SkBitmap& icon) {
    239   new ExtensionInstalledBubble(extension, browser, icon);
    240 }
    241 
    242 ExtensionInstalledBubble::ExtensionInstalledBubble(const Extension* extension,
    243                                                    Browser *browser,
    244                                                    const SkBitmap& icon)
    245     : extension_(extension),
    246       browser_(browser),
    247       icon_(icon),
    248       animation_wait_retries_(0) {
    249   AddRef();  // Balanced in BubbleClosing.
    250 
    251   if (!extension_->omnibox_keyword().empty()) {
    252     type_ = OMNIBOX_KEYWORD;
    253   } else if (extension_->browser_action()) {
    254     type_ = BROWSER_ACTION;
    255   } else if (extension->page_action() &&
    256              !extension->page_action()->default_icon_path().empty()) {
    257     type_ = PAGE_ACTION;
    258   } else {
    259     type_ = GENERIC;
    260   }
    261 
    262   // |extension| has been initialized but not loaded at this point. We need
    263   // to wait on showing the Bubble until not only the EXTENSION_LOADED gets
    264   // fired, but all of the EXTENSION_LOADED Observers have run. Only then can we
    265   // be sure that a BrowserAction or PageAction has had views created which we
    266   // can inspect for the purpose of previewing of pointing to them.
    267   registrar_.Add(this, NotificationType::EXTENSION_LOADED,
    268       Source<Profile>(browser->profile()));
    269   registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
    270       Source<Profile>(browser->profile()));
    271 }
    272 
    273 ExtensionInstalledBubble::~ExtensionInstalledBubble() {}
    274 
    275 void ExtensionInstalledBubble::Observe(NotificationType type,
    276                                        const NotificationSource& source,
    277                                        const NotificationDetails& details) {
    278   if (type == NotificationType::EXTENSION_LOADED) {
    279     const Extension* extension = Details<const Extension>(details).ptr();
    280     if (extension == extension_) {
    281       animation_wait_retries_ = 0;
    282       // PostTask to ourself to allow all EXTENSION_LOADED Observers to run.
    283       MessageLoopForUI::current()->PostTask(FROM_HERE, NewRunnableMethod(this,
    284           &ExtensionInstalledBubble::ShowInternal));
    285     }
    286   } else if (type == NotificationType::EXTENSION_UNLOADED) {
    287     const Extension* extension =
    288         Details<UnloadedExtensionInfo>(details)->extension;
    289     if (extension == extension_)
    290       extension_ = NULL;
    291   } else {
    292     NOTREACHED() << L"Received unexpected notification";
    293   }
    294 }
    295 
    296 void ExtensionInstalledBubble::ShowInternal() {
    297   BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
    298       browser_->window()->GetNativeHandle());
    299 
    300   const views::View* reference_view = NULL;
    301   if (type_ == BROWSER_ACTION) {
    302     BrowserActionsContainer* container =
    303         browser_view->GetToolbarView()->browser_actions();
    304     if (container->animating() &&
    305         animation_wait_retries_++ < kAnimationWaitMaxRetry) {
    306       // We don't know where the view will be until the container has stopped
    307       // animating, so check back in a little while.
    308       MessageLoopForUI::current()->PostDelayedTask(
    309           FROM_HERE, NewRunnableMethod(this,
    310           &ExtensionInstalledBubble::ShowInternal), kAnimationWaitTime);
    311       return;
    312     }
    313     reference_view = container->GetBrowserActionView(
    314         extension_->browser_action());
    315     // If the view is not visible then it is in the chevron, so point the
    316     // install bubble to the chevron instead. If this is an incognito window,
    317     // both could be invisible.
    318     if (!reference_view || !reference_view->IsVisible()) {
    319       reference_view = container->chevron();
    320       if (!reference_view || !reference_view->IsVisible())
    321         reference_view = NULL;  // fall back to app menu below.
    322     }
    323   } else if (type_ == PAGE_ACTION) {
    324     LocationBarView* location_bar_view = browser_view->GetLocationBarView();
    325     location_bar_view->SetPreviewEnabledPageAction(extension_->page_action(),
    326                                                    true);  // preview_enabled
    327     reference_view = location_bar_view->GetPageActionView(
    328         extension_->page_action());
    329     DCHECK(reference_view);
    330   } else if (type_ == OMNIBOX_KEYWORD) {
    331     LocationBarView* location_bar_view = browser_view->GetLocationBarView();
    332     reference_view = location_bar_view;
    333     DCHECK(reference_view);
    334   }
    335 
    336   // Default case.
    337   if (reference_view == NULL)
    338     reference_view = browser_view->GetToolbarView()->app_menu();
    339 
    340   gfx::Point origin;
    341   views::View::ConvertPointToScreen(reference_view, &origin);
    342   gfx::Rect bounds = reference_view->bounds();
    343   bounds.set_origin(origin);
    344   BubbleBorder::ArrowLocation arrow_location = BubbleBorder::TOP_RIGHT;
    345 
    346   // For omnibox keyword bubbles, move the arrow to point to the left edge
    347   // of the omnibox, just to the right of the icon.
    348   if (type_ == OMNIBOX_KEYWORD) {
    349     bounds.set_origin(
    350         browser_view->GetLocationBarView()->GetLocationEntryOrigin());
    351     bounds.set_width(0);
    352     arrow_location = BubbleBorder::TOP_LEFT;
    353   }
    354 
    355   bubble_content_ = new InstalledBubbleContent(extension_, type_, &icon_);
    356   Bubble* bubble = Bubble::Show(browser_view->GetWidget(), bounds,
    357                                 arrow_location, bubble_content_, this);
    358   bubble_content_->set_bubble(bubble);
    359 }
    360 
    361 // BubbleDelegate
    362 void ExtensionInstalledBubble::BubbleClosing(Bubble* bubble,
    363                                              bool closed_by_escape) {
    364   if (extension_ && type_ == PAGE_ACTION) {
    365     BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
    366         browser_->window()->GetNativeHandle());
    367     browser_view->GetLocationBarView()->SetPreviewEnabledPageAction(
    368         extension_->page_action(),
    369         false);  // preview_enabled
    370   }
    371 
    372   Release();  // Balanced in ctor.
    373 }
    374 
    375 bool ExtensionInstalledBubble::CloseOnEscape() {
    376   return true;
    377 }
    378 
    379 bool ExtensionInstalledBubble::FadeInOnShow() {
    380   return true;
    381 }
    382