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