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