omnibox_result_view.cc revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
1// Copyright (c) 2012 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// For WinDDK ATL compatibility, these ATL headers must come first.
6#include "build/build_config.h"
7#if defined(OS_WIN)
8#include <atlbase.h>  // NOLINT
9#include <atlwin.h>  // NOLINT
10#endif
11
12#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
13
14#include <algorithm>  // NOLINT
15
16#include "base/i18n/bidi_line_iterator.h"
17#include "base/memory/scoped_vector.h"
18#include "base/strings/string_number_conversions.h"
19#include "base/strings/string_util.h"
20#include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
21#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
22#include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h"
23#include "grit/generated_resources.h"
24#include "grit/theme_resources.h"
25#include "ui/base/l10n/l10n_util.h"
26#include "ui/base/theme_provider.h"
27#include "ui/gfx/canvas.h"
28#include "ui/gfx/color_utils.h"
29#include "ui/gfx/image/image.h"
30#include "ui/gfx/range/range.h"
31#include "ui/gfx/render_text.h"
32#include "ui/gfx/text_elider.h"
33#include "ui/gfx/text_utils.h"
34#include "ui/native_theme/native_theme.h"
35
36using ui::NativeTheme;
37
38namespace {
39
40// The minimum distance between the top and bottom of the {icon|text} and the
41// top or bottom of the row.
42const int kMinimumIconVerticalPadding = 2;
43const int kMinimumTextVerticalPadding = 3;
44
45// A mapping from OmniboxResultView's ResultViewState/ColorKind types to
46// NativeTheme colors.
47struct TranslationTable {
48  ui::NativeTheme::ColorId id;
49  OmniboxResultView::ResultViewState state;
50  OmniboxResultView::ColorKind kind;
51} static const kTranslationTable[] = {
52  { NativeTheme::kColorId_ResultsTableNormalBackground,
53    OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND },
54  { NativeTheme::kColorId_ResultsTableHoveredBackground,
55    OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND },
56  { NativeTheme::kColorId_ResultsTableSelectedBackground,
57    OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND },
58  { NativeTheme::kColorId_ResultsTableNormalText,
59    OmniboxResultView::NORMAL, OmniboxResultView::TEXT },
60  { NativeTheme::kColorId_ResultsTableHoveredText,
61    OmniboxResultView::HOVERED, OmniboxResultView::TEXT },
62  { NativeTheme::kColorId_ResultsTableSelectedText,
63    OmniboxResultView::SELECTED, OmniboxResultView::TEXT },
64  { NativeTheme::kColorId_ResultsTableNormalDimmedText,
65    OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT },
66  { NativeTheme::kColorId_ResultsTableHoveredDimmedText,
67    OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT },
68  { NativeTheme::kColorId_ResultsTableSelectedDimmedText,
69    OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT },
70  { NativeTheme::kColorId_ResultsTableNormalUrl,
71    OmniboxResultView::NORMAL, OmniboxResultView::URL },
72  { NativeTheme::kColorId_ResultsTableHoveredUrl,
73    OmniboxResultView::HOVERED, OmniboxResultView::URL },
74  { NativeTheme::kColorId_ResultsTableSelectedUrl,
75    OmniboxResultView::SELECTED, OmniboxResultView::URL },
76  { NativeTheme::kColorId_ResultsTableNormalDivider,
77    OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER },
78  { NativeTheme::kColorId_ResultsTableHoveredDivider,
79    OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER },
80  { NativeTheme::kColorId_ResultsTableSelectedDivider,
81    OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER },
82};
83
84}  // namespace
85
86////////////////////////////////////////////////////////////////////////////////
87// OmniboxResultView, public:
88
89// This class is a utility class for calculations affected by whether the result
90// view is horizontally mirrored.  The drawing functions can be written as if
91// all drawing occurs left-to-right, and then use this class to get the actual
92// coordinates to begin drawing onscreen.
93class OmniboxResultView::MirroringContext {
94 public:
95  MirroringContext() : center_(0), right_(0) {}
96
97  // Tells the mirroring context to use the provided range as the physical
98  // bounds of the drawing region.  When coordinate mirroring is needed, the
99  // mirror point will be the center of this range.
100  void Initialize(int x, int width) {
101    center_ = x + width / 2;
102    right_ = x + width;
103  }
104
105  // Given a logical range within the drawing region, returns the coordinate of
106  // the possibly-mirrored "left" side.  (This functions exactly like
107  // View::MirroredLeftPointForRect().)
108  int mirrored_left_coord(int left, int right) const {
109    return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
110  }
111
112  // Given a logical coordinate within the drawing region, returns the remaining
113  // width available.
114  int remaining_width(int x) const {
115    return right_ - x;
116  }
117
118 private:
119  int center_;
120  int right_;
121
122  DISALLOW_COPY_AND_ASSIGN(MirroringContext);
123};
124
125OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
126                                     int model_index,
127                                     LocationBarView* location_bar_view,
128                                     const gfx::FontList& font_list)
129    : edge_item_padding_(LocationBarView::kItemPadding),
130      item_padding_(LocationBarView::kItemPadding),
131      minimum_text_vertical_padding_(kMinimumTextVerticalPadding),
132      model_(model),
133      model_index_(model_index),
134      location_bar_view_(location_bar_view),
135      font_list_(font_list),
136      font_height_(
137          std::max(font_list.GetHeight(),
138                   font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())),
139      mirroring_context_(new MirroringContext()),
140      keyword_icon_(new views::ImageView()),
141      animation_(new gfx::SlideAnimation(this)) {
142  CHECK_GE(model_index, 0);
143  if (default_icon_size_ == 0) {
144    default_icon_size_ =
145        location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
146            AutocompleteMatch::TypeToIcon(
147                AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width();
148  }
149  keyword_icon_->set_owned_by_client();
150  keyword_icon_->EnableCanvasFlippingForRTLUI(true);
151  keyword_icon_->SetImage(GetKeywordIcon());
152  keyword_icon_->SizeToPreferredSize();
153}
154
155OmniboxResultView::~OmniboxResultView() {
156}
157
158SkColor OmniboxResultView::GetColor(
159    ResultViewState state,
160    ColorKind kind) const {
161  for (size_t i = 0; i < arraysize(kTranslationTable); ++i) {
162    if (kTranslationTable[i].state == state &&
163        kTranslationTable[i].kind == kind) {
164      return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id);
165    }
166  }
167
168  NOTREACHED();
169  return SK_ColorRED;
170}
171
172void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
173  match_ = match;
174  ResetRenderTexts();
175  animation_->Reset();
176
177  AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get();
178  if (associated_keyword_match) {
179    keyword_icon_->SetImage(GetKeywordIcon());
180    if (!keyword_icon_->parent())
181      AddChildView(keyword_icon_.get());
182  } else if (keyword_icon_->parent()) {
183    RemoveChildView(keyword_icon_.get());
184  }
185
186  Layout();
187}
188
189void OmniboxResultView::ShowKeyword(bool show_keyword) {
190  if (show_keyword)
191    animation_->Show();
192  else
193    animation_->Hide();
194}
195
196void OmniboxResultView::Invalidate() {
197  keyword_icon_->SetImage(GetKeywordIcon());
198  // While the text in the RenderTexts may not have changed, the styling
199  // (color/bold) may need to change. So we reset them to cause them to be
200  // recomputed in OnPaint().
201  ResetRenderTexts();
202  SchedulePaint();
203}
204
205gfx::Size OmniboxResultView::GetPreferredSize() const {
206  return gfx::Size(0, std::max(
207      default_icon_size_ + (kMinimumIconVerticalPadding * 2),
208      GetTextHeight() + (minimum_text_vertical_padding_ * 2)));
209}
210
211////////////////////////////////////////////////////////////////////////////////
212// OmniboxResultView, protected:
213
214OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
215  if (model_->IsSelectedIndex(model_index_))
216    return SELECTED;
217  return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
218}
219
220int OmniboxResultView::GetTextHeight() const {
221  return font_height_;
222}
223
224void OmniboxResultView::PaintMatch(
225    const AutocompleteMatch& match,
226    gfx::RenderText* contents,
227    gfx::RenderText* description,
228    gfx::Canvas* canvas,
229    int x) const {
230  int y = text_bounds_.y();
231
232  if (!separator_rendertext_) {
233    const base::string16& separator =
234        l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
235    separator_rendertext_.reset(CreateRenderText(separator).release());
236    separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT));
237    separator_width_ = separator_rendertext_->GetContentWidth();
238  }
239
240  int contents_max_width, description_max_width;
241  OmniboxPopupModel::ComputeMatchMaxWidths(
242      contents->GetContentWidth(),
243      separator_width_,
244      description ? description->GetContentWidth() : 0,
245      mirroring_context_->remaining_width(x),
246      !AutocompleteMatch::IsSearchType(match.type),
247      &contents_max_width,
248      &description_max_width);
249
250  x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width);
251
252  if (description_max_width != 0) {
253    x = DrawRenderText(match, separator_rendertext_.get(), false, canvas, x, y,
254                       separator_width_);
255    DrawRenderText(match, description, false, canvas, x, y,
256                   description_max_width);
257  }
258}
259
260int OmniboxResultView::DrawRenderText(
261    const AutocompleteMatch& match,
262    gfx::RenderText* render_text,
263    bool contents,
264    gfx::Canvas* canvas,
265    int x,
266    int y,
267    int max_width) const {
268  DCHECK(!render_text->text().empty());
269
270  const int remaining_width = mirroring_context_->remaining_width(x);
271  int right_x = x + max_width;
272
273  // Infinite suggestions should appear with the leading ellipses vertically
274  // stacked.
275  if (contents &&
276      (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)) {
277    // When the directionality of suggestion doesn't match the UI, we try to
278    // vertically stack the ellipsis by restricting the end edge (right_x).
279    const bool is_ui_rtl = base::i18n::IsRTL();
280    const bool is_match_contents_rtl =
281        (render_text->GetTextDirection() == base::i18n::RIGHT_TO_LEFT);
282    const int offset =
283        GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl);
284
285    scoped_ptr<gfx::RenderText> prefix_render_text(
286        CreateRenderText(base::UTF8ToUTF16(
287            match.GetAdditionalInfo(kACMatchPropertyContentsPrefix))));
288    const int prefix_width = prefix_render_text->GetContentWidth();
289    int prefix_x = x;
290
291    const int max_match_contents_width = model_->max_match_contents_width();
292
293    if (is_ui_rtl != is_match_contents_rtl) {
294      // RTL infinite suggestions appear near the left edge in LTR UI, while LTR
295      // infinite suggestions appear near the right edge in RTL UI. This is
296      // against the natural horizontal alignment of the text. We reduce the
297      // width of the box for suggestion display, so that the suggestions appear
298      // in correct confines.  This reduced width allows us to modify the text
299      // alignment (see below).
300      right_x = x + std::min(remaining_width - prefix_width,
301                             std::max(offset, max_match_contents_width));
302      prefix_x = right_x;
303      // We explicitly set the horizontal alignment so that when LTR suggestions
304      // show in RTL UI (or vice versa), their ellipses appear stacked in a
305      // single column.
306      render_text->SetHorizontalAlignment(
307          is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
308    } else {
309      // If the dropdown is wide enough, place the ellipsis at the position
310      // where the omitted text would have ended. Otherwise reduce the offset of
311      // the ellipsis such that the widest suggestion reaches the end of the
312      // dropdown.
313      const int start_offset = std::max(prefix_width,
314          std::min(remaining_width - max_match_contents_width, offset));
315      right_x = x + std::min(remaining_width, start_offset + max_width);
316      x += start_offset;
317      prefix_x = x - prefix_width;
318    }
319    prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ?
320        gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR);
321    prefix_render_text->SetHorizontalAlignment(
322          is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
323    prefix_render_text->SetDisplayRect(gfx::Rect(
324          mirroring_context_->mirrored_left_coord(
325              prefix_x, prefix_x + prefix_width), y,
326          prefix_width, height()));
327    prefix_render_text->Draw(canvas);
328  }
329
330  // Set the display rect to trigger eliding.
331  render_text->SetDisplayRect(gfx::Rect(
332      mirroring_context_->mirrored_left_coord(x, right_x), y,
333      right_x - x, height()));
334  render_text->Draw(canvas);
335  return right_x;
336}
337
338scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText(
339    const base::string16& text) const {
340  scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance());
341  render_text->SetCursorEnabled(false);
342  render_text->SetElideBehavior(gfx::ELIDE_TAIL);
343  render_text->SetFontList(font_list_);
344  render_text->SetText(text);
345  return render_text.Pass();
346}
347
348scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText(
349    const base::string16& text,
350    const ACMatchClassifications& classifications,
351    bool force_dim) const {
352  scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text));
353  const size_t text_length = render_text->text().length();
354  for (size_t i = 0; i < classifications.size(); ++i) {
355    const size_t text_start = classifications[i].offset;
356    if (text_start >= text_length)
357      break;
358
359    const size_t text_end = (i < (classifications.size() - 1)) ?
360        std::min(classifications[i + 1].offset, text_length) :
361        text_length;
362    const gfx::Range current_range(text_start, text_end);
363
364    // Calculate style-related data.
365    if (classifications[i].style & ACMatchClassification::MATCH)
366      render_text->ApplyStyle(gfx::BOLD, true, current_range);
367
368    ColorKind color_kind = TEXT;
369    if (classifications[i].style & ACMatchClassification::URL) {
370      color_kind = URL;
371      // Consider logical string for domain "ABC.comי/hello" where ABC are
372      // Hebrew (RTL) characters. This string should ideally show as
373      // "CBA.com/hello". If we do not force LTR on URL, it will appear as
374      // "com/hello.CBA".
375      // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs,
376      // but it still has some pitfalls like :
377      // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which
378      // really confuses the path hierarchy of the URL.
379      // Also, if the URL supports https, the appearance will change into LTR
380      // directionality.
381      // In conclusion, LTR rendering of URL is probably the safest bet.
382      render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR);
383    } else if (force_dim ||
384        (classifications[i].style & ACMatchClassification::DIM)) {
385      color_kind = DIMMED_TEXT;
386    }
387    render_text->ApplyColor(GetColor(GetState(), color_kind), current_range);
388  }
389  return render_text.Pass();
390}
391
392int OmniboxResultView::GetMatchContentsWidth() const {
393  InitContentsRenderTextIfNecessary();
394  return contents_rendertext_ ? contents_rendertext_->GetContentWidth() : 0;
395}
396
397// TODO(skanuj): This is probably identical across all OmniboxResultView rows in
398// the omnibox dropdown. Consider sharing the result.
399int OmniboxResultView::GetDisplayOffset(
400    const AutocompleteMatch& match,
401    bool is_ui_rtl,
402    bool is_match_contents_rtl) const {
403  if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)
404    return 0;
405
406  const base::string16& input_text =
407      base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText));
408  int contents_start_index = 0;
409  base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex),
410                    &contents_start_index);
411
412  scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text));
413  const gfx::Range& glyph_bounds =
414      input_render_text->GetGlyphBounds(contents_start_index);
415  const int start_padding = is_match_contents_rtl ?
416      std::max(glyph_bounds.start(), glyph_bounds.end()) :
417      std::min(glyph_bounds.start(), glyph_bounds.end());
418
419  return is_ui_rtl ?
420      (input_render_text->GetContentWidth() - start_padding) : start_padding;
421}
422
423// static
424int OmniboxResultView::default_icon_size_ = 0;
425
426gfx::ImageSkia OmniboxResultView::GetIcon() const {
427  const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
428  if (!image.IsEmpty())
429    return image.AsImageSkia();
430
431  int icon = match_.starred ?
432      IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
433  if (GetState() == SELECTED) {
434    switch (icon) {
435      case IDR_OMNIBOX_EXTENSION_APP:
436        icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
437        break;
438      case IDR_OMNIBOX_HTTP:
439        icon = IDR_OMNIBOX_HTTP_SELECTED;
440        break;
441      case IDR_OMNIBOX_SEARCH:
442        icon = IDR_OMNIBOX_SEARCH_SELECTED;
443        break;
444      case IDR_OMNIBOX_STAR:
445        icon = IDR_OMNIBOX_STAR_SELECTED;
446        break;
447      default:
448        NOTREACHED();
449        break;
450    }
451  }
452  return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
453}
454
455const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const {
456  // NOTE: If we ever begin returning icons of varying size, then callers need
457  // to ensure that |keyword_icon_| is resized each time its image is reset.
458  return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
459      (GetState() == SELECTED) ? IDR_OMNIBOX_TTS_SELECTED : IDR_OMNIBOX_TTS);
460}
461
462bool OmniboxResultView::ShowOnlyKeywordMatch() const {
463  return match_.associated_keyword &&
464      (keyword_icon_->x() <= icon_bounds_.right());
465}
466
467void OmniboxResultView::ResetRenderTexts() const {
468  contents_rendertext_.reset();
469  description_rendertext_.reset();
470  separator_rendertext_.reset();
471  keyword_contents_rendertext_.reset();
472  keyword_description_rendertext_.reset();
473}
474
475void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
476  if (!contents_rendertext_) {
477    contents_rendertext_.reset(
478        CreateClassifiedRenderText(
479            match_.contents, match_.contents_class, false).release());
480  }
481}
482
483void OmniboxResultView::Layout() {
484  const gfx::ImageSkia icon = GetIcon();
485
486  icon_bounds_.SetRect(edge_item_padding_ +
487      ((icon.width() == default_icon_size_) ?
488          0 : LocationBarView::kIconInternalPadding),
489      (height() - icon.height()) / 2, icon.width(), icon.height());
490
491  int text_x = edge_item_padding_ + default_icon_size_ + item_padding_;
492  int text_width = width() - text_x - edge_item_padding_;
493
494  if (match_.associated_keyword.get()) {
495    const int kw_collapsed_size =
496        keyword_icon_->width() + edge_item_padding_;
497    const int max_kw_x = width() - kw_collapsed_size;
498    const int kw_x =
499        animation_->CurrentValueBetween(max_kw_x, edge_item_padding_);
500    const int kw_text_x = kw_x + keyword_icon_->width() + item_padding_;
501
502    text_width = kw_x - text_x - item_padding_;
503    keyword_text_bounds_.SetRect(
504        kw_text_x, 0,
505        std::max(width() - kw_text_x - edge_item_padding_, 0), height());
506    keyword_icon_->SetPosition(
507        gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2));
508  }
509
510  text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height());
511}
512
513void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
514  animation_->SetSlideDuration(width() / 4);
515}
516
517void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
518  const ResultViewState state = GetState();
519  if (state != NORMAL)
520    canvas->DrawColor(GetColor(state, BACKGROUND));
521
522  // NOTE: While animating the keyword match, both matches may be visible.
523
524  if (!ShowOnlyKeywordMatch()) {
525    canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_),
526                         icon_bounds_.y());
527    int x = GetMirroredXForRect(text_bounds_);
528    mirroring_context_->Initialize(x, text_bounds_.width());
529    InitContentsRenderTextIfNecessary();
530    if (!description_rendertext_ && !match_.description.empty()) {
531      description_rendertext_.reset(
532          CreateClassifiedRenderText(
533              match_.description, match_.description_class, true).release());
534    }
535    PaintMatch(match_, contents_rendertext_.get(),
536               description_rendertext_.get(), canvas, x);
537  }
538
539  AutocompleteMatch* keyword_match = match_.associated_keyword.get();
540  if (keyword_match) {
541    int x = GetMirroredXForRect(keyword_text_bounds_);
542    mirroring_context_->Initialize(x, keyword_text_bounds_.width());
543    if (!keyword_contents_rendertext_) {
544      keyword_contents_rendertext_.reset(
545          CreateClassifiedRenderText(keyword_match->contents,
546                                     keyword_match->contents_class,
547                                     false).release());
548    }
549    if (!keyword_description_rendertext_ &&
550        !keyword_match->description.empty()) {
551      keyword_description_rendertext_.reset(
552          CreateClassifiedRenderText(keyword_match->description,
553                                     keyword_match->description_class,
554                                     true).release());
555    }
556    PaintMatch(*keyword_match, keyword_contents_rendertext_.get(),
557               keyword_description_rendertext_.get(), canvas, x);
558  }
559}
560
561void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
562  Layout();
563  SchedulePaint();
564}
565