extension_message_bubble_view.cc revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
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_message_bubble_controller.h" 13#include "chrome/browser/extensions/extension_service.h" 14#include "chrome/browser/extensions/proxy_overridden_bubble_controller.h" 15#include "chrome/browser/extensions/settings_api_bubble_controller.h" 16#include "chrome/browser/extensions/settings_api_helpers.h" 17#include "chrome/browser/extensions/suspicious_extension_bubble_controller.h" 18#include "chrome/browser/profiles/profile.h" 19#include "chrome/browser/ui/view_ids.h" 20#include "chrome/browser/ui/views/frame/browser_view.h" 21#include "chrome/browser/ui/views/toolbar/browser_actions_container.h" 22#include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h" 23#include "chrome/browser/ui/views/toolbar/toolbar_view.h" 24#include "extensions/browser/extension_prefs.h" 25#include "extensions/browser/extension_system.h" 26#include "grit/locale_settings.h" 27#include "ui/accessibility/ax_view_state.h" 28#include "ui/base/resource/resource_bundle.h" 29#include "ui/views/controls/button/label_button.h" 30#include "ui/views/controls/label.h" 31#include "ui/views/controls/link.h" 32#include "ui/views/layout/grid_layout.h" 33#include "ui/views/view.h" 34#include "ui/views/widget/widget.h" 35 36namespace { 37 38base::LazyInstance<std::set<Profile*> > g_profiles_evaluated = 39 LAZY_INSTANCE_INITIALIZER; 40 41// Layout constants. 42const int kExtensionListPadding = 10; 43const int kInsetBottomRight = 13; 44const int kInsetLeft = 14; 45const int kInsetTop = 9; 46const int kHeadlineMessagePadding = 4; 47const int kHeadlineRowPadding = 10; 48const int kMessageBubblePadding = 11; 49 50// How many extensions to show in the bubble (max). 51const size_t kMaxExtensionsToShow = 7; 52 53// How long to wait until showing the bubble (in seconds). 54const int kBubbleAppearanceWaitTime = 5; 55 56} // namespace 57 58namespace extensions { 59 60ExtensionMessageBubbleView::ExtensionMessageBubbleView( 61 views::View* anchor_view, 62 views::BubbleBorder::Arrow arrow_location, 63 scoped_ptr<extensions::ExtensionMessageBubbleController> controller) 64 : BubbleDelegateView(anchor_view, arrow_location), 65 weak_factory_(this), 66 controller_(controller.Pass()), 67 anchor_view_(anchor_view), 68 headline_(NULL), 69 learn_more_(NULL), 70 dismiss_button_(NULL), 71 link_clicked_(false), 72 action_taken_(false) { 73 DCHECK(anchor_view->GetWidget()); 74 set_close_on_deactivate(controller_->CloseOnDeactivate()); 75 set_close_on_esc(true); 76 77 // Compensate for built-in vertical padding in the anchor view's image. 78 set_anchor_view_insets(gfx::Insets(5, 0, 5, 0)); 79} 80 81void ExtensionMessageBubbleView::OnActionButtonClicked( 82 const base::Closure& callback) { 83 action_callback_ = callback; 84} 85 86void ExtensionMessageBubbleView::OnDismissButtonClicked( 87 const base::Closure& callback) { 88 dismiss_callback_ = callback; 89} 90 91void ExtensionMessageBubbleView::OnLinkClicked( 92 const base::Closure& callback) { 93 link_callback_ = callback; 94} 95 96void ExtensionMessageBubbleView::Show() { 97 // Not showing the bubble right away (during startup) has a few benefits: 98 // We don't have to worry about focus being lost due to the Omnibox (or to 99 // other things that want focus at startup). This allows Esc to work to close 100 // the bubble and also solves the keyboard accessibility problem that comes 101 // with focus being lost (we don't have a good generic mechanism of injecting 102 // bubbles into the focus cycle). Another benefit of delaying the show is 103 // that fade-in works (the fade-in isn't apparent if the the bubble appears at 104 // startup). 105 base::MessageLoop::current()->PostDelayedTask( 106 FROM_HERE, 107 base::Bind(&ExtensionMessageBubbleView::ShowBubble, 108 weak_factory_.GetWeakPtr()), 109 base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime)); 110} 111 112void ExtensionMessageBubbleView::OnWidgetDestroying(views::Widget* widget) { 113 // To catch Esc, we monitor destroy message. Unless the link has been clicked, 114 // we assume Dismiss was the action taken. 115 if (!link_clicked_ && !action_taken_) 116 dismiss_callback_.Run(); 117} 118 119//////////////////////////////////////////////////////////////////////////////// 120// ExtensionMessageBubbleView - private. 121 122ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {} 123 124void ExtensionMessageBubbleView::ShowBubble() { 125 GetWidget()->Show(); 126} 127 128void ExtensionMessageBubbleView::Init() { 129 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 130 131 views::GridLayout* layout = views::GridLayout::CreatePanel(this); 132 layout->SetInsets(kInsetTop, kInsetLeft, 133 kInsetBottomRight, kInsetBottomRight); 134 SetLayoutManager(layout); 135 136 ExtensionMessageBubbleController::Delegate* delegate = 137 controller_->delegate(); 138 139 const int headline_column_set_id = 0; 140 views::ColumnSet* top_columns = layout->AddColumnSet(headline_column_set_id); 141 top_columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 142 0, views::GridLayout::USE_PREF, 0, 0); 143 top_columns->AddPaddingColumn(1, 0); 144 layout->StartRow(0, headline_column_set_id); 145 146 headline_ = new views::Label(delegate->GetTitle(), 147 rb.GetFontList(ui::ResourceBundle::MediumFont)); 148 layout->AddView(headline_); 149 150 layout->AddPaddingRow(0, kHeadlineRowPadding); 151 152 const int text_column_set_id = 1; 153 views::ColumnSet* upper_columns = layout->AddColumnSet(text_column_set_id); 154 upper_columns->AddColumn( 155 views::GridLayout::LEADING, views::GridLayout::LEADING, 156 0, views::GridLayout::USE_PREF, 0, 0); 157 layout->StartRow(0, text_column_set_id); 158 159 views::Label* message = new views::Label(); 160 message->SetMultiLine(true); 161 message->SetHorizontalAlignment(gfx::ALIGN_LEFT); 162 message->SetText(delegate->GetMessageBody( 163 anchor_view_->id() == VIEW_ID_BROWSER_ACTION)); 164 message->SizeToFit(views::Widget::GetLocalizedContentsWidth( 165 IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS)); 166 layout->AddView(message); 167 168 if (delegate->ShouldShowExtensionList()) { 169 const int extension_list_column_set_id = 2; 170 views::ColumnSet* middle_columns = 171 layout->AddColumnSet(extension_list_column_set_id); 172 middle_columns->AddPaddingColumn(0, kExtensionListPadding); 173 middle_columns->AddColumn( 174 views::GridLayout::LEADING, views::GridLayout::CENTER, 175 0, views::GridLayout::USE_PREF, 0, 0); 176 177 layout->StartRowWithPadding(0, extension_list_column_set_id, 178 0, kHeadlineMessagePadding); 179 views::Label* extensions = new views::Label(); 180 extensions->SetMultiLine(true); 181 extensions->SetHorizontalAlignment(gfx::ALIGN_LEFT); 182 183 std::vector<base::string16> extension_list; 184 base::char16 bullet_point = 0x2022; 185 186 std::vector<base::string16> suspicious = controller_->GetExtensionList(); 187 size_t i = 0; 188 for (; i < suspicious.size() && i < kMaxExtensionsToShow; ++i) { 189 // Add each extension with bullet point. 190 extension_list.push_back( 191 bullet_point + base::ASCIIToUTF16(" ") + suspicious[i]); 192 } 193 194 if (i > kMaxExtensionsToShow) { 195 base::string16 difference = base::IntToString16(i - kMaxExtensionsToShow); 196 extension_list.push_back(bullet_point + base::ASCIIToUTF16(" ") + 197 delegate->GetOverflowText(difference)); 198 } 199 200 extensions->SetText(JoinString(extension_list, base::ASCIIToUTF16("\n"))); 201 extensions->SizeToFit(views::Widget::GetLocalizedContentsWidth( 202 IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS)); 203 layout->AddView(extensions); 204 } 205 206 base::string16 action_button = delegate->GetActionButtonLabel(); 207 208 const int action_row_column_set_id = 3; 209 views::ColumnSet* bottom_columns = 210 layout->AddColumnSet(action_row_column_set_id); 211 bottom_columns->AddColumn(views::GridLayout::LEADING, 212 views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0); 213 bottom_columns->AddPaddingColumn(1, 0); 214 bottom_columns->AddColumn(views::GridLayout::TRAILING, 215 views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0); 216 if (!action_button.empty()) { 217 bottom_columns->AddColumn(views::GridLayout::TRAILING, 218 views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0); 219 } 220 layout->StartRowWithPadding(0, action_row_column_set_id, 221 0, kMessageBubblePadding); 222 223 learn_more_ = new views::Link(delegate->GetLearnMoreLabel()); 224 learn_more_->set_listener(this); 225 layout->AddView(learn_more_); 226 227 if (!action_button.empty()) { 228 action_button_ = new views::LabelButton(this, action_button.c_str()); 229 action_button_->SetStyle(views::Button::STYLE_BUTTON); 230 layout->AddView(action_button_); 231 } 232 233 dismiss_button_ = new views::LabelButton(this, 234 delegate->GetDismissButtonLabel()); 235 dismiss_button_->SetStyle(views::Button::STYLE_BUTTON); 236 layout->AddView(dismiss_button_); 237} 238 239void ExtensionMessageBubbleView::ButtonPressed(views::Button* sender, 240 const ui::Event& event) { 241 if (sender == action_button_) { 242 action_taken_ = true; 243 action_callback_.Run(); 244 } else { 245 DCHECK_EQ(dismiss_button_, sender); 246 } 247 GetWidget()->Close(); 248} 249 250void ExtensionMessageBubbleView::LinkClicked(views::Link* source, 251 int event_flags) { 252 DCHECK_EQ(learn_more_, source); 253 link_clicked_ = true; 254 link_callback_.Run(); 255 GetWidget()->Close(); 256} 257 258void ExtensionMessageBubbleView::GetAccessibleState( 259 ui::AXViewState* state) { 260 state->role = ui::AX_ROLE_ALERT; 261} 262 263void ExtensionMessageBubbleView::ViewHierarchyChanged( 264 const ViewHierarchyChangedDetails& details) { 265 if (details.is_add && details.child == this) 266 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true); 267} 268 269//////////////////////////////////////////////////////////////////////////////// 270// ExtensionMessageBubbleFactory 271 272ExtensionMessageBubbleFactory::ExtensionMessageBubbleFactory( 273 Profile* profile, 274 ToolbarView* toolbar_view) 275 : profile_(profile), 276 toolbar_view_(toolbar_view), 277 shown_suspicious_extensions_bubble_(false), 278 shown_startup_override_extensions_bubble_(false), 279 shown_proxy_override_extensions_bubble_(false), 280 shown_dev_mode_extensions_bubble_(false), 281 is_observing_(false), 282 stage_(STAGE_START), 283 container_(NULL), 284 anchor_view_(NULL) {} 285 286ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() { 287 MaybeStopObserving(); 288} 289 290void ExtensionMessageBubbleFactory::MaybeShow(views::View* anchor_view) { 291#if defined(OS_WIN) 292 bool is_initial_check = IsInitialProfileCheck(profile_->GetOriginalProfile()); 293 RecordProfileCheck(profile_->GetOriginalProfile()); 294 295 // The list of suspicious extensions takes priority over the dev mode bubble 296 // and the settings API bubble, since that needs to be shown as soon as we 297 // disable something. The settings API bubble is shown on first startup after 298 // an extension has changed the startup pages and it is acceptable if that 299 // waits until the next startup because of the suspicious extension bubble. 300 // The dev mode bubble is not time sensitive like the other two so we'll catch 301 // the dev mode extensions on the next startup/next window that opens. That 302 // way, we're not too spammy with the bubbles. 303 if (!shown_suspicious_extensions_bubble_ && 304 MaybeShowSuspiciousExtensionsBubble(anchor_view)) 305 return; 306 307 if (!shown_startup_override_extensions_bubble_ && 308 is_initial_check && 309 MaybeShowStartupOverrideExtensionsBubble(anchor_view)) 310 return; 311 312 if (!shown_proxy_override_extensions_bubble_ && 313 MaybeShowProxyOverrideExtensionsBubble(anchor_view)) 314 return; 315 316 if (!shown_dev_mode_extensions_bubble_) 317 MaybeShowDevModeExtensionsBubble(anchor_view); 318#endif // OS_WIN 319} 320 321bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble( 322 views::View* anchor_view) { 323 DCHECK(!shown_suspicious_extensions_bubble_); 324 325 scoped_ptr<SuspiciousExtensionBubbleController> suspicious_extensions( 326 new SuspiciousExtensionBubbleController(profile_)); 327 if (!suspicious_extensions->ShouldShow()) 328 return false; 329 330 shown_suspicious_extensions_bubble_ = true; 331 SuspiciousExtensionBubbleController* weak_controller = 332 suspicious_extensions.get(); 333 ExtensionMessageBubbleView* bubble_delegate = new ExtensionMessageBubbleView( 334 anchor_view, 335 views::BubbleBorder::TOP_RIGHT, 336 suspicious_extensions.PassAs<ExtensionMessageBubbleController>()); 337 338 views::BubbleDelegateView::CreateBubble(bubble_delegate); 339 weak_controller->Show(bubble_delegate); 340 341 return true; 342} 343 344bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble( 345 views::View* anchor_view) { 346#if !defined(OS_WIN) 347 return false; 348#else 349 DCHECK(!shown_startup_override_extensions_bubble_); 350 351 const Extension* extension = 352 GetExtensionOverridingStartupPages(profile_, NULL); 353 if (!extension) 354 return false; 355 356 scoped_ptr<SettingsApiBubbleController> settings_api_bubble( 357 new SettingsApiBubbleController(profile_, 358 BUBBLE_TYPE_STARTUP_PAGES)); 359 if (!settings_api_bubble->ShouldShow(extension->id())) 360 return false; 361 362 shown_startup_override_extensions_bubble_ = true; 363 PrepareToHighlightExtensions( 364 settings_api_bubble.PassAs<ExtensionMessageBubbleController>(), 365 anchor_view); 366 return true; 367#endif 368} 369 370bool ExtensionMessageBubbleFactory::MaybeShowProxyOverrideExtensionsBubble( 371 views::View* anchor_view) { 372#if !defined(OS_WIN) 373 return false; 374#else 375 DCHECK(!shown_proxy_override_extensions_bubble_); 376 377 const Extension* extension = GetExtensionOverridingProxy(profile_); 378 if (!extension) 379 return false; 380 381 scoped_ptr<ProxyOverriddenBubbleController> proxy_bubble( 382 new ProxyOverriddenBubbleController(profile_)); 383 if (!proxy_bubble->ShouldShow(extension->id())) 384 return false; 385 386 shown_proxy_override_extensions_bubble_ = true; 387 PrepareToHighlightExtensions( 388 proxy_bubble.PassAs<ExtensionMessageBubbleController>(), anchor_view); 389 return true; 390#endif 391} 392 393bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble( 394 views::View* anchor_view) { 395 DCHECK(!shown_dev_mode_extensions_bubble_); 396 397 // Check the Developer Mode extensions. 398 scoped_ptr<DevModeBubbleController> dev_mode_extensions( 399 new DevModeBubbleController(profile_)); 400 401 // Return early if we have none to show. 402 if (!dev_mode_extensions->ShouldShow()) 403 return false; 404 405 shown_dev_mode_extensions_bubble_ = true; 406 PrepareToHighlightExtensions( 407 dev_mode_extensions.PassAs<ExtensionMessageBubbleController>(), 408 anchor_view); 409 return true; 410} 411 412void ExtensionMessageBubbleFactory::MaybeObserve() { 413 if (!is_observing_) { 414 is_observing_ = true; 415 container_->AddObserver(this); 416 } 417} 418 419void ExtensionMessageBubbleFactory::MaybeStopObserving() { 420 if (is_observing_) { 421 is_observing_ = false; 422 container_->RemoveObserver(this); 423 } 424} 425 426void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile* profile) { 427 g_profiles_evaluated.Get().insert(profile); 428} 429 430bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile* profile) { 431 return g_profiles_evaluated.Get().count(profile) == 0; 432} 433 434void ExtensionMessageBubbleFactory::OnBrowserActionsContainerAnimationEnded() { 435 MaybeStopObserving(); 436 if (stage_ == STAGE_START) { 437 HighlightExtensions(); 438 } else if (stage_ == STAGE_HIGHLIGHTED) { 439 ShowHighlightingBubble(); 440 } else { // We shouldn't be observing if we've completed the process. 441 NOTREACHED(); 442 Finish(); 443 } 444} 445 446void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() { 447 // If the container associated with the bubble is destroyed, abandon the 448 // process. 449 Finish(); 450} 451 452void ExtensionMessageBubbleFactory::PrepareToHighlightExtensions( 453 scoped_ptr<ExtensionMessageBubbleController> controller, 454 views::View* anchor_view) { 455 // We should be in the start stage (i.e., should not have a pending attempt to 456 // show a bubble). 457 DCHECK_EQ(stage_, STAGE_START); 458 459 // Prepare to display and highlight the extensions before showing the bubble. 460 // Since this is an asynchronous process, set member variables for later use. 461 controller_ = controller.Pass(); 462 anchor_view_ = anchor_view; 463 container_ = toolbar_view_->browser_actions(); 464 465 if (container_->animating()) 466 MaybeObserve(); 467 else 468 HighlightExtensions(); 469} 470 471void ExtensionMessageBubbleFactory::HighlightExtensions() { 472 DCHECK_EQ(STAGE_START, stage_); 473 stage_ = STAGE_HIGHLIGHTED; 474 475 const ExtensionIdList extension_list = controller_->GetExtensionIdList(); 476 DCHECK(!extension_list.empty()); 477 ExtensionToolbarModel::Get(profile_)->HighlightExtensions(extension_list); 478 if (container_->animating()) 479 MaybeObserve(); 480 else 481 ShowHighlightingBubble(); 482} 483 484void ExtensionMessageBubbleFactory::ShowHighlightingBubble() { 485 DCHECK_EQ(stage_, STAGE_HIGHLIGHTED); 486 stage_ = STAGE_COMPLETE; 487 488 views::View* reference_view = NULL; 489 if (container_->num_browser_actions() > 0) 490 reference_view = container_->GetBrowserActionViewAt(0); 491 if (reference_view && reference_view->visible()) 492 anchor_view_ = reference_view; 493 494 ExtensionMessageBubbleController* weak_controller = controller_.get(); 495 ExtensionMessageBubbleView* bubble_delegate = 496 new ExtensionMessageBubbleView( 497 anchor_view_, 498 views::BubbleBorder::TOP_RIGHT, 499 scoped_ptr<ExtensionMessageBubbleController>( 500 controller_.release())); 501 views::BubbleDelegateView::CreateBubble(bubble_delegate); 502 weak_controller->Show(bubble_delegate); 503 504 Finish(); 505} 506 507void ExtensionMessageBubbleFactory::Finish() { 508 MaybeStopObserving(); 509 controller_.reset(); 510 anchor_view_ = NULL; 511 container_ = NULL; 512} 513 514} // namespace extensions 515