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_elider.h" 33 #include "ui/gfx/text_utils.h" 34 #include "ui/native_theme/native_theme.h" 35 36 using ui::NativeTheme; 37 38 namespace { 39 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; 44 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 }, 82 }; 83 84 } // namespace 85 86 //////////////////////////////////////////////////////////////////////////////// 87 // OmniboxResultView, public: 88 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) {} 96 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; 103 } 104 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; 110 } 111 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; 116 } 117 118 private: 119 int center_; 120 int right_; 121 122 DISALLOW_COPY_AND_ASSIGN(MirroringContext); 123 }; 124 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(); 148 } 149 keyword_icon_->set_owned_by_client(); 150 keyword_icon_->EnableCanvasFlippingForRTLUI(true); 151 keyword_icon_->SetImage(GetKeywordIcon()); 152 keyword_icon_->SizeToPreferredSize(); 153 } 154 155 OmniboxResultView::~OmniboxResultView() { 156 } 157 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); 165 } 166 } 167 168 NOTREACHED(); 169 return SK_ColorRED; 170 } 171 172 void OmniboxResultView::SetMatch(const AutocompleteMatch& match) { 173 match_ = match; 174 ResetRenderTexts(); 175 animation_->Reset(); 176 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()); 184 } 185 186 Layout(); 187 } 188 189 void OmniboxResultView::ShowKeyword(bool show_keyword) { 190 if (show_keyword) 191 animation_->Show(); 192 else 193 animation_->Hide(); 194 } 195 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(); 203 } 204 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))); 209 } 210 211 //////////////////////////////////////////////////////////////////////////////// 212 // OmniboxResultView, protected: 213 214 OmniboxResultView::ResultViewState OmniboxResultView::GetState() const { 215 if (model_->IsSelectedIndex(model_index_)) 216 return SELECTED; 217 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL; 218 } 219 220 int OmniboxResultView::GetTextHeight() const { 221 return font_height_; 222 } 223 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(); 231 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(); 238 } 239 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); 249 250 x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width); 251 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); 257 } 258 } 259 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()); 269 270 const int remaining_width = mirroring_context_->remaining_width(x); 271 int right_x = x + max_width; 272 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); 284 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; 290 291 const int max_match_contents_width = model_->max_match_contents_width(); 292 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; 318 } 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); 328 } 329 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; 336 } 337 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_TAIL); 343 render_text->SetFontList(font_list_); 344 render_text->SetText(text); 345 return render_text.Pass(); 346 } 347 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; 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 392 int 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. 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; 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 424 int OmniboxResultView::default_icon_size_ = 0; 425 426 gfx::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 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); 460 } 461 462 bool OmniboxResultView::ShowOnlyKeywordMatch() const { 463 return match_.associated_keyword && 464 (keyword_icon_->x() <= icon_bounds_.right()); 465 } 466 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(); 473 } 474 475 void OmniboxResultView::InitContentsRenderTextIfNecessary() const { 476 if (!contents_rendertext_) { 477 contents_rendertext_.reset( 478 CreateClassifiedRenderText( 479 match_.contents, match_.contents_class, false).release()); 480 } 481 } 482 483 void 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 513 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 514 animation_->SetSlideDuration(width() / 4); 515 } 516 517 void 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 561 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) { 562 Layout(); 563 SchedulePaint(); 564 } 565