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