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"
36 base::LazyInstance
<std::set
<Profile
*> > g_profiles_evaluated
=
37 LAZY_INSTANCE_INITIALIZER
;
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;
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
),
64 controller_(controller
.Pass()),
67 dismiss_button_(NULL
),
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
103 base::MessageLoop::current()->PostDelayedTask(
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() {
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();
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();
242 DCHECK_EQ(dismiss_button_
, sender
);
244 GetWidget()->Close();
247 void ExtensionMessageBubbleView::LinkClicked(views::Link
* source
,
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(
271 ToolbarView
* toolbar_view
)
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),
280 anchor_view_(NULL
) {}
282 ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
283 MaybeStopObserving();
286 void ExtensionMessageBubbleFactory::MaybeShow(views::View
* anchor_view
) {
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
))
301 if (!shown_startup_override_extensions_bubble_
&&
302 IsInitialProfileCheck(profile_
->GetOriginalProfile()) &&
303 MaybeShowStartupOverrideExtensionsBubble(anchor_view
))
306 if (!shown_dev_mode_extensions_bubble_
)
307 MaybeShowDevModeExtensionsBubble(anchor_view
);
309 RecordProfileCheck(profile_
->GetOriginalProfile());
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())
322 shown_suspicious_extensions_bubble_
= true;
323 SuspiciousExtensionBubbleController
* weak_controller
=
324 suspicious_extensions
.get();
325 ExtensionMessageBubbleView
* bubble_delegate
= new ExtensionMessageBubbleView(
327 views::BubbleBorder::TOP_RIGHT
,
328 suspicious_extensions
.PassAs
<ExtensionMessageBubbleController
>());
330 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
331 weak_controller
->Show(bubble_delegate
);
336 bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
337 views::View
* anchor_view
) {
342 DCHECK(!shown_startup_override_extensions_bubble_
);
344 const Extension
* extension
= OverridesStartupPages(profile_
, NULL
);
348 scoped_ptr
<SettingsApiBubbleController
> settings_api_bubble(
349 new SettingsApiBubbleController(profile_
,
350 BUBBLE_TYPE_STARTUP_PAGES
));
351 if (!settings_api_bubble
->ShouldShow(extension
->id()))
354 shown_startup_override_extensions_bubble_
= true;
355 SettingsApiBubbleController
* weak_controller
= settings_api_bubble
.get();
356 ExtensionMessageBubbleView
* bubble_delegate
= new ExtensionMessageBubbleView(
358 views::BubbleBorder::TOP_RIGHT
,
359 settings_api_bubble
.PassAs
<ExtensionMessageBubbleController
>());
360 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
361 weak_controller
->Show(bubble_delegate
);
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())
378 shown_dev_mode_extensions_bubble_
= true;
380 // We should be in the start stage (i.e., should not have a pending attempt to
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())
394 HighlightDevModeExtensions();
399 void ExtensionMessageBubbleFactory::MaybeObserve() {
400 if (!is_observing_
) {
401 is_observing_
= true;
402 container_
->AddObserver(this);
406 void ExtensionMessageBubbleFactory::MaybeStopObserving() {
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
) {
427 } else { // We shouldn't be observing if we've completed the process.
433 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
434 // If the container associated with the bubble is destroyed, abandon the
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())
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(
465 views::BubbleBorder::TOP_RIGHT
,
466 scoped_ptr
<ExtensionMessageBubbleController
>(controller_
.release()));
467 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
468 weak_controller
->Show(bubble_delegate
);
473 void ExtensionMessageBubbleFactory::Finish() {
474 MaybeStopObserving();
480 } // namespace extensions