omnibox_result_view.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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_utils.h" 33#include "ui/native_theme/native_theme.h" 34 35using ui::NativeTheme; 36 37namespace { 38 39// The minimum distance between the top and bottom of the {icon|text} and the 40// top or bottom of the row. 41const int kMinimumIconVerticalPadding = 2; 42const int kMinimumTextVerticalPadding = 3; 43 44// A mapping from OmniboxResultView's ResultViewState/ColorKind types to 45// NativeTheme colors. 46struct TranslationTable { 47 ui::NativeTheme::ColorId id; 48 OmniboxResultView::ResultViewState state; 49 OmniboxResultView::ColorKind kind; 50} static const kTranslationTable[] = { 51 { NativeTheme::kColorId_ResultsTableNormalBackground, 52 OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND }, 53 { NativeTheme::kColorId_ResultsTableHoveredBackground, 54 OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND }, 55 { NativeTheme::kColorId_ResultsTableSelectedBackground, 56 OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND }, 57 { NativeTheme::kColorId_ResultsTableNormalText, 58 OmniboxResultView::NORMAL, OmniboxResultView::TEXT }, 59 { NativeTheme::kColorId_ResultsTableHoveredText, 60 OmniboxResultView::HOVERED, OmniboxResultView::TEXT }, 61 { NativeTheme::kColorId_ResultsTableSelectedText, 62 OmniboxResultView::SELECTED, OmniboxResultView::TEXT }, 63 { NativeTheme::kColorId_ResultsTableNormalDimmedText, 64 OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT }, 65 { NativeTheme::kColorId_ResultsTableHoveredDimmedText, 66 OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT }, 67 { NativeTheme::kColorId_ResultsTableSelectedDimmedText, 68 OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT }, 69 { NativeTheme::kColorId_ResultsTableNormalUrl, 70 OmniboxResultView::NORMAL, OmniboxResultView::URL }, 71 { NativeTheme::kColorId_ResultsTableHoveredUrl, 72 OmniboxResultView::HOVERED, OmniboxResultView::URL }, 73 { NativeTheme::kColorId_ResultsTableSelectedUrl, 74 OmniboxResultView::SELECTED, OmniboxResultView::URL }, 75 { NativeTheme::kColorId_ResultsTableNormalDivider, 76 OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER }, 77 { NativeTheme::kColorId_ResultsTableHoveredDivider, 78 OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER }, 79 { NativeTheme::kColorId_ResultsTableSelectedDivider, 80 OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER }, 81}; 82 83} // namespace 84 85//////////////////////////////////////////////////////////////////////////////// 86// OmniboxResultView, public: 87 88// This class is a utility class for calculations affected by whether the result 89// view is horizontally mirrored. The drawing functions can be written as if 90// all drawing occurs left-to-right, and then use this class to get the actual 91// coordinates to begin drawing onscreen. 92class OmniboxResultView::MirroringContext { 93 public: 94 MirroringContext() : center_(0), right_(0) {} 95 96 // Tells the mirroring context to use the provided range as the physical 97 // bounds of the drawing region. When coordinate mirroring is needed, the 98 // mirror point will be the center of this range. 99 void Initialize(int x, int width) { 100 center_ = x + width / 2; 101 right_ = x + width; 102 } 103 104 // Given a logical range within the drawing region, returns the coordinate of 105 // the possibly-mirrored "left" side. (This functions exactly like 106 // View::MirroredLeftPointForRect().) 107 int mirrored_left_coord(int left, int right) const { 108 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left; 109 } 110 111 // Given a logical coordinate within the drawing region, returns the remaining 112 // width available. 113 int remaining_width(int x) const { 114 return right_ - x; 115 } 116 117 private: 118 int center_; 119 int right_; 120 121 DISALLOW_COPY_AND_ASSIGN(MirroringContext); 122}; 123 124OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model, 125 int model_index, 126 LocationBarView* location_bar_view, 127 const gfx::FontList& font_list) 128 : edge_item_padding_(LocationBarView::kItemPadding), 129 item_padding_(LocationBarView::kItemPadding), 130 minimum_text_vertical_padding_(kMinimumTextVerticalPadding), 131 model_(model), 132 model_index_(model_index), 133 location_bar_view_(location_bar_view), 134 font_list_(font_list), 135 font_height_( 136 std::max(font_list.GetHeight(), 137 font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())), 138 mirroring_context_(new MirroringContext()), 139 keyword_icon_(new views::ImageView()), 140 animation_(new gfx::SlideAnimation(this)) { 141 CHECK_GE(model_index, 0); 142 if (default_icon_size_ == 0) { 143 default_icon_size_ = 144 location_bar_view_->GetThemeProvider()->GetImageSkiaNamed( 145 AutocompleteMatch::TypeToIcon( 146 AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width(); 147 } 148 keyword_icon_->set_owned_by_client(); 149 keyword_icon_->EnableCanvasFlippingForRTLUI(true); 150 keyword_icon_->SetImage(GetKeywordIcon()); 151 keyword_icon_->SizeToPreferredSize(); 152} 153 154OmniboxResultView::~OmniboxResultView() { 155} 156 157SkColor OmniboxResultView::GetColor( 158 ResultViewState state, 159 ColorKind kind) const { 160 for (size_t i = 0; i < arraysize(kTranslationTable); ++i) { 161 if (kTranslationTable[i].state == state && 162 kTranslationTable[i].kind == kind) { 163 return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id); 164 } 165 } 166 167 NOTREACHED(); 168 return SK_ColorRED; 169} 170 171void OmniboxResultView::SetMatch(const AutocompleteMatch& match) { 172 match_ = match; 173 ResetRenderTexts(); 174 animation_->Reset(); 175 176 AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get(); 177 if (associated_keyword_match) { 178 keyword_icon_->SetImage(GetKeywordIcon()); 179 if (!keyword_icon_->parent()) 180 AddChildView(keyword_icon_.get()); 181 } else if (keyword_icon_->parent()) { 182 RemoveChildView(keyword_icon_.get()); 183 } 184 185 Layout(); 186} 187 188void OmniboxResultView::ShowKeyword(bool show_keyword) { 189 if (show_keyword) 190 animation_->Show(); 191 else 192 animation_->Hide(); 193} 194 195void OmniboxResultView::Invalidate() { 196 keyword_icon_->SetImage(GetKeywordIcon()); 197 // While the text in the RenderTexts may not have changed, the styling 198 // (color/bold) may need to change. So we reset them to cause them to be 199 // recomputed in OnPaint(). 200 ResetRenderTexts(); 201 SchedulePaint(); 202} 203 204gfx::Size OmniboxResultView::GetPreferredSize() const { 205 return gfx::Size(0, std::max( 206 default_icon_size_ + (kMinimumIconVerticalPadding * 2), 207 GetTextHeight() + (minimum_text_vertical_padding_ * 2))); 208} 209 210//////////////////////////////////////////////////////////////////////////////// 211// OmniboxResultView, protected: 212 213OmniboxResultView::ResultViewState OmniboxResultView::GetState() const { 214 if (model_->IsSelectedIndex(model_index_)) 215 return SELECTED; 216 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL; 217} 218 219int OmniboxResultView::GetTextHeight() const { 220 return font_height_; 221} 222 223void OmniboxResultView::PaintMatch( 224 const AutocompleteMatch& match, 225 gfx::RenderText* contents, 226 gfx::RenderText* description, 227 gfx::Canvas* canvas, 228 int x) const { 229 int y = text_bounds_.y(); 230 231 if (!separator_rendertext_) { 232 const base::string16& separator = 233 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR); 234 separator_rendertext_.reset(CreateRenderText(separator).release()); 235 separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT)); 236 separator_width_ = separator_rendertext_->GetContentWidth(); 237 } 238 239 int contents_max_width, description_max_width; 240 OmniboxPopupModel::ComputeMatchMaxWidths( 241 contents->GetContentWidth(), 242 separator_width_, 243 description ? description->GetContentWidth() : 0, 244 mirroring_context_->remaining_width(x), 245 !AutocompleteMatch::IsSearchType(match.type), 246 &contents_max_width, 247 &description_max_width); 248 249 x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width); 250 251 if (description_max_width != 0) { 252 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas, x, y, 253 separator_width_); 254 DrawRenderText(match, description, false, canvas, x, y, 255 description_max_width); 256 } 257} 258 259int OmniboxResultView::DrawRenderText( 260 const AutocompleteMatch& match, 261 gfx::RenderText* render_text, 262 bool contents, 263 gfx::Canvas* canvas, 264 int x, 265 int y, 266 int max_width) const { 267 DCHECK(!render_text->text().empty()); 268 269 const int remaining_width = mirroring_context_->remaining_width(x); 270 int right_x = x + max_width; 271 272 // Infinite suggestions should appear with the leading ellipses vertically 273 // stacked. 274 if (contents && 275 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)) { 276 // When the directionality of suggestion doesn't match the UI, we try to 277 // vertically stack the ellipsis by restricting the end edge (right_x). 278 const bool is_ui_rtl = base::i18n::IsRTL(); 279 const bool is_match_contents_rtl = 280 (render_text->GetTextDirection() == base::i18n::RIGHT_TO_LEFT); 281 const int offset = 282 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl); 283 284 scoped_ptr<gfx::RenderText> prefix_render_text( 285 CreateRenderText(base::UTF8ToUTF16( 286 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix)))); 287 const int prefix_width = prefix_render_text->GetContentWidth(); 288 int prefix_x = x; 289 290 const int max_match_contents_width = model_->max_match_contents_width(); 291 292 if (is_ui_rtl != is_match_contents_rtl) { 293 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR 294 // infinite suggestions appear near the right edge in RTL UI. This is 295 // against the natural horizontal alignment of the text. We reduce the 296 // width of the box for suggestion display, so that the suggestions appear 297 // in correct confines. This reduced width allows us to modify the text 298 // alignment (see below). 299 right_x = x + std::min(remaining_width - prefix_width, 300 std::max(offset, max_match_contents_width)); 301 prefix_x = right_x; 302 // We explicitly set the horizontal alignment so that when LTR suggestions 303 // show in RTL UI (or vice versa), their ellipses appear stacked in a 304 // single column. 305 render_text->SetHorizontalAlignment( 306 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 307 } else { 308 // If the dropdown is wide enough, place the ellipsis at the position 309 // where the omitted text would have ended. Otherwise reduce the offset of 310 // the ellipsis such that the widest suggestion reaches the end of the 311 // dropdown. 312 const int start_offset = std::max(prefix_width, 313 std::min(remaining_width - max_match_contents_width, offset)); 314 right_x = x + std::min(remaining_width, start_offset + max_width); 315 x += start_offset; 316 prefix_x = x - prefix_width; 317 } 318 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ? 319 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR); 320 prefix_render_text->SetHorizontalAlignment( 321 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 322 prefix_render_text->SetDisplayRect(gfx::Rect( 323 mirroring_context_->mirrored_left_coord( 324 prefix_x, prefix_x + prefix_width), y, 325 prefix_width, height())); 326 prefix_render_text->Draw(canvas); 327 } 328 329 // Set the display rect to trigger eliding. 330 render_text->SetDisplayRect(gfx::Rect( 331 mirroring_context_->mirrored_left_coord(x, right_x), y, 332 right_x - x, height())); 333 render_text->Draw(canvas); 334 return right_x; 335} 336 337scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText( 338 const base::string16& text) const { 339 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance()); 340 render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0))); 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 348scoped_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 392int 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. 399int 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 424int OmniboxResultView::default_icon_size_ = 0; 425 426gfx::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 455const 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 462bool OmniboxResultView::ShowOnlyKeywordMatch() const { 463 return match_.associated_keyword && 464 (keyword_icon_->x() <= icon_bounds_.right()); 465} 466 467void 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 475void OmniboxResultView::InitContentsRenderTextIfNecessary() const { 476 if (!contents_rendertext_) { 477 contents_rendertext_.reset( 478 CreateClassifiedRenderText( 479 match_.contents, match_.contents_class, false).release()); 480 } 481} 482 483void 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 513void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 514 animation_->SetSlideDuration(width() / 4); 515} 516 517void 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 561void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) { 562 Layout(); 563 SchedulePaint(); 564} 565