OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 // | 4 // |
5 // This file implements utility functions for eliding and formatting UI text. | 5 // This file implements utility functions for eliding and formatting UI text. |
6 // | 6 // |
7 // Note that several of the functions declared in text_elider.h are implemented | 7 // Note that several of the functions declared in text_elider.h are implemented |
8 // in this file using helper classes in an unnamed namespace. | 8 // in this file using helper classes in an unnamed namespace. |
9 | 9 |
10 #include "ui/gfx/text_elider.h" | 10 #include "ui/gfx/text_elider.h" |
11 | 11 |
12 #include <string> | 12 #include <string> |
13 #include <vector> | 13 #include <vector> |
14 | 14 |
15 #include "base/files/file_path.h" | 15 #include "base/files/file_path.h" |
16 #include "base/i18n/break_iterator.h" | 16 #include "base/i18n/break_iterator.h" |
17 #include "base/i18n/char_iterator.h" | 17 #include "base/i18n/char_iterator.h" |
18 #include "base/i18n/rtl.h" | 18 #include "base/i18n/rtl.h" |
19 #include "base/memory/scoped_ptr.h" | 19 #include "base/memory/scoped_ptr.h" |
20 #include "base/strings/string_split.h" | 20 #include "base/strings/string_split.h" |
21 #include "base/strings/string_util.h" | 21 #include "base/strings/string_util.h" |
22 #include "base/strings/sys_string_conversions.h" | 22 #include "base/strings/sys_string_conversions.h" |
23 #include "base/strings/utf_string_conversions.h" | 23 #include "base/strings/utf_string_conversions.h" |
24 #include "net/base/escape.h" | |
25 #include "net/base/net_util.h" | |
26 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" | |
27 #include "third_party/icu/source/common/unicode/rbbi.h" | 24 #include "third_party/icu/source/common/unicode/rbbi.h" |
28 #include "third_party/icu/source/common/unicode/uloc.h" | 25 #include "third_party/icu/source/common/unicode/uloc.h" |
29 #include "ui/gfx/font_list.h" | 26 #include "ui/gfx/font_list.h" |
30 #include "ui/gfx/text_utils.h" | 27 #include "ui/gfx/text_utils.h" |
31 #include "url/gurl.h" | |
32 | 28 |
33 using base::ASCIIToUTF16; | 29 using base::ASCIIToUTF16; |
34 using base::UTF8ToUTF16; | 30 using base::UTF8ToUTF16; |
35 using base::WideToUTF16; | 31 using base::WideToUTF16; |
36 | 32 |
37 namespace gfx { | 33 namespace gfx { |
38 | 34 |
39 // U+2026 in utf8 | 35 // U+2026 in utf8 |
40 const char kEllipsis[] = "\xE2\x80\xA6"; | 36 const char kEllipsis[] = "\xE2\x80\xA6"; |
41 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; | 37 const base::char16 kEllipsisUTF16[] = { 0x2026, 0 }; |
42 const base::char16 kForwardSlash = '/'; | 38 const base::char16 kForwardSlash = '/'; |
43 | 39 |
44 namespace { | |
45 | |
46 | |
47 // Build a path from the first |num_components| elements in |path_elements|. | |
48 // Prepends |path_prefix|, appends |filename|, inserts ellipsis if appropriate. | |
49 base::string16 BuildPathFromComponents( | |
50 const base::string16& path_prefix, | |
51 const std::vector<base::string16>& path_elements, | |
52 const base::string16& filename, | |
53 size_t num_components) { | |
54 // Add the initial elements of the path. | |
55 base::string16 path = path_prefix; | |
56 | |
57 // Build path from first |num_components| elements. | |
58 for (size_t j = 0; j < num_components; ++j) | |
59 path += path_elements[j] + kForwardSlash; | |
60 | |
61 // Add |filename|, ellipsis if necessary. | |
62 if (num_components != (path_elements.size() - 1)) | |
63 path += base::string16(kEllipsisUTF16) + kForwardSlash; | |
64 path += filename; | |
65 | |
66 return path; | |
67 } | |
68 | |
69 // Takes a prefix (Domain, or Domain+subdomain) and a collection of path | |
70 // components and elides if possible. Returns a string containing the longest | |
71 // possible elided path, or an empty string if elision is not possible. | |
72 base::string16 ElideComponentizedPath( | |
73 const base::string16& url_path_prefix, | |
74 const std::vector<base::string16>& url_path_elements, | |
75 const base::string16& url_filename, | |
76 const base::string16& url_query, | |
77 const FontList& font_list, | |
78 float available_pixel_width) { | |
79 const size_t url_path_number_of_elements = url_path_elements.size(); | |
80 | |
81 CHECK(url_path_number_of_elements); | |
82 for (size_t i = url_path_number_of_elements - 1; i > 0; --i) { | |
83 base::string16 elided_path = BuildPathFromComponents(url_path_prefix, | |
84 url_path_elements, url_filename, i); | |
85 if (available_pixel_width >= GetStringWidthF(elided_path, font_list)) | |
86 return ElideText(elided_path + url_query, font_list, | |
87 available_pixel_width, ELIDE_AT_END); | |
88 } | |
89 | |
90 return base::string16(); | |
91 } | |
92 | |
93 } // namespace | |
94 | |
95 StringSlicer::StringSlicer(const base::string16& text, | 40 StringSlicer::StringSlicer(const base::string16& text, |
96 const base::string16& ellipsis, | 41 const base::string16& ellipsis, |
97 bool elide_in_middle) | 42 bool elide_in_middle) |
98 : text_(text), | 43 : text_(text), |
99 ellipsis_(ellipsis), | 44 ellipsis_(ellipsis), |
100 elide_in_middle_(elide_in_middle) { | 45 elide_in_middle_(elide_in_middle) { |
101 } | 46 } |
102 | 47 |
103 base::string16 StringSlicer::CutString(size_t length, bool insert_ellipsis) { | 48 base::string16 StringSlicer::CutString(size_t length, bool insert_ellipsis) { |
104 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ | 49 const base::string16 ellipsis_text = insert_ellipsis ? ellipsis_ |
(...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
184 // Fit the username in the remaining width (at this point the elided username | 129 // Fit the username in the remaining width (at this point the elided username |
185 // is guaranteed to fit with at least one character remaining given all the | 130 // is guaranteed to fit with at least one character remaining given all the |
186 // precautions taken earlier). | 131 // precautions taken earlier). |
187 available_pixel_width -= GetStringWidthF(domain, font_list); | 132 available_pixel_width -= GetStringWidthF(domain, font_list); |
188 username = ElideText(username, font_list, available_pixel_width, | 133 username = ElideText(username, font_list, available_pixel_width, |
189 ELIDE_AT_END); | 134 ELIDE_AT_END); |
190 | 135 |
191 return username + kAtSignUTF16 + domain; | 136 return username + kAtSignUTF16 + domain; |
192 } | 137 } |
193 | 138 |
194 // TODO(pkasting): http://crbug.com/77883 This whole function gets | |
195 // kerning/ligatures/etc. issues potentially wrong by assuming that the width of | |
196 // a rendered string is always the sum of the widths of its substrings. Also I | |
197 // suspect it could be made simpler. | |
198 base::string16 ElideUrl(const GURL& url, | |
199 const FontList& font_list, | |
200 float available_pixel_width, | |
201 const std::string& languages) { | |
202 // Get a formatted string and corresponding parsing of the url. | |
203 url_parse::Parsed parsed; | |
204 const base::string16 url_string = | |
205 net::FormatUrl(url, languages, net::kFormatUrlOmitAll, | |
206 net::UnescapeRule::SPACES, &parsed, NULL, NULL); | |
207 if (available_pixel_width <= 0) | |
208 return url_string; | |
209 | |
210 // If non-standard, return plain eliding. | |
211 if (!url.IsStandard()) | |
212 return ElideText(url_string, font_list, available_pixel_width, | |
213 ELIDE_AT_END); | |
214 | |
215 // Now start eliding url_string to fit within available pixel width. | |
216 // Fist pass - check to see whether entire url_string fits. | |
217 const float pixel_width_url_string = GetStringWidthF(url_string, font_list); | |
218 if (available_pixel_width >= pixel_width_url_string) | |
219 return url_string; | |
220 | |
221 // Get the path substring, including query and reference. | |
222 const size_t path_start_index = parsed.path.begin; | |
223 const size_t path_len = parsed.path.len; | |
224 base::string16 url_path_query_etc = url_string.substr(path_start_index); | |
225 base::string16 url_path = url_string.substr(path_start_index, path_len); | |
226 | |
227 // Return general elided text if url minus the query fits. | |
228 const base::string16 url_minus_query = | |
229 url_string.substr(0, path_start_index + path_len); | |
230 if (available_pixel_width >= GetStringWidthF(url_minus_query, font_list)) | |
231 return ElideText(url_string, font_list, available_pixel_width, | |
232 ELIDE_AT_END); | |
233 | |
234 // Get Host. | |
235 base::string16 url_host = UTF8ToUTF16(url.host()); | |
236 | |
237 // Get domain and registry information from the URL. | |
238 base::string16 url_domain = UTF8ToUTF16( | |
239 net::registry_controlled_domains::GetDomainAndRegistry( | |
240 url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES)); | |
241 if (url_domain.empty()) | |
242 url_domain = url_host; | |
243 | |
244 // Add port if required. | |
245 if (!url.port().empty()) { | |
246 url_host += UTF8ToUTF16(":" + url.port()); | |
247 url_domain += UTF8ToUTF16(":" + url.port()); | |
248 } | |
249 | |
250 // Get sub domain. | |
251 base::string16 url_subdomain; | |
252 const size_t domain_start_index = url_host.find(url_domain); | |
253 if (domain_start_index != base::string16::npos) | |
254 url_subdomain = url_host.substr(0, domain_start_index); | |
255 const base::string16 kWwwPrefix = UTF8ToUTF16("www."); | |
256 if ((url_subdomain == kWwwPrefix || url_subdomain.empty() || | |
257 url.SchemeIsFile())) { | |
258 url_subdomain.clear(); | |
259 } | |
260 | |
261 // If this is a file type, the path is now defined as everything after ":". | |
262 // For example, "C:/aa/aa/bb", the path is "/aa/bb/cc". Interesting, the | |
263 // domain is now C: - this is a nice hack for eliding to work pleasantly. | |
264 if (url.SchemeIsFile()) { | |
265 // Split the path string using ":" | |
266 std::vector<base::string16> file_path_split; | |
267 base::SplitString(url_path, ':', &file_path_split); | |
268 if (file_path_split.size() > 1) { // File is of type "file:///C:/.." | |
269 url_host.clear(); | |
270 url_domain.clear(); | |
271 url_subdomain.clear(); | |
272 | |
273 const base::string16 kColon = UTF8ToUTF16(":"); | |
274 url_host = url_domain = file_path_split.at(0).substr(1) + kColon; | |
275 url_path_query_etc = url_path = file_path_split.at(1); | |
276 } | |
277 } | |
278 | |
279 // Second Pass - remove scheme - the rest fits. | |
280 const float pixel_width_url_host = GetStringWidthF(url_host, font_list); | |
281 const float pixel_width_url_path = GetStringWidthF(url_path_query_etc, | |
282 font_list); | |
283 if (available_pixel_width >= | |
284 pixel_width_url_host + pixel_width_url_path) | |
285 return url_host + url_path_query_etc; | |
286 | |
287 // Third Pass: Subdomain, domain and entire path fits. | |
288 const float pixel_width_url_domain = GetStringWidthF(url_domain, font_list); | |
289 const float pixel_width_url_subdomain = | |
290 GetStringWidthF(url_subdomain, font_list); | |
291 if (available_pixel_width >= | |
292 pixel_width_url_subdomain + pixel_width_url_domain + | |
293 pixel_width_url_path) | |
294 return url_subdomain + url_domain + url_path_query_etc; | |
295 | |
296 // Query element. | |
297 base::string16 url_query; | |
298 const float kPixelWidthDotsTrailer = GetStringWidthF( | |
299 base::string16(kEllipsisUTF16), font_list); | |
300 if (parsed.query.is_nonempty()) { | |
301 url_query = UTF8ToUTF16("?") + url_string.substr(parsed.query.begin); | |
302 if (available_pixel_width >= | |
303 (pixel_width_url_subdomain + pixel_width_url_domain + | |
304 pixel_width_url_path - GetStringWidthF(url_query, font_list))) { | |
305 return ElideText(url_subdomain + url_domain + url_path_query_etc, | |
306 font_list, available_pixel_width, ELIDE_AT_END); | |
307 } | |
308 } | |
309 | |
310 // Parse url_path using '/'. | |
311 std::vector<base::string16> url_path_elements; | |
312 base::SplitString(url_path, kForwardSlash, &url_path_elements); | |
313 | |
314 // Get filename - note that for a path ending with / | |
315 // such as www.google.com/intl/ads/, the file name is ads/. | |
316 size_t url_path_number_of_elements = url_path_elements.size(); | |
317 DCHECK(url_path_number_of_elements != 0); | |
318 base::string16 url_filename; | |
319 if ((url_path_elements.at(url_path_number_of_elements - 1)).length() > 0) { | |
320 url_filename = *(url_path_elements.end() - 1); | |
321 } else if (url_path_number_of_elements > 1) { // Path ends with a '/'. | |
322 url_filename = url_path_elements.at(url_path_number_of_elements - 2) + | |
323 kForwardSlash; | |
324 url_path_number_of_elements--; | |
325 } | |
326 DCHECK(url_path_number_of_elements != 0); | |
327 | |
328 const size_t kMaxNumberOfUrlPathElementsAllowed = 1024; | |
329 if (url_path_number_of_elements <= 1 || | |
330 url_path_number_of_elements > kMaxNumberOfUrlPathElementsAllowed) { | |
331 // No path to elide, or too long of a path (could overflow in loop below) | |
332 // Just elide this as a text string. | |
333 return ElideText(url_subdomain + url_domain + url_path_query_etc, font_list, | |
334 available_pixel_width, ELIDE_AT_END); | |
335 } | |
336 | |
337 // Start eliding the path and replacing elements by ".../". | |
338 const base::string16 kEllipsisAndSlash = | |
339 base::string16(kEllipsisUTF16) + kForwardSlash; | |
340 const float pixel_width_ellipsis_slash = | |
341 GetStringWidthF(kEllipsisAndSlash, font_list); | |
342 | |
343 // Check with both subdomain and domain. | |
344 base::string16 elided_path = | |
345 ElideComponentizedPath(url_subdomain + url_domain, url_path_elements, | |
346 url_filename, url_query, font_list, | |
347 available_pixel_width); | |
348 if (!elided_path.empty()) | |
349 return elided_path; | |
350 | |
351 // Check with only domain. | |
352 // If a subdomain is present, add an ellipsis before domain. | |
353 // This is added only if the subdomain pixel width is larger than | |
354 // the pixel width of kEllipsis. Otherwise, subdomain remains, | |
355 // which means that this case has been resolved earlier. | |
356 base::string16 url_elided_domain = url_subdomain + url_domain; | |
357 if (pixel_width_url_subdomain > kPixelWidthDotsTrailer) { | |
358 if (!url_subdomain.empty()) | |
359 url_elided_domain = kEllipsisAndSlash[0] + url_domain; | |
360 else | |
361 url_elided_domain = url_domain; | |
362 | |
363 elided_path = ElideComponentizedPath(url_elided_domain, url_path_elements, | |
364 url_filename, url_query, font_list, | |
365 available_pixel_width); | |
366 | |
367 if (!elided_path.empty()) | |
368 return elided_path; | |
369 } | |
370 | |
371 // Return elided domain/.../filename anyway. | |
372 base::string16 final_elided_url_string(url_elided_domain); | |
373 const float url_elided_domain_width = GetStringWidthF(url_elided_domain, | |
374 font_list); | |
375 | |
376 // A hack to prevent trailing ".../...". | |
377 if ((available_pixel_width - url_elided_domain_width) > | |
378 pixel_width_ellipsis_slash + kPixelWidthDotsTrailer + | |
379 GetStringWidthF(ASCIIToUTF16("UV"), font_list)) { | |
380 final_elided_url_string += BuildPathFromComponents(base::string16(), | |
381 url_path_elements, url_filename, 1); | |
382 } else { | |
383 final_elided_url_string += url_path; | |
384 } | |
385 | |
386 return ElideText(final_elided_url_string, font_list, available_pixel_width, | |
387 ELIDE_AT_END); | |
388 } | |
389 | |
390 base::string16 ElideFilename(const base::FilePath& filename, | 139 base::string16 ElideFilename(const base::FilePath& filename, |
391 const FontList& font_list, | 140 const FontList& font_list, |
392 float available_pixel_width) { | 141 float available_pixel_width) { |
393 #if defined(OS_WIN) | 142 #if defined(OS_WIN) |
394 base::string16 filename_utf16 = filename.value(); | 143 base::string16 filename_utf16 = filename.value(); |
395 base::string16 extension = filename.Extension(); | 144 base::string16 extension = filename.Extension(); |
396 base::string16 rootname = filename.BaseName().RemoveExtension().value(); | 145 base::string16 rootname = filename.BaseName().RemoveExtension().value(); |
397 #elif defined(OS_POSIX) | 146 #elif defined(OS_POSIX) |
398 base::string16 filename_utf16 = WideToUTF16(base::SysNativeMBToWide( | 147 base::string16 filename_utf16 = WideToUTF16(base::SysNativeMBToWide( |
399 filename.value())); | 148 filename.value())); |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
491 if (hi < lo) | 240 if (hi < lo) |
492 lo = hi; | 241 lo = hi; |
493 } else { | 242 } else { |
494 lo = guess + 1; | 243 lo = guess + 1; |
495 } | 244 } |
496 } | 245 } |
497 | 246 |
498 return slicer.CutString(guess, insert_ellipsis); | 247 return slicer.CutString(guess, insert_ellipsis); |
499 } | 248 } |
500 | 249 |
501 SortedDisplayURL::SortedDisplayURL(const GURL& url, | |
502 const std::string& languages) { | |
503 net::AppendFormattedHost(url, languages, &sort_host_); | |
504 base::string16 host_minus_www = net::StripWWW(sort_host_); | |
505 url_parse::Parsed parsed; | |
506 display_url_ = | |
507 net::FormatUrl(url, languages, net::kFormatUrlOmitAll, | |
508 net::UnescapeRule::SPACES, &parsed, &prefix_end_, NULL); | |
509 if (sort_host_.length() > host_minus_www.length()) { | |
510 prefix_end_ += sort_host_.length() - host_minus_www.length(); | |
511 sort_host_.swap(host_minus_www); | |
512 } | |
513 } | |
514 | |
515 SortedDisplayURL::SortedDisplayURL() : prefix_end_(0) { | |
516 } | |
517 | |
518 SortedDisplayURL::~SortedDisplayURL() { | |
519 } | |
520 | |
521 int SortedDisplayURL::Compare(const SortedDisplayURL& other, | |
522 icu::Collator* collator) const { | |
523 // Compare on hosts first. The host won't contain 'www.'. | |
524 UErrorCode compare_status = U_ZERO_ERROR; | |
525 UCollationResult host_compare_result = collator->compare( | |
526 static_cast<const UChar*>(sort_host_.c_str()), | |
527 static_cast<int>(sort_host_.length()), | |
528 static_cast<const UChar*>(other.sort_host_.c_str()), | |
529 static_cast<int>(other.sort_host_.length()), | |
530 compare_status); | |
531 DCHECK(U_SUCCESS(compare_status)); | |
532 if (host_compare_result != 0) | |
533 return host_compare_result; | |
534 | |
535 // Hosts match, compare on the portion of the url after the host. | |
536 base::string16 path = this->AfterHost(); | |
537 base::string16 o_path = other.AfterHost(); | |
538 compare_status = U_ZERO_ERROR; | |
539 UCollationResult path_compare_result = collator->compare( | |
540 static_cast<const UChar*>(path.c_str()), | |
541 static_cast<int>(path.length()), | |
542 static_cast<const UChar*>(o_path.c_str()), | |
543 static_cast<int>(o_path.length()), | |
544 compare_status); | |
545 DCHECK(U_SUCCESS(compare_status)); | |
546 if (path_compare_result != 0) | |
547 return path_compare_result; | |
548 | |
549 // Hosts and paths match, compare on the complete url. This'll push the www. | |
550 // ones to the end. | |
551 compare_status = U_ZERO_ERROR; | |
552 UCollationResult display_url_compare_result = collator->compare( | |
553 static_cast<const UChar*>(display_url_.c_str()), | |
554 static_cast<int>(display_url_.length()), | |
555 static_cast<const UChar*>(other.display_url_.c_str()), | |
556 static_cast<int>(other.display_url_.length()), | |
557 compare_status); | |
558 DCHECK(U_SUCCESS(compare_status)); | |
559 return display_url_compare_result; | |
560 } | |
561 | |
562 base::string16 SortedDisplayURL::AfterHost() const { | |
563 const size_t slash_index = display_url_.find(sort_host_, prefix_end_); | |
564 if (slash_index == base::string16::npos) { | |
565 NOTREACHED(); | |
566 return base::string16(); | |
567 } | |
568 return display_url_.substr(slash_index + sort_host_.length()); | |
569 } | |
570 | |
571 bool ElideString(const base::string16& input, int max_len, | 250 bool ElideString(const base::string16& input, int max_len, |
572 base::string16* output) { | 251 base::string16* output) { |
573 DCHECK_GE(max_len, 0); | 252 DCHECK_GE(max_len, 0); |
574 if (static_cast<int>(input.length()) <= max_len) { | 253 if (static_cast<int>(input.length()) <= max_len) { |
575 output->assign(input); | 254 output->assign(input); |
576 return false; | 255 return false; |
577 } | 256 } |
578 | 257 |
579 switch (max_len) { | 258 switch (max_len) { |
580 case 0: | 259 case 0: |
(...skipping 536 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1117 index = char_iterator.getIndex(); | 796 index = char_iterator.getIndex(); |
1118 } else { | 797 } else { |
1119 // String has leading whitespace, return the elide string. | 798 // String has leading whitespace, return the elide string. |
1120 return kElideString; | 799 return kElideString; |
1121 } | 800 } |
1122 } | 801 } |
1123 return string.substr(0, index) + kElideString; | 802 return string.substr(0, index) + kElideString; |
1124 } | 803 } |
1125 | 804 |
1126 } // namespace gfx | 805 } // namespace gfx |
OLD | NEW |