extension_message_bubble_view.cc revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2013 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/views/extensions/extension_message_bubble_view.h"
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/strings/string_number_conversions.h"
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/strings/string_util.h"
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/strings/utf_string_conversions.h"
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/dev_mode_bubble_controller.h"
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/extension_action_manager.h"
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/extension_message_bubble_controller.h"
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/extension_service.h"
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/settings_api_bubble_controller.h"
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/settings_api_helpers.h"
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/suspicious_extension_bubble_controller.h"
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/profiles/profile.h"
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/views/frame/browser_view.h"
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h"
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "extensions/browser/extension_prefs.h"
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "extensions/browser/extension_system.h"
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "grit/locale_settings.h"
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/accessibility/ax_view_state.h"
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/base/resource/resource_bundle.h"
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/controls/button/label_button.h"
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/controls/label.h"
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/controls/link.h"
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/layout/grid_layout.h"
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/view.h"
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "ui/views/widget/widget.h"
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)namespace {
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)base::LazyInstance<std::set<Profile*> > g_profiles_evaluated =
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    LAZY_INSTANCE_INITIALIZER;
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Layout constants.
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kExtensionListPadding = 10;
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kInsetBottomRight = 13;
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kInsetLeft = 14;
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kInsetTop = 9;
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kHeadlineMessagePadding = 4;
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kHeadlineRowPadding = 10;
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kMessageBubblePadding = 11;
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// How many extensions to show in the bubble (max).
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const size_t kMaxExtensionsToShow = 7;
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// How long to wait until showing the bubble (in seconds).
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const int kBubbleAppearanceWaitTime = 5;
53424c4d7b64af9d0d8fd9624f381f469654d5e3d2Torne (Richard Coles)
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}  // namespace
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)namespace 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_move_with_anchor(true);
73  set_close_on_esc(true);
74
75  // Compensate for built-in vertical padding in the anchor view's image.
76  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
77}
78
79void ExtensionMessageBubbleView::OnActionButtonClicked(
80    const base::Closure& callback) {
81  action_callback_ = callback;
82}
83
84void ExtensionMessageBubbleView::OnDismissButtonClicked(
85    const base::Closure& callback) {
86  dismiss_callback_ = callback;
87}
88
89void ExtensionMessageBubbleView::OnLinkClicked(
90    const base::Closure& callback) {
91  link_callback_ = callback;
92}
93
94void 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));
108}
109
110void 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();
115}
116
117////////////////////////////////////////////////////////////////////////////////
118// ExtensionMessageBubbleView - private.
119
120ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
121
122void ExtensionMessageBubbleView::ShowBubble() {
123  GetWidget()->Show();
124}
125
126void ExtensionMessageBubbleView::Init() {
127  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
128
129  views::GridLayout* layout = views::GridLayout::CreatePanel(this);
130  layout->SetInsets(kInsetTop, kInsetLeft,
131                    kInsetBottomRight, kInsetBottomRight);
132  SetLayoutManager(layout);
133
134  ExtensionMessageBubbleController::Delegate* delegate =
135      controller_->delegate();
136
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);
143
144  headline_ = new views::Label(delegate->GetTitle(),
145                               rb.GetFontList(ui::ResourceBundle::MediumFont));
146  layout->AddView(headline_);
147
148  layout->AddPaddingRow(0, kHeadlineRowPadding);
149
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);
156
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);
164
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);
173
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);
179
180    std::vector<base::string16> extension_list;
181    base::char16 bullet_point = 0x2022;
182
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]);
189    }
190
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));
195    }
196
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);
201  }
202
203  base::string16 action_button = delegate->GetActionButtonLabel();
204
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);
216  }
217  layout->StartRowWithPadding(0, action_row_column_set_id,
218                              0, kMessageBubblePadding);
219
220  learn_more_ = new views::Link(delegate->GetLearnMoreLabel());
221  learn_more_->set_listener(this);
222  layout->AddView(learn_more_);
223
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_);
228  }
229
230  dismiss_button_ = new views::LabelButton(this,
231      delegate->GetDismissButtonLabel());
232  dismiss_button_->SetStyle(views::Button::STYLE_BUTTON);
233  layout->AddView(dismiss_button_);
234}
235
236void 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);
243  }
244  GetWidget()->Close();
245}
246
247void 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();
253}
254
255void ExtensionMessageBubbleView::GetAccessibleState(
256    ui::AXViewState* state) {
257  state->role = ui::AX_ROLE_ALERT;
258}
259
260void ExtensionMessageBubbleView::ViewHierarchyChanged(
261    const ViewHierarchyChangedDetails& details) {
262  if (details.is_add && details.child == this)
263    NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
264}
265
266////////////////////////////////////////////////////////////////////////////////
267// ExtensionMessageBubbleFactory
268
269ExtensionMessageBubbleFactory::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) {}
281
282ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
283  MaybeStopObserving();
284}
285
286void 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;
299  }
300
301  if (!shown_startup_override_extensions_bubble_ &&
302      IsInitialProfileCheck(profile_->GetOriginalProfile()) &&
303      MaybeShowStartupOverrideExtensionsBubble(anchor_view))
304    return;
305
306  if (!shown_dev_mode_extensions_bubble_)
307    MaybeShowDevModeExtensionsBubble(anchor_view);
308
309  RecordProfileCheck(profile_->GetOriginalProfile());
310#endif  // OS_WIN
311}
312
313bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
314    views::View* anchor_view) {
315  DCHECK(!shown_suspicious_extensions_bubble_);
316
317  scoped_ptr<SuspiciousExtensionBubbleController> suspicious_extensions(
318      new SuspiciousExtensionBubbleController(profile_));
319  if (!suspicious_extensions->ShouldShow())
320    return false;
321
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>());
329
330  views::BubbleDelegateView::CreateBubble(bubble_delegate);
331  weak_controller->Show(bubble_delegate);
332
333  return true;
334}
335
336bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
337    views::View* anchor_view) {
338#if !defined(OS_WIN)
339  return false;
340#endif
341
342  DCHECK(!shown_startup_override_extensions_bubble_);
343
344  const Extension* extension = OverridesStartupPages(profile_, NULL);
345  if (!extension)
346    return false;
347
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;
353
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);
362
363  return true;
364}
365
366bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
367    views::View* anchor_view) {
368  DCHECK(!shown_dev_mode_extensions_bubble_);
369
370  // Check the Developer Mode extensions.
371  scoped_ptr<DevModeBubbleController> dev_mode_extensions(
372      new DevModeBubbleController(profile_));
373
374  // Return early if we have none to show.
375  if (!dev_mode_extensions->ShouldShow())
376    return false;
377
378  shown_dev_mode_extensions_bubble_ = true;
379
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);
383
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();
390
391  if (container_->animating())
392    MaybeObserve();
393  else
394    HighlightDevModeExtensions();
395
396  return true;
397}
398
399void ExtensionMessageBubbleFactory::MaybeObserve() {
400  if (!is_observing_) {
401    is_observing_ = true;
402    container_->AddObserver(this);
403  }
404}
405
406void ExtensionMessageBubbleFactory::MaybeStopObserving() {
407  if (is_observing_) {
408    is_observing_ = false;
409    container_->RemoveObserver(this);
410  }
411}
412
413void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile* profile) {
414  g_profiles_evaluated.Get().insert(profile);
415}
416
417bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile* profile) {
418  return g_profiles_evaluated.Get().count(profile) == 0;
419}
420
421void 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();
430  }
431}
432
433void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
434  // If the container associated with the bubble is destroyed, abandon the
435  // process.
436  Finish();
437}
438
439void ExtensionMessageBubbleFactory::HighlightDevModeExtensions() {
440  DCHECK_EQ(STAGE_START, stage_);
441  stage_ = STAGE_HIGHLIGHTED;
442
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();
450}
451
452void ExtensionMessageBubbleFactory::ShowDevModeBubble() {
453  DCHECK_EQ(stage_, STAGE_HIGHLIGHTED);
454  stage_ = STAGE_COMPLETE;
455
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;
461
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);
469
470  Finish();
471}
472
473void ExtensionMessageBubbleFactory::Finish() {
474  MaybeStopObserving();
475  controller_.reset();
476  anchor_view_ = NULL;
477  container_ = NULL;
478}
479
480}  // namespace extensions
481