// Copyright (c) 2010 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/gtk/first_run_dialog.h" #include "app/l10n_util.h" #include "app/resource_bundle.h" #include "base/i18n/rtl.h" #include "base/message_loop.h" #include "base/utf_string_conversions.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/gtk/gtk_chrome_link_button.h" #include "chrome/browser/gtk/gtk_floating_container.h" #include "chrome/browser/gtk/gtk_util.h" #include "chrome/browser/platform_util.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/process_singleton.h" #include "chrome/browser/profile.h" #include "chrome/browser/search_engines/template_url.h" #include "chrome/browser/search_engines/template_url_model.h" #include "chrome/browser/shell_integration.h" #include "chrome/common/pref_names.h" #include "chrome/installer/util/google_update_settings.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" #include "grit/locale_settings.h" #include "grit/theme_resources.h" #if defined(USE_LINUX_BREAKPAD) #include "chrome/app/breakpad_linux.h" #endif namespace { const gchar* kSearchEngineKey = "template-url-search-engine"; // Height of the label that displays the search engine's logo (in lieu of the // actual logo) in chromium. const int kLogoLabelHeight = 100; // Size of the small logo (for when we show 4 search engines). const int kLogoLabelWidthSmall = 132; const int kLogoLabelHeightSmall = 88; // The number of search engine options we normally show. It may be less than // this number if there are not enough search engines for the current locale, // or more if the user's imported default is not one of the top search engines // for the current locale. const size_t kNormalBallotSize = 3; // The width of the explanatory label. The 180 is the width of the large images. const int kExplanationWidth = kNormalBallotSize * 180; // Horizontal spacing between search engine choices. const int kSearchEngineSpacing = 6; // Set the (x, y) coordinates of the welcome message (which floats on top of // the omnibox image at the top of the first run dialog). void SetWelcomePosition(GtkFloatingContainer* container, GtkAllocation* allocation, GtkWidget* label) { GValue value = { 0, }; g_value_init(&value, G_TYPE_INT); GtkRequisition req; gtk_widget_size_request(label, &req); int x = base::i18n::IsRTL() ? allocation->width - req.width - gtk_util::kContentAreaSpacing : gtk_util::kContentAreaSpacing; g_value_set_int(&value, x); gtk_container_child_set_property(GTK_CONTAINER(container), label, "x", &value); int y = allocation->height / 2 - req.height / 2; g_value_set_int(&value, y); gtk_container_child_set_property(GTK_CONTAINER(container), label, "y", &value); g_value_unset(&value); } } // namespace // static bool FirstRunDialog::Show(Profile* profile, bool randomize_search_engine_order) { // Figure out which dialogs we will show. // If the default search is managed via policy, we won't ask. const TemplateURLModel* search_engines_model = profile->GetTemplateURLModel(); bool show_search_engines_dialog = search_engines_model && !search_engines_model->is_default_search_managed(); #if defined(GOOGLE_CHROME_BUILD) // If the metrics reporting is managed, we won't ask. const PrefService::Preference* metrics_reporting_pref = g_browser_process->local_state()->FindPreference( prefs::kMetricsReportingEnabled); bool show_reporting_dialog = !metrics_reporting_pref || !metrics_reporting_pref->IsManaged(); #else bool show_reporting_dialog = false; #endif if (!show_search_engines_dialog && !show_reporting_dialog) return true; // Nothing to do int response = -1; // Object deletes itself. new FirstRunDialog(profile, show_reporting_dialog, show_search_engines_dialog, &response); // TODO(port): it should be sufficient to just run the dialog: // int response = gtk_dialog_run(GTK_DIALOG(dialog)); // but that spins a nested message loop and hoses us. :( // http://code.google.com/p/chromium/issues/detail?id=12552 // Instead, run a loop and extract the response manually. MessageLoop::current()->Run(); return (response == GTK_RESPONSE_ACCEPT); } FirstRunDialog::FirstRunDialog(Profile* profile, bool show_reporting_dialog, bool show_search_engines_dialog, int* response) : search_engine_window_(NULL), dialog_(NULL), report_crashes_(NULL), make_default_(NULL), profile_(profile), chosen_search_engine_(NULL), show_reporting_dialog_(show_reporting_dialog), response_(response) { if (!show_search_engines_dialog) { ShowReportingDialog(); return; } search_engines_model_ = profile_->GetTemplateURLModel(); ShowSearchEngineWindow(); search_engines_model_->AddObserver(this); if (search_engines_model_->loaded()) OnTemplateURLModelChanged(); else search_engines_model_->Load(); } FirstRunDialog::~FirstRunDialog() { } void FirstRunDialog::ShowSearchEngineWindow() { search_engine_window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_deletable(GTK_WINDOW(search_engine_window_), FALSE); gtk_window_set_title( GTK_WINDOW(search_engine_window_), l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_TITLE).c_str()); gtk_window_set_resizable(GTK_WINDOW(search_engine_window_), FALSE); g_signal_connect(search_engine_window_, "destroy", G_CALLBACK(OnSearchEngineWindowDestroyThunk), this); GtkWidget* content_area = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(search_engine_window_), content_area); GdkPixbuf* pixbuf = ResourceBundle::GetSharedInstance().GetRTLEnabledPixbufNamed( IDR_SEARCH_ENGINE_DIALOG_TOP); GtkWidget* top_image = gtk_image_new_from_pixbuf(pixbuf); // Right align the image. gtk_misc_set_alignment(GTK_MISC(top_image), 1, 0); gtk_widget_set_size_request(top_image, 0, -1); GtkWidget* welcome_message = gtk_util::CreateBoldLabel( l10n_util::GetStringUTF8(IDS_FR_SEARCH_MAIN_LABEL)); // Force the font size to make sure the label doesn't overlap the image. // 13.4px == 10pt @ 96dpi gtk_util::ForceFontSizePixels(welcome_message, 13.4); GtkWidget* top_area = gtk_floating_container_new(); gtk_container_add(GTK_CONTAINER(top_area), top_image); gtk_floating_container_add_floating(GTK_FLOATING_CONTAINER(top_area), welcome_message); g_signal_connect(top_area, "set-floating-position", G_CALLBACK(SetWelcomePosition), welcome_message); gtk_box_pack_start(GTK_BOX(content_area), top_area, FALSE, FALSE, 0); GtkWidget* bubble_area_background = gtk_event_box_new(); gtk_widget_modify_bg(bubble_area_background, GTK_STATE_NORMAL, >k_util::kGdkWhite); GtkWidget* bubble_area_box = gtk_vbox_new(FALSE, 0); gtk_container_set_border_width(GTK_CONTAINER(bubble_area_box), gtk_util::kContentAreaSpacing); gtk_container_add(GTK_CONTAINER(bubble_area_background), bubble_area_box); GtkWidget* explanation = gtk_label_new( l10n_util::GetStringFUTF8(IDS_FR_SEARCH_TEXT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)).c_str()); gtk_misc_set_alignment(GTK_MISC(explanation), 0, 0.5); gtk_util::SetLabelColor(explanation, >k_util::kGdkBlack); gtk_util::SetLabelWidth(explanation, kExplanationWidth); gtk_box_pack_start(GTK_BOX(bubble_area_box), explanation, FALSE, FALSE, 0); // We will fill this in after the TemplateURLModel has loaded. // GtkHButtonBox because we want all children to have the same size. search_engine_hbox_ = gtk_hbutton_box_new(); gtk_box_set_spacing(GTK_BOX(search_engine_hbox_), kSearchEngineSpacing); gtk_box_pack_start(GTK_BOX(bubble_area_box), search_engine_hbox_, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(content_area), bubble_area_background, TRUE, TRUE, 0); gtk_widget_show_all(content_area); gtk_window_present(GTK_WINDOW(search_engine_window_)); } void FirstRunDialog::ShowReportingDialog() { // The purpose of the dialog is to ask the user to enable stats and crash // reporting. This setting may be controlled through configuration management // in enterprise scenarios. If that is the case, skip the dialog entirely, // it's not worth bothering the user for only the default browser question // (which is likely to be forced in enterprise deployments anyway). if (!show_reporting_dialog_) { OnResponseDialog(NULL, GTK_RESPONSE_ACCEPT); return; } dialog_ = gtk_dialog_new_with_buttons( l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_TITLE).c_str(), NULL, // No parent (GtkDialogFlags) (GTK_DIALOG_MODAL | GTK_DIALOG_NO_SEPARATOR), NULL); gtk_util::AddButtonToDialog(dialog_, l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_OK).c_str(), GTK_STOCK_APPLY, GTK_RESPONSE_ACCEPT); gtk_window_set_deletable(GTK_WINDOW(dialog_), FALSE); gtk_window_set_resizable(GTK_WINDOW(dialog_), FALSE); g_signal_connect(dialog_, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); GtkWidget* content_area = GTK_DIALOG(dialog_)->vbox; make_default_ = gtk_check_button_new_with_label( l10n_util::GetStringUTF8(IDS_FR_CUSTOMIZE_DEFAULT_BROWSER).c_str()); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(make_default_), TRUE); gtk_box_pack_start(GTK_BOX(content_area), make_default_, FALSE, FALSE, 0); report_crashes_ = gtk_check_button_new(); GtkWidget* check_label = gtk_label_new( l10n_util::GetStringUTF8(IDS_OPTIONS_ENABLE_LOGGING).c_str()); gtk_label_set_line_wrap(GTK_LABEL(check_label), TRUE); gtk_container_add(GTK_CONTAINER(report_crashes_), check_label); GtkWidget* learn_more_vbox = gtk_vbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(learn_more_vbox), report_crashes_, FALSE, FALSE, 0); GtkWidget* learn_more_link = gtk_chrome_link_button_new( l10n_util::GetStringUTF8(IDS_LEARN_MORE).c_str()); gtk_button_set_alignment(GTK_BUTTON(learn_more_link), 0.0, 0.5); gtk_box_pack_start(GTK_BOX(learn_more_vbox), gtk_util::IndentWidget(learn_more_link), FALSE, FALSE, 0); g_signal_connect(learn_more_link, "clicked", G_CALLBACK(OnLearnMoreLinkClickedThunk), this); gtk_box_pack_start(GTK_BOX(content_area), learn_more_vbox, FALSE, FALSE, 0); g_signal_connect(dialog_, "response", G_CALLBACK(OnResponseDialogThunk), this); gtk_widget_show_all(dialog_); } void FirstRunDialog::OnTemplateURLModelChanged() { // We only watch the search engine model change once, on load. Remove // observer so we don't try to redraw if engines change under us. search_engines_model_->RemoveObserver(this); // Add search engines in |search_engines_model_| to buttons list. std::vector ballot_engines = search_engines_model_->GetTemplateURLs(); // Drop any not in the first 3. if (ballot_engines.size() > kNormalBallotSize) ballot_engines.resize(kNormalBallotSize); const TemplateURL* default_search_engine = search_engines_model_->GetDefaultSearchProvider(); if (std::find(ballot_engines.begin(), ballot_engines.end(), default_search_engine) == ballot_engines.end()) { ballot_engines.push_back(default_search_engine); } std::string choose_text = l10n_util::GetStringUTF8(IDS_FR_SEARCH_CHOOSE); for (std::vector::iterator search_engine_iter = ballot_engines.begin(); search_engine_iter < ballot_engines.end(); ++search_engine_iter) { // Create a container for the search engine widgets. GtkWidget* vbox = gtk_vbox_new(FALSE, gtk_util::kControlSpacing); // We show text on Chromium and images on Google Chrome. bool show_images = false; #if defined(GOOGLE_CHROME_BUILD) show_images = true; #endif // Create the image (maybe). int logo_id = (*search_engine_iter)->logo_id(); if (show_images && logo_id > 0) { GdkPixbuf* pixbuf = ResourceBundle::GetSharedInstance().GetPixbufNamed(logo_id); if (ballot_engines.size() > kNormalBallotSize) { pixbuf = gdk_pixbuf_scale_simple(pixbuf, kLogoLabelWidthSmall, kLogoLabelHeightSmall, GDK_INTERP_HYPER); } else { g_object_ref(pixbuf); } GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf); gtk_box_pack_start(GTK_BOX(vbox), image, FALSE, FALSE, 0); g_object_unref(pixbuf); } else { GtkWidget* logo_label = gtk_label_new(NULL); char* markup = g_markup_printf_escaped( "%s", WideToUTF8((*search_engine_iter)->short_name()).c_str()); gtk_label_set_markup(GTK_LABEL(logo_label), markup); g_free(markup); gtk_widget_set_size_request(logo_label, -1, ballot_engines.size() > kNormalBallotSize ? kLogoLabelHeightSmall : kLogoLabelHeight); gtk_box_pack_start(GTK_BOX(vbox), logo_label, FALSE, FALSE, 0); } // Create the button. GtkWidget* button = gtk_button_new_with_label(choose_text.c_str()); g_signal_connect(button, "clicked", G_CALLBACK(OnSearchEngineButtonClickedThunk), this); g_object_set_data(G_OBJECT(button), kSearchEngineKey, const_cast(*search_engine_iter)); GtkWidget* button_centerer = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(button_centerer), button, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), button_centerer, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(search_engine_hbox_), vbox); gtk_widget_show_all(search_engine_hbox_); } } void FirstRunDialog::OnSearchEngineButtonClicked(GtkWidget* sender) { chosen_search_engine_ = static_cast( g_object_get_data(G_OBJECT(sender), kSearchEngineKey)); gtk_widget_destroy(search_engine_window_); } void FirstRunDialog::OnSearchEngineWindowDestroy(GtkWidget* sender) { search_engine_window_ = NULL; if (chosen_search_engine_) { search_engines_model_->SetDefaultSearchProvider(chosen_search_engine_); ShowReportingDialog(); } else { FirstRunDone(); } } void FirstRunDialog::OnResponseDialog(GtkWidget* widget, int response) { if (dialog_) gtk_widget_hide_all(dialog_); *response_ = response; // Mark that first run has ran. FirstRun::CreateSentinel(); // Check if user has opted into reporting. if (report_crashes_ && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(report_crashes_))) { #if defined(USE_LINUX_BREAKPAD) if (GoogleUpdateSettings::SetCollectStatsConsent(true)) InitCrashReporter(); #endif } else { GoogleUpdateSettings::SetCollectStatsConsent(false); } // If selected set as default browser. if (make_default_ && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(make_default_))) { ShellIntegration::SetAsDefaultBrowser(); } FirstRunDone(); } void FirstRunDialog::OnLearnMoreLinkClicked(GtkButton* button) { platform_util::OpenExternal(GURL( l10n_util::GetStringUTF8(IDS_LEARN_MORE_REPORTING_URL))); } void FirstRunDialog::FirstRunDone() { FirstRun::SetShowWelcomePagePref(); if (dialog_) gtk_widget_destroy(dialog_); MessageLoop::current()->Quit(); delete this; }