extension_message_bubble_view.cc revision 010d83a9304c5a91596085d917d248abff47903a
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_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"
33
34namespace {
35
36base::LazyInstance<std::set<Profile*> > g_profiles_evaluated =
37    LAZY_INSTANCE_INITIALIZER;
38
39// Layout constants.
40const int kExtensionListPadding = 10;
41const int kInsetBottomRight = 13;
42const int kInsetLeft = 14;
43const int kInsetTop = 9;
44const int kHeadlineMessagePadding = 4;
45const int kHeadlineRowPadding = 10;
46const int kMessageBubblePadding = 11;
47
48// How many extensions to show in the bubble (max).
49const size_t kMaxExtensionsToShow = 7;
50
51// How long to wait until showing the bubble (in seconds).
52const int kBubbleAppearanceWaitTime = 5;
53
54}  // namespace
55
56namespace extensions {
57
58ExtensionMessageBubbleView::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_close_on_esc(true);
73
74  // Compensate for built-in vertical padding in the anchor view's image.
75  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
76}
77
78void ExtensionMessageBubbleView::OnActionButtonClicked(
79    const base::Closure& callback) {
80  action_callback_ = callback;
81}
82
83void ExtensionMessageBubbleView::OnDismissButtonClicked(
84    const base::Closure& callback) {
85  dismiss_callback_ = callback;
86}
87
88void ExtensionMessageBubbleView::OnLinkClicked(
89    const base::Closure& callback) {
90  link_callback_ = callback;
91}
92
93void 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
101  // startup).
102  base::MessageLoop::current()->PostDelayedTask(
103      FROM_HERE,
104      base::Bind(&ExtensionMessageBubbleView::ShowBubble,
105                 weak_factory_.GetWeakPtr()),
106      base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime));
107}
108
109void 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();
114}
115
116////////////////////////////////////////////////////////////////////////////////
117// ExtensionMessageBubbleView - private.
118
119ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
120
121void ExtensionMessageBubbleView::ShowBubble() {
122  GetWidget()->Show();
123}
124
125void ExtensionMessageBubbleView::Init() {
126  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
127
128  views::GridLayout* layout = views::GridLayout::CreatePanel(this);
129  layout->SetInsets(kInsetTop, kInsetLeft,
130                    kInsetBottomRight, kInsetBottomRight);
131  SetLayoutManager(layout);
132
133  ExtensionMessageBubbleController::Delegate* delegate =
134      controller_->delegate();
135
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);
142
143  headline_ = new views::Label(delegate->GetTitle(),
144                               rb.GetFontList(ui::ResourceBundle::MediumFont));
145  layout->AddView(headline_);
146
147  layout->AddPaddingRow(0, kHeadlineRowPadding);
148
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);
155
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);
163
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);
172
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);
178
179    std::vector<base::string16> extension_list;
180    base::char16 bullet_point = 0x2022;
181
182    std::vector<base::string16> suspicious = controller_->GetExtensionList();
183    size_t i = 0;
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]);
188    }
189
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));
194    }
195
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);
200  }
201
202  base::string16 action_button = delegate->GetActionButtonLabel();
203
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);
215  }
216  layout->StartRowWithPadding(0, action_row_column_set_id,
217                              0, kMessageBubblePadding);
218
219  learn_more_ = new views::Link(delegate->GetLearnMoreLabel());
220  learn_more_->set_listener(this);
221  layout->AddView(learn_more_);
222
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_);
227  }
228
229  dismiss_button_ = new views::LabelButton(this,
230      delegate->GetDismissButtonLabel());
231  dismiss_button_->SetStyle(views::Button::STYLE_BUTTON);
232  layout->AddView(dismiss_button_);
233}
234
235void ExtensionMessageBubbleView::ButtonPressed(views::Button* sender,
236                                               const ui::Event& event) {
237  if (sender == action_button_) {
238    action_taken_ = true;
239    action_callback_.Run();
240  } else {
241    DCHECK_EQ(dismiss_button_, sender);
242  }
243  GetWidget()->Close();
244}
245
246void ExtensionMessageBubbleView::LinkClicked(views::Link* source,
247                                             int event_flags) {
248  DCHECK_EQ(learn_more_, source);
249  link_clicked_ = true;
250  link_callback_.Run();
251  GetWidget()->Close();
252}
253
254void ExtensionMessageBubbleView::GetAccessibleState(
255    ui::AXViewState* state) {
256  state->role = ui::AX_ROLE_ALERT;
257}
258
259void ExtensionMessageBubbleView::ViewHierarchyChanged(
260    const ViewHierarchyChangedDetails& details) {
261  if (details.is_add && details.child == this)
262    NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
263}
264
265////////////////////////////////////////////////////////////////////////////////
266// ExtensionMessageBubbleFactory
267
268ExtensionMessageBubbleFactory::ExtensionMessageBubbleFactory(
269    Profile* profile,
270    ToolbarView* toolbar_view)
271    : profile_(profile),
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),
277      stage_(STAGE_START),
278      container_(NULL),
279      anchor_view_(NULL) {}
280
281ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
282  MaybeStopObserving();
283}
284
285void ExtensionMessageBubbleFactory::MaybeShow(views::View* anchor_view) {
286#if defined(OS_WIN)
287  bool is_initial_check = IsInitialProfileCheck(profile_->GetOriginalProfile());
288  RecordProfileCheck(profile_->GetOriginalProfile());
289
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))
300    return;
301
302  if (!shown_startup_override_extensions_bubble_ &&
303      is_initial_check &&
304      MaybeShowStartupOverrideExtensionsBubble(anchor_view))
305    return;
306
307  if (!shown_dev_mode_extensions_bubble_)
308    MaybeShowDevModeExtensionsBubble(anchor_view);
309#endif  // OS_WIN
310}
311
312bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
313    views::View* anchor_view) {
314  DCHECK(!shown_suspicious_extensions_bubble_);
315
316  scoped_ptr<SuspiciousExtensionBubbleController> suspicious_extensions(
317      new SuspiciousExtensionBubbleController(profile_));
318  if (!suspicious_extensions->ShouldShow())
319    return false;
320
321  shown_suspicious_extensions_bubble_ = true;
322  SuspiciousExtensionBubbleController* weak_controller =
323      suspicious_extensions.get();
324  ExtensionMessageBubbleView* bubble_delegate = new ExtensionMessageBubbleView(
325      anchor_view,
326      views::BubbleBorder::TOP_RIGHT,
327      suspicious_extensions.PassAs<ExtensionMessageBubbleController>());
328
329  views::BubbleDelegateView::CreateBubble(bubble_delegate);
330  weak_controller->Show(bubble_delegate);
331
332  return true;
333}
334
335bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
336    views::View* anchor_view) {
337#if !defined(OS_WIN)
338  return false;
339#endif
340
341  DCHECK(!shown_startup_override_extensions_bubble_);
342
343  const Extension* extension = OverridesStartupPages(profile_, NULL);
344  if (!extension)
345    return false;
346
347  scoped_ptr<SettingsApiBubbleController> settings_api_bubble(
348      new SettingsApiBubbleController(profile_,
349                                      BUBBLE_TYPE_STARTUP_PAGES));
350  if (!settings_api_bubble->ShouldShow(extension->id()))
351    return false;
352
353  shown_startup_override_extensions_bubble_ = true;
354  SettingsApiBubbleController* weak_controller = settings_api_bubble.get();
355  ExtensionMessageBubbleView* bubble_delegate = new ExtensionMessageBubbleView(
356      anchor_view,
357      views::BubbleBorder::TOP_RIGHT,
358      settings_api_bubble.PassAs<ExtensionMessageBubbleController>());
359  views::BubbleDelegateView::CreateBubble(bubble_delegate);
360  weak_controller->Show(bubble_delegate);
361
362  return true;
363}
364
365bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
366    views::View* anchor_view) {
367  DCHECK(!shown_dev_mode_extensions_bubble_);
368
369  // Check the Developer Mode extensions.
370  scoped_ptr<DevModeBubbleController> dev_mode_extensions(
371      new DevModeBubbleController(profile_));
372
373  // Return early if we have none to show.
374  if (!dev_mode_extensions->ShouldShow())
375    return false;
376
377  shown_dev_mode_extensions_bubble_ = true;
378
379  // We should be in the start stage (i.e., should not have a pending attempt to
380  // show a bubble).
381  DCHECK_EQ(stage_, STAGE_START);
382
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();
389
390  if (container_->animating())
391    MaybeObserve();
392  else
393    HighlightDevModeExtensions();
394
395  return true;
396}
397
398void ExtensionMessageBubbleFactory::MaybeObserve() {
399  if (!is_observing_) {
400    is_observing_ = true;
401    container_->AddObserver(this);
402  }
403}
404
405void ExtensionMessageBubbleFactory::MaybeStopObserving() {
406  if (is_observing_) {
407    is_observing_ = false;
408    container_->RemoveObserver(this);
409  }
410}
411
412void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile* profile) {
413  g_profiles_evaluated.Get().insert(profile);
414}
415
416bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile* profile) {
417  return g_profiles_evaluated.Get().count(profile) == 0;
418}
419
420void ExtensionMessageBubbleFactory::OnBrowserActionsContainerAnimationEnded() {
421  MaybeStopObserving();
422  if (stage_ == STAGE_START) {
423    HighlightDevModeExtensions();
424  } else if (stage_ == STAGE_HIGHLIGHTED) {
425    ShowDevModeBubble();
426  } else {  // We shouldn't be observing if we've completed the process.
427    NOTREACHED();
428    Finish();
429  }
430}
431
432void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
433  // If the container associated with the bubble is destroyed, abandon the
434  // process.
435  Finish();
436}
437
438void ExtensionMessageBubbleFactory::HighlightDevModeExtensions() {
439  DCHECK_EQ(STAGE_START, stage_);
440  stage_ = STAGE_HIGHLIGHTED;
441
442  const ExtensionIdList extension_list = controller_->GetExtensionIdList();
443  DCHECK(!extension_list.empty());
444  ExtensionToolbarModel::Get(profile_)->HighlightExtensions(extension_list);
445  if (container_->animating())
446    MaybeObserve();
447  else
448    ShowDevModeBubble();
449}
450
451void ExtensionMessageBubbleFactory::ShowDevModeBubble() {
452  DCHECK_EQ(stage_, STAGE_HIGHLIGHTED);
453  stage_ = STAGE_COMPLETE;
454
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;
460
461  DevModeBubbleController* weak_controller = controller_.get();
462  ExtensionMessageBubbleView* bubble_delegate = new ExtensionMessageBubbleView(
463      anchor_view_,
464      views::BubbleBorder::TOP_RIGHT,
465      scoped_ptr<ExtensionMessageBubbleController>(controller_.release()));
466  views::BubbleDelegateView::CreateBubble(bubble_delegate);
467  weak_controller->Show(bubble_delegate);
468
469  Finish();
470}
471
472void ExtensionMessageBubbleFactory::Finish() {
473  MaybeStopObserving();
474  controller_.reset();
475  anchor_view_ = NULL;
476  container_ = NULL;
477}
478
479}  // namespace extensions
480