Home | History | Annotate | Download | only in views
      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