MacViews: Use Mac's "Constrained Window Button" style for Button::STYLE_BUTTON LabelB...
[chromium-blink-merge.git] / chrome / browser / ui / views / omnibox / omnibox_result_view.cc
blob6dba94d54ec48676b13f88ac62383d9bb33538cd
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 "chrome/grit/generated_resources.h"
24 #include "grit/components_scaled_resources.h"
25 #include "grit/theme_resources.h"
26 #include "third_party/skia/include/core/SkColor.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/base/resource/resource_bundle.h"
29 #include "ui/base/theme_provider.h"
30 #include "ui/gfx/canvas.h"
31 #include "ui/gfx/color_utils.h"
32 #include "ui/gfx/image/image.h"
33 #include "ui/gfx/range/range.h"
34 #include "ui/gfx/render_text.h"
35 #include "ui/gfx/text_utils.h"
37 using ui::NativeTheme;
39 namespace {
41 // The minimum distance between the top and bottom of the icon and the
42 // top or bottom of the row.
43 const int kMinimumIconVerticalPadding = 2;
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 struct TextStyle {
85 ui::ResourceBundle::FontStyle font;
86 ui::NativeTheme::ColorId colors[OmniboxResultView::NUM_STATES];
87 gfx::BaselineStyle baseline;
88 } const kTextStyles[] = {
89 // 1 ANSWER_TEXT
90 {ui::ResourceBundle::LargeFont,
91 {NativeTheme::kColorId_ResultsTableNormalText,
92 NativeTheme::kColorId_ResultsTableHoveredText,
93 NativeTheme::kColorId_ResultsTableSelectedText},
94 gfx::NORMAL_BASELINE},
95 // 2 HEADLINE_TEXT
96 {ui::ResourceBundle::LargeFont,
97 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
98 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
99 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
100 gfx::NORMAL_BASELINE},
101 // 3 TOP_ALIGNED_TEXT
102 {ui::ResourceBundle::LargeFont,
103 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
104 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
105 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
106 gfx::SUPERIOR},
107 // 4 DESCRIPTION_TEXT
108 {ui::ResourceBundle::BaseFont,
109 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
110 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
111 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
112 gfx::NORMAL_BASELINE},
113 // 5 DESCRIPTION_TEXT_NEGATIVE
114 {ui::ResourceBundle::LargeFont,
115 {NativeTheme::kColorId_ResultsTableNegativeText,
116 NativeTheme::kColorId_ResultsTableNegativeHoveredText,
117 NativeTheme::kColorId_ResultsTableNegativeSelectedText},
118 gfx::INFERIOR},
119 // 6 DESCRIPTION_TEXT_POSITIVE
120 {ui::ResourceBundle::LargeFont,
121 {NativeTheme::kColorId_ResultsTablePositiveText,
122 NativeTheme::kColorId_ResultsTablePositiveHoveredText,
123 NativeTheme::kColorId_ResultsTablePositiveSelectedText},
124 gfx::INFERIOR},
125 // 7 MORE_INFO_TEXT
126 {ui::ResourceBundle::BaseFont,
127 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
128 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
129 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
130 gfx::INFERIOR},
131 // 8 SUGGESTION_TEXT
132 {ui::ResourceBundle::BaseFont,
133 {NativeTheme::kColorId_ResultsTableNormalText,
134 NativeTheme::kColorId_ResultsTableHoveredText,
135 NativeTheme::kColorId_ResultsTableSelectedText},
136 gfx::NORMAL_BASELINE},
137 // 9 SUGGESTION_TEXT_POSITIVE
138 {ui::ResourceBundle::BaseFont,
139 {NativeTheme::kColorId_ResultsTablePositiveText,
140 NativeTheme::kColorId_ResultsTablePositiveHoveredText,
141 NativeTheme::kColorId_ResultsTablePositiveSelectedText},
142 gfx::NORMAL_BASELINE},
143 // 10 SUGGESTION_TEXT_NEGATIVE
144 {ui::ResourceBundle::BaseFont,
145 {NativeTheme::kColorId_ResultsTableNegativeText,
146 NativeTheme::kColorId_ResultsTableNegativeHoveredText,
147 NativeTheme::kColorId_ResultsTableNegativeSelectedText},
148 gfx::NORMAL_BASELINE},
149 // 11 SUGGESTION_LINK_COLOR
150 {ui::ResourceBundle::BaseFont,
151 {NativeTheme::kColorId_ResultsTableNormalUrl,
152 NativeTheme::kColorId_ResultsTableHoveredUrl,
153 NativeTheme::kColorId_ResultsTableSelectedUrl},
154 gfx::NORMAL_BASELINE},
155 // 12 STATUS_TEXT
156 {ui::ResourceBundle::LargeFont,
157 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
158 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
159 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
160 gfx::INFERIOR},
161 // 13 PERSONALIZED_SUGGESTION_TEXT
162 {ui::ResourceBundle::BaseFont,
163 {NativeTheme::kColorId_ResultsTableNormalText,
164 NativeTheme::kColorId_ResultsTableHoveredText,
165 NativeTheme::kColorId_ResultsTableSelectedText},
166 gfx::NORMAL_BASELINE},
169 const TextStyle& GetTextStyle(int type) {
170 if (type < 1 || static_cast<size_t>(type) > arraysize(kTextStyles))
171 type = 1;
172 // Subtract one because the types are one based (not zero based).
173 return kTextStyles[type - 1];
176 } // namespace
178 ////////////////////////////////////////////////////////////////////////////////
179 // OmniboxResultView, public:
181 // This class is a utility class for calculations affected by whether the result
182 // view is horizontally mirrored. The drawing functions can be written as if
183 // all drawing occurs left-to-right, and then use this class to get the actual
184 // coordinates to begin drawing onscreen.
185 class OmniboxResultView::MirroringContext {
186 public:
187 MirroringContext() : center_(0), right_(0) {}
189 // Tells the mirroring context to use the provided range as the physical
190 // bounds of the drawing region. When coordinate mirroring is needed, the
191 // mirror point will be the center of this range.
192 void Initialize(int x, int width) {
193 center_ = x + width / 2;
194 right_ = x + width;
197 // Given a logical range within the drawing region, returns the coordinate of
198 // the possibly-mirrored "left" side. (This functions exactly like
199 // View::MirroredLeftPointForRect().)
200 int mirrored_left_coord(int left, int right) const {
201 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
204 // Given a logical coordinate within the drawing region, returns the remaining
205 // width available.
206 int remaining_width(int x) const {
207 return right_ - x;
210 private:
211 int center_;
212 int right_;
214 DISALLOW_COPY_AND_ASSIGN(MirroringContext);
217 OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
218 int model_index,
219 LocationBarView* location_bar_view,
220 const gfx::FontList& font_list)
221 : edge_item_padding_(LocationBarView::kItemPadding),
222 item_padding_(LocationBarView::kItemPadding),
223 model_(model),
224 model_index_(model_index),
225 location_bar_view_(location_bar_view),
226 font_list_(font_list),
227 font_height_(
228 std::max(font_list.GetHeight(),
229 font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())),
230 mirroring_context_(new MirroringContext()),
231 keyword_icon_(new views::ImageView()),
232 animation_(new gfx::SlideAnimation(this)) {
233 CHECK_GE(model_index, 0);
234 if (default_icon_size_ == 0) {
235 default_icon_size_ =
236 location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
237 AutocompleteMatch::TypeToIcon(
238 AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width();
240 keyword_icon_->set_owned_by_client();
241 keyword_icon_->EnableCanvasFlippingForRTLUI(true);
242 keyword_icon_->SetImage(GetKeywordIcon());
243 keyword_icon_->SizeToPreferredSize();
246 OmniboxResultView::~OmniboxResultView() {
249 SkColor OmniboxResultView::GetColor(
250 ResultViewState state,
251 ColorKind kind) const {
252 for (size_t i = 0; i < arraysize(kTranslationTable); ++i) {
253 if (kTranslationTable[i].state == state &&
254 kTranslationTable[i].kind == kind) {
255 return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id);
259 NOTREACHED();
260 return SK_ColorRED;
263 void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
264 match_ = match;
265 match_.PossiblySwapContentsAndDescriptionForDisplay();
266 ResetRenderTexts();
267 animation_->Reset();
268 answer_image_ = gfx::ImageSkia();
270 AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get();
271 if (associated_keyword_match) {
272 keyword_icon_->SetImage(GetKeywordIcon());
273 if (!keyword_icon_->parent())
274 AddChildView(keyword_icon_.get());
275 } else if (keyword_icon_->parent()) {
276 RemoveChildView(keyword_icon_.get());
279 Layout();
282 void OmniboxResultView::ShowKeyword(bool show_keyword) {
283 if (show_keyword)
284 animation_->Show();
285 else
286 animation_->Hide();
289 void OmniboxResultView::Invalidate() {
290 keyword_icon_->SetImage(GetKeywordIcon());
291 // While the text in the RenderTexts may not have changed, the styling
292 // (color/bold) may need to change. So we reset them to cause them to be
293 // recomputed in OnPaint().
294 ResetRenderTexts();
295 SchedulePaint();
298 gfx::Size OmniboxResultView::GetPreferredSize() const {
299 if (!match_.answer)
300 return gfx::Size(0, GetContentLineHeight());
301 // An answer implies a match and a description in a large font.
302 return gfx::Size(0, GetContentLineHeight() + GetAnswerLineHeight());
305 ////////////////////////////////////////////////////////////////////////////////
306 // OmniboxResultView, protected:
308 OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
309 if (model_->IsSelectedIndex(model_index_))
310 return SELECTED;
311 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
314 int OmniboxResultView::GetTextHeight() const {
315 return font_height_;
318 void OmniboxResultView::PaintMatch(const AutocompleteMatch& match,
319 gfx::RenderText* contents,
320 gfx::RenderText* description,
321 gfx::Canvas* canvas,
322 int x) const {
323 int y = text_bounds_.y();
325 if (!separator_rendertext_) {
326 const base::string16& separator =
327 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
328 separator_rendertext_.reset(CreateRenderText(separator).release());
329 separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT));
330 separator_width_ = separator_rendertext_->GetContentWidth();
333 contents->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
334 if (description)
335 description->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
336 int contents_max_width, description_max_width;
337 OmniboxPopupModel::ComputeMatchMaxWidths(
338 contents->GetContentWidth(),
339 separator_width_,
340 description ? description->GetContentWidth() : 0,
341 mirroring_context_->remaining_width(x),
342 !AutocompleteMatch::IsSearchType(match.type),
343 &contents_max_width,
344 &description_max_width);
346 int after_contents_x =
347 DrawRenderText(match, contents, true, canvas, x, y, contents_max_width);
349 if (description_max_width != 0) {
350 if (match.answer) {
351 y += GetContentLineHeight();
352 if (!answer_image_.isNull()) {
353 int answer_icon_size = GetAnswerLineHeight();
354 canvas->DrawImageInt(
355 answer_image_,
356 0, 0, answer_image_.width(), answer_image_.height(),
357 GetMirroredXInView(x), y, answer_icon_size, answer_icon_size, true);
358 x += answer_icon_size + LocationBarView::kIconInternalPadding;
360 } else {
361 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas,
362 after_contents_x, y, separator_width_);
365 DrawRenderText(match, description, false, canvas, x, y,
366 description_max_width);
370 int OmniboxResultView::DrawRenderText(
371 const AutocompleteMatch& match,
372 gfx::RenderText* render_text,
373 bool contents,
374 gfx::Canvas* canvas,
375 int x,
376 int y,
377 int max_width) const {
378 DCHECK(!render_text->text().empty());
380 const int remaining_width = mirroring_context_->remaining_width(x);
381 int right_x = x + max_width;
383 // Infinite suggestions should appear with the leading ellipses vertically
384 // stacked.
385 if (contents &&
386 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL)) {
387 // When the directionality of suggestion doesn't match the UI, we try to
388 // vertically stack the ellipsis by restricting the end edge (right_x).
389 const bool is_ui_rtl = base::i18n::IsRTL();
390 const bool is_match_contents_rtl =
391 (render_text->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT);
392 const int offset =
393 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl);
395 scoped_ptr<gfx::RenderText> prefix_render_text(
396 CreateRenderText(base::UTF8ToUTF16(
397 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix))));
398 const int prefix_width = prefix_render_text->GetContentWidth();
399 int prefix_x = x;
401 const int max_match_contents_width = model_->max_match_contents_width();
403 if (is_ui_rtl != is_match_contents_rtl) {
404 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR
405 // infinite suggestions appear near the right edge in RTL UI. This is
406 // against the natural horizontal alignment of the text. We reduce the
407 // width of the box for suggestion display, so that the suggestions appear
408 // in correct confines. This reduced width allows us to modify the text
409 // alignment (see below).
410 right_x = x + std::min(remaining_width - prefix_width,
411 std::max(offset, max_match_contents_width));
412 prefix_x = right_x;
413 // We explicitly set the horizontal alignment so that when LTR suggestions
414 // show in RTL UI (or vice versa), their ellipses appear stacked in a
415 // single column.
416 render_text->SetHorizontalAlignment(
417 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
418 } else {
419 // If the dropdown is wide enough, place the ellipsis at the position
420 // where the omitted text would have ended. Otherwise reduce the offset of
421 // the ellipsis such that the widest suggestion reaches the end of the
422 // dropdown.
423 const int start_offset = std::max(prefix_width,
424 std::min(remaining_width - max_match_contents_width, offset));
425 right_x = x + std::min(remaining_width, start_offset + max_width);
426 x += start_offset;
427 prefix_x = x - prefix_width;
429 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ?
430 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR);
431 prefix_render_text->SetHorizontalAlignment(
432 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
433 prefix_render_text->SetDisplayRect(
434 gfx::Rect(mirroring_context_->mirrored_left_coord(
435 prefix_x, prefix_x + prefix_width),
436 y, prefix_width, GetContentLineHeight()));
437 prefix_render_text->Draw(canvas);
440 // Set the display rect to trigger eliding.
441 render_text->SetDisplayRect(
442 gfx::Rect(mirroring_context_->mirrored_left_coord(x, right_x), y,
443 right_x - x, GetContentLineHeight()));
444 render_text->Draw(canvas);
445 return right_x;
448 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText(
449 const base::string16& text) const {
450 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance());
451 render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
452 render_text->SetCursorEnabled(false);
453 render_text->SetElideBehavior(gfx::ELIDE_TAIL);
454 render_text->SetFontList(font_list_);
455 render_text->SetText(text);
456 return render_text.Pass();
459 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText(
460 const base::string16& text,
461 const ACMatchClassifications& classifications,
462 bool force_dim) const {
463 scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text));
464 const size_t text_length = render_text->text().length();
465 for (size_t i = 0; i < classifications.size(); ++i) {
466 const size_t text_start = classifications[i].offset;
467 if (text_start >= text_length)
468 break;
470 const size_t text_end = (i < (classifications.size() - 1)) ?
471 std::min(classifications[i + 1].offset, text_length) :
472 text_length;
473 const gfx::Range current_range(text_start, text_end);
475 // Calculate style-related data.
476 if (classifications[i].style & ACMatchClassification::MATCH)
477 render_text->ApplyStyle(gfx::BOLD, true, current_range);
479 ColorKind color_kind = TEXT;
480 if (classifications[i].style & ACMatchClassification::URL) {
481 color_kind = URL;
482 // Consider logical string for domain "ABC.com×™/hello" where ABC are
483 // Hebrew (RTL) characters. This string should ideally show as
484 // "CBA.com/hello". If we do not force LTR on URL, it will appear as
485 // "com/hello.CBA".
486 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs,
487 // but it still has some pitfalls like :
488 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which
489 // really confuses the path hierarchy of the URL.
490 // Also, if the URL supports https, the appearance will change into LTR
491 // directionality.
492 // In conclusion, LTR rendering of URL is probably the safest bet.
493 render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR);
494 } else if (force_dim ||
495 (classifications[i].style & ACMatchClassification::DIM)) {
496 color_kind = DIMMED_TEXT;
498 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range);
500 return render_text.Pass();
503 int OmniboxResultView::GetMatchContentsWidth() const {
504 InitContentsRenderTextIfNecessary();
505 contents_rendertext_->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
506 return contents_rendertext_->GetContentWidth();
509 void OmniboxResultView::SetAnswerImage(const gfx::ImageSkia& image) {
510 answer_image_ = image;
511 SchedulePaint();
514 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in
515 // the omnibox dropdown. Consider sharing the result.
516 int OmniboxResultView::GetDisplayOffset(
517 const AutocompleteMatch& match,
518 bool is_ui_rtl,
519 bool is_match_contents_rtl) const {
520 if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_TAIL)
521 return 0;
523 const base::string16& input_text =
524 base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText));
525 int contents_start_index = 0;
526 base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex),
527 &contents_start_index);
529 scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text));
530 const gfx::Range& glyph_bounds =
531 input_render_text->GetGlyphBounds(contents_start_index);
532 const int start_padding = is_match_contents_rtl ?
533 std::max(glyph_bounds.start(), glyph_bounds.end()) :
534 std::min(glyph_bounds.start(), glyph_bounds.end());
536 return is_ui_rtl ?
537 (input_render_text->GetContentWidth() - start_padding) : start_padding;
540 // static
541 int OmniboxResultView::default_icon_size_ = 0;
543 const char* OmniboxResultView::GetClassName() const {
544 return "OmniboxResultView";
547 gfx::ImageSkia OmniboxResultView::GetIcon() const {
548 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
549 if (!image.IsEmpty())
550 return image.AsImageSkia();
552 int icon = model_->IsStarredMatch(match_) ?
553 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
554 if (GetState() == SELECTED) {
555 switch (icon) {
556 case IDR_OMNIBOX_CALCULATOR:
557 icon = IDR_OMNIBOX_CALCULATOR_SELECTED;
558 break;
559 case IDR_OMNIBOX_EXTENSION_APP:
560 icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
561 break;
562 case IDR_OMNIBOX_HTTP:
563 icon = IDR_OMNIBOX_HTTP_SELECTED;
564 break;
565 case IDR_OMNIBOX_SEARCH:
566 icon = IDR_OMNIBOX_SEARCH_SELECTED;
567 break;
568 case IDR_OMNIBOX_STAR:
569 icon = IDR_OMNIBOX_STAR_SELECTED;
570 break;
571 default:
572 NOTREACHED();
573 break;
576 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
579 const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const {
580 // NOTE: If we ever begin returning icons of varying size, then callers need
581 // to ensure that |keyword_icon_| is resized each time its image is reset.
582 return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
583 (GetState() == SELECTED) ? IDR_OMNIBOX_TTS_SELECTED : IDR_OMNIBOX_TTS);
586 bool OmniboxResultView::ShowOnlyKeywordMatch() const {
587 return match_.associated_keyword &&
588 (keyword_icon_->x() <= icon_bounds_.right());
591 void OmniboxResultView::ResetRenderTexts() const {
592 contents_rendertext_.reset();
593 description_rendertext_.reset();
594 separator_rendertext_.reset();
595 keyword_contents_rendertext_.reset();
596 keyword_description_rendertext_.reset();
599 void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
600 if (!contents_rendertext_) {
601 contents_rendertext_.reset(
602 CreateClassifiedRenderText(
603 match_.contents, match_.contents_class, false).release());
607 void OmniboxResultView::Layout() {
608 const gfx::ImageSkia icon = GetIcon();
610 icon_bounds_.SetRect(
611 edge_item_padding_ + ((icon.width() == default_icon_size_)
613 : LocationBarView::kIconInternalPadding),
614 (GetContentLineHeight() - icon.height()) / 2, icon.width(),
615 icon.height());
617 int text_x = edge_item_padding_ + default_icon_size_ + item_padding_;
618 int text_width = width() - text_x - edge_item_padding_;
620 if (match_.associated_keyword.get()) {
621 const int kw_collapsed_size =
622 keyword_icon_->width() + edge_item_padding_;
623 const int max_kw_x = width() - kw_collapsed_size;
624 const int kw_x =
625 animation_->CurrentValueBetween(max_kw_x, edge_item_padding_);
626 const int kw_text_x = kw_x + keyword_icon_->width() + item_padding_;
628 text_width = kw_x - text_x - item_padding_;
629 keyword_text_bounds_.SetRect(
630 kw_text_x, 0,
631 std::max(width() - kw_text_x - edge_item_padding_, 0), height());
632 keyword_icon_->SetPosition(
633 gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2));
636 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height());
639 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
640 animation_->SetSlideDuration(width() / 4);
643 void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
644 const ResultViewState state = GetState();
645 if (state != NORMAL)
646 canvas->DrawColor(GetColor(state, BACKGROUND));
648 // NOTE: While animating the keyword match, both matches may be visible.
650 if (!ShowOnlyKeywordMatch()) {
651 canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_),
652 icon_bounds_.y());
653 int x = GetMirroredXForRect(text_bounds_);
654 mirroring_context_->Initialize(x, text_bounds_.width());
655 InitContentsRenderTextIfNecessary();
657 if (!description_rendertext_) {
658 if (match_.answer) {
659 contents_rendertext_ =
660 CreateAnswerLine(match_.answer->first_line(), font_list_);
661 description_rendertext_ = CreateAnswerLine(
662 match_.answer->second_line(),
663 ui::ResourceBundle::GetSharedInstance().GetFontList(
664 ui::ResourceBundle::LargeFont));
665 } else if (!match_.description.empty()) {
666 description_rendertext_ = CreateClassifiedRenderText(
667 match_.description, match_.description_class, true);
670 PaintMatch(match_, contents_rendertext_.get(),
671 description_rendertext_.get(), canvas, x);
674 AutocompleteMatch* keyword_match = match_.associated_keyword.get();
675 if (keyword_match) {
676 int x = GetMirroredXForRect(keyword_text_bounds_);
677 mirroring_context_->Initialize(x, keyword_text_bounds_.width());
678 if (!keyword_contents_rendertext_) {
679 keyword_contents_rendertext_.reset(
680 CreateClassifiedRenderText(keyword_match->contents,
681 keyword_match->contents_class,
682 false).release());
684 if (!keyword_description_rendertext_ &&
685 !keyword_match->description.empty()) {
686 keyword_description_rendertext_.reset(
687 CreateClassifiedRenderText(keyword_match->description,
688 keyword_match->description_class,
689 true).release());
691 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(),
692 keyword_description_rendertext_.get(), canvas, x);
696 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
697 Layout();
698 SchedulePaint();
701 int OmniboxResultView::GetAnswerLineHeight() const {
702 // GetTextStyle(1) is the largest font used and so defines the boundary that
703 // all the other answer styles fit within.
704 return ui::ResourceBundle::GetSharedInstance()
705 .GetFontList(GetTextStyle(1).font)
706 .GetHeight();
709 int OmniboxResultView::GetContentLineHeight() const {
710 return std::max(default_icon_size_ + (kMinimumIconVerticalPadding * 2),
711 GetTextHeight() + (kMinimumTextVerticalPadding * 2));
714 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateAnswerLine(
715 const SuggestionAnswer::ImageLine& line,
716 gfx::FontList font_list) {
717 scoped_ptr<gfx::RenderText> destination = CreateRenderText(base::string16());
718 destination->SetFontList(font_list);
720 for (const SuggestionAnswer::TextField& text_field : line.text_fields())
721 AppendAnswerText(destination.get(), text_field.text(), text_field.type());
722 const base::char16 space(' ');
723 const auto* text_field = line.additional_text();
724 if (text_field) {
725 AppendAnswerText(destination.get(), space + text_field->text(),
726 text_field->type());
728 text_field = line.status_text();
729 if (text_field) {
730 AppendAnswerText(destination.get(), space + text_field->text(),
731 text_field->type());
733 return destination.Pass();
736 void OmniboxResultView::AppendAnswerText(gfx::RenderText* destination,
737 const base::string16& text,
738 int text_type) {
739 // TODO(dschuyler): make this better. Right now this only supports unnested
740 // bold tags. In the future we'll need to flag unexpected tags while adding
741 // support for b, i, u, sub, and sup. We'll also need to support HTML
742 // entities (&lt; for '<', etc.).
743 const base::string16 begin_tag = base::ASCIIToUTF16("<b>");
744 const base::string16 end_tag = base::ASCIIToUTF16("</b>");
745 size_t begin = 0;
746 while (true) {
747 size_t end = text.find(begin_tag, begin);
748 if (end == base::string16::npos) {
749 AppendAnswerTextHelper(destination, text.substr(begin), text_type, false);
750 break;
752 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
753 text_type, false);
754 begin = end + begin_tag.length();
755 end = text.find(end_tag, begin);
756 if (end == base::string16::npos)
757 break;
758 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
759 text_type, true);
760 begin = end + end_tag.length();
764 void OmniboxResultView::AppendAnswerTextHelper(gfx::RenderText* destination,
765 const base::string16& text,
766 int text_type,
767 bool is_bold) {
768 if (text.empty())
769 return;
770 int offset = destination->text().length();
771 gfx::Range range(offset, offset + text.length());
772 destination->AppendText(text);
773 const TextStyle& text_style = GetTextStyle(text_type);
774 // TODO(dschuyler): follow up on the problem of different font sizes within
775 // one RenderText. Maybe with destination->SetFontList(...).
776 destination->ApplyStyle(gfx::BOLD, is_bold, range);
777 destination->ApplyColor(
778 GetNativeTheme()->GetSystemColor(text_style.colors[GetState()]), range);
779 destination->ApplyBaselineStyle(text_style.baseline, range);