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).
SetWelcomePosition(GtkFloatingContainer * container,GtkAllocation * allocation,GtkWidget * label)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
ShowFirstRunDialog(Profile * profile,bool randomize_search_engine_order)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
Show(Profile * profile,bool randomize_search_engine_order)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
FirstRunDialog(Profile * profile,bool show_reporting_dialog,bool show_search_engines_dialog,int * response)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
~FirstRunDialog()172 FirstRunDialog::~FirstRunDialog() {
173 }
174
ShowSearchEngineWindow()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
ShowReportingDialog()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
OnTemplateURLModelChanged()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
OnSearchEngineButtonClicked(GtkWidget * sender)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
OnSearchEngineWindowDestroy(GtkWidget * sender)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
OnResponseDialog(GtkWidget * widget,int response)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
OnLearnMoreLinkClicked(GtkButton * button)425 void FirstRunDialog::OnLearnMoreLinkClicked(GtkButton* button) {
426 platform_util::OpenExternal(google_util::AppendGoogleLocaleParam(
427 GURL(chrome::kLearnMoreReportingURL)));
428 }
429
FirstRunDone()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