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 "chrome/grit/generated_resources.h"
24 #include "grit/components_scaled_resources.h"
25 #include "grit/theme_resources.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/theme_provider.h"
28 #include "ui/gfx/canvas.h"
29 #include "ui/gfx/color_utils.h"
30 #include "ui/gfx/image/image.h"
31 #include "ui/gfx/range/range.h"
32 #include "ui/gfx/render_text.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
->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX
, 0)));
342 render_text
->SetCursorEnabled(false);
343 render_text
->SetElideBehavior(gfx::ELIDE_TAIL
);
344 render_text
->SetFontList(font_list_
);
345 render_text
->SetText(text
);
346 return render_text
.Pass();
349 scoped_ptr
<gfx::RenderText
> OmniboxResultView::CreateClassifiedRenderText(
350 const base::string16
& text
,
351 const ACMatchClassifications
& classifications
,
352 bool force_dim
) const {
353 scoped_ptr
<gfx::RenderText
> render_text(CreateRenderText(text
));
354 const size_t text_length
= render_text
->text().length();
355 for (size_t i
= 0; i
< classifications
.size(); ++i
) {
356 const size_t text_start
= classifications
[i
].offset
;
357 if (text_start
>= text_length
)
360 const size_t text_end
= (i
< (classifications
.size() - 1)) ?
361 std::min(classifications
[i
+ 1].offset
, text_length
) :
363 const gfx::Range
current_range(text_start
, text_end
);
365 // Calculate style-related data.
366 if (classifications
[i
].style
& ACMatchClassification::MATCH
)
367 render_text
->ApplyStyle(gfx::BOLD
, true, current_range
);
369 ColorKind color_kind
= TEXT
;
370 if (classifications
[i
].style
& ACMatchClassification::URL
) {
372 // Consider logical string for domain "ABC.com×™/hello" where ABC are
373 // Hebrew (RTL) characters. This string should ideally show as
374 // "CBA.com/hello". If we do not force LTR on URL, it will appear as
376 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs,
377 // but it still has some pitfalls like :
378 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which
379 // really confuses the path hierarchy of the URL.
380 // Also, if the URL supports https, the appearance will change into LTR
382 // In conclusion, LTR rendering of URL is probably the safest bet.
383 render_text
->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR
);
384 } else if (force_dim
||
385 (classifications
[i
].style
& ACMatchClassification::DIM
)) {
386 color_kind
= DIMMED_TEXT
;
388 render_text
->ApplyColor(GetColor(GetState(), color_kind
), current_range
);
390 return render_text
.Pass();
393 int OmniboxResultView::GetMatchContentsWidth() const {
394 InitContentsRenderTextIfNecessary();
395 return contents_rendertext_
? contents_rendertext_
->GetContentWidth() : 0;
398 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in
399 // the omnibox dropdown. Consider sharing the result.
400 int OmniboxResultView::GetDisplayOffset(
401 const AutocompleteMatch
& match
,
403 bool is_match_contents_rtl
) const {
404 if (match
.type
!= AutocompleteMatchType::SEARCH_SUGGEST_INFINITE
)
407 const base::string16
& input_text
=
408 base::UTF8ToUTF16(match
.GetAdditionalInfo(kACMatchPropertyInputText
));
409 int contents_start_index
= 0;
410 base::StringToInt(match
.GetAdditionalInfo(kACMatchPropertyContentsStartIndex
),
411 &contents_start_index
);
413 scoped_ptr
<gfx::RenderText
> input_render_text(CreateRenderText(input_text
));
414 const gfx::Range
& glyph_bounds
=
415 input_render_text
->GetGlyphBounds(contents_start_index
);
416 const int start_padding
= is_match_contents_rtl
?
417 std::max(glyph_bounds
.start(), glyph_bounds
.end()) :
418 std::min(glyph_bounds
.start(), glyph_bounds
.end());
421 (input_render_text
->GetContentWidth() - start_padding
) : start_padding
;
425 int OmniboxResultView::default_icon_size_
= 0;
427 const char* OmniboxResultView::GetClassName() const {
428 return "OmniboxResultView";
431 gfx::ImageSkia
OmniboxResultView::GetIcon() const {
432 const gfx::Image image
= model_
->GetIconIfExtensionMatch(model_index_
);
433 if (!image
.IsEmpty())
434 return image
.AsImageSkia();
436 int icon
= model_
->IsStarredMatch(match_
) ?
437 IDR_OMNIBOX_STAR
: AutocompleteMatch::TypeToIcon(match_
.type
);
438 if (GetState() == SELECTED
) {
440 case IDR_OMNIBOX_EXTENSION_APP
:
441 icon
= IDR_OMNIBOX_EXTENSION_APP_SELECTED
;
443 case IDR_OMNIBOX_HTTP
:
444 icon
= IDR_OMNIBOX_HTTP_SELECTED
;
446 case IDR_OMNIBOX_SEARCH
:
447 icon
= IDR_OMNIBOX_SEARCH_SELECTED
;
449 case IDR_OMNIBOX_STAR
:
450 icon
= IDR_OMNIBOX_STAR_SELECTED
;
457 return *(location_bar_view_
->GetThemeProvider()->GetImageSkiaNamed(icon
));
460 const gfx::ImageSkia
* OmniboxResultView::GetKeywordIcon() const {
461 // NOTE: If we ever begin returning icons of varying size, then callers need
462 // to ensure that |keyword_icon_| is resized each time its image is reset.
463 return location_bar_view_
->GetThemeProvider()->GetImageSkiaNamed(
464 (GetState() == SELECTED
) ? IDR_OMNIBOX_TTS_SELECTED
: IDR_OMNIBOX_TTS
);
467 bool OmniboxResultView::ShowOnlyKeywordMatch() const {
468 return match_
.associated_keyword
&&
469 (keyword_icon_
->x() <= icon_bounds_
.right());
472 void OmniboxResultView::ResetRenderTexts() const {
473 contents_rendertext_
.reset();
474 description_rendertext_
.reset();
475 separator_rendertext_
.reset();
476 keyword_contents_rendertext_
.reset();
477 keyword_description_rendertext_
.reset();
480 void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
481 if (!contents_rendertext_
) {
482 contents_rendertext_
.reset(
483 CreateClassifiedRenderText(
484 match_
.contents
, match_
.contents_class
, false).release());
488 void OmniboxResultView::Layout() {
489 const gfx::ImageSkia icon
= GetIcon();
491 icon_bounds_
.SetRect(edge_item_padding_
+
492 ((icon
.width() == default_icon_size_
) ?
493 0 : LocationBarView::kIconInternalPadding
),
494 (height() - icon
.height()) / 2, icon
.width(), icon
.height());
496 int text_x
= edge_item_padding_
+ default_icon_size_
+ item_padding_
;
497 int text_width
= width() - text_x
- edge_item_padding_
;
499 if (match_
.associated_keyword
.get()) {
500 const int kw_collapsed_size
=
501 keyword_icon_
->width() + edge_item_padding_
;
502 const int max_kw_x
= width() - kw_collapsed_size
;
504 animation_
->CurrentValueBetween(max_kw_x
, edge_item_padding_
);
505 const int kw_text_x
= kw_x
+ keyword_icon_
->width() + item_padding_
;
507 text_width
= kw_x
- text_x
- item_padding_
;
508 keyword_text_bounds_
.SetRect(
510 std::max(width() - kw_text_x
- edge_item_padding_
, 0), height());
511 keyword_icon_
->SetPosition(
512 gfx::Point(kw_x
, (height() - keyword_icon_
->height()) / 2));
515 text_bounds_
.SetRect(text_x
, 0, std::max(text_width
, 0), height());
518 void OmniboxResultView::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
519 animation_
->SetSlideDuration(width() / 4);
522 void OmniboxResultView::OnPaint(gfx::Canvas
* canvas
) {
523 const ResultViewState state
= GetState();
525 canvas
->DrawColor(GetColor(state
, BACKGROUND
));
527 // NOTE: While animating the keyword match, both matches may be visible.
529 if (!ShowOnlyKeywordMatch()) {
530 canvas
->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_
),
532 int x
= GetMirroredXForRect(text_bounds_
);
533 mirroring_context_
->Initialize(x
, text_bounds_
.width());
534 InitContentsRenderTextIfNecessary();
535 if (!description_rendertext_
&& !match_
.description
.empty()) {
536 description_rendertext_
.reset(
537 CreateClassifiedRenderText(
538 match_
.description
, match_
.description_class
, true).release());
540 PaintMatch(match_
, contents_rendertext_
.get(),
541 description_rendertext_
.get(), canvas
, x
);
544 AutocompleteMatch
* keyword_match
= match_
.associated_keyword
.get();
546 int x
= GetMirroredXForRect(keyword_text_bounds_
);
547 mirroring_context_
->Initialize(x
, keyword_text_bounds_
.width());
548 if (!keyword_contents_rendertext_
) {
549 keyword_contents_rendertext_
.reset(
550 CreateClassifiedRenderText(keyword_match
->contents
,
551 keyword_match
->contents_class
,
554 if (!keyword_description_rendertext_
&&
555 !keyword_match
->description
.empty()) {
556 keyword_description_rendertext_
.reset(
557 CreateClassifiedRenderText(keyword_match
->description
,
558 keyword_match
->description_class
,
561 PaintMatch(*keyword_match
, keyword_contents_rendertext_
.get(),
562 keyword_description_rendertext_
.get(), canvas
, x
);
566 void OmniboxResultView::AnimationProgressed(const gfx::Animation
* animation
) {