omnibox_result_view.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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_utils.h"
33#include "ui/native_theme/native_theme.h"
34
35using ui::NativeTheme;
36
37namespace {
38
39// The minimum distance between the top and bottom of the {icon|text} and the
40// top or bottom of the row.
41const int kMinimumIconVerticalPadding = 2;
42const int kMinimumTextVerticalPadding = 3;
43
44// A mapping from OmniboxResultView's ResultViewState/ColorKind types to
45// NativeTheme colors.
46struct TranslationTable {
47  ui::NativeTheme::ColorId id;
48  OmniboxResultView::ResultViewState state;
49  OmniboxResultView::ColorKind kind;
50} static const kTranslationTable[] = {
51  { NativeTheme::kColorId_ResultsTableNormalBackground,
52    OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND },
53  { NativeTheme::kColorId_ResultsTableHoveredBackground,
54    OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND },
55  { NativeTheme::kColorId_ResultsTableSelectedBackground,
56    OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND },
57  { NativeTheme::kColorId_ResultsTableNormalText,
58    OmniboxResultView::NORMAL, OmniboxResultView::TEXT },
59  { NativeTheme::kColorId_ResultsTableHoveredText,
60    OmniboxResultView::HOVERED, OmniboxResultView::TEXT },
61  { NativeTheme::kColorId_ResultsTableSelectedText,
62    OmniboxResultView::SELECTED, OmniboxResultView::TEXT },
63  { NativeTheme::kColorId_ResultsTableNormalDimmedText,
64    OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT },
65  { NativeTheme::kColorId_ResultsTableHoveredDimmedText,
66    OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT },
67  { NativeTheme::kColorId_ResultsTableSelectedDimmedText,
68    OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT },
69  { NativeTheme::kColorId_ResultsTableNormalUrl,
70    OmniboxResultView::NORMAL, OmniboxResultView::URL },
71  { NativeTheme::kColorId_ResultsTableHoveredUrl,
72    OmniboxResultView::HOVERED, OmniboxResultView::URL },
73  { NativeTheme::kColorId_ResultsTableSelectedUrl,
74    OmniboxResultView::SELECTED, OmniboxResultView::URL },
75  { NativeTheme::kColorId_ResultsTableNormalDivider,
76    OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER },
77  { NativeTheme::kColorId_ResultsTableHoveredDivider,
78    OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER },
79  { NativeTheme::kColorId_ResultsTableSelectedDivider,
80    OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER },
81};
82
83}  // namespace
84
85////////////////////////////////////////////////////////////////////////////////
86// OmniboxResultView, public:
87
88// This class is a utility class for calculations affected by whether the result
89// view is horizontally mirrored.  The drawing functions can be written as if
90// all drawing occurs left-to-right, and then use this class to get the actual
91// coordinates to begin drawing onscreen.
92class OmniboxResultView::MirroringContext {
93 public:
94  MirroringContext() : center_(0), right_(0) {}
95
96  // Tells the mirroring context to use the provided range as the physical
97  // bounds of the drawing region.  When coordinate mirroring is needed, the
98  // mirror point will be the center of this range.
99  void Initialize(int x, int width) {
100    center_ = x + width / 2;
101    right_ = x + width;
102  }
103
104  // Given a logical range within the drawing region, returns the coordinate of
105  // the possibly-mirrored "left" side.  (This functions exactly like
106  // View::MirroredLeftPointForRect().)
107  int mirrored_left_coord(int left, int right) const {
108    return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
109  }
110
111  // Given a logical coordinate within the drawing region, returns the remaining
112  // width available.
113  int remaining_width(int x) const {
114    return right_ - x;
115  }
116
117 private:
118  int center_;
119  int right_;
120
121  DISALLOW_COPY_AND_ASSIGN(MirroringContext);
122};
123
124OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
125                                     int model_index,
126                                     LocationBarView* location_bar_view,
127                                     const gfx::FontList& font_list)
128    : edge_item_padding_(LocationBarView::kItemPadding),
129      item_padding_(LocationBarView::kItemPadding),
130      minimum_text_vertical_padding_(kMinimumTextVerticalPadding),
131      model_(model),
132      model_index_(model_index),
133      location_bar_view_(location_bar_view),
134      font_list_(font_list),
135      font_height_(
136          std::max(font_list.GetHeight(),
137                   font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())),
138      mirroring_context_(new MirroringContext()),
139      keyword_icon_(new views::ImageView()),
140      animation_(new gfx::SlideAnimation(this)) {
141  CHECK_GE(model_index, 0);
142  if (default_icon_size_ == 0) {
143    default_icon_size_ =
144        location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
145            AutocompleteMatch::TypeToIcon(
146                AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width();
147  }
148  keyword_icon_->set_owned_by_client();
149  keyword_icon_->EnableCanvasFlippingForRTLUI(true);
150  keyword_icon_->SetImage(GetKeywordIcon());
151  keyword_icon_->SizeToPreferredSize();
152}
153
154OmniboxResultView::~OmniboxResultView() {
155}
156
157SkColor OmniboxResultView::GetColor(
158    ResultViewState state,
159    ColorKind kind) const {
160  for (size_t i = 0; i < arraysize(kTranslationTable); ++i) {
161    if (kTranslationTable[i].state == state &&
162        kTranslationTable[i].kind == kind) {
163      return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id);
164    }
165  }
166
167  NOTREACHED();
168  return SK_ColorRED;
169}
170
171void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
172  match_ = match;
173  ResetRenderTexts();
174  animation_->Reset();
175
176  AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get();
177  if (associated_keyword_match) {
178    keyword_icon_->SetImage(GetKeywordIcon());
179    if (!keyword_icon_->parent())
180      AddChildView(keyword_icon_.get());
181  } else if (keyword_icon_->parent()) {
182    RemoveChildView(keyword_icon_.get());
183  }
184
185  Layout();
186}
187
188void OmniboxResultView::ShowKeyword(bool show_keyword) {
189  if (show_keyword)
190    animation_->Show();
191  else
192    animation_->Hide();
193}
194
195void OmniboxResultView::Invalidate() {
196  keyword_icon_->SetImage(GetKeywordIcon());
197  // While the text in the RenderTexts may not have changed, the styling
198  // (color/bold) may need to change. So we reset them to cause them to be
199  // recomputed in OnPaint().
200  ResetRenderTexts();
201  SchedulePaint();
202}
203
204gfx::Size OmniboxResultView::GetPreferredSize() const {
205  return gfx::Size(0, std::max(
206      default_icon_size_ + (kMinimumIconVerticalPadding * 2),
207      GetTextHeight() + (minimum_text_vertical_padding_ * 2)));
208}
209
210////////////////////////////////////////////////////////////////////////////////
211// OmniboxResultView, protected:
212
213OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
214  if (model_->IsSelectedIndex(model_index_))
215    return SELECTED;
216  return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
217}
218
219int OmniboxResultView::GetTextHeight() const {
220  return font_height_;
221}
222
223void OmniboxResultView::PaintMatch(
224    const AutocompleteMatch& match,
225    gfx::RenderText* contents,
226    gfx::RenderText* description,
227    gfx::Canvas* canvas,
228    int x) const {
229  int y = text_bounds_.y();
230
231  if (!separator_rendertext_) {
232    const base::string16& separator =
233        l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
234    separator_rendertext_.reset(CreateRenderText(separator).release());
235    separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT));
236    separator_width_ = separator_rendertext_->GetContentWidth();
237  }
238
239  int contents_max_width, description_max_width;
240  OmniboxPopupModel::ComputeMatchMaxWidths(
241      contents->GetContentWidth(),
242      separator_width_,
243      description ? description->GetContentWidth() : 0,
244      mirroring_context_->remaining_width(x),
245      !AutocompleteMatch::IsSearchType(match.type),
246      &contents_max_width,
247      &description_max_width);
248
249  x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width);
250
251  if (description_max_width != 0) {
252    x = DrawRenderText(match, separator_rendertext_.get(), false, canvas, x, y,
253                       separator_width_);
254    DrawRenderText(match, description, false, canvas, x, y,
255                   description_max_width);
256  }
257}
258
259int OmniboxResultView::DrawRenderText(
260    const AutocompleteMatch& match,
261    gfx::RenderText* render_text,
262    bool contents,
263    gfx::Canvas* canvas,
264    int x,
265    int y,
266    int max_width) const {
267  DCHECK(!render_text->text().empty());
268
269  const int remaining_width = mirroring_context_->remaining_width(x);
270  int right_x = x + max_width;
271
272  // Infinite suggestions should appear with the leading ellipses vertically
273  // stacked.
274  if (contents &&
275      (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)) {
276    // When the directionality of suggestion doesn't match the UI, we try to
277    // vertically stack the ellipsis by restricting the end edge (right_x).
278    const bool is_ui_rtl = base::i18n::IsRTL();
279    const bool is_match_contents_rtl =
280        (render_text->GetTextDirection() == base::i18n::RIGHT_TO_LEFT);
281    const int offset =
282        GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl);
283
284    scoped_ptr<gfx::RenderText> prefix_render_text(
285        CreateRenderText(base::UTF8ToUTF16(
286            match.GetAdditionalInfo(kACMatchPropertyContentsPrefix))));
287    const int prefix_width = prefix_render_text->GetContentWidth();
288    int prefix_x = x;
289
290    const int max_match_contents_width = model_->max_match_contents_width();
291
292    if (is_ui_rtl != is_match_contents_rtl) {
293      // RTL infinite suggestions appear near the left edge in LTR UI, while LTR
294      // infinite suggestions appear near the right edge in RTL UI. This is
295      // against the natural horizontal alignment of the text. We reduce the
296      // width of the box for suggestion display, so that the suggestions appear
297      // in correct confines.  This reduced width allows us to modify the text
298      // alignment (see below).
299      right_x = x + std::min(remaining_width - prefix_width,
300                             std::max(offset, max_match_contents_width));
301      prefix_x = right_x;
302      // We explicitly set the horizontal alignment so that when LTR suggestions
303      // show in RTL UI (or vice versa), their ellipses appear stacked in a
304      // single column.
305      render_text->SetHorizontalAlignment(
306          is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
307    } else {
308      // If the dropdown is wide enough, place the ellipsis at the position
309      // where the omitted text would have ended. Otherwise reduce the offset of
310      // the ellipsis such that the widest suggestion reaches the end of the
311      // dropdown.
312      const int start_offset = std::max(prefix_width,
313          std::min(remaining_width - max_match_contents_width, offset));
314      right_x = x + std::min(remaining_width, start_offset + max_width);
315      x += start_offset;
316      prefix_x = x - prefix_width;
317    }
318    prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ?
319        gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR);
320    prefix_render_text->SetHorizontalAlignment(
321          is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
322    prefix_render_text->SetDisplayRect(gfx::Rect(
323          mirroring_context_->mirrored_left_coord(
324              prefix_x, prefix_x + prefix_width), y,
325          prefix_width, height()));
326    prefix_render_text->Draw(canvas);
327  }
328
329  // Set the display rect to trigger eliding.
330  render_text->SetDisplayRect(gfx::Rect(
331      mirroring_context_->mirrored_left_coord(x, right_x), y,
332      right_x - x, height()));
333  render_text->Draw(canvas);
334  return right_x;
335}
336
337scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText(
338    const base::string16& text) const {
339  scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance());
340  render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
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