Home | History | Annotate | Download | only in gtk
      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/gtk/first_run_dialog.h"
      6 
      7 #include <string>
      8 #include <vector>
      9 
     10 #include "base/i18n/rtl.h"
     11 #include "base/message_loop.h"
     12 #include "base/utf_string_conversions.h"
     13 #include "chrome/browser/first_run/first_run_dialog.h"
     14 #include "chrome/browser/google/google_util.h"
     15 #include "chrome/browser/platform_util.h"
     16 #include "chrome/browser/process_singleton.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/browser/search_engines/template_url.h"
     19 #include "chrome/browser/search_engines/template_url_model.h"
     20 #include "chrome/browser/shell_integration.h"
     21 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h"
     22 #include "chrome/browser/ui/gtk/gtk_floating_container.h"
     23 #include "chrome/browser/ui/gtk/gtk_util.h"
     24 #include "chrome/common/pref_names.h"
     25 #include "chrome/common/url_constants.h"
     26 #include "chrome/installer/util/google_update_settings.h"
     27 #include "grit/chromium_strings.h"
     28 #include "grit/generated_resources.h"
     29 #include "grit/locale_settings.h"
     30 #include "grit/theme_resources.h"
     31 #include "ui/base/l10n/l10n_util.h"
     32 #include "ui/base/resource/resource_bundle.h"
     33 
     34 #if defined(USE_LINUX_BREAKPAD)
     35 #include "chrome/app/breakpad_linux.h"
     36 #endif
     37 
     38 #if defined(GOOGLE_CHROME_BUILD)
     39 #include "chrome/browser/browser_process.h"
     40 #include "chrome/browser/prefs/pref_service.h"
     41 #endif
     42 
     43 namespace {
     44 
     45 const gchar* kSearchEngineKey = "template-url-search-engine";
     46 
     47 // Height of the label that displays the search engine's logo (in lieu of the
     48 // actual logo) in chromium.
     49 const int kLogoLabelHeight = 100;
     50 
     51 // Size of the small logo (for when we show 4 search engines).
     52 const int kLogoLabelWidthSmall = 132;
     53 const int kLogoLabelHeightSmall = 88;
     54 
     55 // The number of search engine options we normally show. It may be less than
     56 // this number if there are not enough search engines for the current locale,
     57 // or more if the user's imported default is not one of the top search engines
     58 // for the current locale.
     59 const size_t kNormalBallotSize = 3;
     60 
     61 // The width of the explanatory label. The 180 is the width of the large images.
     62 const int kExplanationWidth = kNormalBallotSize * 180;
     63 
     64 // Horizontal spacing between search engine choices.
     65 const int kSearchEngineSpacing = 6;
     66 
     67 // Set the (x, y) coordinates of the welcome message (which floats on top of
     68 // the omnibox image at the top of the first run dialog).
     69 void SetWelcomePosition(GtkFloatingContainer* container,
     70                         GtkAllocation* allocation,
     71                         GtkWidget* label) {
     72   GValue value = { 0, };
     73   g_value_init(&value, G_TYPE_INT);
     74 
     75   GtkRequisition req;
     76   gtk_widget_size_request(label, &req);
     77 
     78   int x = base::i18n::IsRTL() ?
     79       allocation->width - req.width - gtk_util::kContentAreaSpacing :
     80       gtk_util::kContentAreaSpacing;
     81   g_value_set_int(&value, x);
     82   gtk_container_child_set_property(GTK_CONTAINER(container),
     83                                    label, "x", &value);
     84 
     85   int y = allocation->height / 2 - req.height / 2;
     86   g_value_set_int(&value, y);
     87   gtk_container_child_set_property(GTK_CONTAINER(container),
     88                                    label, "y", &value);
     89   g_value_unset(&value);
     90 }
     91 
     92 }  // namespace
     93 
     94 namespace first_run {
     95 
     96 void ShowFirstRunDialog(Profile* profile,
     97                         bool randomize_search_engine_order) {
     98   FirstRunDialog::Show(profile, randomize_search_engine_order);
     99 }
    100 
    101 }  // namespace first_run
    102 
    103 // static
    104 bool FirstRunDialog::Show(Profile* profile,
    105                           bool randomize_search_engine_order) {
    106   // Figure out which dialogs we will show.
    107   // If the default search is managed via policy, we won't ask.
    108   const TemplateURLModel* search_engines_model = profile->GetTemplateURLModel();
    109   bool show_search_engines_dialog =
    110       !FirstRun::SearchEngineSelectorDisallowed() &&
    111       search_engines_model &&
    112       !search_engines_model->is_default_search_managed();
    113 
    114 #if defined(GOOGLE_CHROME_BUILD)
    115   // If the metrics reporting is managed, we won't ask.
    116   const PrefService::Preference* metrics_reporting_pref =
    117       g_browser_process->local_state()->FindPreference(
    118           prefs::kMetricsReportingEnabled);
    119   bool show_reporting_dialog = !metrics_reporting_pref ||
    120       !metrics_reporting_pref->IsManaged();
    121 #else
    122   bool show_reporting_dialog = false;
    123 #endif
    124 
    125   if (!show_search_engines_dialog && !show_reporting_dialog)
    126     return true;  // Nothing to do
    127 
    128   int response = -1;
    129   // Object deletes itself.
    130   new FirstRunDialog(profile,
    131                      show_reporting_dialog,
    132                      show_search_engines_dialog,
    133                      &response);
    134 
    135   // TODO(port): it should be sufficient to just run the dialog:
    136   // int response = gtk_dialog_run(GTK_DIALOG(dialog));
    137   // but that spins a nested message loop and hoses us.  :(
    138   // http://code.google.com/p/chromium/issues/detail?id=12552
    139   // Instead, run a loop and extract the response manually.
    140   MessageLoop::current()->Run();
    141 
    142   return (response == GTK_RESPONSE_ACCEPT);
    143 }
    144 
    145 FirstRunDialog::FirstRunDialog(Profile* profile,
    146                                bool show_reporting_dialog,
    147                                bool show_search_engines_dialog,
    148                                int* response)
    149     : search_engine_window_(NULL),
    150       dialog_(NULL),
    151       report_crashes_(NULL),
    152       make_default_(NULL),
    153       profile_(profile),
    154       chosen_search_engine_(NULL),
    155       show_reporting_dialog_(show_reporting_dialog),
    156       response_(response) {
    157   if (!show_search_engines_dialog) {
    158     ShowReportingDialog();
    159     return;
    160   }
    161   search_engines_model_ = profile_->GetTemplateURLModel();
    162 
    163   ShowSearchEngineWindow();
    164 
    165   search_engines_model_->AddObserver(this);
    166   if (search_engines_model_->loaded())
    167     OnTemplateURLModelChanged();
    168   else
    169     search_engines_model_->Load();
    170 }
    171 
    172 FirstRunDialog::~FirstRunDialog() {
    173 }
    174 
    175 void FirstRunDialog::ShowSearchEngineWindow() {
    176   search_engine_window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    177   gtk_window_set_deletable(GTK_WINDOW(search_engine_window_), FALSE);
    178   gtk_window_set_title(
    179       GTK_WINDOW(search_engine_window_),
    180       l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_TITLE).c_str());
    181   gtk_window_set_resizable(GTK_WINDOW(search_engine_window_), FALSE);
    182   g_signal_connect(search_engine_window_, "destroy",
    183                    G_CALLBACK(OnSearchEngineWindowDestroyThunk), this);
    184   GtkWidget* content_area = gtk_vbox_new(FALSE, 0);
    185   gtk_container_add(GTK_CONTAINER(search_engine_window_), content_area);
    186 
    187   GdkPixbuf* pixbuf =
    188       ResourceBundle::GetSharedInstance().GetRTLEnabledPixbufNamed(
    189           IDR_SEARCH_ENGINE_DIALOG_TOP);
    190   GtkWidget* top_image = gtk_image_new_from_pixbuf(pixbuf);
    191   // Right align the image.
    192   gtk_misc_set_alignment(GTK_MISC(top_image), 1, 0);
    193   gtk_widget_set_size_request(top_image, 0, -1);
    194 
    195   GtkWidget* welcome_message = gtk_util::CreateBoldLabel(
    196       l10n_util::GetStringUTF8(IDS_FR_SEARCH_MAIN_LABEL));
    197   // Force the font size to make sure the label doesn't overlap the image.
    198   // 13.4px == 10pt @ 96dpi
    199   gtk_util::ForceFontSizePixels(welcome_message, 13.4);
    200 
    201   GtkWidget* top_area = gtk_floating_container_new();
    202   gtk_container_add(GTK_CONTAINER(top_area), top_image);
    203   gtk_floating_container_add_floating(GTK_FLOATING_CONTAINER(top_area),
    204                                       welcome_message);
    205   g_signal_connect(top_area, "set-floating-position",
    206                    G_CALLBACK(SetWelcomePosition), welcome_message);
    207 
    208   gtk_box_pack_start(GTK_BOX(content_area), top_area,
    209                      FALSE, FALSE, 0);
    210 
    211   GtkWidget* bubble_area_background = gtk_event_box_new();
    212   gtk_widget_modify_bg(bubble_area_background,
    213                        GTK_STATE_NORMAL, &gtk_util::kGdkWhite);
    214 
    215   GtkWidget* bubble_area_box = gtk_vbox_new(FALSE, 0);
    216   gtk_container_set_border_width(GTK_CONTAINER(bubble_area_box),
    217                                  gtk_util::kContentAreaSpacing);
    218   gtk_container_add(GTK_CONTAINER(bubble_area_background),
    219                     bubble_area_box);
    220 
    221   GtkWidget* explanation = gtk_label_new(
    222       l10n_util::GetStringFUTF8(IDS_FR_SEARCH_TEXT,
    223           l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)).c_str());
    224   gtk_util::SetLabelColor(explanation, &gtk_util::kGdkBlack);
    225   gtk_util::SetLabelWidth(explanation, kExplanationWidth);
    226   gtk_box_pack_start(GTK_BOX(bubble_area_box), explanation, FALSE, FALSE, 0);
    227 
    228   // We will fill this in after the TemplateURLModel has loaded.
    229   // GtkHButtonBox because we want all children to have the same size.
    230   search_engine_hbox_ = gtk_hbutton_box_new();
    231   gtk_box_set_spacing(GTK_BOX(search_engine_hbox_), kSearchEngineSpacing);
    232   gtk_box_pack_start(GTK_BOX(bubble_area_box), search_engine_hbox_,
    233                      FALSE, FALSE, 0);
    234 
    235   gtk_box_pack_start(GTK_BOX(content_area), bubble_area_background,
    236                      TRUE, TRUE, 0);
    237 
    238   gtk_widget_show_all(content_area);
    239   gtk_window_present(GTK_WINDOW(search_engine_window_));
    240 }
    241 
    242 void FirstRunDialog::ShowReportingDialog() {
    243   // The purpose of the dialog is to ask the user to enable stats and crash
    244   // reporting. This setting may be controlled through configuration management
    245   // in enterprise scenarios. If that is the case, skip the dialog entirely,
    246   // it's not worth bothering the user for only the default browser question
    247   // (which is likely to be forced in enterprise deployments anyway).
    248   if (!show_reporting_dialog_) {
    249     OnResponseDialog(NULL, GTK_RESPONSE_ACCEPT);
    250     return;
    251   }
    252 
    253   dialog_ = gtk_dialog_new_with_buttons(
    254       l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_TITLE).c_str(),
    255       NULL,  // No parent
    256       (GtkDialogFlags) (GTK_DIALOG_MODAL | GTK_DIALOG_NO_SEPARATOR),
    257       NULL);
    258   gtk_util::AddButtonToDialog(dialog_,
    259       l10n_util::GetStringUTF8(IDS_FIRSTRUN_DLG_OK).c_str(),
    260       GTK_STOCK_APPLY, GTK_RESPONSE_ACCEPT);
    261   gtk_window_set_deletable(GTK_WINDOW(dialog_), FALSE);
    262 
    263   gtk_window_set_resizable(GTK_WINDOW(dialog_), FALSE);
    264 
    265   g_signal_connect(dialog_, "delete-event",
    266                    G_CALLBACK(gtk_widget_hide_on_delete), NULL);
    267 
    268   GtkWidget* content_area = GTK_DIALOG(dialog_)->vbox;
    269 
    270   make_default_ = gtk_check_button_new_with_label(
    271       l10n_util::GetStringUTF8(IDS_FR_CUSTOMIZE_DEFAULT_BROWSER).c_str());
    272   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(make_default_), TRUE);
    273   gtk_box_pack_start(GTK_BOX(content_area), make_default_, FALSE, FALSE, 0);
    274 
    275   report_crashes_ = gtk_check_button_new();
    276   GtkWidget* check_label = gtk_label_new(
    277       l10n_util::GetStringUTF8(IDS_OPTIONS_ENABLE_LOGGING).c_str());
    278   gtk_label_set_line_wrap(GTK_LABEL(check_label), TRUE);
    279   gtk_container_add(GTK_CONTAINER(report_crashes_), check_label);
    280   GtkWidget* learn_more_vbox = gtk_vbox_new(FALSE, 0);
    281   gtk_box_pack_start(GTK_BOX(learn_more_vbox), report_crashes_,
    282                      FALSE, FALSE, 0);
    283 
    284   GtkWidget* learn_more_link = gtk_chrome_link_button_new(
    285       l10n_util::GetStringUTF8(IDS_LEARN_MORE).c_str());
    286   gtk_button_set_alignment(GTK_BUTTON(learn_more_link), 0.0, 0.5);
    287   gtk_box_pack_start(GTK_BOX(learn_more_vbox),
    288                      gtk_util::IndentWidget(learn_more_link),
    289                      FALSE, FALSE, 0);
    290   g_signal_connect(learn_more_link, "clicked",
    291                    G_CALLBACK(OnLearnMoreLinkClickedThunk), this);
    292 
    293   gtk_box_pack_start(GTK_BOX(content_area), learn_more_vbox, FALSE, FALSE, 0);
    294 
    295   g_signal_connect(dialog_, "response",
    296                    G_CALLBACK(OnResponseDialogThunk), this);
    297   gtk_widget_show_all(dialog_);
    298 }
    299 
    300 void FirstRunDialog::OnTemplateURLModelChanged() {
    301   // We only watch the search engine model change once, on load.  Remove
    302   // observer so we don't try to redraw if engines change under us.
    303   search_engines_model_->RemoveObserver(this);
    304 
    305   // Add search engines in |search_engines_model_| to buttons list.
    306   std::vector<const TemplateURL*> ballot_engines =
    307       search_engines_model_->GetTemplateURLs();
    308   // Drop any not in the first 3.
    309   if (ballot_engines.size() > kNormalBallotSize)
    310     ballot_engines.resize(kNormalBallotSize);
    311 
    312   const TemplateURL* default_search_engine =
    313       search_engines_model_->GetDefaultSearchProvider();
    314   if (std::find(ballot_engines.begin(),
    315                 ballot_engines.end(),
    316                 default_search_engine) ==
    317       ballot_engines.end()) {
    318     ballot_engines.push_back(default_search_engine);
    319   }
    320 
    321   std::string choose_text = l10n_util::GetStringUTF8(IDS_FR_SEARCH_CHOOSE);
    322   for (std::vector<const TemplateURL*>::iterator search_engine_iter =
    323            ballot_engines.begin();
    324        search_engine_iter < ballot_engines.end();
    325        ++search_engine_iter) {
    326     // Create a container for the search engine widgets.
    327     GtkWidget* vbox = gtk_vbox_new(FALSE, gtk_util::kControlSpacing);
    328 
    329     // We show text on Chromium and images on Google Chrome.
    330     bool show_images = false;
    331 #if defined(GOOGLE_CHROME_BUILD)
    332     show_images = true;
    333 #endif
    334 
    335     // Create the image (maybe).
    336     int logo_id = (*search_engine_iter)->logo_id();
    337     if (show_images && logo_id > 0) {
    338       GdkPixbuf* pixbuf =
    339           ResourceBundle::GetSharedInstance().GetPixbufNamed(logo_id);
    340       if (ballot_engines.size() > kNormalBallotSize) {
    341         pixbuf = gdk_pixbuf_scale_simple(pixbuf,
    342                                          kLogoLabelWidthSmall,
    343                                          kLogoLabelHeightSmall,
    344                                          GDK_INTERP_HYPER);
    345       } else {
    346         g_object_ref(pixbuf);
    347       }
    348 
    349       GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf);
    350       gtk_box_pack_start(GTK_BOX(vbox), image, FALSE, FALSE, 0);
    351       g_object_unref(pixbuf);
    352     } else {
    353       GtkWidget* logo_label = gtk_label_new(NULL);
    354       char* markup = g_markup_printf_escaped(
    355           "<span weight='bold' size='x-large' color='black'>%s</span>",
    356           UTF16ToUTF8((*search_engine_iter)->short_name()).c_str());
    357       gtk_label_set_markup(GTK_LABEL(logo_label), markup);
    358       g_free(markup);
    359       gtk_widget_set_size_request(logo_label, -1,
    360           ballot_engines.size() > kNormalBallotSize ? kLogoLabelHeightSmall :
    361                                                       kLogoLabelHeight);
    362       gtk_box_pack_start(GTK_BOX(vbox), logo_label, FALSE, FALSE, 0);
    363     }
    364 
    365     // Create the button.
    366     GtkWidget* button = gtk_button_new_with_label(choose_text.c_str());
    367     g_signal_connect(button, "clicked",
    368                      G_CALLBACK(OnSearchEngineButtonClickedThunk), this);
    369     g_object_set_data(G_OBJECT(button), kSearchEngineKey,
    370                       const_cast<TemplateURL*>(*search_engine_iter));
    371 
    372     GtkWidget* button_centerer = gtk_hbox_new(FALSE, 0);
    373     gtk_box_pack_start(GTK_BOX(button_centerer), button, TRUE, FALSE, 0);
    374     gtk_box_pack_start(GTK_BOX(vbox), button_centerer, FALSE, FALSE, 0);
    375 
    376     gtk_container_add(GTK_CONTAINER(search_engine_hbox_), vbox);
    377     gtk_widget_show_all(search_engine_hbox_);
    378   }
    379 }
    380 
    381 void FirstRunDialog::OnSearchEngineButtonClicked(GtkWidget* sender) {
    382   chosen_search_engine_ = static_cast<TemplateURL*>(
    383       g_object_get_data(G_OBJECT(sender), kSearchEngineKey));
    384   gtk_widget_destroy(search_engine_window_);
    385 }
    386 
    387 void FirstRunDialog::OnSearchEngineWindowDestroy(GtkWidget* sender) {
    388   search_engine_window_ = NULL;
    389   if (chosen_search_engine_) {
    390     search_engines_model_->SetDefaultSearchProvider(chosen_search_engine_);
    391     ShowReportingDialog();
    392   } else {
    393     FirstRunDone();
    394   }
    395 }
    396 
    397 void FirstRunDialog::OnResponseDialog(GtkWidget* widget, int response) {
    398   if (dialog_)
    399     gtk_widget_hide_all(dialog_);
    400   *response_ = response;
    401 
    402   // Mark that first run has ran.
    403   FirstRun::CreateSentinel();
    404 
    405   // Check if user has opted into reporting.
    406   if (report_crashes_ &&
    407       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(report_crashes_))) {
    408 #if defined(USE_LINUX_BREAKPAD)
    409     if (GoogleUpdateSettings::SetCollectStatsConsent(true))
    410       InitCrashReporter();
    411 #endif
    412   } else {
    413     GoogleUpdateSettings::SetCollectStatsConsent(false);
    414   }
    415 
    416   // If selected set as default browser.
    417   if (make_default_ &&
    418       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(make_default_))) {
    419     ShellIntegration::SetAsDefaultBrowser();
    420   }
    421 
    422   FirstRunDone();
    423 }
    424 
    425 void FirstRunDialog::OnLearnMoreLinkClicked(GtkButton* button) {
    426   platform_util::OpenExternal(google_util::AppendGoogleLocaleParam(
    427       GURL(chrome::kLearnMoreReportingURL)));
    428 }
    429 
    430 void FirstRunDialog::FirstRunDone() {
    431   FirstRun::SetShowWelcomePagePref();
    432 
    433   if (dialog_)
    434     gtk_widget_destroy(dialog_);
    435   MessageLoop::current()->Quit();
    436   delete this;
    437 }
    438