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/views/first_run_search_engine_view.h"
6
7 #include <algorithm>
8 #include <map>
9 #include <vector>
10
11 #include "base/i18n/rtl.h"
12 #include "base/rand_util.h"
13 #include "base/time.h"
14 #include "base/utf_string_conversions.h"
15 #include "chrome/browser/first_run/first_run.h"
16 #include "chrome/browser/first_run/first_run_dialog.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/search_engines/search_engine_type.h"
19 #include "chrome/browser/search_engines/template_url.h"
20 #include "chrome/browser/search_engines/template_url_model.h"
21 #include "chrome/browser/ui/options/options_window.h"
22 #include "grit/chromium_strings.h"
23 #include "grit/generated_resources.h"
24 #include "grit/google_chrome_strings.h"
25 #include "grit/locale_settings.h"
26 #include "grit/theme_resources.h"
27 #include "ui/base/accessibility/accessible_view_state.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/gfx/canvas.h"
31 #include "ui/gfx/font.h"
32 #include "views/controls/button/button.h"
33 #include "views/controls/image_view.h"
34 #include "views/controls/label.h"
35 #include "views/controls/separator.h"
36 #include "views/focus/accelerator_handler.h"
37 #include "views/layout/layout_constants.h"
38 #include "views/view_text_utils.h"
39 #include "views/widget/widget.h"
40 #include "views/window/window.h"
41
42 namespace {
43
44 // Size to scale logos down to if showing 4 instead of 3 choices. Logo images
45 // are all originally sized at 180 x 120 pixels, with the logo text baseline
46 // located 74 pixels beneath the top of the image.
47 const int kSmallLogoWidth = 132;
48 const int kSmallLogoHeight = 88;
49
50 // Used to pad text label height so it fits nicely in view.
51 const int kLabelPadding = 25;
52
53 } // namespace
54
55 namespace first_run {
56
ShowFirstRunDialog(Profile * profile,bool randomize_search_engine_experiment)57 void ShowFirstRunDialog(Profile* profile,
58 bool randomize_search_engine_experiment) {
59 // If the default search is managed via policy, we don't ask the user to
60 // choose.
61 TemplateURLModel* model = profile->GetTemplateURLModel();
62 if (FirstRun::SearchEngineSelectorDisallowed() || !model ||
63 model->is_default_search_managed()) {
64 return;
65 }
66
67 views::Window* window = views::Window::CreateChromeWindow(
68 NULL,
69 gfx::Rect(),
70 new FirstRunSearchEngineView(
71 profile, randomize_search_engine_experiment));
72 DCHECK(window);
73
74 window->SetIsAlwaysOnTop(true);
75 window->Show();
76 views::AcceleratorHandler accelerator_handler;
77 MessageLoopForUI::current()->Run(&accelerator_handler);
78 window->CloseWindow();
79 }
80
81 } // namespace first_run
82
SearchEngineChoice(views::ButtonListener * listener,const TemplateURL * search_engine,bool use_small_logos)83 SearchEngineChoice::SearchEngineChoice(views::ButtonListener* listener,
84 const TemplateURL* search_engine,
85 bool use_small_logos)
86 : NativeButton(
87 listener,
88 UTF16ToWide(l10n_util::GetStringUTF16(IDS_FR_SEARCH_CHOOSE))),
89 is_image_label_(false),
90 search_engine_(search_engine),
91 slot_(0) {
92 bool use_images = false;
93 #if defined(GOOGLE_CHROME_BUILD)
94 use_images = true;
95 #endif
96 int logo_id = search_engine_->logo_id();
97 if (use_images && logo_id != kNoSearchEngineLogo) {
98 is_image_label_ = true;
99 views::ImageView* logo_image = new views::ImageView();
100 SkBitmap* logo_bmp =
101 ResourceBundle::GetSharedInstance().GetBitmapNamed(logo_id);
102 logo_image->SetImage(logo_bmp);
103 if (use_small_logos)
104 logo_image->SetImageSize(gfx::Size(kSmallLogoWidth, kSmallLogoHeight));
105 // Tooltip text provides accessibility for low-vision users.
106 logo_image->SetTooltipText(search_engine_->short_name());
107 choice_view_ = logo_image;
108 } else {
109 // No logo -- we must show a text label.
110 views::Label* logo_label = new views::Label(search_engine_->short_name());
111 logo_label->SetColor(SK_ColorDKGRAY);
112 logo_label->SetFont(logo_label->font().DeriveFont(3, gfx::Font::BOLD));
113 logo_label->SetHorizontalAlignment(views::Label::ALIGN_CENTER);
114 logo_label->SetTooltipText(search_engine_->short_name());
115 logo_label->SetMultiLine(true);
116 logo_label->SizeToFit(kSmallLogoWidth);
117 choice_view_ = logo_label;
118 }
119
120 // The accessible name of the button provides accessibility for
121 // screenreaders. It uses the browser name rather than the text of the
122 // button "Choose", since it's not obvious to a screenreader user which
123 // browser each button corresponds to.
124 SetAccessibleName(WideToUTF16Hack(search_engine_->short_name()));
125 }
126
GetChoiceViewWidth()127 int SearchEngineChoice::GetChoiceViewWidth() {
128 if (is_image_label_)
129 return choice_view_->GetPreferredSize().width();
130 else
131 return kSmallLogoWidth;
132 }
133
GetChoiceViewHeight()134 int SearchEngineChoice::GetChoiceViewHeight() {
135 if (!is_image_label_) {
136 // Labels need to be padded to look nicer.
137 return choice_view_->GetPreferredSize().height() + kLabelPadding;
138 } else {
139 return choice_view_->GetPreferredSize().height();
140 }
141 }
142
SetChoiceViewBounds(int x,int y,int width,int height)143 void SearchEngineChoice::SetChoiceViewBounds(int x, int y, int width,
144 int height) {
145 choice_view_->SetBounds(x, y, width, height);
146 }
147
FirstRunSearchEngineView(Profile * profile,bool randomize)148 FirstRunSearchEngineView::FirstRunSearchEngineView(
149 Profile* profile, bool randomize)
150 : background_image_(NULL),
151 profile_(profile),
152 text_direction_is_rtl_(base::i18n::IsRTL()),
153 randomize_(randomize) {
154 // Don't show ourselves until all the search engines have loaded from
155 // the profile -- otherwise we have nothing to show.
156 SetVisible(false);
157
158 // Start loading the search engines for the given profile.
159 search_engines_model_ = profile_->GetTemplateURLModel();
160 if (search_engines_model_) {
161 DCHECK(!search_engines_model_->loaded());
162 search_engines_model_->AddObserver(this);
163 search_engines_model_->Load();
164 } else {
165 NOTREACHED();
166 }
167 SetupControls();
168 }
169
~FirstRunSearchEngineView()170 FirstRunSearchEngineView::~FirstRunSearchEngineView() {
171 search_engines_model_->RemoveObserver(this);
172 }
173
ButtonPressed(views::Button * sender,const views::Event & event)174 void FirstRunSearchEngineView::ButtonPressed(views::Button* sender,
175 const views::Event& event) {
176 SearchEngineChoice* choice = static_cast<SearchEngineChoice*>(sender);
177 TemplateURLModel* template_url_model = profile_->GetTemplateURLModel();
178 DCHECK(template_url_model);
179 template_url_model->SetSearchEngineDialogSlot(choice->slot());
180 const TemplateURL* default_search = choice->GetSearchEngine();
181 if (default_search)
182 template_url_model->SetDefaultSearchProvider(default_search);
183
184 MessageLoop::current()->Quit();
185 }
186
OnPaint(gfx::Canvas * canvas)187 void FirstRunSearchEngineView::OnPaint(gfx::Canvas* canvas) {
188 // Fill in behind the background image with the standard gray toolbar color.
189 canvas->FillRectInt(SkColorSetRGB(237, 238, 237), 0, 0, width(),
190 background_image_->height());
191 // The rest of the dialog background should be white.
192 DCHECK(height() > background_image_->height());
193 canvas->FillRectInt(SK_ColorWHITE, 0, background_image_->height(), width(),
194 height() - background_image_->height());
195 }
196
OnTemplateURLModelChanged()197 void FirstRunSearchEngineView::OnTemplateURLModelChanged() {
198 using views::ImageView;
199
200 // We only watch the search engine model change once, on load. Remove
201 // observer so we don't try to redraw if engines change under us.
202 search_engines_model_->RemoveObserver(this);
203
204 // Add search engines in search_engines_model_ to buttons list. The
205 // first three will always be from prepopulated data.
206 std::vector<const TemplateURL*> template_urls =
207 search_engines_model_->GetTemplateURLs();
208
209 // If we have fewer than two search engines, end search engine dialog
210 // immediately, leaving imported default search engine setting intact.
211 if (template_urls.size() < 2) {
212 MessageLoop::current()->Quit();
213 return;
214 }
215
216 std::vector<const TemplateURL*>::iterator search_engine_iter;
217
218 // Is user's default search engine included in first three prepopulated
219 // set? If not, we need to expand the dialog to include a fourth engine.
220 const TemplateURL* default_search_engine =
221 search_engines_model_->GetDefaultSearchProvider();
222 // If the user's default choice is not in the first three search engines
223 // in template_urls, store it in |default_choice| and provide it as a
224 // fourth option.
225 SearchEngineChoice* default_choice = NULL;
226
227 // First, see if we have 4 logos to show (in which case we use small logos).
228 // We show 4 logos when the default search engine the user has chosen is
229 // not one of the first three prepopulated engines.
230 if (template_urls.size() > 3) {
231 for (search_engine_iter = template_urls.begin() + 3;
232 search_engine_iter != template_urls.end();
233 ++search_engine_iter) {
234 if (default_search_engine == *search_engine_iter) {
235 default_choice = new SearchEngineChoice(this, *search_engine_iter,
236 true);
237 }
238 }
239 }
240
241 // Now that we know what size the logos should be, create new search engine
242 // choices for the view. If there are 2 search engines, only show 2
243 // choices; for 3 or more, show 3 (unless the default is not one of the
244 // top 3, in which case show 4).
245 for (search_engine_iter = template_urls.begin();
246 search_engine_iter < template_urls.begin() +
247 (template_urls.size() < 3 ? 2 : 3);
248 ++search_engine_iter) {
249 // Push first three engines into buttons:
250 SearchEngineChoice* choice = new SearchEngineChoice(this,
251 *search_engine_iter, default_choice != NULL);
252 search_engine_choices_.push_back(choice);
253 AddChildView(choice->GetView()); // The logo or text view.
254 AddChildView(choice); // The button associated with the choice.
255 }
256 // Push the default choice to the fourth position.
257 if (default_choice) {
258 search_engine_choices_.push_back(default_choice);
259 AddChildView(default_choice->GetView()); // The logo or text view.
260 AddChildView(default_choice); // The button associated with the choice.
261 }
262
263 // Randomize order of logos if option has been set.
264 if (randomize_) {
265 std::random_shuffle(search_engine_choices_.begin(),
266 search_engine_choices_.end(),
267 base::RandGenerator);
268 // Assign to each choice the position in which it is shown on the screen.
269 std::vector<SearchEngineChoice*>::iterator it;
270 int slot = 0;
271 for (it = search_engine_choices_.begin();
272 it != search_engine_choices_.end();
273 it++) {
274 (*it)->set_slot(slot++);
275 }
276 }
277
278 // Now that we know how many logos to show, lay out and become visible.
279 SetVisible(true);
280 Layout();
281 SchedulePaint();
282
283 // If the widget has detected that a screenreader is running, change the
284 // button names from "Choose" to the name of the search engine. This works
285 // around a bug that JAWS ignores the accessible name of a native button.
286 if (GetWidget() && GetWidget()->IsAccessibleWidget()) {
287 std::vector<SearchEngineChoice*>::iterator it;
288 for (it = search_engine_choices_.begin();
289 it != search_engine_choices_.end();
290 it++) {
291 (*it)->SetLabel((*it)->GetSearchEngine()->short_name());
292 }
293 }
294
295 // This will tell screenreaders that they should read the full text
296 // of this dialog to the user now (rather than waiting for the user
297 // to explore it).
298 GetWidget()->NotifyAccessibilityEvent(
299 this, ui::AccessibilityTypes::EVENT_ALERT, true);
300 }
301
GetPreferredSize()302 gfx::Size FirstRunSearchEngineView::GetPreferredSize() {
303 return views::Window::GetLocalizedContentsSize(
304 IDS_FIRSTRUN_SEARCH_ENGINE_SELECTION_WIDTH_CHARS,
305 IDS_FIRSTRUN_SEARCH_ENGINE_SELECTION_HEIGHT_LINES);
306 }
307
SetupControls()308 void FirstRunSearchEngineView::SetupControls() {
309 using views::Background;
310 using views::ImageView;
311 using views::Label;
312 using views::NativeButton;
313
314 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
315 background_image_ = new views::ImageView();
316 background_image_->SetImage(rb.GetBitmapNamed(IDR_SEARCH_ENGINE_DIALOG_TOP));
317 background_image_->EnableCanvasFlippingForRTLUI(true);
318 if (text_direction_is_rtl_) {
319 background_image_->SetHorizontalAlignment(ImageView::LEADING);
320 } else {
321 background_image_->SetHorizontalAlignment(ImageView::TRAILING);
322 }
323
324 AddChildView(background_image_);
325
326 int label_width = GetPreferredSize().width() - 2 * views::kPanelHorizMargin;
327
328 // Add title and text asking the user to choose a search engine:
329 title_label_ = new Label(UTF16ToWide(l10n_util::GetStringUTF16(
330 IDS_FR_SEARCH_MAIN_LABEL)));
331 title_label_->SetColor(SK_ColorBLACK);
332 title_label_->SetFont(title_label_->font().DeriveFont(3, gfx::Font::BOLD));
333 title_label_->SetMultiLine(true);
334 title_label_->SetHorizontalAlignment(Label::ALIGN_LEFT);
335 title_label_->SizeToFit(label_width);
336 AddChildView(title_label_);
337
338 text_label_ = new Label(UTF16ToWide(l10n_util::GetStringFUTF16(
339 IDS_FR_SEARCH_TEXT,
340 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))));
341 text_label_->SetColor(SK_ColorBLACK);
342 text_label_->SetFont(text_label_->font().DeriveFont(1, gfx::Font::NORMAL));
343 text_label_->SetMultiLine(true);
344 text_label_->SetHorizontalAlignment(Label::ALIGN_LEFT);
345 text_label_->SizeToFit(label_width);
346 AddChildView(text_label_);
347 }
348
Layout()349 void FirstRunSearchEngineView::Layout() {
350 // Disable the close button.
351 GetWindow()->EnableClose(false);
352
353 gfx::Size pref_size = background_image_->GetPreferredSize();
354 background_image_->SetBounds(0, 0, GetPreferredSize().width(),
355 pref_size.height());
356
357 // General vertical spacing between elements:
358 const int kVertSpacing = 8;
359 // Percentage of vertical space around logos to use for upper padding.
360 const double kUpperPaddingPercent = 0.4;
361
362 int num_choices = search_engine_choices_.size();
363 int label_width = GetPreferredSize().width() - 2 * views::kPanelHorizMargin;
364 int label_height = GetPreferredSize().height() - 2 * views::kPanelVertMargin;
365
366 // Set title.
367 title_label_->SetBounds(
368 views::kPanelHorizMargin,
369 pref_size.height() / 2 - title_label_->GetPreferredSize().height() / 2,
370 label_width,
371 title_label_->GetPreferredSize().height());
372
373 int next_v_space = background_image_->height() + kVertSpacing * 2;
374
375 // Set text describing search engine hooked into omnibox.
376 text_label_->SetBounds(views::kPanelHorizMargin,
377 next_v_space,
378 label_width,
379 text_label_->GetPreferredSize().height());
380 next_v_space = text_label_->y() +
381 text_label_->height() + kVertSpacing;
382
383 // Logos and buttons
384 if (num_choices > 0) {
385 // All search engine logos are sized the same, so the size of the first is
386 // generally valid as the size of all.
387 int logo_width = search_engine_choices_[0]->GetChoiceViewWidth();
388 int logo_height = search_engine_choices_[0]->GetChoiceViewHeight();
389 int button_width = search_engine_choices_[0]->GetPreferredSize().width();
390 int button_height = search_engine_choices_[0]->GetPreferredSize().height();
391
392 int logo_section_height = logo_height + kVertSpacing + button_height;
393 // Upper logo margin gives the amount of whitespace between the text label
394 // and the logo field. The total amount of whitespace available is equal
395 // to the height of the whole label subtracting the heights of the logo
396 // section itself, the top image, the text label, and vertical spacing
397 // between those elements.
398 int upper_logo_margin =
399 static_cast<int>((label_height - logo_section_height -
400 background_image_->height() - text_label_->height()
401 - kVertSpacing + views::kPanelVertMargin) * kUpperPaddingPercent);
402
403 next_v_space = text_label_->y() + text_label_->height() +
404 upper_logo_margin;
405
406 // The search engine logos (which all have equal size):
407 int logo_padding =
408 (label_width - (num_choices * logo_width)) / (num_choices + 1);
409
410 search_engine_choices_[0]->SetChoiceViewBounds(
411 views::kPanelHorizMargin + logo_padding, next_v_space, logo_width,
412 logo_height);
413
414 int next_h_space = search_engine_choices_[0]->GetView()->x() +
415 logo_width + logo_padding;
416 search_engine_choices_[1]->SetChoiceViewBounds(
417 next_h_space, next_v_space, logo_width, logo_height);
418
419 next_h_space = search_engine_choices_[1]->GetView()->x() + logo_width +
420 logo_padding;
421 if (num_choices > 2) {
422 search_engine_choices_[2]->SetChoiceViewBounds(
423 next_h_space, next_v_space, logo_width, logo_height);
424 }
425
426 if (num_choices > 3) {
427 next_h_space = search_engine_choices_[2]->GetView()->x() + logo_width +
428 logo_padding;
429 search_engine_choices_[3]->SetChoiceViewBounds(
430 next_h_space, next_v_space, logo_width, logo_height);
431 }
432
433 next_v_space = search_engine_choices_[0]->GetView()->y() + logo_height +
434 kVertSpacing;
435
436 // The buttons for search engine selection:
437 int button_padding = logo_padding + logo_width / 2 - button_width / 2;
438
439 search_engine_choices_[0]->SetBounds(
440 views::kPanelHorizMargin + button_padding, next_v_space,
441 button_width, button_height);
442
443 next_h_space = search_engine_choices_[0]->x() + logo_width + logo_padding;
444 search_engine_choices_[1]->SetBounds(next_h_space, next_v_space,
445 button_width, button_height);
446 next_h_space = search_engine_choices_[1]->x() + logo_width + logo_padding;
447 if (num_choices > 2) {
448 search_engine_choices_[2]->SetBounds(next_h_space, next_v_space,
449 button_width, button_height);
450 }
451
452 if (num_choices > 3) {
453 next_h_space = search_engine_choices_[2]->x() + logo_width +
454 logo_padding;
455 search_engine_choices_[3]->SetBounds(next_h_space, next_v_space,
456 button_width, button_height);
457 }
458 } // if (search_engine_choices.size() > 0)
459 }
460
GetAccessibleState(ui::AccessibleViewState * state)461 void FirstRunSearchEngineView::GetAccessibleState(
462 ui::AccessibleViewState* state) {
463 state->role = ui::AccessibilityTypes::ROLE_ALERT;
464 }
465
GetWindowTitle() const466 std::wstring FirstRunSearchEngineView::GetWindowTitle() const {
467 return UTF16ToWide(l10n_util::GetStringUTF16(IDS_FIRSTRUN_DLG_TITLE));
468 }
469