1// Copyright (c) 2011 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/first_run_bubble.h"
6
7#include "base/utf_string_conversions.h"
8#include "chrome/browser/first_run/first_run.h"
9#include "chrome/browser/metrics/user_metrics.h"
10#include "chrome/browser/search_engines/util.h"
11#include "chrome/browser/ui/browser.h"
12#include "chrome/browser/ui/browser_list.h"
13#include "chrome/browser/ui/browser_window.h"
14#include "grit/chromium_strings.h"
15#include "grit/generated_resources.h"
16#include "grit/locale_settings.h"
17#include "grit/theme_resources.h"
18#include "ui/base/l10n/l10n_font_util.h"
19#include "ui/base/l10n/l10n_util.h"
20#include "ui/base/resource/resource_bundle.h"
21#include "views/controls/button/image_button.h"
22#include "views/controls/button/native_button.h"
23#include "views/controls/label.h"
24#include "views/events/event.h"
25#include "views/focus/focus_manager.h"
26#include "views/layout/layout_constants.h"
27#include "views/widget/widget_win.h"
28#include "views/window/window.h"
29
30namespace {
31
32// How much extra padding to put around our content over what the Bubble
33// provides.
34const int kBubblePadding = 4;
35
36// How much extra padding to put around our content over what the Bubble
37// provides in alternative OEM bubble.
38const int kOEMBubblePadding = 4;
39
40// Padding between parts of strings on the same line (for instance,
41// "New!" and "Search from the address bar!"
42const int kStringSeparationPadding = 2;
43
44// Margin around close button.
45const int kMarginRightOfCloseButton = 7;
46
47}  // namespace
48
49// Base class for implementations of the client view which appears inside the
50// first run bubble. It is a dialog-ish view, but is not a true dialog.
51class FirstRunBubbleViewBase : public views::View,
52                               public views::ButtonListener,
53                               public views::FocusChangeListener {
54 public:
55  // Called by FirstRunBubble::Show to request focus for the proper button
56  // in the FirstRunBubbleView when it is shown.
57  virtual void BubbleShown() = 0;
58};
59
60// FirstRunBubbleView ---------------------------------------------------------
61
62class FirstRunBubbleView : public FirstRunBubbleViewBase {
63 public:
64  FirstRunBubbleView(FirstRunBubble* bubble_window, Profile* profile);
65
66 private:
67  virtual ~FirstRunBubbleView() {}
68
69  // FirstRunBubbleViewBase:
70  virtual void BubbleShown();
71
72  // Overridden from View:
73  virtual void ButtonPressed(views::Button* sender, const views::Event& event);
74  virtual void Layout();
75  virtual gfx::Size GetPreferredSize();
76
77  // FocusChangeListener:
78  virtual void FocusWillChange(View* focused_before, View* focused_now);
79
80  FirstRunBubble* bubble_window_;
81  views::Label* label1_;
82  views::Label* label2_;
83  views::Label* label3_;
84  views::NativeButton* change_button_;
85  views::NativeButton* keep_button_;
86  Profile* profile_;
87
88  DISALLOW_COPY_AND_ASSIGN(FirstRunBubbleView);
89};
90
91FirstRunBubbleView::FirstRunBubbleView(FirstRunBubble* bubble_window,
92                                       Profile* profile)
93    : bubble_window_(bubble_window),
94      label1_(NULL),
95      label2_(NULL),
96      label3_(NULL),
97      keep_button_(NULL),
98      change_button_(NULL),
99      profile_(profile) {
100  const gfx::Font& font =
101      ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont);
102
103  label1_ = new views::Label(
104      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_TITLE)));
105  label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD));
106  label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
107  AddChildView(label1_);
108
109  gfx::Size ps = GetPreferredSize();
110
111  label2_ = new views::Label(
112      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_SUBTEXT)));
113  label2_->SetMultiLine(true);
114  label2_->SetFont(font);
115  label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
116  label2_->SizeToFit(ps.width() - kBubblePadding * 2);
117  AddChildView(label2_);
118
119  std::wstring question_str = UTF16ToWide(l10n_util::GetStringFUTF16(
120      IDS_FR_BUBBLE_QUESTION,
121      GetDefaultSearchEngineName(profile)));
122  label3_ = new views::Label(question_str);
123  label3_->SetMultiLine(true);
124  label3_->SetFont(font);
125  label3_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
126  label3_->SizeToFit(ps.width() - kBubblePadding * 2);
127  AddChildView(label3_);
128
129  std::wstring keep_str = UTF16ToWide(l10n_util::GetStringFUTF16(
130      IDS_FR_BUBBLE_OK,
131      GetDefaultSearchEngineName(profile)));
132  keep_button_ = new views::NativeButton(this, keep_str);
133  keep_button_->SetIsDefault(true);
134  AddChildView(keep_button_);
135
136  std::wstring change_str =
137      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_CHANGE));
138  change_button_ = new views::NativeButton(this, change_str);
139  AddChildView(change_button_);
140}
141
142void FirstRunBubbleView::BubbleShown() {
143  keep_button_->RequestFocus();
144}
145
146void FirstRunBubbleView::ButtonPressed(views::Button* sender,
147                                       const views::Event& event) {
148  UserMetrics::RecordAction(UserMetricsAction("FirstRunBubbleView_Clicked"),
149                            profile_);
150  bubble_window_->set_fade_away_on_close(true);
151  bubble_window_->Close();
152  if (change_button_ == sender) {
153    UserMetrics::RecordAction(
154                    UserMetricsAction("FirstRunBubbleView_ChangeButton"),
155                    profile_);
156
157    Browser* browser = BrowserList::GetLastActive();
158    if (browser) {
159      browser->OpenSearchEngineOptionsDialog();
160    }
161  }
162}
163
164void FirstRunBubbleView::Layout() {
165  gfx::Size canvas = GetPreferredSize();
166
167  // The multiline business that follows is dirty hacks to get around
168  // bug 1325257.
169  label1_->SetMultiLine(false);
170  gfx::Size pref_size = label1_->GetPreferredSize();
171  label1_->SetMultiLine(true);
172  label1_->SizeToFit(canvas.width() - kBubblePadding * 2);
173  label1_->SetBounds(kBubblePadding, kBubblePadding,
174                     canvas.width() - kBubblePadding * 2,
175                     pref_size.height());
176
177  int next_v_space = label1_->y() + pref_size.height() +
178                     views::kRelatedControlSmallVerticalSpacing;
179
180  pref_size = label2_->GetPreferredSize();
181  label2_->SetBounds(kBubblePadding, next_v_space,
182                     canvas.width() - kBubblePadding * 2,
183                     pref_size.height());
184
185  next_v_space = label2_->y() + label2_->height() +
186                 views::kPanelSubVerticalSpacing;
187
188  pref_size = label3_->GetPreferredSize();
189  label3_->SetBounds(kBubblePadding, next_v_space,
190                     canvas.width() - kBubblePadding * 2,
191                     pref_size.height());
192
193  pref_size = change_button_->GetPreferredSize();
194  change_button_->SetBounds(
195      canvas.width() - pref_size.width() - kBubblePadding,
196      canvas.height() - pref_size.height() - views::kButtonVEdgeMargin,
197      pref_size.width(), pref_size.height());
198
199  pref_size = keep_button_->GetPreferredSize();
200  keep_button_->SetBounds(change_button_->x() - pref_size.width() -
201                          views::kRelatedButtonHSpacing, change_button_->y(),
202                          pref_size.width(), pref_size.height());
203}
204
205gfx::Size FirstRunBubbleView::GetPreferredSize() {
206  return gfx::Size(views::Window::GetLocalizedContentsSize(
207      IDS_FIRSTRUNBUBBLE_DIALOG_WIDTH_CHARS,
208      IDS_FIRSTRUNBUBBLE_DIALOG_HEIGHT_LINES));
209}
210
211void FirstRunBubbleView::FocusWillChange(View* focused_before,
212                                         View* focused_now) {
213  if (focused_before &&
214      (focused_before->GetClassName() == views::NativeButton::kViewClassName)) {
215    views::NativeButton* before =
216        static_cast<views::NativeButton*>(focused_before);
217    before->SetIsDefault(false);
218  }
219  if (focused_now &&
220      (focused_now->GetClassName() == views::NativeButton::kViewClassName)) {
221    views::NativeButton* after = static_cast<views::NativeButton*>(focused_now);
222    after->SetIsDefault(true);
223  }
224}
225
226// FirstRunOEMBubbleView ------------------------------------------------------
227
228class FirstRunOEMBubbleView : public FirstRunBubbleViewBase {
229 public:
230  FirstRunOEMBubbleView(FirstRunBubble* bubble_window, Profile* profile);
231
232 private:
233  virtual ~FirstRunOEMBubbleView() { }
234
235  // FirstRunBubbleViewBase:
236  virtual void BubbleShown();
237
238  // Overridden from View:
239  virtual void ButtonPressed(views::Button* sender, const views::Event& event);
240  virtual void Layout();
241  virtual gfx::Size GetPreferredSize();
242
243  // FocusChangeListener:
244  virtual void FocusWillChange(View* focused_before, View* focused_now);
245
246  FirstRunBubble* bubble_window_;
247  views::Label* label1_;
248  views::Label* label2_;
249  views::Label* label3_;
250  views::ImageButton* close_button_;
251  Profile* profile_;
252
253  DISALLOW_COPY_AND_ASSIGN(FirstRunOEMBubbleView);
254};
255
256FirstRunOEMBubbleView::FirstRunOEMBubbleView(FirstRunBubble* bubble_window,
257                                             Profile* profile)
258    : bubble_window_(bubble_window),
259      label1_(NULL),
260      label2_(NULL),
261      label3_(NULL),
262      close_button_(NULL),
263      profile_(profile) {
264  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
265  const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont);
266
267  label1_ = new views::Label(
268      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_TITLE_1)));
269  label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD));
270  label1_->SetColor(SK_ColorRED);
271  label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
272  AddChildView(label1_);
273
274  label2_ = new views::Label(
275      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_TITLE_2)));
276  label2_->SetFont(font.DeriveFont(3, gfx::Font::BOLD));
277  label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
278  AddChildView(label2_);
279
280  gfx::Size ps = GetPreferredSize();
281
282  label3_ = new views::Label(
283      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_SUBTEXT)));
284  label3_->SetMultiLine(true);
285  label3_->SetFont(font);
286  label3_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
287  label3_->SizeToFit(ps.width() - kOEMBubblePadding * 2);
288  AddChildView(label3_);
289
290  close_button_ = new views::ImageButton(this);
291  close_button_->SetImage(views::CustomButton::BS_NORMAL,
292                          rb.GetBitmapNamed(IDR_CLOSE_BAR));
293  close_button_->SetImage(views::CustomButton::BS_HOT,
294                          rb.GetBitmapNamed(IDR_CLOSE_BAR_H));
295  close_button_->SetImage(views::CustomButton::BS_PUSHED,
296                          rb.GetBitmapNamed(IDR_CLOSE_BAR_P));
297
298  AddChildView(close_button_);
299}
300
301void FirstRunOEMBubbleView::BubbleShown() {
302  RequestFocus();
303  // No button in oem_bubble to request focus.
304}
305
306void FirstRunOEMBubbleView::ButtonPressed(views::Button* sender,
307                                          const views::Event& event) {
308  UserMetrics::RecordAction(UserMetricsAction("FirstRunOEMBubbleView_Clicked"),
309                            profile_);
310  bubble_window_->set_fade_away_on_close(true);
311  bubble_window_->Close();
312}
313
314void FirstRunOEMBubbleView::Layout() {
315  gfx::Size canvas = GetPreferredSize();
316
317  // First, draw the close button on the far right.
318  gfx::Size sz = close_button_->GetPreferredSize();
319  close_button_->SetBounds(
320      canvas.width() - sz.width() - kMarginRightOfCloseButton,
321      kOEMBubblePadding, sz.width(), sz.height());
322
323  gfx::Size pref_size = label1_->GetPreferredSize();
324  label1_->SetBounds(kOEMBubblePadding, kOEMBubblePadding,
325                     pref_size.width() + kOEMBubblePadding * 2,
326                     pref_size.height());
327
328  pref_size = label2_->GetPreferredSize();
329  label2_->SetBounds(
330      kOEMBubblePadding * 2 + label1_->GetPreferredSize().width(),
331      kOEMBubblePadding, canvas.width() - kOEMBubblePadding * 2,
332      pref_size.height());
333
334  int next_v_space =
335      label1_->y() + pref_size.height() +
336          views::kRelatedControlSmallVerticalSpacing;
337
338  pref_size = label3_->GetPreferredSize();
339  label3_->SetBounds(kOEMBubblePadding, next_v_space,
340                     canvas.width() - kOEMBubblePadding * 2,
341                     pref_size.height());
342}
343
344gfx::Size FirstRunOEMBubbleView::GetPreferredSize() {
345  // Calculate width based on font and text.
346  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
347  const gfx::Font& font = rb.GetFont(
348      ResourceBundle::MediumFont).DeriveFont(3, gfx::Font::BOLD);
349  gfx::Size size = gfx::Size(
350      ui::GetLocalizedContentsWidthForFont(
351          IDS_FIRSTRUNOEMBUBBLE_DIALOG_WIDTH_CHARS, font),
352      ui::GetLocalizedContentsHeightForFont(
353          IDS_FIRSTRUNOEMBUBBLE_DIALOG_HEIGHT_LINES, font));
354
355  // WARNING: HACK. Vista and XP calculate font size differently; this means
356  // that a dialog box correctly proportioned for XP will appear too large in
357  // Vista. The correct thing to do is to change font size calculations in
358  // XP or Vista so that the length of a string is calculated properly. For
359  // now, we force Vista to show a correctly-sized box by taking account of
360  // the difference in font size calculation. The coefficient should not be
361  // stored in a variable because it's a hack and should go away.
362  if (views::WidgetWin::IsAeroGlassEnabled()) {
363    size.set_width(static_cast<int>(size.width() * 0.85));
364    size.set_height(static_cast<int>(size.height() * 0.85));
365  }
366  return size;
367}
368
369void FirstRunOEMBubbleView::FocusWillChange(View* focused_before,
370                                            View* focused_now) {
371  // No buttons in oem_bubble to register focus changes.
372}
373
374// FirstRunMinimalBubbleView --------------------------------------------------
375// TODO(mirandac): combine FRBubbles more elegantly.  http://crbug.com/41353
376
377class FirstRunMinimalBubbleView : public FirstRunBubbleViewBase {
378 public:
379  FirstRunMinimalBubbleView(FirstRunBubble* bubble_window, Profile* profile);
380
381 private:
382  virtual ~FirstRunMinimalBubbleView() { }
383
384  // FirstRunBubbleViewBase:
385  virtual void BubbleShown();
386
387  // Overridden from View:
388  virtual void ButtonPressed(views::Button* sender,
389                             const views::Event& event) { }
390  virtual void Layout();
391  virtual gfx::Size GetPreferredSize();
392
393  // FocusChangeListener:
394  virtual void FocusWillChange(View* focused_before, View* focused_now);
395
396  FirstRunBubble* bubble_window_;
397  Profile* profile_;
398  views::Label* label1_;
399  views::Label* label2_;
400
401  DISALLOW_COPY_AND_ASSIGN(FirstRunMinimalBubbleView);
402};
403
404FirstRunMinimalBubbleView::FirstRunMinimalBubbleView(
405    FirstRunBubble* bubble_window,
406    Profile* profile)
407    : bubble_window_(bubble_window),
408      profile_(profile),
409      label1_(NULL),
410      label2_(NULL) {
411  const gfx::Font& font =
412      ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont);
413
414  label1_ = new views::Label(UTF16ToWide(l10n_util::GetStringFUTF16(
415      IDS_FR_SE_BUBBLE_TITLE,
416      GetDefaultSearchEngineName(profile_))));
417  label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD));
418  label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
419  AddChildView(label1_);
420
421  gfx::Size ps = GetPreferredSize();
422
423  label2_ = new views::Label(
424      UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_SUBTEXT)));
425  label2_->SetMultiLine(true);
426  label2_->SetFont(font);
427  label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
428  label2_->SizeToFit(ps.width() - kBubblePadding * 2);
429  AddChildView(label2_);
430}
431
432void FirstRunMinimalBubbleView::BubbleShown() {
433  RequestFocus();
434}
435
436void FirstRunMinimalBubbleView::Layout() {
437  gfx::Size canvas = GetPreferredSize();
438
439  // See comments in FirstRunOEMBubbleView::Layout explaining this hack.
440  label1_->SetMultiLine(false);
441  gfx::Size pref_size = label1_->GetPreferredSize();
442  label1_->SetMultiLine(true);
443  label1_->SizeToFit(canvas.width() - kBubblePadding * 2);
444  label1_->SetBounds(kBubblePadding, kBubblePadding,
445                     canvas.width() - kBubblePadding * 2,
446                     pref_size.height());
447
448  int next_v_space = label1_->y() + pref_size.height() +
449                     views::kRelatedControlSmallVerticalSpacing;
450
451  pref_size = label2_->GetPreferredSize();
452  label2_->SetBounds(kBubblePadding, next_v_space,
453                     canvas.width() - kBubblePadding * 2,
454                     pref_size.height());
455}
456
457gfx::Size FirstRunMinimalBubbleView::GetPreferredSize() {
458  return gfx::Size(views::Window::GetLocalizedContentsSize(
459      IDS_FIRSTRUN_MINIMAL_BUBBLE_DIALOG_WIDTH_CHARS,
460      IDS_FIRSTRUN_MINIMAL_BUBBLE_DIALOG_HEIGHT_LINES));
461}
462
463void FirstRunMinimalBubbleView::FocusWillChange(View* focused_before,
464                                                View* focused_now) {
465  // No buttons in minimal bubble to register focus changes.
466}
467
468
469// FirstRunBubble -------------------------------------------------------------
470
471// static
472FirstRunBubble* FirstRunBubble::Show(Profile* profile,
473                                     views::Widget* parent,
474                                     const gfx::Rect& position_relative_to,
475                                     BubbleBorder::ArrowLocation arrow_location,
476                                     FirstRun::BubbleType bubble_type) {
477  FirstRunBubble* bubble = new FirstRunBubble();
478  FirstRunBubbleViewBase* view = NULL;
479
480  switch (bubble_type) {
481    case FirstRun::OEM_BUBBLE:
482      view = new FirstRunOEMBubbleView(bubble, profile);
483      break;
484    case FirstRun::LARGE_BUBBLE:
485      view = new FirstRunBubbleView(bubble, profile);
486      break;
487    case FirstRun::MINIMAL_BUBBLE:
488      view = new FirstRunMinimalBubbleView(bubble, profile);
489      break;
490    default:
491      NOTREACHED();
492  }
493  bubble->set_view(view);
494  bubble->InitBubble(
495      parent, position_relative_to, arrow_location, view, bubble);
496  bubble->GetFocusManager()->AddFocusChangeListener(view);
497  view->BubbleShown();
498  return bubble;
499}
500
501FirstRunBubble::FirstRunBubble()
502    : has_been_activated_(false),
503      ALLOW_THIS_IN_INITIALIZER_LIST(enable_window_method_factory_(this)),
504      view_(NULL) {
505}
506
507FirstRunBubble::~FirstRunBubble() {
508  enable_window_method_factory_.RevokeAll();
509  GetFocusManager()->RemoveFocusChangeListener(view_);
510}
511
512void FirstRunBubble::EnableParent() {
513  ::EnableWindow(GetParent(), true);
514  // The EnableWindow() call above causes the parent to become active, which
515  // resets the flag set by Bubble's call to DisableInactiveRendering(), so we
516  // have to call it again before activating the bubble to prevent the parent
517  // window from rendering inactive.
518  // TODO(beng): this only works in custom-frame mode, not glass-frame mode.
519  views::NativeWidget* parent =
520      views::NativeWidget::GetNativeWidgetForNativeView(GetParent());
521  if (parent)
522    parent->GetWidget()->GetWindow()->DisableInactiveRendering();
523  // Reactivate the FirstRunBubble so it responds to OnActivate messages.
524  SetWindowPos(GetParent(), 0, 0, 0, 0,
525               SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_SHOWWINDOW);
526}
527
528void FirstRunBubble::OnActivate(UINT action, BOOL minimized, HWND window) {
529  // Keep the bubble around for kLingerTime milliseconds, to prevent accidental
530  // closure.
531  const int kLingerTime = 3000;
532
533  // We might get re-enabled right before we are closed (sequence is: we get
534  // deactivated, we call close, before we are actually closed we get
535  // reactivated). Don't do the disabling of the parent in such cases.
536  if (action == WA_ACTIVE && !has_been_activated_) {
537    has_been_activated_ = true;
538
539    ::EnableWindow(GetParent(), false);
540
541    MessageLoop::current()->PostDelayedTask(FROM_HERE,
542        enable_window_method_factory_.NewRunnableMethod(
543            &FirstRunBubble::EnableParent),
544        kLingerTime);
545    return;
546  }
547
548  // Keep window from automatically closing until kLingerTime has passed.
549  if (::IsWindowEnabled(GetParent()))
550    Bubble::OnActivate(action, minimized, window);
551}
552
553void FirstRunBubble::BubbleClosing(Bubble* bubble, bool closed_by_escape) {
554  // Make sure our parent window is re-enabled.
555  if (!IsWindowEnabled(GetParent()))
556    ::EnableWindow(GetParent(), true);
557}
558