[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / views / omnibox / omnibox_result_view.cc
blob15f6cf6f3840cd95b0d97c4a1a015b1a18f582c9
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.
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
12 #include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
14 #include <algorithm> // NOLINT
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"
36 using ui::NativeTheme;
38 namespace {
40 // The minimum distance between the top and bottom of the {icon|text} and the
41 // top or bottom of the row.
42 const int kMinimumIconVerticalPadding = 2;
43 const int kMinimumTextVerticalPadding = 3;
45 // A mapping from OmniboxResultView's ResultViewState/ColorKind types to
46 // NativeTheme colors.
47 struct 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 },
84 } // namespace
86 ////////////////////////////////////////////////////////////////////////////////
87 // OmniboxResultView, public:
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.
93 class OmniboxResultView::MirroringContext {
94 public:
95 MirroringContext() : center_(0), right_(0) {}
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;
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;
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;
118 private:
119 int center_;
120 int right_;
122 DISALLOW_COPY_AND_ASSIGN(MirroringContext);
125 OmniboxResultView::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();
149 keyword_icon_->set_owned_by_client();
150 keyword_icon_->EnableCanvasFlippingForRTLUI(true);
151 keyword_icon_->SetImage(GetKeywordIcon());
152 keyword_icon_->SizeToPreferredSize();
155 OmniboxResultView::~OmniboxResultView() {
158 SkColor 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);
168 NOTREACHED();
169 return SK_ColorRED;
172 void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
173 match_ = match;
174 ResetRenderTexts();
175 animation_->Reset();
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());
186 Layout();
189 void OmniboxResultView::ShowKeyword(bool show_keyword) {
190 if (show_keyword)
191 animation_->Show();
192 else
193 animation_->Hide();
196 void 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();
205 gfx::Size OmniboxResultView::GetPreferredSize() const {
206 return gfx::Size(0, std::max(
207 default_icon_size_ + (kMinimumIconVerticalPadding * 2),
208 GetTextHeight() + (minimum_text_vertical_padding_ * 2)));
211 ////////////////////////////////////////////////////////////////////////////////
212 // OmniboxResultView, protected:
214 OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
215 if (model_->IsSelectedIndex(model_index_))
216 return SELECTED;
217 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
220 int OmniboxResultView::GetTextHeight() const {
221 return font_height_;
224 void 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();
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();
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);
250 x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width);
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);
260 int 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());
270 const int remaining_width = mirroring_context_->remaining_width(x);
271 int right_x = x + max_width;
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);
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;
291 const int max_match_contents_width = model_->max_match_contents_width();
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;
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);
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;
338 scoped_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_AT_END);
343 render_text->SetFontList(font_list_);
344 render_text->SetText(text);
345 return render_text.Pass();
348 scoped_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;
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);
364 // Calculate style-related data.
365 if (classifications[i].style & ACMatchClassification::MATCH)
366 render_text->ApplyStyle(gfx::BOLD, true, current_range);
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;
387 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range);
389 return render_text.Pass();
392 int OmniboxResultView::GetMatchContentsWidth() const {
393 InitContentsRenderTextIfNecessary();
394 return contents_rendertext_ ? contents_rendertext_->GetContentWidth() : 0;
397 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in
398 // the omnibox dropdown. Consider sharing the result.
399 int 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;
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);
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());
419 return is_ui_rtl ?
420 (input_render_text->GetContentWidth() - start_padding) : start_padding;
423 // static
424 int OmniboxResultView::default_icon_size_ = 0;
426 gfx::ImageSkia OmniboxResultView::GetIcon() const {
427 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
428 if (!image.IsEmpty())
429 return image.AsImageSkia();
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;
452 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
455 const 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);
462 bool OmniboxResultView::ShowOnlyKeywordMatch() const {
463 return match_.associated_keyword &&
464 (keyword_icon_->x() <= icon_bounds_.right());
467 void 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();
475 void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
476 if (!contents_rendertext_) {
477 contents_rendertext_.reset(
478 CreateClassifiedRenderText(
479 match_.contents, match_.contents_class, false).release());
483 void OmniboxResultView::Layout() {
484 const gfx::ImageSkia icon = GetIcon();
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());
491 int text_x = edge_item_padding_ + default_icon_size_ + item_padding_;
492 int text_width = width() - text_x - edge_item_padding_;
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_;
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));
510 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height());
513 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
514 animation_->SetSlideDuration(width() / 4);
517 void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
518 const ResultViewState state = GetState();
519 if (state != NORMAL)
520 canvas->DrawColor(GetColor(state, BACKGROUND));
522 // NOTE: While animating the keyword match, both matches may be visible.
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());
535 PaintMatch(match_, contents_rendertext_.get(),
536 description_rendertext_.get(), canvas, x);
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());
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());
556 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(),
557 keyword_description_rendertext_.get(), canvas, x);
561 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
562 Layout();
563 SchedulePaint();