OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011 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 #include "chrome/browser/ui/views/autocomplete/autocomplete_result_view.h" |
| 6 |
| 7 #include "base/i18n/bidi_line_iterator.h" |
| 8 #include "chrome/browser/ui/views/location_bar/location_bar_view.h" |
| 9 #include "grit/generated_resources.h" |
| 10 #include "grit/theme_resources.h" |
| 11 #include "ui/base/l10n/l10n_util.h" |
| 12 #include "ui/base/resource/resource_bundle.h" |
| 13 #include "ui/base/text/text_elider.h" |
| 14 #include "ui/gfx/canvas_skia.h" |
| 15 #include "ui/gfx/color_utils.h" |
| 16 |
| 17 #if defined(OS_LINUX) |
| 18 #include "chrome/browser/ui/gtk/gtk_util.h" |
| 19 #include "ui/gfx/skia_utils_gtk.h" |
| 20 #endif |
| 21 |
| 22 namespace { |
| 23 |
| 24 const char16 kEllipsis[] = { 0x2026 }; |
| 25 |
| 26 // The minimum distance between the top and bottom of the {icon|text} and the |
| 27 // top or bottom of the row. "Minimum" is used because the vertical padding |
| 28 // may be larger, depending on the size of the text. |
| 29 const int kMinimalIconVerticalPadding = 2; |
| 30 const int kMinimalTextVerticalPadding = 3; |
| 31 |
| 32 }; |
| 33 |
| 34 //////////////////////////////////////////////////////////////////////////////// |
| 35 // AutocompleteResultView, public: |
| 36 |
| 37 // static |
| 38 int AutocompleteResultView::icon_size_ = 0; |
| 39 |
| 40 // This class is a utility class for calculations affected by whether the result |
| 41 // view is horizontally mirrored. The drawing functions can be written as if |
| 42 // all drawing occurs left-to-right, and then use this class to get the actual |
| 43 // coordinates to begin drawing onscreen. |
| 44 class AutocompleteResultView::MirroringContext { |
| 45 public: |
| 46 MirroringContext() : center_(0), right_(0) {} |
| 47 |
| 48 // Tells the mirroring context to use the provided range as the physical |
| 49 // bounds of the drawing region. When coordinate mirroring is needed, the |
| 50 // mirror point will be the center of this range. |
| 51 void Initialize(int x, int width) { |
| 52 center_ = x + width / 2; |
| 53 right_ = x + width; |
| 54 } |
| 55 |
| 56 // Given a logical range within the drawing region, returns the coordinate of |
| 57 // the possibly-mirrored "left" side. (This functions exactly like |
| 58 // View::MirroredLeftPointForRect().) |
| 59 int mirrored_left_coord(int left, int right) const { |
| 60 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left; |
| 61 } |
| 62 |
| 63 // Given a logical coordinate within the drawing region, returns the remaining |
| 64 // width available. |
| 65 int remaining_width(int x) const { |
| 66 return right_ - x; |
| 67 } |
| 68 |
| 69 private: |
| 70 int center_; |
| 71 int right_; |
| 72 |
| 73 DISALLOW_COPY_AND_ASSIGN(MirroringContext); |
| 74 }; |
| 75 |
| 76 AutocompleteResultView::AutocompleteResultView( |
| 77 AutocompleteResultViewModel* model, |
| 78 int model_index, |
| 79 const gfx::Font& font, |
| 80 const gfx::Font& bold_font) |
| 81 : icon_vertical_padding_(kMinimalIconVerticalPadding), |
| 82 text_vertical_padding_(kMinimalTextVerticalPadding), |
| 83 model_(model), |
| 84 model_index_(model_index), |
| 85 normal_font_(font), |
| 86 bold_font_(bold_font), |
| 87 ellipsis_width_(font.GetStringWidth(string16(kEllipsis))), |
| 88 mirroring_context_(new MirroringContext()), |
| 89 match_(NULL, 0, false, AutocompleteMatch::URL_WHAT_YOU_TYPED) { |
| 90 CHECK(model_index >= 0); |
| 91 if (icon_size_ == 0) { |
| 92 icon_size_ = ResourceBundle::GetSharedInstance().GetBitmapNamed( |
| 93 AutocompleteMatch::TypeToIcon(AutocompleteMatch::URL_WHAT_YOU_TYPED))-> |
| 94 width(); |
| 95 } |
| 96 } |
| 97 |
| 98 AutocompleteResultView::~AutocompleteResultView() { |
| 99 } |
| 100 |
| 101 void AutocompleteResultView::Paint(gfx::Canvas* canvas) { |
| 102 const ResultViewState state = GetState(); |
| 103 if (state != NORMAL) |
| 104 canvas->AsCanvasSkia()->drawColor(GetColor(state, BACKGROUND)); |
| 105 |
| 106 // Paint the icon. |
| 107 canvas->DrawBitmapInt(*GetIcon(), GetMirroredXForRect(icon_bounds_), |
| 108 icon_bounds_.y()); |
| 109 |
| 110 // Paint the text. |
| 111 int x = GetMirroredXForRect(text_bounds_); |
| 112 mirroring_context_->Initialize(x, text_bounds_.width()); |
| 113 PaintMatch(canvas, match_, x); |
| 114 } |
| 115 |
| 116 void AutocompleteResultView::Layout() { |
| 117 icon_bounds_.SetRect(LocationBarView::kEdgeItemPadding, |
| 118 (height() - icon_size_) / 2, icon_size_, icon_size_); |
| 119 int text_x = icon_bounds_.right() + LocationBarView::kItemPadding; |
| 120 int font_height = std::max(normal_font_.GetHeight(), bold_font_.GetHeight()); |
| 121 text_bounds_.SetRect(text_x, std::max(0, (height() - font_height) / 2), |
| 122 std::max(bounds().width() - text_x - LocationBarView::kEdgeItemPadding, |
| 123 0), font_height); |
| 124 } |
| 125 |
| 126 gfx::Size AutocompleteResultView::GetPreferredSize() { |
| 127 return gfx::Size(0, GetPreferredHeight(normal_font_, bold_font_)); |
| 128 } |
| 129 |
| 130 int AutocompleteResultView::GetPreferredHeight( |
| 131 const gfx::Font& font, |
| 132 const gfx::Font& bold_font) { |
| 133 int text_height = std::max(font.GetHeight(), bold_font.GetHeight()) + |
| 134 (text_vertical_padding_ * 2); |
| 135 int icon_height = icon_size_ + (icon_vertical_padding_ * 2); |
| 136 return std::max(icon_height, text_height); |
| 137 } |
| 138 |
| 139 // static |
| 140 SkColor AutocompleteResultView::GetColor(ResultViewState state, |
| 141 ColorKind kind) { |
| 142 static bool initialized = false; |
| 143 static SkColor colors[NUM_STATES][NUM_KINDS]; |
| 144 if (!initialized) { |
| 145 #if defined(OS_WIN) |
| 146 colors[NORMAL][BACKGROUND] = color_utils::GetSysSkColor(COLOR_WINDOW); |
| 147 colors[SELECTED][BACKGROUND] = color_utils::GetSysSkColor(COLOR_HIGHLIGHT); |
| 148 colors[NORMAL][TEXT] = color_utils::GetSysSkColor(COLOR_WINDOWTEXT); |
| 149 colors[SELECTED][TEXT] = color_utils::GetSysSkColor(COLOR_HIGHLIGHTTEXT); |
| 150 #elif defined(OS_LINUX) |
| 151 GdkColor bg_color, selected_bg_color, text_color, selected_text_color; |
| 152 gtk_util::GetTextColors( |
| 153 &bg_color, &selected_bg_color, &text_color, &selected_text_color); |
| 154 colors[NORMAL][BACKGROUND] = gfx::GdkColorToSkColor(bg_color); |
| 155 colors[SELECTED][BACKGROUND] = gfx::GdkColorToSkColor(selected_bg_color); |
| 156 colors[NORMAL][TEXT] = gfx::GdkColorToSkColor(text_color); |
| 157 colors[SELECTED][TEXT] = gfx::GdkColorToSkColor(selected_text_color); |
| 158 #else |
| 159 // TODO(beng): source from theme provider. |
| 160 colors[NORMAL][BACKGROUND] = SK_ColorWHITE; |
| 161 colors[SELECTED][BACKGROUND] = SK_ColorBLUE; |
| 162 colors[NORMAL][TEXT] = SK_ColorBLACK; |
| 163 colors[SELECTED][TEXT] = SK_ColorWHITE; |
| 164 #endif |
| 165 colors[HOVERED][BACKGROUND] = |
| 166 color_utils::AlphaBlend(colors[SELECTED][BACKGROUND], |
| 167 colors[NORMAL][BACKGROUND], 64); |
| 168 colors[HOVERED][TEXT] = colors[NORMAL][TEXT]; |
| 169 for (int i = 0; i < NUM_STATES; ++i) { |
| 170 colors[i][DIMMED_TEXT] = |
| 171 color_utils::AlphaBlend(colors[i][TEXT], colors[i][BACKGROUND], 128); |
| 172 colors[i][URL] = color_utils::GetReadableColor(SkColorSetRGB(0, 128, 0), |
| 173 colors[i][BACKGROUND]); |
| 174 } |
| 175 initialized = true; |
| 176 } |
| 177 |
| 178 return colors[state][kind]; |
| 179 } |
| 180 |
| 181 //////////////////////////////////////////////////////////////////////////////// |
| 182 // AutocompleteResultView, protected: |
| 183 |
| 184 void AutocompleteResultView::PaintMatch(gfx::Canvas* canvas, |
| 185 const AutocompleteMatch& match, |
| 186 int x) { |
| 187 x = DrawString(canvas, match.contents, match.contents_class, false, x, |
| 188 text_bounds_.y()); |
| 189 |
| 190 // Paint the description. |
| 191 // TODO(pkasting): Because we paint in multiple separate pieces, we can wind |
| 192 // up with no space even for an ellipsis for one or both of these pieces. |
| 193 // Instead, we should paint the entire match as a single long string. This |
| 194 // would also let us use a more properly-localizable string than we get with |
| 195 // just the IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR. |
| 196 if (!match.description.empty()) { |
| 197 string16 separator = |
| 198 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR); |
| 199 ACMatchClassifications classifications; |
| 200 classifications.push_back( |
| 201 ACMatchClassification(0, ACMatchClassification::NONE)); |
| 202 x = DrawString(canvas, separator, classifications, true, x, |
| 203 text_bounds_.y()); |
| 204 |
| 205 DrawString(canvas, match.description, match.description_class, true, x, |
| 206 text_bounds_.y()); |
| 207 } |
| 208 } |
| 209 |
| 210 // static |
| 211 bool AutocompleteResultView::SortRunsLogically(const RunData& lhs, |
| 212 const RunData& rhs) { |
| 213 return lhs.run_start < rhs.run_start; |
| 214 } |
| 215 |
| 216 // static |
| 217 bool AutocompleteResultView::SortRunsVisually(const RunData& lhs, |
| 218 const RunData& rhs) { |
| 219 return lhs.visual_order < rhs.visual_order; |
| 220 } |
| 221 |
| 222 AutocompleteResultView::ResultViewState |
| 223 AutocompleteResultView::GetState() const { |
| 224 if (model_->IsSelectedIndex(model_index_)) |
| 225 return SELECTED; |
| 226 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL; |
| 227 } |
| 228 |
| 229 const SkBitmap* AutocompleteResultView::GetIcon() const { |
| 230 const SkBitmap* bitmap = model_->GetSpecialIcon(model_index_); |
| 231 if (bitmap) |
| 232 return bitmap; |
| 233 |
| 234 int icon = match_.starred ? |
| 235 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type); |
| 236 if (model_->IsSelectedIndex(model_index_)) { |
| 237 switch (icon) { |
| 238 case IDR_OMNIBOX_HTTP: icon = IDR_OMNIBOX_HTTP_SELECTED; break; |
| 239 case IDR_OMNIBOX_HISTORY: icon = IDR_OMNIBOX_HISTORY_SELECTED; break; |
| 240 case IDR_OMNIBOX_SEARCH: icon = IDR_OMNIBOX_SEARCH_SELECTED; break; |
| 241 case IDR_OMNIBOX_STAR: icon = IDR_OMNIBOX_STAR_SELECTED; break; |
| 242 default: NOTREACHED(); break; |
| 243 } |
| 244 } |
| 245 return ResourceBundle::GetSharedInstance().GetBitmapNamed(icon); |
| 246 } |
| 247 |
| 248 int AutocompleteResultView::DrawString( |
| 249 gfx::Canvas* canvas, |
| 250 const string16& text, |
| 251 const ACMatchClassifications& classifications, |
| 252 bool force_dim, |
| 253 int x, |
| 254 int y) { |
| 255 if (text.empty()) |
| 256 return x; |
| 257 |
| 258 // Check whether or not this text is a URL. URLs are always displayed LTR |
| 259 // regardless of locale. |
| 260 bool is_url = true; |
| 261 for (ACMatchClassifications::const_iterator i(classifications.begin()); |
| 262 i != classifications.end(); ++i) { |
| 263 if (!(i->style & ACMatchClassification::URL)) { |
| 264 is_url = false; |
| 265 break; |
| 266 } |
| 267 } |
| 268 |
| 269 // Split the text into visual runs. We do this first so that we don't need to |
| 270 // worry about whether our eliding might change the visual display in |
| 271 // unintended ways, e.g. by removing directional markings or by adding an |
| 272 // ellipsis that's not enclosed in appropriate markings. |
| 273 base::i18n::BiDiLineIterator bidi_line; |
| 274 if (!bidi_line.Open(text, base::i18n::IsRTL(), is_url)) |
| 275 return x; |
| 276 const int num_runs = bidi_line.CountRuns(); |
| 277 Runs runs; |
| 278 for (int run = 0; run < num_runs; ++run) { |
| 279 int run_start_int = 0, run_length_int = 0; |
| 280 // The index we pass to GetVisualRun corresponds to the position of the run |
| 281 // in the displayed text. For example, the string "Google in HEBREW" (where |
| 282 // HEBREW is text in the Hebrew language) has two runs: "Google in " which |
| 283 // is an LTR run, and "HEBREW" which is an RTL run. In an LTR context, the |
| 284 // run "Google in " has the index 0 (since it is the leftmost run |
| 285 // displayed). In an RTL context, the same run has the index 1 because it |
| 286 // is the rightmost run. This is why the order in which we traverse the |
| 287 // runs is different depending on the locale direction. |
| 288 const UBiDiDirection run_direction = bidi_line.GetVisualRun( |
| 289 (base::i18n::IsRTL() && !is_url) ? (num_runs - run - 1) : run, |
| 290 &run_start_int, &run_length_int); |
| 291 DCHECK_GT(run_length_int, 0); |
| 292 runs.push_back(RunData()); |
| 293 RunData* current_run = &runs.back(); |
| 294 current_run->run_start = run_start_int; |
| 295 const size_t run_end = current_run->run_start + run_length_int; |
| 296 current_run->visual_order = run; |
| 297 current_run->is_rtl = !is_url && (run_direction == UBIDI_RTL); |
| 298 current_run->pixel_width = 0; |
| 299 |
| 300 // Compute classifications for this run. |
| 301 for (size_t i = 0; i < classifications.size(); ++i) { |
| 302 const size_t text_start = |
| 303 std::max(classifications[i].offset, current_run->run_start); |
| 304 if (text_start >= run_end) |
| 305 break; // We're past the last classification in the run. |
| 306 |
| 307 const size_t text_end = (i < (classifications.size() - 1)) ? |
| 308 std::min(classifications[i + 1].offset, run_end) : run_end; |
| 309 if (text_end <= current_run->run_start) |
| 310 continue; // We haven't reached the first classification in the run. |
| 311 |
| 312 current_run->classifications.push_back(ClassificationData()); |
| 313 ClassificationData* current_data = |
| 314 ¤t_run->classifications.back(); |
| 315 current_data->text = text.substr(text_start, text_end - text_start); |
| 316 |
| 317 // Calculate style-related data. |
| 318 const int style = classifications[i].style; |
| 319 const bool use_bold_font = !!(style & ACMatchClassification::MATCH); |
| 320 current_data->font = &(use_bold_font ? bold_font_ : normal_font_); |
| 321 const ResultViewState state = GetState(); |
| 322 if (style & ACMatchClassification::URL) |
| 323 current_data->color = GetColor(state, URL); |
| 324 else if (style & ACMatchClassification::DIM) |
| 325 current_data->color = GetColor(state, DIMMED_TEXT); |
| 326 else |
| 327 current_data->color = GetColor(state, force_dim ? DIMMED_TEXT : TEXT); |
| 328 current_data->pixel_width = |
| 329 current_data->font->GetStringWidth(current_data->text); |
| 330 current_run->pixel_width += current_data->pixel_width; |
| 331 } |
| 332 DCHECK(!current_run->classifications.empty()); |
| 333 } |
| 334 DCHECK(!runs.empty()); |
| 335 |
| 336 // Sort into logical order so we can elide logically. |
| 337 std::sort(runs.begin(), runs.end(), &SortRunsLogically); |
| 338 |
| 339 // Now determine what to elide, if anything. Several subtle points: |
| 340 // * Because we have the run data, we can get edge cases correct, like |
| 341 // whether to place an ellipsis before or after the end of a run when the |
| 342 // text needs to be elided at the run boundary. |
| 343 // * The "or one before it" comments below refer to cases where an earlier |
| 344 // classification fits completely, but leaves too little space for an |
| 345 // ellipsis that turns out to be needed later. These cases are commented |
| 346 // more completely in Elide(). |
| 347 int remaining_width = mirroring_context_->remaining_width(x); |
| 348 for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) { |
| 349 if (i->pixel_width > remaining_width) { |
| 350 // This run or one before it needs to be elided. |
| 351 for (Classifications::iterator j(i->classifications.begin()); |
| 352 j != i->classifications.end(); ++j) { |
| 353 if (j->pixel_width > remaining_width) { |
| 354 // This classification or one before it needs to be elided. Erase all |
| 355 // further classifications and runs so Elide() can simply reverse- |
| 356 // iterate over everything to find the specific classification to |
| 357 // elide. |
| 358 i->classifications.erase(++j, i->classifications.end()); |
| 359 runs.erase(++i, runs.end()); |
| 360 Elide(&runs, remaining_width); |
| 361 break; |
| 362 } |
| 363 remaining_width -= j->pixel_width; |
| 364 } |
| 365 break; |
| 366 } |
| 367 remaining_width -= i->pixel_width; |
| 368 } |
| 369 |
| 370 // Sort back into visual order so we can display the runs correctly. |
| 371 std::sort(runs.begin(), runs.end(), &SortRunsVisually); |
| 372 |
| 373 // Draw the runs. |
| 374 for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) { |
| 375 const bool reverse_visible_order = (i->is_rtl != base::i18n::IsRTL()); |
| 376 int flags = gfx::Canvas::NO_ELLIPSIS; // We've already elided. |
| 377 if (reverse_visible_order) { |
| 378 std::reverse(i->classifications.begin(), i->classifications.end()); |
| 379 if (i->is_rtl) |
| 380 flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY; |
| 381 } |
| 382 for (Classifications::const_iterator j(i->classifications.begin()); |
| 383 j != i->classifications.end(); ++j) { |
| 384 int left = mirroring_context_->mirrored_left_coord(x, x + j->pixel_width); |
| 385 canvas->DrawStringInt(j->text, *j->font, j->color, left, |
| 386 y, j->pixel_width, j->font->GetHeight(), flags); |
| 387 x += j->pixel_width; |
| 388 } |
| 389 } |
| 390 |
| 391 return x; |
| 392 } |
| 393 |
| 394 void AutocompleteResultView::Elide(Runs* runs, int remaining_width) const { |
| 395 // The complexity of this function is due to edge cases like the following: |
| 396 // We have 100 px of available space, an initial classification that takes 86 |
| 397 // px, and a font that has a 15 px wide ellipsis character. Now if the first |
| 398 // classification is followed by several very narrow classifications (e.g. 3 |
| 399 // px wide each), we don't know whether we need to elide or not at the time we |
| 400 // see the first classification -- it depends on how many subsequent |
| 401 // classifications follow, and some of those may be in the next run (or |
| 402 // several runs!). This is why instead we let our caller move forward until |
| 403 // we know we definitely need to elide, and then in this function we move |
| 404 // backward again until we find a string that we can successfully do the |
| 405 // eliding on. |
| 406 bool first_classification = true; |
| 407 for (Runs::reverse_iterator i(runs->rbegin()); i != runs->rend(); ++i) { |
| 408 for (Classifications::reverse_iterator j(i->classifications.rbegin()); |
| 409 j != i->classifications.rend(); ++j) { |
| 410 if (!first_classification) { |
| 411 // For all but the first classification we consider, we need to append |
| 412 // an ellipsis, since there isn't enough room to draw it after this |
| 413 // classification. |
| 414 j->text += kEllipsis; |
| 415 |
| 416 // We also add this classification's width (sans ellipsis) back to the |
| 417 // available width since we want to consider the available space we'll |
| 418 // have when we draw this classification. |
| 419 remaining_width += j->pixel_width; |
| 420 } |
| 421 first_classification = false; |
| 422 |
| 423 // Can we fit at least an ellipsis? |
| 424 string16 elided_text = |
| 425 ui::ElideText(j->text, *j->font, remaining_width, false); |
| 426 Classifications::reverse_iterator prior_classification(j); |
| 427 ++prior_classification; |
| 428 const bool on_first_classification = |
| 429 (prior_classification == i->classifications.rend()); |
| 430 if (elided_text.empty() && (remaining_width >= ellipsis_width_) && |
| 431 on_first_classification) { |
| 432 // Edge case: This classification is bold, we can't fit a bold ellipsis |
| 433 // but we can fit a normal one, and this is the first classification in |
| 434 // the run. We should display a lone normal ellipsis, because appending |
| 435 // one to the end of the previous run might put it in the wrong visual |
| 436 // location (if the previous run is reversed from the normal visual |
| 437 // order). |
| 438 // NOTE: If this isn't the first classification in the run, we don't |
| 439 // need to bother with this; see note below. |
| 440 elided_text = kEllipsis; |
| 441 } |
| 442 if (!elided_text.empty()) { |
| 443 // Success. Elide this classification and stop. |
| 444 j->text = elided_text; |
| 445 |
| 446 // If we could only fit an ellipsis, then only make it bold if there was |
| 447 // an immediate prior classification in this run that was also bold, or |
| 448 // it will look orphaned. |
| 449 if ((elided_text.length() == 1) && |
| 450 (on_first_classification || |
| 451 (prior_classification->font == &normal_font_))) |
| 452 j->font = &normal_font_; |
| 453 |
| 454 j->pixel_width = j->font->GetStringWidth(elided_text); |
| 455 |
| 456 // Erase any other classifications that come after the elided one. |
| 457 i->classifications.erase(j.base(), i->classifications.end()); |
| 458 runs->erase(i.base(), runs->end()); |
| 459 return; |
| 460 } |
| 461 |
| 462 // We couldn't fit an ellipsis. Move back one classification, |
| 463 // append an ellipsis, and try again. |
| 464 // NOTE: In the edge case that a bold ellipsis doesn't fit but a |
| 465 // normal one would, and we reach here, then there is a previous |
| 466 // classification in this run, and so either: |
| 467 // * It's normal, and will be able to draw successfully with the |
| 468 // ellipsis we'll append to it, or |
| 469 // * It is also bold, in which case we don't want to fall back |
| 470 // to a normal ellipsis anyway (see comment above). |
| 471 } |
| 472 } |
| 473 |
| 474 // We couldn't draw anything. |
| 475 runs->clear(); |
| 476 } |
OLD | NEW |