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, >k_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, >k_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