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/extension_toolbar_model.h"
15 #include "chrome/browser/extensions/proxy_overridden_bubble_controller.h"
16 #include "chrome/browser/extensions/settings_api_bubble_controller.h"
17 #include "chrome/browser/extensions/settings_api_helpers.h"
18 #include "chrome/browser/extensions/suspicious_extension_bubble_controller.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/ui/view_ids.h"
21 #include "chrome/browser/ui/views/frame/browser_view.h"
22 #include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
23 #include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h"
24 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
25 #include "chrome/grit/locale_settings.h"
26 #include "extensions/browser/extension_prefs.h"
27 #include "extensions/browser/extension_system.h"
28 #include "ui/accessibility/ax_view_state.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/views/controls/button/label_button.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/controls/link.h"
33 #include "ui/views/layout/grid_layout.h"
34 #include "ui/views/view.h"
35 #include "ui/views/widget/widget.h"
39 base::LazyInstance
<std::set
<Profile
*> > g_profiles_evaluated
=
40 LAZY_INSTANCE_INITIALIZER
;
43 const int kExtensionListPadding
= 10;
44 const int kInsetBottomRight
= 13;
45 const int kInsetLeft
= 14;
46 const int kInsetTop
= 9;
47 const int kHeadlineMessagePadding
= 4;
48 const int kHeadlineRowPadding
= 10;
49 const int kMessageBubblePadding
= 11;
51 // How many extensions to show in the bubble (max).
52 const size_t kMaxExtensionsToShow
= 7;
54 // How long to wait until showing the bubble (in seconds).
55 const int kBubbleAppearanceWaitTime
= 5;
59 namespace extensions
{
61 ExtensionMessageBubbleView::ExtensionMessageBubbleView(
62 views::View
* anchor_view
,
63 views::BubbleBorder::Arrow arrow_location
,
64 scoped_ptr
<extensions::ExtensionMessageBubbleController
> controller
)
65 : BubbleDelegateView(anchor_view
, arrow_location
),
66 controller_(controller
.Pass()),
67 anchor_view_(anchor_view
),
70 dismiss_button_(NULL
),
74 DCHECK(anchor_view
->GetWidget());
75 set_close_on_deactivate(controller_
->CloseOnDeactivate());
76 set_close_on_esc(true);
78 // Compensate for built-in vertical padding in the anchor view's image.
79 set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
82 void ExtensionMessageBubbleView::OnActionButtonClicked(
83 const base::Closure
& callback
) {
84 action_callback_
= callback
;
87 void ExtensionMessageBubbleView::OnDismissButtonClicked(
88 const base::Closure
& callback
) {
89 dismiss_callback_
= callback
;
92 void ExtensionMessageBubbleView::OnLinkClicked(
93 const base::Closure
& callback
) {
94 link_callback_
= callback
;
97 void ExtensionMessageBubbleView::Show() {
98 // Not showing the bubble right away (during startup) has a few benefits:
99 // We don't have to worry about focus being lost due to the Omnibox (or to
100 // other things that want focus at startup). This allows Esc to work to close
101 // the bubble and also solves the keyboard accessibility problem that comes
102 // with focus being lost (we don't have a good generic mechanism of injecting
103 // bubbles into the focus cycle). Another benefit of delaying the show is
104 // that fade-in works (the fade-in isn't apparent if the the bubble appears at
106 base::MessageLoop::current()->PostDelayedTask(
108 base::Bind(&ExtensionMessageBubbleView::ShowBubble
,
109 weak_factory_
.GetWeakPtr()),
110 base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime
));
113 void ExtensionMessageBubbleView::OnWidgetDestroying(views::Widget
* widget
) {
114 // To catch Esc, we monitor destroy message. Unless the link has been clicked,
115 // we assume Dismiss was the action taken.
116 if (!link_clicked_
&& !action_taken_
)
117 dismiss_callback_
.Run();
120 ////////////////////////////////////////////////////////////////////////////////
121 // ExtensionMessageBubbleView - private.
123 ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
125 void ExtensionMessageBubbleView::ShowBubble() {
129 void ExtensionMessageBubbleView::Init() {
130 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
132 views::GridLayout
* layout
= views::GridLayout::CreatePanel(this);
133 layout
->SetInsets(kInsetTop
, kInsetLeft
,
134 kInsetBottomRight
, kInsetBottomRight
);
135 SetLayoutManager(layout
);
137 ExtensionMessageBubbleController::Delegate
* delegate
=
138 controller_
->delegate();
140 const int headline_column_set_id
= 0;
141 views::ColumnSet
* top_columns
= layout
->AddColumnSet(headline_column_set_id
);
142 top_columns
->AddColumn(views::GridLayout::LEADING
, views::GridLayout::CENTER
,
143 0, views::GridLayout::USE_PREF
, 0, 0);
144 top_columns
->AddPaddingColumn(1, 0);
145 layout
->StartRow(0, headline_column_set_id
);
147 headline_
= new views::Label(delegate
->GetTitle(),
148 rb
.GetFontList(ui::ResourceBundle::MediumFont
));
149 layout
->AddView(headline_
);
151 layout
->AddPaddingRow(0, kHeadlineRowPadding
);
153 const int text_column_set_id
= 1;
154 views::ColumnSet
* upper_columns
= layout
->AddColumnSet(text_column_set_id
);
155 upper_columns
->AddColumn(
156 views::GridLayout::LEADING
, views::GridLayout::LEADING
,
157 0, views::GridLayout::USE_PREF
, 0, 0);
158 layout
->StartRow(0, text_column_set_id
);
160 views::Label
* message
= new views::Label();
161 message
->SetMultiLine(true);
162 message
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
163 message
->SetText(delegate
->GetMessageBody(
164 anchor_view_
->id() == VIEW_ID_BROWSER_ACTION
));
165 message
->SizeToFit(views::Widget::GetLocalizedContentsWidth(
166 IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS
));
167 layout
->AddView(message
);
169 if (delegate
->ShouldShowExtensionList()) {
170 const int extension_list_column_set_id
= 2;
171 views::ColumnSet
* middle_columns
=
172 layout
->AddColumnSet(extension_list_column_set_id
);
173 middle_columns
->AddPaddingColumn(0, kExtensionListPadding
);
174 middle_columns
->AddColumn(
175 views::GridLayout::LEADING
, views::GridLayout::CENTER
,
176 0, views::GridLayout::USE_PREF
, 0, 0);
178 layout
->StartRowWithPadding(0, extension_list_column_set_id
,
179 0, kHeadlineMessagePadding
);
180 views::Label
* extensions
= new views::Label();
181 extensions
->SetMultiLine(true);
182 extensions
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
184 std::vector
<base::string16
> extension_list
;
185 base::char16 bullet_point
= 0x2022;
187 std::vector
<base::string16
> suspicious
= controller_
->GetExtensionList();
189 for (; i
< suspicious
.size() && i
< kMaxExtensionsToShow
; ++i
) {
190 // Add each extension with bullet point.
191 extension_list
.push_back(
192 bullet_point
+ base::ASCIIToUTF16(" ") + suspicious
[i
]);
195 if (i
> kMaxExtensionsToShow
) {
196 base::string16 difference
= base::IntToString16(i
- kMaxExtensionsToShow
);
197 extension_list
.push_back(bullet_point
+ base::ASCIIToUTF16(" ") +
198 delegate
->GetOverflowText(difference
));
201 extensions
->SetText(JoinString(extension_list
, base::ASCIIToUTF16("\n")));
202 extensions
->SizeToFit(views::Widget::GetLocalizedContentsWidth(
203 IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS
));
204 layout
->AddView(extensions
);
207 base::string16 action_button
= delegate
->GetActionButtonLabel();
209 const int action_row_column_set_id
= 3;
210 views::ColumnSet
* bottom_columns
=
211 layout
->AddColumnSet(action_row_column_set_id
);
212 bottom_columns
->AddColumn(views::GridLayout::LEADING
,
213 views::GridLayout::CENTER
, 0, views::GridLayout::USE_PREF
, 0, 0);
214 bottom_columns
->AddPaddingColumn(1, 0);
215 bottom_columns
->AddColumn(views::GridLayout::TRAILING
,
216 views::GridLayout::CENTER
, 0, views::GridLayout::USE_PREF
, 0, 0);
217 if (!action_button
.empty()) {
218 bottom_columns
->AddColumn(views::GridLayout::TRAILING
,
219 views::GridLayout::CENTER
, 0, views::GridLayout::USE_PREF
, 0, 0);
221 layout
->StartRowWithPadding(0, action_row_column_set_id
,
222 0, kMessageBubblePadding
);
224 learn_more_
= new views::Link(delegate
->GetLearnMoreLabel());
225 learn_more_
->set_listener(this);
226 layout
->AddView(learn_more_
);
228 if (!action_button
.empty()) {
229 action_button_
= new views::LabelButton(this, action_button
.c_str());
230 action_button_
->SetStyle(views::Button::STYLE_BUTTON
);
231 layout
->AddView(action_button_
);
234 dismiss_button_
= new views::LabelButton(this,
235 delegate
->GetDismissButtonLabel());
236 dismiss_button_
->SetStyle(views::Button::STYLE_BUTTON
);
237 layout
->AddView(dismiss_button_
);
240 void ExtensionMessageBubbleView::ButtonPressed(views::Button
* sender
,
241 const ui::Event
& event
) {
242 if (sender
== action_button_
) {
243 action_taken_
= true;
244 action_callback_
.Run();
246 DCHECK_EQ(dismiss_button_
, sender
);
248 GetWidget()->Close();
251 void ExtensionMessageBubbleView::LinkClicked(views::Link
* source
,
253 DCHECK_EQ(learn_more_
, source
);
254 link_clicked_
= true;
255 link_callback_
.Run();
256 GetWidget()->Close();
259 void ExtensionMessageBubbleView::GetAccessibleState(
260 ui::AXViewState
* state
) {
261 state
->role
= ui::AX_ROLE_ALERT
;
264 void ExtensionMessageBubbleView::ViewHierarchyChanged(
265 const ViewHierarchyChangedDetails
& details
) {
266 if (details
.is_add
&& details
.child
== this)
267 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT
, true);
270 ////////////////////////////////////////////////////////////////////////////////
271 // ExtensionMessageBubbleFactory
273 ExtensionMessageBubbleFactory::ExtensionMessageBubbleFactory(
275 ToolbarView
* toolbar_view
)
277 toolbar_view_(toolbar_view
),
278 shown_suspicious_extensions_bubble_(false),
279 shown_startup_override_extensions_bubble_(false),
280 shown_proxy_override_extensions_bubble_(false),
281 shown_dev_mode_extensions_bubble_(false),
282 is_observing_(false),
285 anchor_view_(NULL
) {}
287 ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
288 MaybeStopObserving();
291 void ExtensionMessageBubbleFactory::MaybeShow(views::View
* anchor_view
) {
293 bool is_initial_check
= IsInitialProfileCheck(profile_
->GetOriginalProfile());
294 RecordProfileCheck(profile_
->GetOriginalProfile());
296 // The list of suspicious extensions takes priority over the dev mode bubble
297 // and the settings API bubble, since that needs to be shown as soon as we
298 // disable something. The settings API bubble is shown on first startup after
299 // an extension has changed the startup pages and it is acceptable if that
300 // waits until the next startup because of the suspicious extension bubble.
301 // The dev mode bubble is not time sensitive like the other two so we'll catch
302 // the dev mode extensions on the next startup/next window that opens. That
303 // way, we're not too spammy with the bubbles.
304 if (!shown_suspicious_extensions_bubble_
&&
305 MaybeShowSuspiciousExtensionsBubble(anchor_view
))
308 if (!shown_startup_override_extensions_bubble_
&&
310 MaybeShowStartupOverrideExtensionsBubble(anchor_view
))
313 if (!shown_proxy_override_extensions_bubble_
&&
314 MaybeShowProxyOverrideExtensionsBubble(anchor_view
))
317 if (!shown_dev_mode_extensions_bubble_
)
318 MaybeShowDevModeExtensionsBubble(anchor_view
);
322 bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
323 views::View
* anchor_view
) {
324 DCHECK(!shown_suspicious_extensions_bubble_
);
326 scoped_ptr
<SuspiciousExtensionBubbleController
> suspicious_extensions(
327 new SuspiciousExtensionBubbleController(profile_
));
328 if (!suspicious_extensions
->ShouldShow())
331 shown_suspicious_extensions_bubble_
= true;
332 SuspiciousExtensionBubbleController
* weak_controller
=
333 suspicious_extensions
.get();
334 ExtensionMessageBubbleView
* bubble_delegate
=
335 new ExtensionMessageBubbleView(anchor_view
,
336 views::BubbleBorder::TOP_RIGHT
,
337 suspicious_extensions
.Pass());
339 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
340 weak_controller
->Show(bubble_delegate
);
345 bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
346 views::View
* anchor_view
) {
350 DCHECK(!shown_startup_override_extensions_bubble_
);
352 const Extension
* extension
= GetExtensionOverridingStartupPages(profile_
);
356 scoped_ptr
<SettingsApiBubbleController
> settings_api_bubble(
357 new SettingsApiBubbleController(profile_
,
358 BUBBLE_TYPE_STARTUP_PAGES
));
359 if (!settings_api_bubble
->ShouldShow(extension
->id()))
362 shown_startup_override_extensions_bubble_
= true;
363 PrepareToHighlightExtensions(settings_api_bubble
.Pass(), anchor_view
);
368 bool ExtensionMessageBubbleFactory::MaybeShowProxyOverrideExtensionsBubble(
369 views::View
* anchor_view
) {
373 DCHECK(!shown_proxy_override_extensions_bubble_
);
375 const Extension
* extension
= GetExtensionOverridingProxy(profile_
);
379 scoped_ptr
<ProxyOverriddenBubbleController
> proxy_bubble(
380 new ProxyOverriddenBubbleController(profile_
));
381 if (!proxy_bubble
->ShouldShow(extension
->id()))
384 shown_proxy_override_extensions_bubble_
= true;
385 PrepareToHighlightExtensions(proxy_bubble
.Pass(), anchor_view
);
390 bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
391 views::View
* anchor_view
) {
392 DCHECK(!shown_dev_mode_extensions_bubble_
);
394 // Check the Developer Mode extensions.
395 scoped_ptr
<DevModeBubbleController
> dev_mode_extensions(
396 new DevModeBubbleController(profile_
));
398 // Return early if we have none to show.
399 if (!dev_mode_extensions
->ShouldShow())
402 shown_dev_mode_extensions_bubble_
= true;
403 PrepareToHighlightExtensions(dev_mode_extensions
.Pass(), anchor_view
);
407 void ExtensionMessageBubbleFactory::MaybeObserve() {
408 if (!is_observing_
) {
409 is_observing_
= true;
410 container_
->AddObserver(this);
414 void ExtensionMessageBubbleFactory::MaybeStopObserving() {
416 is_observing_
= false;
417 container_
->RemoveObserver(this);
421 void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile
* profile
) {
422 g_profiles_evaluated
.Get().insert(profile
);
425 bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile
* profile
) {
426 return g_profiles_evaluated
.Get().count(profile
) == 0;
429 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerAnimationEnded() {
430 MaybeStopObserving();
431 if (stage_
== STAGE_START
) {
432 HighlightExtensions();
433 } else if (stage_
== STAGE_HIGHLIGHTED
) {
434 ShowHighlightingBubble();
435 } else { // We shouldn't be observing if we've completed the process.
441 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
442 // If the container associated with the bubble is destroyed, abandon the
447 void ExtensionMessageBubbleFactory::PrepareToHighlightExtensions(
448 scoped_ptr
<ExtensionMessageBubbleController
> controller
,
449 views::View
* anchor_view
) {
450 // We should be in the start stage (i.e., should not have a pending attempt to
452 DCHECK_EQ(stage_
, STAGE_START
);
454 // Prepare to display and highlight the extensions before showing the bubble.
455 // Since this is an asynchronous process, set member variables for later use.
456 controller_
= controller
.Pass();
457 anchor_view_
= anchor_view
;
458 container_
= toolbar_view_
->browser_actions();
460 if (container_
->animating())
463 HighlightExtensions();
466 void ExtensionMessageBubbleFactory::HighlightExtensions() {
467 DCHECK_EQ(STAGE_START
, stage_
);
468 stage_
= STAGE_HIGHLIGHTED
;
470 const ExtensionIdList extension_list
= controller_
->GetExtensionIdList();
471 DCHECK(!extension_list
.empty());
472 ExtensionToolbarModel::Get(profile_
)->HighlightExtensions(extension_list
);
473 if (container_
->animating())
476 ShowHighlightingBubble();
479 void ExtensionMessageBubbleFactory::ShowHighlightingBubble() {
480 DCHECK_EQ(stage_
, STAGE_HIGHLIGHTED
);
481 stage_
= STAGE_COMPLETE
;
483 views::View
* reference_view
= NULL
;
484 if (container_
->num_toolbar_actions() > 0u)
485 reference_view
= container_
->GetToolbarActionViewAt(0);
486 if (reference_view
&& reference_view
->visible())
487 anchor_view_
= reference_view
;
489 ExtensionMessageBubbleController
* weak_controller
= controller_
.get();
490 ExtensionMessageBubbleView
* bubble_delegate
=
491 new ExtensionMessageBubbleView(
493 views::BubbleBorder::TOP_RIGHT
,
494 scoped_ptr
<ExtensionMessageBubbleController
>(
495 controller_
.release()));
496 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
497 weak_controller
->Show(bubble_delegate
);
502 void ExtensionMessageBubbleFactory::Finish() {
503 MaybeStopObserving();
509 } // namespace extensions