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_close_on_esc(true);
74 // Compensate for built-in vertical padding in the anchor view's image.
75 set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
78 void ExtensionMessageBubbleView::OnActionButtonClicked(
79 const base::Closure
& callback
) {
80 action_callback_
= callback
;
83 void ExtensionMessageBubbleView::OnDismissButtonClicked(
84 const base::Closure
& callback
) {
85 dismiss_callback_
= callback
;
88 void ExtensionMessageBubbleView::OnLinkClicked(
89 const base::Closure
& callback
) {
90 link_callback_
= callback
;
93 void 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
102 base::MessageLoop::current()->PostDelayedTask(
104 base::Bind(&ExtensionMessageBubbleView::ShowBubble
,
105 weak_factory_
.GetWeakPtr()),
106 base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime
));
109 void 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();
116 ////////////////////////////////////////////////////////////////////////////////
117 // ExtensionMessageBubbleView - private.
119 ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
121 void ExtensionMessageBubbleView::ShowBubble() {
125 void ExtensionMessageBubbleView::Init() {
126 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
128 views::GridLayout
* layout
= views::GridLayout::CreatePanel(this);
129 layout
->SetInsets(kInsetTop
, kInsetLeft
,
130 kInsetBottomRight
, kInsetBottomRight
);
131 SetLayoutManager(layout
);
133 ExtensionMessageBubbleController::Delegate
* delegate
=
134 controller_
->delegate();
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
);
143 headline_
= new views::Label(delegate
->GetTitle(),
144 rb
.GetFontList(ui::ResourceBundle::MediumFont
));
145 layout
->AddView(headline_
);
147 layout
->AddPaddingRow(0, kHeadlineRowPadding
);
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
);
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
);
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);
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
);
179 std::vector
<base::string16
> extension_list
;
180 base::char16 bullet_point
= 0x2022;
182 std::vector
<base::string16
> suspicious
= controller_
->GetExtensionList();
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
]);
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
));
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
);
202 base::string16 action_button
= delegate
->GetActionButtonLabel();
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);
216 layout
->StartRowWithPadding(0, action_row_column_set_id
,
217 0, kMessageBubblePadding
);
219 learn_more_
= new views::Link(delegate
->GetLearnMoreLabel());
220 learn_more_
->set_listener(this);
221 layout
->AddView(learn_more_
);
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_
);
229 dismiss_button_
= new views::LabelButton(this,
230 delegate
->GetDismissButtonLabel());
231 dismiss_button_
->SetStyle(views::Button::STYLE_BUTTON
);
232 layout
->AddView(dismiss_button_
);
235 void ExtensionMessageBubbleView::ButtonPressed(views::Button
* sender
,
236 const ui::Event
& event
) {
237 if (sender
== action_button_
) {
238 action_taken_
= true;
239 action_callback_
.Run();
241 DCHECK_EQ(dismiss_button_
, sender
);
243 GetWidget()->Close();
246 void ExtensionMessageBubbleView::LinkClicked(views::Link
* source
,
248 DCHECK_EQ(learn_more_
, source
);
249 link_clicked_
= true;
250 link_callback_
.Run();
251 GetWidget()->Close();
254 void ExtensionMessageBubbleView::GetAccessibleState(
255 ui::AXViewState
* state
) {
256 state
->role
= ui::AX_ROLE_ALERT
;
259 void ExtensionMessageBubbleView::ViewHierarchyChanged(
260 const ViewHierarchyChangedDetails
& details
) {
261 if (details
.is_add
&& details
.child
== this)
262 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT
, true);
265 ////////////////////////////////////////////////////////////////////////////////
266 // ExtensionMessageBubbleFactory
268 ExtensionMessageBubbleFactory::ExtensionMessageBubbleFactory(
270 ToolbarView
* toolbar_view
)
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),
279 anchor_view_(NULL
) {}
281 ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
282 MaybeStopObserving();
285 void ExtensionMessageBubbleFactory::MaybeShow(views::View
* anchor_view
) {
287 bool is_initial_check
= IsInitialProfileCheck(profile_
->GetOriginalProfile());
288 RecordProfileCheck(profile_
->GetOriginalProfile());
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
))
302 if (!shown_startup_override_extensions_bubble_
&&
304 MaybeShowStartupOverrideExtensionsBubble(anchor_view
))
307 if (!shown_dev_mode_extensions_bubble_
)
308 MaybeShowDevModeExtensionsBubble(anchor_view
);
312 bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
313 views::View
* anchor_view
) {
314 DCHECK(!shown_suspicious_extensions_bubble_
);
316 scoped_ptr
<SuspiciousExtensionBubbleController
> suspicious_extensions(
317 new SuspiciousExtensionBubbleController(profile_
));
318 if (!suspicious_extensions
->ShouldShow())
321 shown_suspicious_extensions_bubble_
= true;
322 SuspiciousExtensionBubbleController
* weak_controller
=
323 suspicious_extensions
.get();
324 ExtensionMessageBubbleView
* bubble_delegate
= new ExtensionMessageBubbleView(
326 views::BubbleBorder::TOP_RIGHT
,
327 suspicious_extensions
.PassAs
<ExtensionMessageBubbleController
>());
329 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
330 weak_controller
->Show(bubble_delegate
);
335 bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
336 views::View
* anchor_view
) {
341 DCHECK(!shown_startup_override_extensions_bubble_
);
343 const Extension
* extension
= OverridesStartupPages(profile_
, NULL
);
347 scoped_ptr
<SettingsApiBubbleController
> settings_api_bubble(
348 new SettingsApiBubbleController(profile_
,
349 BUBBLE_TYPE_STARTUP_PAGES
));
350 if (!settings_api_bubble
->ShouldShow(extension
->id()))
353 shown_startup_override_extensions_bubble_
= true;
354 SettingsApiBubbleController
* weak_controller
= settings_api_bubble
.get();
355 ExtensionMessageBubbleView
* bubble_delegate
= new ExtensionMessageBubbleView(
357 views::BubbleBorder::TOP_RIGHT
,
358 settings_api_bubble
.PassAs
<ExtensionMessageBubbleController
>());
359 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
360 weak_controller
->Show(bubble_delegate
);
365 bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
366 views::View
* anchor_view
) {
367 DCHECK(!shown_dev_mode_extensions_bubble_
);
369 // Check the Developer Mode extensions.
370 scoped_ptr
<DevModeBubbleController
> dev_mode_extensions(
371 new DevModeBubbleController(profile_
));
373 // Return early if we have none to show.
374 if (!dev_mode_extensions
->ShouldShow())
377 shown_dev_mode_extensions_bubble_
= true;
379 // We should be in the start stage (i.e., should not have a pending attempt to
381 DCHECK_EQ(stage_
, STAGE_START
);
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();
390 if (container_
->animating())
393 HighlightDevModeExtensions();
398 void ExtensionMessageBubbleFactory::MaybeObserve() {
399 if (!is_observing_
) {
400 is_observing_
= true;
401 container_
->AddObserver(this);
405 void ExtensionMessageBubbleFactory::MaybeStopObserving() {
407 is_observing_
= false;
408 container_
->RemoveObserver(this);
412 void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile
* profile
) {
413 g_profiles_evaluated
.Get().insert(profile
);
416 bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile
* profile
) {
417 return g_profiles_evaluated
.Get().count(profile
) == 0;
420 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerAnimationEnded() {
421 MaybeStopObserving();
422 if (stage_
== STAGE_START
) {
423 HighlightDevModeExtensions();
424 } else if (stage_
== STAGE_HIGHLIGHTED
) {
426 } else { // We shouldn't be observing if we've completed the process.
432 void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
433 // If the container associated with the bubble is destroyed, abandon the
438 void ExtensionMessageBubbleFactory::HighlightDevModeExtensions() {
439 DCHECK_EQ(STAGE_START
, stage_
);
440 stage_
= STAGE_HIGHLIGHTED
;
442 const ExtensionIdList extension_list
= controller_
->GetExtensionIdList();
443 DCHECK(!extension_list
.empty());
444 ExtensionToolbarModel::Get(profile_
)->HighlightExtensions(extension_list
);
445 if (container_
->animating())
451 void ExtensionMessageBubbleFactory::ShowDevModeBubble() {
452 DCHECK_EQ(stage_
, STAGE_HIGHLIGHTED
);
453 stage_
= STAGE_COMPLETE
;
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
;
461 DevModeBubbleController
* weak_controller
= controller_
.get();
462 ExtensionMessageBubbleView
* bubble_delegate
= new ExtensionMessageBubbleView(
464 views::BubbleBorder::TOP_RIGHT
,
465 scoped_ptr
<ExtensionMessageBubbleController
>(controller_
.release()));
466 views::BubbleDelegateView::CreateBubble(bubble_delegate
);
467 weak_controller
->Show(bubble_delegate
);
472 void ExtensionMessageBubbleFactory::Finish() {
473 MaybeStopObserving();
479 } // namespace extensions