Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2013 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_message_bubble_view.h"
      6 
      7 #include "base/strings/string_number_conversions.h"
      8 #include "base/strings/string_util.h"
      9 #include "base/strings/utf_string_conversions.h"
     10 #include "chrome/browser/extensions/dev_mode_bubble_controller.h"
     11 #include "chrome/browser/extensions/extension_action_manager.h"
     12 #include "chrome/browser/extensions/extension_prefs.h"
     13 #include "chrome/browser/extensions/extension_service.h"
     14 #include "chrome/browser/extensions/extension_system.h"
     15 #include "chrome/browser/extensions/suspicious_extension_bubble_controller.h"
     16 #include "chrome/browser/profiles/profile.h"
     17 #include "chrome/browser/ui/browser.h"
     18 #include "chrome/browser/ui/views/frame/browser_view.h"
     19 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
     20 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
     21 #include "grit/locale_settings.h"
     22 #include "ui/base/accessibility/accessible_view_state.h"
     23 #include "ui/base/resource/resource_bundle.h"
     24 #include "ui/views/controls/button/label_button.h"
     25 #include "ui/views/controls/label.h"
     26 #include "ui/views/controls/link.h"
     27 #include "ui/views/layout/grid_layout.h"
     28 #include "ui/views/view.h"
     29 #include "ui/views/widget/widget.h"
     30 
     31 namespace {
     32 
     33 // Layout constants.
     34 const int kExtensionListPadding = 10;
     35 const int kInsetBottomRight = 13;
     36 const int kInsetLeft = 14;
     37 const int kInsetTop = 9;
     38 const int kHeadlineMessagePadding = 4;
     39 const int kHeadlineRowPadding = 10;
     40 const int kMessageBubblePadding = 11;
     41 
     42 // How many extensions to show in the bubble (max).
     43 const size_t kMaxExtensionsToShow = 7;
     44 
     45 // How long to wait until showing the bubble (in seconds).
     46 const int kBubbleAppearanceWaitTime = 5;
     47 
     48 }  // namespace
     49 
     50 ////////////////////////////////////////////////////////////////////////////////
     51 // ExtensionMessageBubbleView
     52 
     53 namespace extensions {
     54 
     55 ExtensionMessageBubbleView::ExtensionMessageBubbleView(
     56     views::View* anchor_view,
     57     ExtensionMessageBubbleController::Delegate* delegate)
     58     : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
     59       weak_factory_(this),
     60       delegate_(delegate),
     61       headline_(NULL),
     62       learn_more_(NULL),
     63       dismiss_button_(NULL),
     64       link_clicked_(false),
     65       action_taken_(false) {
     66   DCHECK(anchor_view->GetWidget());
     67   set_close_on_deactivate(false);
     68   set_move_with_anchor(true);
     69   set_close_on_esc(true);
     70 
     71   // Compensate for built-in vertical padding in the anchor view's image.
     72   set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
     73 }
     74 
     75 // static
     76 void ExtensionMessageBubbleView::MaybeShow(
     77     Browser* browser,
     78     ToolbarView* toolbar_view,
     79     views::View* anchor_view) {
     80 #if defined(OS_WIN)
     81   // The list of suspicious extensions takes priority over the dev mode bubble,
     82   // since that needs to be shown as soon as we disable something. The dev mode
     83   // bubble is not as time sensitive so we'll catch the dev mode extensions on
     84   // the next startup/next window that opens. That way, we're not too spammy
     85   // with the bubbles.
     86   SuspiciousExtensionBubbleController* suspicious_extensions =
     87       extensions::SuspiciousExtensionBubbleController::Get(
     88           browser->profile());
     89   if (suspicious_extensions->ShouldShow()) {
     90     ExtensionMessageBubbleView* bubble_delegate =
     91         new ExtensionMessageBubbleView(anchor_view, suspicious_extensions);
     92     views::BubbleDelegateView::CreateBubble(bubble_delegate);
     93     suspicious_extensions->Show(bubble_delegate);
     94     return;
     95   }
     96 
     97   DevModeBubbleController* dev_mode_extensions =
     98       extensions::DevModeBubbleController::Get(
     99           browser->profile());
    100   if (dev_mode_extensions->ShouldShow()) {
    101     views::View* reference_view = NULL;
    102     BrowserActionsContainer* container = toolbar_view->browser_actions();
    103     if (container->animating())
    104       return;
    105 
    106     ExtensionService* service = extensions::ExtensionSystem::Get(
    107         browser->profile())->extension_service();
    108     extensions::ExtensionActionManager* extension_action_manager =
    109         extensions::ExtensionActionManager::Get(browser->profile());
    110 
    111     const ExtensionIdList extension_list =
    112         dev_mode_extensions->GetExtensionIdList();
    113     ExtensionToolbarModel::Get(
    114         browser->profile())->EnsureVisibility(extension_list);
    115     for (size_t i = 0; i < extension_list.size(); ++i) {
    116       const Extension* extension =
    117           service->GetExtensionById(extension_list[i], false);
    118       if (!extension)
    119         continue;
    120       reference_view = container->GetBrowserActionView(
    121           extension_action_manager->GetBrowserAction(*extension));
    122       if (reference_view && reference_view->visible())
    123         break;  // Found a good candidate.
    124     }
    125     if (reference_view) {
    126       // If we have a view, it means we found a browser action and we want to
    127       // point to the chevron, not the hotdog menu.
    128       if (!reference_view->visible())
    129         reference_view = container->chevron();  // It's hidden, use the chevron.
    130     }
    131     if (reference_view && reference_view->visible())
    132       anchor_view = reference_view;  // Catch-all is the hotdog menu.
    133 
    134     // Show the bubble.
    135     ExtensionMessageBubbleView* bubble_delegate =
    136         new ExtensionMessageBubbleView(anchor_view, dev_mode_extensions);
    137     views::BubbleDelegateView::CreateBubble(bubble_delegate);
    138     dev_mode_extensions->Show(bubble_delegate);
    139   }
    140 #endif
    141 }
    142 
    143 void ExtensionMessageBubbleView::OnActionButtonClicked(
    144     const base::Closure& callback) {
    145   action_callback_ = callback;
    146 }
    147 
    148 void ExtensionMessageBubbleView::OnDismissButtonClicked(
    149     const base::Closure& callback) {
    150   dismiss_callback_ = callback;
    151 }
    152 
    153 void ExtensionMessageBubbleView::OnLinkClicked(
    154     const base::Closure& callback) {
    155   link_callback_ = callback;
    156 }
    157 
    158 void ExtensionMessageBubbleView::Show() {
    159   // Not showing the bubble right away (during startup) has a few benefits:
    160   // We don't have to worry about focus being lost due to the Omnibox (or to
    161   // other things that want focus at startup). This allows Esc to work to close
    162   // the bubble and also solves the keyboard accessibility problem that comes
    163   // with focus being lost (we don't have a good generic mechanism of injecting
    164   // bubbles into the focus cycle). Another benefit of delaying the show is
    165   // that fade-in works (the fade-in isn't apparent if the the bubble appears at
    166   // startup).
    167   base::MessageLoop::current()->PostDelayedTask(
    168       FROM_HERE,
    169       base::Bind(&ExtensionMessageBubbleView::ShowBubble,
    170                  weak_factory_.GetWeakPtr()),
    171       base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime));
    172 }
    173 
    174 void ExtensionMessageBubbleView::OnWidgetDestroying(views::Widget* widget) {
    175   // To catch Esc, we monitor destroy message. Unless the link has been clicked,
    176   // we assume Dismiss was the action taken.
    177   if (!link_clicked_ && !action_taken_)
    178     dismiss_callback_.Run();
    179 }
    180 
    181 ////////////////////////////////////////////////////////////////////////////////
    182 // ExtensionMessageBubbleView - private.
    183 
    184 ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {
    185 }
    186 
    187 void ExtensionMessageBubbleView::ShowBubble() {
    188   StartFade(true);
    189 }
    190 
    191 void ExtensionMessageBubbleView::Init() {
    192   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    193 
    194   views::GridLayout* layout = views::GridLayout::CreatePanel(this);
    195   layout->SetInsets(kInsetTop, kInsetLeft,
    196                     kInsetBottomRight, kInsetBottomRight);
    197   SetLayoutManager(layout);
    198 
    199   const int headline_column_set_id = 0;
    200   views::ColumnSet* top_columns = layout->AddColumnSet(headline_column_set_id);
    201   top_columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
    202                          0, views::GridLayout::USE_PREF, 0, 0);
    203   top_columns->AddPaddingColumn(1, 0);
    204   layout->StartRow(0, headline_column_set_id);
    205 
    206   headline_ = new views::Label();
    207   headline_->SetFont(rb.GetFont(ui::ResourceBundle::MediumFont));
    208   headline_->SetText(delegate_->GetTitle());
    209   layout->AddView(headline_);
    210 
    211   layout->AddPaddingRow(0, kHeadlineRowPadding);
    212 
    213   const int text_column_set_id = 1;
    214   views::ColumnSet* upper_columns = layout->AddColumnSet(text_column_set_id);
    215   upper_columns->AddColumn(
    216       views::GridLayout::LEADING, views::GridLayout::LEADING,
    217       0, views::GridLayout::USE_PREF, 0, 0);
    218   layout->StartRow(0, text_column_set_id);
    219 
    220   views::Label* message = new views::Label();
    221   message->SetMultiLine(true);
    222   message->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    223   message->SetText(delegate_->GetMessageBody());
    224   message->SizeToFit(views::Widget::GetLocalizedContentsWidth(
    225       IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS));
    226   layout->AddView(message);
    227 
    228   if (delegate_->ShouldShowExtensionList()) {
    229     const int extension_list_column_set_id = 2;
    230     views::ColumnSet* middle_columns =
    231         layout->AddColumnSet(extension_list_column_set_id);
    232     middle_columns->AddPaddingColumn(0, kExtensionListPadding);
    233     middle_columns->AddColumn(
    234         views::GridLayout::LEADING, views::GridLayout::CENTER,
    235         0, views::GridLayout::USE_PREF, 0, 0);
    236 
    237     layout->StartRowWithPadding(0, extension_list_column_set_id,
    238         0, kHeadlineMessagePadding);
    239     views::Label* extensions = new views::Label();
    240     extensions->SetMultiLine(true);
    241     extensions->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    242 
    243     std::vector<string16> extension_list;
    244     char16 bullet_point = 0x2022;
    245 
    246     std::vector<string16> suspicious = delegate_->GetExtensions();
    247     size_t i = 0;
    248     for (; i < suspicious.size() && i < kMaxExtensionsToShow; ++i) {
    249       // Add each extension with bullet point.
    250       extension_list.push_back(
    251           bullet_point + ASCIIToUTF16(" ") + suspicious[i]);
    252     }
    253 
    254     if (i > kMaxExtensionsToShow) {
    255       string16 difference = base::IntToString16(i - kMaxExtensionsToShow);
    256       extension_list.push_back(bullet_point + ASCIIToUTF16(" ") +
    257           delegate_->GetOverflowText(difference));
    258     }
    259 
    260     extensions->SetText(JoinString(extension_list, ASCIIToUTF16("\n")));
    261     extensions->SizeToFit(views::Widget::GetLocalizedContentsWidth(
    262         IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS));
    263     layout->AddView(extensions);
    264   }
    265 
    266   string16 action_button = delegate_->GetActionButtonLabel();
    267 
    268   const int action_row_column_set_id = 3;
    269   views::ColumnSet* bottom_columns =
    270       layout->AddColumnSet(action_row_column_set_id);
    271   bottom_columns->AddColumn(views::GridLayout::LEADING,
    272       views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
    273   bottom_columns->AddPaddingColumn(1, 0);
    274   bottom_columns->AddColumn(views::GridLayout::TRAILING,
    275       views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
    276   if (!action_button.empty()) {
    277     bottom_columns->AddColumn(views::GridLayout::TRAILING,
    278         views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
    279   }
    280   layout->StartRowWithPadding(0, action_row_column_set_id,
    281                               0, kMessageBubblePadding);
    282 
    283   learn_more_ = new views::Link(delegate_->GetLearnMoreLabel());
    284   learn_more_->set_listener(this);
    285   layout->AddView(learn_more_);
    286 
    287   if (!action_button.empty()) {
    288     action_button_ = new views::LabelButton(this, action_button.c_str());
    289     action_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
    290     layout->AddView(action_button_);
    291   }
    292 
    293   dismiss_button_ = new views::LabelButton(this,
    294       delegate_->GetDismissButtonLabel());
    295   dismiss_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
    296   layout->AddView(dismiss_button_);
    297 }
    298 
    299 void ExtensionMessageBubbleView::ButtonPressed(views::Button* sender,
    300                                                const ui::Event& event) {
    301   if (sender == action_button_) {
    302     action_taken_ = true;
    303     action_callback_.Run();
    304   } else {
    305     DCHECK_EQ(dismiss_button_, sender);
    306   }
    307   GetWidget()->Close();
    308 }
    309 
    310 void ExtensionMessageBubbleView::LinkClicked(views::Link* source,
    311                                              int event_flags) {
    312   DCHECK_EQ(learn_more_, source);
    313   link_clicked_ = true;
    314   link_callback_.Run();
    315   GetWidget()->Close();
    316 }
    317 
    318 void ExtensionMessageBubbleView::GetAccessibleState(
    319     ui::AccessibleViewState* state) {
    320   state->role = ui::AccessibilityTypes::ROLE_ALERT;
    321 }
    322 
    323 void ExtensionMessageBubbleView::ViewHierarchyChanged(
    324     const ViewHierarchyChangedDetails& details) {
    325   if (details.is_add && details.child == this)
    326     NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_ALERT, true);
    327 }
    328 
    329 }  // namespace extensions
    330