// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/first_run_bubble.h" #include "base/utf_string_conversions.h" #include "chrome/browser/first_run/first_run.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/search_engines/util.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_window.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" #include "grit/locale_settings.h" #include "grit/theme_resources.h" #include "ui/base/l10n/l10n_font_util.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "views/controls/button/image_button.h" #include "views/controls/button/native_button.h" #include "views/controls/label.h" #include "views/events/event.h" #include "views/focus/focus_manager.h" #include "views/layout/layout_constants.h" #include "views/widget/widget_win.h" #include "views/window/window.h" namespace { // How much extra padding to put around our content over what the Bubble // provides. const int kBubblePadding = 4; // How much extra padding to put around our content over what the Bubble // provides in alternative OEM bubble. const int kOEMBubblePadding = 4; // Padding between parts of strings on the same line (for instance, // "New!" and "Search from the address bar!" const int kStringSeparationPadding = 2; // Margin around close button. const int kMarginRightOfCloseButton = 7; } // namespace // Base class for implementations of the client view which appears inside the // first run bubble. It is a dialog-ish view, but is not a true dialog. class FirstRunBubbleViewBase : public views::View, public views::ButtonListener, public views::FocusChangeListener { public: // Called by FirstRunBubble::Show to request focus for the proper button // in the FirstRunBubbleView when it is shown. virtual void BubbleShown() = 0; }; // FirstRunBubbleView --------------------------------------------------------- class FirstRunBubbleView : public FirstRunBubbleViewBase { public: FirstRunBubbleView(FirstRunBubble* bubble_window, Profile* profile); private: virtual ~FirstRunBubbleView() {} // FirstRunBubbleViewBase: virtual void BubbleShown(); // Overridden from View: virtual void ButtonPressed(views::Button* sender, const views::Event& event); virtual void Layout(); virtual gfx::Size GetPreferredSize(); // FocusChangeListener: virtual void FocusWillChange(View* focused_before, View* focused_now); FirstRunBubble* bubble_window_; views::Label* label1_; views::Label* label2_; views::Label* label3_; views::NativeButton* change_button_; views::NativeButton* keep_button_; Profile* profile_; DISALLOW_COPY_AND_ASSIGN(FirstRunBubbleView); }; FirstRunBubbleView::FirstRunBubbleView(FirstRunBubble* bubble_window, Profile* profile) : bubble_window_(bubble_window), label1_(NULL), label2_(NULL), label3_(NULL), keep_button_(NULL), change_button_(NULL), profile_(profile) { const gfx::Font& font = ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont); label1_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_TITLE))); label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD)); label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(label1_); gfx::Size ps = GetPreferredSize(); label2_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_SUBTEXT))); label2_->SetMultiLine(true); label2_->SetFont(font); label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); label2_->SizeToFit(ps.width() - kBubblePadding * 2); AddChildView(label2_); std::wstring question_str = UTF16ToWide(l10n_util::GetStringFUTF16( IDS_FR_BUBBLE_QUESTION, GetDefaultSearchEngineName(profile))); label3_ = new views::Label(question_str); label3_->SetMultiLine(true); label3_->SetFont(font); label3_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); label3_->SizeToFit(ps.width() - kBubblePadding * 2); AddChildView(label3_); std::wstring keep_str = UTF16ToWide(l10n_util::GetStringFUTF16( IDS_FR_BUBBLE_OK, GetDefaultSearchEngineName(profile))); keep_button_ = new views::NativeButton(this, keep_str); keep_button_->SetIsDefault(true); AddChildView(keep_button_); std::wstring change_str = UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_CHANGE)); change_button_ = new views::NativeButton(this, change_str); AddChildView(change_button_); } void FirstRunBubbleView::BubbleShown() { keep_button_->RequestFocus(); } void FirstRunBubbleView::ButtonPressed(views::Button* sender, const views::Event& event) { UserMetrics::RecordAction(UserMetricsAction("FirstRunBubbleView_Clicked"), profile_); bubble_window_->set_fade_away_on_close(true); bubble_window_->Close(); if (change_button_ == sender) { UserMetrics::RecordAction( UserMetricsAction("FirstRunBubbleView_ChangeButton"), profile_); Browser* browser = BrowserList::GetLastActive(); if (browser) { browser->OpenSearchEngineOptionsDialog(); } } } void FirstRunBubbleView::Layout() { gfx::Size canvas = GetPreferredSize(); // The multiline business that follows is dirty hacks to get around // bug 1325257. label1_->SetMultiLine(false); gfx::Size pref_size = label1_->GetPreferredSize(); label1_->SetMultiLine(true); label1_->SizeToFit(canvas.width() - kBubblePadding * 2); label1_->SetBounds(kBubblePadding, kBubblePadding, canvas.width() - kBubblePadding * 2, pref_size.height()); int next_v_space = label1_->y() + pref_size.height() + views::kRelatedControlSmallVerticalSpacing; pref_size = label2_->GetPreferredSize(); label2_->SetBounds(kBubblePadding, next_v_space, canvas.width() - kBubblePadding * 2, pref_size.height()); next_v_space = label2_->y() + label2_->height() + views::kPanelSubVerticalSpacing; pref_size = label3_->GetPreferredSize(); label3_->SetBounds(kBubblePadding, next_v_space, canvas.width() - kBubblePadding * 2, pref_size.height()); pref_size = change_button_->GetPreferredSize(); change_button_->SetBounds( canvas.width() - pref_size.width() - kBubblePadding, canvas.height() - pref_size.height() - views::kButtonVEdgeMargin, pref_size.width(), pref_size.height()); pref_size = keep_button_->GetPreferredSize(); keep_button_->SetBounds(change_button_->x() - pref_size.width() - views::kRelatedButtonHSpacing, change_button_->y(), pref_size.width(), pref_size.height()); } gfx::Size FirstRunBubbleView::GetPreferredSize() { return gfx::Size(views::Window::GetLocalizedContentsSize( IDS_FIRSTRUNBUBBLE_DIALOG_WIDTH_CHARS, IDS_FIRSTRUNBUBBLE_DIALOG_HEIGHT_LINES)); } void FirstRunBubbleView::FocusWillChange(View* focused_before, View* focused_now) { if (focused_before && (focused_before->GetClassName() == views::NativeButton::kViewClassName)) { views::NativeButton* before = static_cast<views::NativeButton*>(focused_before); before->SetIsDefault(false); } if (focused_now && (focused_now->GetClassName() == views::NativeButton::kViewClassName)) { views::NativeButton* after = static_cast<views::NativeButton*>(focused_now); after->SetIsDefault(true); } } // FirstRunOEMBubbleView ------------------------------------------------------ class FirstRunOEMBubbleView : public FirstRunBubbleViewBase { public: FirstRunOEMBubbleView(FirstRunBubble* bubble_window, Profile* profile); private: virtual ~FirstRunOEMBubbleView() { } // FirstRunBubbleViewBase: virtual void BubbleShown(); // Overridden from View: virtual void ButtonPressed(views::Button* sender, const views::Event& event); virtual void Layout(); virtual gfx::Size GetPreferredSize(); // FocusChangeListener: virtual void FocusWillChange(View* focused_before, View* focused_now); FirstRunBubble* bubble_window_; views::Label* label1_; views::Label* label2_; views::Label* label3_; views::ImageButton* close_button_; Profile* profile_; DISALLOW_COPY_AND_ASSIGN(FirstRunOEMBubbleView); }; FirstRunOEMBubbleView::FirstRunOEMBubbleView(FirstRunBubble* bubble_window, Profile* profile) : bubble_window_(bubble_window), label1_(NULL), label2_(NULL), label3_(NULL), close_button_(NULL), profile_(profile) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont); label1_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_TITLE_1))); label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD)); label1_->SetColor(SK_ColorRED); label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(label1_); label2_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_TITLE_2))); label2_->SetFont(font.DeriveFont(3, gfx::Font::BOLD)); label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(label2_); gfx::Size ps = GetPreferredSize(); label3_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_OEM_BUBBLE_SUBTEXT))); label3_->SetMultiLine(true); label3_->SetFont(font); label3_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); label3_->SizeToFit(ps.width() - kOEMBubblePadding * 2); AddChildView(label3_); close_button_ = new views::ImageButton(this); close_button_->SetImage(views::CustomButton::BS_NORMAL, rb.GetBitmapNamed(IDR_CLOSE_BAR)); close_button_->SetImage(views::CustomButton::BS_HOT, rb.GetBitmapNamed(IDR_CLOSE_BAR_H)); close_button_->SetImage(views::CustomButton::BS_PUSHED, rb.GetBitmapNamed(IDR_CLOSE_BAR_P)); AddChildView(close_button_); } void FirstRunOEMBubbleView::BubbleShown() { RequestFocus(); // No button in oem_bubble to request focus. } void FirstRunOEMBubbleView::ButtonPressed(views::Button* sender, const views::Event& event) { UserMetrics::RecordAction(UserMetricsAction("FirstRunOEMBubbleView_Clicked"), profile_); bubble_window_->set_fade_away_on_close(true); bubble_window_->Close(); } void FirstRunOEMBubbleView::Layout() { gfx::Size canvas = GetPreferredSize(); // First, draw the close button on the far right. gfx::Size sz = close_button_->GetPreferredSize(); close_button_->SetBounds( canvas.width() - sz.width() - kMarginRightOfCloseButton, kOEMBubblePadding, sz.width(), sz.height()); gfx::Size pref_size = label1_->GetPreferredSize(); label1_->SetBounds(kOEMBubblePadding, kOEMBubblePadding, pref_size.width() + kOEMBubblePadding * 2, pref_size.height()); pref_size = label2_->GetPreferredSize(); label2_->SetBounds( kOEMBubblePadding * 2 + label1_->GetPreferredSize().width(), kOEMBubblePadding, canvas.width() - kOEMBubblePadding * 2, pref_size.height()); int next_v_space = label1_->y() + pref_size.height() + views::kRelatedControlSmallVerticalSpacing; pref_size = label3_->GetPreferredSize(); label3_->SetBounds(kOEMBubblePadding, next_v_space, canvas.width() - kOEMBubblePadding * 2, pref_size.height()); } gfx::Size FirstRunOEMBubbleView::GetPreferredSize() { // Calculate width based on font and text. ResourceBundle& rb = ResourceBundle::GetSharedInstance(); const gfx::Font& font = rb.GetFont( ResourceBundle::MediumFont).DeriveFont(3, gfx::Font::BOLD); gfx::Size size = gfx::Size( ui::GetLocalizedContentsWidthForFont( IDS_FIRSTRUNOEMBUBBLE_DIALOG_WIDTH_CHARS, font), ui::GetLocalizedContentsHeightForFont( IDS_FIRSTRUNOEMBUBBLE_DIALOG_HEIGHT_LINES, font)); // WARNING: HACK. Vista and XP calculate font size differently; this means // that a dialog box correctly proportioned for XP will appear too large in // Vista. The correct thing to do is to change font size calculations in // XP or Vista so that the length of a string is calculated properly. For // now, we force Vista to show a correctly-sized box by taking account of // the difference in font size calculation. The coefficient should not be // stored in a variable because it's a hack and should go away. if (views::WidgetWin::IsAeroGlassEnabled()) { size.set_width(static_cast<int>(size.width() * 0.85)); size.set_height(static_cast<int>(size.height() * 0.85)); } return size; } void FirstRunOEMBubbleView::FocusWillChange(View* focused_before, View* focused_now) { // No buttons in oem_bubble to register focus changes. } // FirstRunMinimalBubbleView -------------------------------------------------- // TODO(mirandac): combine FRBubbles more elegantly. http://crbug.com/41353 class FirstRunMinimalBubbleView : public FirstRunBubbleViewBase { public: FirstRunMinimalBubbleView(FirstRunBubble* bubble_window, Profile* profile); private: virtual ~FirstRunMinimalBubbleView() { } // FirstRunBubbleViewBase: virtual void BubbleShown(); // Overridden from View: virtual void ButtonPressed(views::Button* sender, const views::Event& event) { } virtual void Layout(); virtual gfx::Size GetPreferredSize(); // FocusChangeListener: virtual void FocusWillChange(View* focused_before, View* focused_now); FirstRunBubble* bubble_window_; Profile* profile_; views::Label* label1_; views::Label* label2_; DISALLOW_COPY_AND_ASSIGN(FirstRunMinimalBubbleView); }; FirstRunMinimalBubbleView::FirstRunMinimalBubbleView( FirstRunBubble* bubble_window, Profile* profile) : bubble_window_(bubble_window), profile_(profile), label1_(NULL), label2_(NULL) { const gfx::Font& font = ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont); label1_ = new views::Label(UTF16ToWide(l10n_util::GetStringFUTF16( IDS_FR_SE_BUBBLE_TITLE, GetDefaultSearchEngineName(profile_)))); label1_->SetFont(font.DeriveFont(3, gfx::Font::BOLD)); label1_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); AddChildView(label1_); gfx::Size ps = GetPreferredSize(); label2_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_BUBBLE_SUBTEXT))); label2_->SetMultiLine(true); label2_->SetFont(font); label2_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); label2_->SizeToFit(ps.width() - kBubblePadding * 2); AddChildView(label2_); } void FirstRunMinimalBubbleView::BubbleShown() { RequestFocus(); } void FirstRunMinimalBubbleView::Layout() { gfx::Size canvas = GetPreferredSize(); // See comments in FirstRunOEMBubbleView::Layout explaining this hack. label1_->SetMultiLine(false); gfx::Size pref_size = label1_->GetPreferredSize(); label1_->SetMultiLine(true); label1_->SizeToFit(canvas.width() - kBubblePadding * 2); label1_->SetBounds(kBubblePadding, kBubblePadding, canvas.width() - kBubblePadding * 2, pref_size.height()); int next_v_space = label1_->y() + pref_size.height() + views::kRelatedControlSmallVerticalSpacing; pref_size = label2_->GetPreferredSize(); label2_->SetBounds(kBubblePadding, next_v_space, canvas.width() - kBubblePadding * 2, pref_size.height()); } gfx::Size FirstRunMinimalBubbleView::GetPreferredSize() { return gfx::Size(views::Window::GetLocalizedContentsSize( IDS_FIRSTRUN_MINIMAL_BUBBLE_DIALOG_WIDTH_CHARS, IDS_FIRSTRUN_MINIMAL_BUBBLE_DIALOG_HEIGHT_LINES)); } void FirstRunMinimalBubbleView::FocusWillChange(View* focused_before, View* focused_now) { // No buttons in minimal bubble to register focus changes. } // FirstRunBubble ------------------------------------------------------------- // static FirstRunBubble* FirstRunBubble::Show(Profile* profile, views::Widget* parent, const gfx::Rect& position_relative_to, BubbleBorder::ArrowLocation arrow_location, FirstRun::BubbleType bubble_type) { FirstRunBubble* bubble = new FirstRunBubble(); FirstRunBubbleViewBase* view = NULL; switch (bubble_type) { case FirstRun::OEM_BUBBLE: view = new FirstRunOEMBubbleView(bubble, profile); break; case FirstRun::LARGE_BUBBLE: view = new FirstRunBubbleView(bubble, profile); break; case FirstRun::MINIMAL_BUBBLE: view = new FirstRunMinimalBubbleView(bubble, profile); break; default: NOTREACHED(); } bubble->set_view(view); bubble->InitBubble( parent, position_relative_to, arrow_location, view, bubble); bubble->GetFocusManager()->AddFocusChangeListener(view); view->BubbleShown(); return bubble; } FirstRunBubble::FirstRunBubble() : has_been_activated_(false), ALLOW_THIS_IN_INITIALIZER_LIST(enable_window_method_factory_(this)), view_(NULL) { } FirstRunBubble::~FirstRunBubble() { enable_window_method_factory_.RevokeAll(); GetFocusManager()->RemoveFocusChangeListener(view_); } void FirstRunBubble::EnableParent() { ::EnableWindow(GetParent(), true); // The EnableWindow() call above causes the parent to become active, which // resets the flag set by Bubble's call to DisableInactiveRendering(), so we // have to call it again before activating the bubble to prevent the parent // window from rendering inactive. // TODO(beng): this only works in custom-frame mode, not glass-frame mode. views::NativeWidget* parent = views::NativeWidget::GetNativeWidgetForNativeView(GetParent()); if (parent) parent->GetWidget()->GetWindow()->DisableInactiveRendering(); // Reactivate the FirstRunBubble so it responds to OnActivate messages. SetWindowPos(GetParent(), 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_SHOWWINDOW); } void FirstRunBubble::OnActivate(UINT action, BOOL minimized, HWND window) { // Keep the bubble around for kLingerTime milliseconds, to prevent accidental // closure. const int kLingerTime = 3000; // We might get re-enabled right before we are closed (sequence is: we get // deactivated, we call close, before we are actually closed we get // reactivated). Don't do the disabling of the parent in such cases. if (action == WA_ACTIVE && !has_been_activated_) { has_been_activated_ = true; ::EnableWindow(GetParent(), false); MessageLoop::current()->PostDelayedTask(FROM_HERE, enable_window_method_factory_.NewRunnableMethod( &FirstRunBubble::EnableParent), kLingerTime); return; } // Keep window from automatically closing until kLingerTime has passed. if (::IsWindowEnabled(GetParent())) Bubble::OnActivate(action, minimized, window); } void FirstRunBubble::BubbleClosing(Bubble* bubble, bool closed_by_escape) { // Make sure our parent window is re-enabled. if (!IsWindowEnabled(GetParent())) ::EnableWindow(GetParent(), true); }