Add unit test for the Settings API Bubble.
[chromium-blink-merge.git] / chrome / browser / ui / views / extensions / extension_message_bubble_view.cc
blob2e58aae416edbeb8a84dbc4af8f24957d72d3778
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.
5 #include "chrome/browser/ui/views/extensions/extension_message_bubble_view.h"
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"
34 namespace {
36 base::LazyInstance<std::set<Profile*> > g_profiles_evaluated =
37 LAZY_INSTANCE_INITIALIZER;
39 // Layout constants.
40 const int kExtensionListPadding = 10;
41 const int kInsetBottomRight = 13;
42 const int kInsetLeft = 14;
43 const int kInsetTop = 9;
44 const int kHeadlineMessagePadding = 4;
45 const int kHeadlineRowPadding = 10;
46 const int kMessageBubblePadding = 11;
48 // How many extensions to show in the bubble (max).
49 const size_t kMaxExtensionsToShow = 7;
51 // How long to wait until showing the bubble (in seconds).
52 const int kBubbleAppearanceWaitTime = 5;
54 } // namespace
56 namespace extensions {
58 ExtensionMessageBubbleView::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);
75 // Compensate for built-in vertical padding in the anchor view's image.
76 set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
79 void ExtensionMessageBubbleView::OnActionButtonClicked(
80 const base::Closure& callback) {
81 action_callback_ = callback;
84 void ExtensionMessageBubbleView::OnDismissButtonClicked(
85 const base::Closure& callback) {
86 dismiss_callback_ = callback;
89 void ExtensionMessageBubbleView::OnLinkClicked(
90 const base::Closure& callback) {
91 link_callback_ = callback;
94 void 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));
110 void 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();
117 ////////////////////////////////////////////////////////////////////////////////
118 // ExtensionMessageBubbleView - private.
120 ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
122 void ExtensionMessageBubbleView::ShowBubble() {
123 GetWidget()->Show();
126 void ExtensionMessageBubbleView::Init() {
127 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
129 views::GridLayout* layout = views::GridLayout::CreatePanel(this);
130 layout->SetInsets(kInsetTop, kInsetLeft,
131 kInsetBottomRight, kInsetBottomRight);
132 SetLayoutManager(layout);
134 ExtensionMessageBubbleController::Delegate* delegate =
135 controller_->delegate();
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);
144 headline_ = new views::Label(delegate->GetTitle(),
145 rb.GetFontList(ui::ResourceBundle::MediumFont));
146 layout->AddView(headline_);
148 layout->AddPaddingRow(0, kHeadlineRowPadding);
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);
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);
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);
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);
180 std::vector<base::string16> extension_list;
181 base::char16 bullet_point = 0x2022;
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]);
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));
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);
203 base::string16 action_button = delegate->GetActionButtonLabel();
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);
217 layout->StartRowWithPadding(0, action_row_column_set_id,
218 0, kMessageBubblePadding);
220 learn_more_ = new views::Link(delegate->GetLearnMoreLabel());
221 learn_more_->set_listener(this);
222 layout->AddView(learn_more_);
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_);
230 dismiss_button_ = new views::LabelButton(this,
231 delegate->GetDismissButtonLabel());
232 dismiss_button_->SetStyle(views::Button::STYLE_BUTTON);
233 layout->AddView(dismiss_button_);
236 void 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);
244 GetWidget()->Close();
247 void 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();
255 void ExtensionMessageBubbleView::GetAccessibleState(
256 ui::AXViewState* state) {
257 state->role = ui::AX_ROLE_ALERT;
260 void ExtensionMessageBubbleView::ViewHierarchyChanged(
261 const ViewHierarchyChangedDetails& details) {
262 if (details.is_add && details.child == this)
263 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
266 ////////////////////////////////////////////////////////////////////////////////
267 // ExtensionMessageBubbleFactory
269 ExtensionMessageBubbleFactory::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) {}
282 ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
283 MaybeStopObserving();
286 void 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;
301 if (!shown_startup_override_extensions_bubble_ &&
302 IsInitialProfileCheck(profile_->GetOriginalProfile()) &&
303 MaybeShowStartupOverrideExtensionsBubble(anchor_view))
304 return;
306 if (!shown_dev_mode_extensions_bubble_)
307 MaybeShowDevModeExtensionsBubble(anchor_view);
309 RecordProfileCheck(profile_->GetOriginalProfile());
310 #endif // OS_WIN
313 bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
314 views::View* anchor_view) {
315 DCHECK(!shown_suspicious_extensions_bubble_);
317 scoped_ptr<SuspiciousExtensionBubbleController> suspicious_extensions(
318 new SuspiciousExtensionBubbleController(profile_));
319 if (!suspicious_extensions->ShouldShow())
320 return false;
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>());
330 views::BubbleDelegateView::CreateBubble(bubble_delegate);
331 weak_controller->Show(bubble_delegate);
333 return true;
336 bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
337 views::View* anchor_view) {
338 #if !defined(OS_WIN)
339 return false;
340 #endif
342 DCHECK(!shown_startup_override_extensions_bubble_);
344 const Extension* extension = OverridesStartupPages(profile_, NULL);
345 if (!extension)
346 return false;
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;
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);
363 return true;
366 bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
367 views::View* anchor_view) {
368 DCHECK(!shown_dev_mode_extensions_bubble_);
370 // Check the Developer Mode extensions.
371 scoped_ptr<DevModeBubbleController> dev_mode_extensions(
372 new DevModeBubbleController(profile_));
374 // Return early if we have none to show.
375 if (!dev_mode_extensions->ShouldShow())
376 return false;
378 shown_dev_mode_extensions_bubble_ = true;
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);
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();
391 if (container_->animating())
392 MaybeObserve();
393 else
394 HighlightDevModeExtensions();
396 return true;
399 void ExtensionMessageBubbleFactory::MaybeObserve() {
400 if (!is_observing_) {
401 is_observing_ = true;
402 container_->AddObserver(this);
406 void ExtensionMessageBubbleFactory::MaybeStopObserving() {
407 if (is_observing_) {
408 is_observing_ = false;
409 container_->RemoveObserver(this);
413 void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile* profile) {
414 g_profiles_evaluated.Get().insert(profile);
417 bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile* profile) {
418 return g_profiles_evaluated.Get().count(profile) == 0;
421 void 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();
433 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
434 // If the container associated with the bubble is destroyed, abandon the
435 // process.
436 Finish();
439 void ExtensionMessageBubbleFactory::HighlightDevModeExtensions() {
440 DCHECK_EQ(STAGE_START, stage_);
441 stage_ = STAGE_HIGHLIGHTED;
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();
452 void ExtensionMessageBubbleFactory::ShowDevModeBubble() {
453 DCHECK_EQ(stage_, STAGE_HIGHLIGHTED);
454 stage_ = STAGE_COMPLETE;
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;
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);
470 Finish();
473 void ExtensionMessageBubbleFactory::Finish() {
474 MaybeStopObserving();
475 controller_.reset();
476 anchor_view_ = NULL;
477 container_ = NULL;
480 } // namespace extensions