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