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