diff options
author | tsepez@chromium.org <tsepez@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-22 21:19:02 +0000 |
---|---|---|
committer | tsepez@chromium.org <tsepez@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-22 21:19:02 +0000 |
commit | af4b9df213e776858d025ec39059831ce15904d0 (patch) | |
tree | 8eb5cf37b46397605469d6506329693b5735802e | |
parent | ed56a7a029ec8515ab30ba13ee1dcdb4721c32d4 (diff) | |
download | chromium_src-af4b9df213e776858d025ec39059831ce15904d0.zip chromium_src-af4b9df213e776858d025ec39059831ce15904d0.tar.gz chromium_src-af4b9df213e776858d025ec39059831ce15904d0.tar.bz2 |
Last part of change for bug 49747.
Implements the two-dimensional elider in app/text_elider.cc, and calls it from js_modal_dialog to reformat the message text prior to displaying an alert box.
The elider is implemented using the abstractions introduced in the previous CLs for bug 49747.
BUG=49746
TEST=TextEliderTest.*
Review URL: http://codereview.chromium.org/5964007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@69981 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | app/text_elider.cc | 167 | ||||
-rw-r--r-- | app/text_elider.h | 19 | ||||
-rw-r--r-- | app/text_elider_unittest.cc | 118 | ||||
-rw-r--r-- | chrome/browser/ui/app_modal_dialogs/js_modal_dialog.cc | 9 |
4 files changed, 307 insertions, 6 deletions
diff --git a/app/text_elider.cc b/app/text_elider.cc index bfaec34..855e3c3 100644 --- a/app/text_elider.cc +++ b/app/text_elider.cc @@ -6,6 +6,8 @@ #include "app/text_elider.h" #include "base/file_path.h" +#include "base/i18n/break_iterator.h" +#include "base/i18n/char_iterator.h" #include "base/i18n/rtl.h" #include "base/string_split.h" #include "base/string_util.h" @@ -498,3 +500,168 @@ bool ElideString(const std::wstring& input, int max_len, std::wstring* output) { } } // namespace gfx + +namespace { + +// Internal class used to track progress of a rectangular string elide +// operation. Exists so the top-level ElideRectangleString() function +// can be broken into smaller methods sharing this state. +class RectangleString { + public: + RectangleString(size_t max_rows, size_t max_cols, string16 *output) + : max_rows_(max_rows), + max_cols_(max_cols), + current_row_(0), + current_col_(0), + suppressed_(false), + output_(output) {} + + // Perform deferred initializions following creation. Must be called + // before any input can be added via AddString(). + void Init() { output_->clear(); } + + // Add an input string, reformatting to fit the desired dimensions. + // AddString() may be called multiple times to concatenate together + // multiple strings into the region (the current caller doesn't do + // this, however). + void AddString(const string16& input); + + // Perform any deferred output processing. Must be called after the + // last AddString() call has occured. + bool Finalize(); + + private: + // Add a line to the rectangular region at the current position, + // either by itself or by breaking it into words. + void AddLine(const string16& line); + + // Add a word to the rectangluar region at the current position, + // either by itelf or by breaking it into characters. + void AddWord(const string16& word); + + // Add text to the output string if the rectangular boundaries + // have not been exceeded, advancing the current position. + void Append(const string16& string); + + // Add a newline to the output string if the rectangular boundaries + // have not been exceeded, resetting the current position to the + // beginning of the next line. + void NewLine(); + + // Maximum number of rows allowed in the output string. + size_t max_rows_; + + // Maximum number of characters allowed in the output string. + size_t max_cols_; + + // Current row position, always incremented and may exceed max_rows_ + // when the input can not fit in the region. We stop appending to + // the output string, however, when this condition occurs. In the + // future, we may want to expose this value to allow the caller to + // determine how many rows would actually be required to hold the + // formatted string. + size_t current_row_; + + // Current character position, should never exceed max_cols_. + size_t current_col_; + + // True when some of the input has been truncated. + bool suppressed_; + + // String onto which the output is accumulated. + string16 *output_; +}; + +void RectangleString::AddString(const string16& input) { + base::BreakIterator lines(&input, base::BreakIterator::BREAK_NEWLINE); + if (lines.Init()) { + while (lines.Advance()) + AddLine(lines.GetString()); + } else { + NOTREACHED() << "BreakIterator (lines) init failed"; + } +} + +bool RectangleString::Finalize() { + if (suppressed_) { + output_->append(ASCIIToUTF16("...")); + return true; + } + return false; +} + +void RectangleString::AddLine(const string16& line) { + if (line.length() < max_cols_) { + Append(line); + } else { + base::BreakIterator words(&line, base::BreakIterator::BREAK_SPACE); + if (words.Init()) { + while (words.Advance()) + AddWord(words.GetString()); + } else { + NOTREACHED() << "BreakIterator (words) init failed"; + } + } + // Account for naturally-occuring newlines. + ++current_row_; + current_col_ = 0; +} + +void RectangleString::AddWord(const string16& word) { + if (word.length() < max_cols_) { + // Word can be made to fit, no need to fragment it. + if (current_col_ + word.length() >= max_cols_) + NewLine(); + Append(word); + } else { + // Word is so big that it must be fragmented. + int array_start = 0; + int char_start = 0; + base::UTF16CharIterator chars(&word); + while (!chars.end()) { + // When boundary is hit, add as much as will fit on this line. + if (current_col_ + (chars.char_pos() - char_start) >= max_cols_) { + Append(word.substr(array_start, chars.array_pos() - array_start)); + NewLine(); + array_start = chars.array_pos(); + char_start = chars.char_pos(); + } + chars.Advance(); + } + // add last remaining fragment, if any. + if (array_start != chars.array_pos()) + Append(word.substr(array_start, chars.array_pos() - array_start)); + } +} + +void RectangleString::Append(const string16& string) { + if (current_row_ < max_rows_) + output_->append(string); + else + suppressed_ = true; + current_col_ += string.length(); +} + +void RectangleString::NewLine() { + if (current_row_ < max_rows_) + output_->append(ASCIIToUTF16("\n")); + else + suppressed_ = true; + ++current_row_; + current_col_ = 0; +} + +} // namespace + +namespace gfx { + +bool ElideRectangleString(const string16& input, size_t max_rows, + size_t max_cols, string16* output) { + RectangleString rect(max_rows, max_cols, output); + rect.Init(); + rect.AddString(input); + return rect.Finalize(); +} + +} // namespace gfx + diff --git a/app/text_elider.h b/app/text_elider.h index c0d6c33..d6da860 100644 --- a/app/text_elider.h +++ b/app/text_elider.h @@ -88,9 +88,11 @@ class SortedDisplayURL { string16 display_url_; }; -// Function to elide strings when the font information is unknown. As -// opposed to the above functions, the ElideString() function operates -// in terms of character units, not pixels. +// Functions to elide strings when the font information is unknown. As +// opposed to the above functions, the ElideString() and +// ElideRectangleString() functions operate in terms of character units, +// not pixels. + // If the size of |input| is more than |max_len|, this function returns // true and |input| is shortened into |output| by removing chars in the // middle (they are replaced with up to 3 dots, as size permits). @@ -101,6 +103,17 @@ class SortedDisplayURL { // TODO(tsepez): Doesn't handle bidi properly bool ElideString(const std::wstring& input, int max_len, std::wstring* output); +// Reformat |input| into |output| so that it fits into a |max_rows| by +// |max_cols| rectangle of characters. Input newlines are respected, but +// lines that are too long are broken into pieces, first at naturally +// occuring whitespace boundaries, and then intra-word (respecting UTF-16 +// surrogate pairs) as necssary. Truncation (indicated by an added 3 dots) +// occurs if the result is still too long. Returns true if the input had +// to be truncated (and not just reformatted). +bool ElideRectangleString(const string16& input, size_t max_rows, + size_t max_cols, string16* output); + + } // namespace gfx. #endif // APP_TEXT_ELIDER_H_ diff --git a/app/text_elider_unittest.cc b/app/text_elider_unittest.cc index e5a29d9..ab1a73f 100644 --- a/app/text_elider_unittest.cc +++ b/app/text_elider_unittest.cc @@ -319,6 +319,122 @@ TEST(TextEliderTest, ElideString) { std::wstring output; EXPECT_EQ(cases[i].result, gfx::ElideString(cases[i].input, cases[i].max_len, &output)); - EXPECT_TRUE(output == cases[i].output); + EXPECT_EQ(cases[i].output, output); } } + +TEST(TextEliderTest, ElideRectangleString) { + struct TestData { + const wchar_t* input; + int max_rows; + int max_cols; + bool result; + const wchar_t* output; + } cases[] = { + { L"", 0, 0, false, L"" }, + { L"", 1, 1, false, L"" }, + { L"Hi, my name is\nTom", 0, 0, true, L"..." }, + { L"Hi, my name is\nTom", 1, 0, true, L"\n..." }, + { L"Hi, my name is\nTom", 0, 1, true, L"..." }, + { L"Hi, my name is\nTom", 1, 1, true, L"H\n..." }, + { L"Hi, my name is\nTom", 2, 1, true, L"H\ni\n..." }, + { L"Hi, my name is\nTom", 3, 1, true, L"H\ni\n,\n..." }, + { L"Hi, my name is\nTom", 4, 1, true, L"H\ni\n,\n \n..." }, + { L"Hi, my name is\nTom", 5, 1, true, L"H\ni\n,\n \nm\n..." }, + { L"Hi, my name is\nTom", 0, 2, true, L"..." }, + { L"Hi, my name is\nTom", 1, 2, true, L"Hi\n..." }, + { L"Hi, my name is\nTom", 2, 2, true, L"Hi\n, \n..." }, + { L"Hi, my name is\nTom", 3, 2, true, L"Hi\n, \nmy\n..." }, + { L"Hi, my name is\nTom", 4, 2, true, L"Hi\n, \nmy\n n\n..." }, + { L"Hi, my name is\nTom", 5, 2, true, L"Hi\n, \nmy\n n\nam\n..." }, + { L"Hi, my name is\nTom", 0, 3, true, L"..." }, + { L"Hi, my name is\nTom", 1, 3, true, L"Hi,\n..." }, + { L"Hi, my name is\nTom", 2, 3, true, L"Hi,\n my\n..." }, + { L"Hi, my name is\nTom", 3, 3, true, L"Hi,\n my\n na\n..." }, + { L"Hi, my name is\nTom", 4, 3, true, L"Hi,\n my\n na\nme \n..." }, + { L"Hi, my name is\nTom", 5, 3, true, L"Hi,\n my\n na\nme \nis\n..." }, + { L"Hi, my name is\nTom", 1, 4, true, L"Hi, \n..." }, + { L"Hi, my name is\nTom", 2, 4, true, L"Hi, \nmy n\n..." }, + { L"Hi, my name is\nTom", 3, 4, true, L"Hi, \nmy n\name \n..." }, + { L"Hi, my name is\nTom", 4, 4, true, L"Hi, \nmy n\name \nis\n..." }, + { L"Hi, my name is\nTom", 5, 4, false, L"Hi, \nmy n\name \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 5, true, L"Hi, \n..." }, + { L"Hi, my name is\nTom", 2, 5, true, L"Hi, \nmy na\n..." }, + { L"Hi, my name is\nTom", 3, 5, true, L"Hi, \nmy na\nme \n..." }, + { L"Hi, my name is\nTom", 4, 5, true, L"Hi, \nmy na\nme \nis\n..." }, + { L"Hi, my name is\nTom", 5, 5, false, L"Hi, \nmy na\nme \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 6, true, L"Hi, \n..." }, + { L"Hi, my name is\nTom", 2, 6, true, L"Hi, \nmy \n..." }, + { L"Hi, my name is\nTom", 3, 6, true, L"Hi, \nmy \nname \n..." }, + { L"Hi, my name is\nTom", 4, 6, true, L"Hi, \nmy \nname \nis\n..." }, + { L"Hi, my name is\nTom", 5, 6, false, L"Hi, \nmy \nname \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 7, true, L"Hi, \n..." }, + { L"Hi, my name is\nTom", 2, 7, true, L"Hi, \nmy \n..." }, + { L"Hi, my name is\nTom", 3, 7, true, L"Hi, \nmy \nname \n..." }, + { L"Hi, my name is\nTom", 4, 7, true, L"Hi, \nmy \nname \nis\n..." }, + { L"Hi, my name is\nTom", 5, 7, false, L"Hi, \nmy \nname \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 8, true, L"Hi, my \n..." }, + { L"Hi, my name is\nTom", 2, 8, true, L"Hi, my \nname \n..." }, + { L"Hi, my name is\nTom", 3, 8, true, L"Hi, my \nname \nis\n..." }, + { L"Hi, my name is\nTom", 4, 8, false, L"Hi, my \nname \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 9, true, L"Hi, my \n..." }, + { L"Hi, my name is\nTom", 2, 9, true, L"Hi, my \nname is\n..." }, + { L"Hi, my name is\nTom", 3, 9, false, L"Hi, my \nname is\nTom" }, + { L"Hi, my name is\nTom", 1, 10, true, L"Hi, my \n..." }, + { L"Hi, my name is\nTom", 2, 10, true, L"Hi, my \nname is\n..." }, + { L"Hi, my name is\nTom", 3, 10, false, L"Hi, my \nname is\nTom" }, + { L"Hi, my name is\nTom", 1, 11, true, L"Hi, my \n..." }, + { L"Hi, my name is\nTom", 2, 11, true, L"Hi, my \nname is\n..." }, + { L"Hi, my name is\nTom", 3, 11, false, L"Hi, my \nname is\nTom" }, + { L"Hi, my name is\nTom", 1, 12, true, L"Hi, my \n..." }, + { L"Hi, my name is\nTom", 2, 12, true, L"Hi, my \nname is\n..." }, + { L"Hi, my name is\nTom", 3, 12, false, L"Hi, my \nname is\nTom" }, + { L"Hi, my name is\nTom", 1, 13, true, L"Hi, my name \n..." }, + { L"Hi, my name is\nTom", 2, 13, true, L"Hi, my name \nis\n..." }, + { L"Hi, my name is\nTom", 3, 13, false, L"Hi, my name \nis\nTom" }, + { L"Hi, my name is\nTom", 1, 20, true, L"Hi, my name is\n..." }, + { L"Hi, my name is\nTom", 2, 20, false, L"Hi, my name is\nTom" }, + { L"Hi, my name is Tom", 1, 40, false, L"Hi, my name is Tom" }, + }; + string16 output; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + EXPECT_EQ(cases[i].result, + gfx::ElideRectangleString(WideToUTF16(cases[i].input), + cases[i].max_rows, cases[i].max_cols, + &output)); + EXPECT_EQ(cases[i].output, UTF16ToWide(output)); + } +} + +TEST(TextEliderTest, ElideRectangleWide16) { + // Two greek words separated by space. + const string16 str(WideToUTF16( + L"\x03a0\x03b1\x03b3\x03ba\x03cc\x03c3\x03bc\x03b9" + L"\x03bf\x03c2\x0020\x0399\x03c3\x03c4\x03cc\x03c2")); + const string16 out1(WideToUTF16( + L"\x03a0\x03b1\x03b3\x03ba\n" + L"\x03cc\x03c3\x03bc\x03b9\n" + L"...")); + const string16 out2(WideToUTF16( + L"\x03a0\x03b1\x03b3\x03ba\x03cc\x03c3\x03bc\x03b9\x03bf\x03c2\x0020\n" + L"\x0399\x03c3\x03c4\x03cc\x03c2")); + string16 output; + EXPECT_TRUE(gfx::ElideRectangleString(str, 2, 4, &output)); + EXPECT_EQ(out1, output); + EXPECT_FALSE(gfx::ElideRectangleString(str, 2, 12, &output)); + EXPECT_EQ(out2, output); +} + +TEST(TextEliderTest, ElideRectangleWide32) { + // Four U+1D49C MATHEMATICAL SCRIPT CAPITAL A followed by space "aaaaa". + const string16 str(UTF8ToUTF16( + "\xF0\x9D\x92\x9C\xF0\x9D\x92\x9C\xF0\x9D\x92\x9C\xF0\x9D\x92\x9C" + " aaaaa")); + const string16 out(UTF8ToUTF16( + "\xF0\x9D\x92\x9C\xF0\x9D\x92\x9C\xF0\x9D\x92\x9C\n" + "\xF0\x9D\x92\x9C \naaa\n...")); + string16 output; + EXPECT_TRUE(gfx::ElideRectangleString(str, 3, 3, &output)); + EXPECT_EQ(out, output); +} + diff --git a/chrome/browser/ui/app_modal_dialogs/js_modal_dialog.cc b/chrome/browser/ui/app_modal_dialogs/js_modal_dialog.cc index 8fe0499..801562d 100644 --- a/chrome/browser/ui/app_modal_dialogs/js_modal_dialog.cc +++ b/chrome/browser/ui/app_modal_dialogs/js_modal_dialog.cc @@ -6,6 +6,7 @@ #include "app/text_elider.h" #include "base/string_util.h" +#include "base/utf_string_conversions.h" #include "chrome/browser/browser_shutdown.h" #include "chrome/browser/extensions/extension_host.h" #include "chrome/browser/tab_contents/tab_contents.h" @@ -17,7 +18,8 @@ namespace { // The maximum sizes of various texts passed to us from javascript. -const int kMessageTextMaxSize = 3000; +const int kMessageTextMaxRows = 32; +const int kMessageTextMaxCols = 132; const int kDefaultPromptTextSize = 2000; } // namespace @@ -40,7 +42,10 @@ JavaScriptAppModalDialog::JavaScriptAppModalDialog( reply_msg_(reply_msg) { // We trim the various parts of the message dialog because otherwise we can // overflow the message dialog (and crash/hang the GTK+ version). - gfx::ElideString(message_text, kMessageTextMaxSize, &message_text_); + string16 elided_text; + gfx::ElideRectangleString(WideToUTF16(message_text), + kMessageTextMaxRows, kMessageTextMaxCols, &elided_text); + message_text_ = UTF16ToWide(elided_text); gfx::ElideString(default_prompt_text, kDefaultPromptTextSize, &default_prompt_text_); |