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"
8 #include <atlbase.h> // NOLINT
9 #include <atlwin.h> // NOLINT
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
;
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
},
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
{
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;
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
114 int remaining_width(int x
) const {
122 DISALLOW_COPY_AND_ASSIGN(MirroringContext
);
125 OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView
* model
,
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
),
133 model_index_(model_index
),
134 location_bar_view_(location_bar_view
),
135 font_list_(font_list
),
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) {
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
);
172 void OmniboxResultView::SetMatch(const AutocompleteMatch
& match
) {
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());
189 void OmniboxResultView::ShowKeyword(bool show_keyword
) {
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().
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_
))
217 return model_
->IsHoveredIndex(model_index_
) ? HOVERED
: NORMAL
;
220 int OmniboxResultView::GetTextHeight() const {
224 void OmniboxResultView::PaintMatch(
225 const AutocompleteMatch
& match
,
226 gfx::RenderText
* contents
,
227 gfx::RenderText
* description
,
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(),
244 description
? description
->GetContentWidth() : 0,
245 mirroring_context_
->remaining_width(x
),
246 !AutocompleteMatch::IsSearchType(match
.type
),
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
,
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
,
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
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
);
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();
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
));
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
306 render_text
->SetHorizontalAlignment(
307 is_match_contents_rtl
? gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
);
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
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
);
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
);
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
)
359 const size_t text_end
= (i
< (classifications
.size() - 1)) ?
360 std::min(classifications
[i
+ 1].offset
, 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
) {
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
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
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
,
402 bool is_match_contents_rtl
) const {
403 if (match
.type
!= AutocompleteMatchType::SEARCH_SUGGEST_INFINITE
)
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());
420 (input_render_text
->GetContentWidth() - start_padding
) : start_padding
;
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
) {
435 case IDR_OMNIBOX_EXTENSION_APP
:
436 icon
= IDR_OMNIBOX_EXTENSION_APP_SELECTED
;
438 case IDR_OMNIBOX_HTTP
:
439 icon
= IDR_OMNIBOX_HTTP_SELECTED
;
441 case IDR_OMNIBOX_SEARCH
:
442 icon
= IDR_OMNIBOX_SEARCH_SELECTED
;
444 case IDR_OMNIBOX_STAR
:
445 icon
= IDR_OMNIBOX_STAR_SELECTED
;
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
;
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(
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();
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_
),
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();
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
,
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
,
556 PaintMatch(*keyword_match
, keyword_contents_rendertext_
.get(),
557 keyword_description_rendertext_
.get(), canvas
, x
);
561 void OmniboxResultView::AnimationProgressed(const gfx::Animation
* animation
) {