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