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 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 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 127 int SearchEngineChoice::GetChoiceViewWidth() { 128 if (is_image_label_) 129 return choice_view_->GetPreferredSize().width(); 130 else 131 return kSmallLogoWidth; 132 } 133 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 143 void SearchEngineChoice::SetChoiceViewBounds(int x, int y, int width, 144 int height) { 145 choice_view_->SetBounds(x, y, width, height); 146 } 147 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 170 FirstRunSearchEngineView::~FirstRunSearchEngineView() { 171 search_engines_model_->RemoveObserver(this); 172 } 173 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 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 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 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 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 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 461 void FirstRunSearchEngineView::GetAccessibleState( 462 ui::AccessibleViewState* state) { 463 state->role = ui::AccessibilityTypes::ROLE_ALERT; 464 } 465 466 std::wstring FirstRunSearchEngineView::GetWindowTitle() const { 467 return UTF16ToWide(l10n_util::GetStringUTF16(IDS_FIRSTRUN_DLG_TITLE)); 468 } 469