diff options
-rw-r--r-- | views/controls/textfield/native_textfield_views.cc | 24 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views.h | 5 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views_unittest.cc | 105 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.cc | 488 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.h | 102 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model_unittest.cc | 391 | ||||
-rw-r--r-- | views/examples/examples_main.cc | 4 | ||||
-rw-r--r-- | views/examples/textfield_example.cc | 5 | ||||
-rw-r--r-- | views/examples/textfield_example.h | 1 |
9 files changed, 1065 insertions, 60 deletions
diff --git a/views/controls/textfield/native_textfield_views.cc b/views/controls/textfield/native_textfield_views.cc index 5b8e556..6136ebc 100644 --- a/views/controls/textfield/native_textfield_views.cc +++ b/views/controls/textfield/native_textfield_views.cc @@ -205,13 +205,19 @@ int NativeTextfieldViews::OnPerformDrop(const DropTargetEvent& event) { drop_destination -= selected_range.length(); else if (selected_range.GetMin() <= drop_destination) drop_destination = selected_range.GetMin(); - model_->DeleteSelection(); + // TODO(oshima): Deletion and insertion has to be treated as one + // edit. + model_->DeleteSelection(true); } model_->MoveCursorTo(drop_destination, false); string16 text; event.data().GetString(&text); - InsertText(text); - UpdateCursorBoundsAndTextOffset(); + + skip_input_method_cancel_composition_ = true; + // Drop always inserts a text even if insert_ == false. + model_->InsertText(text); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); OnAfterUserAction(); return move ? ui::DragDropTypes::DRAG_MOVE : ui::DragDropTypes::DRAG_COPY; } @@ -670,7 +676,7 @@ bool NativeTextfieldViews::DeleteRange(const ui::Range& range) { OnBeforeUserAction(); model_->SelectRange(range); if (model_->HasSelection()) { - model_->DeleteSelection(); + model_->DeleteSelection(true); UpdateAfterChange(true, true); } OnAfterUserAction(); @@ -831,7 +837,7 @@ void NativeTextfieldViews::PaintTextAndCursor(gfx::Canvas* canvas) { } bool NativeTextfieldViews::HandleKeyEvent(const KeyEvent& key_event) { - // TODO(oshima): handle IME. + // TODO(oshima): Refactor and consolidate with ExecuteCommand. if (key_event.type() == ui::ET_KEY_PRESSED) { ui::KeyboardCode key_code = key_event.key_code(); // TODO(oshima): shift-tab does not work. Figure out why and fix. @@ -845,6 +851,14 @@ bool NativeTextfieldViews::HandleKeyEvent(const KeyEvent& key_event) { bool text_changed = false; bool cursor_changed = false; switch (key_code) { + case ui::VKEY_Z: + if (control && editable) + cursor_changed = text_changed = model_->Undo(); + break; + case ui::VKEY_Y: + if (control && editable) + cursor_changed = text_changed = model_->Redo(); + break; case ui::VKEY_A: if (control) { model_->SelectAll(); diff --git a/views/controls/textfield/native_textfield_views.h b/views/controls/textfield/native_textfield_views.h index b20fcf1..8889e26 100644 --- a/views/controls/textfield/native_textfield_views.h +++ b/views/controls/textfield/native_textfield_views.h @@ -33,12 +33,11 @@ class Menu2; // A views/skia only implementation of NativeTextfieldWrapper. // No platform specific code is used. // Following features are not yet supported. -// * BIDI -// * IME/i18n support. +// * BIDI/Complex script. +// * Support surrogate pair, or maybe we should just use UTF32 internally. // * X selection (only if we want to support). // * STYLE_MULTILINE, STYLE_LOWERCASE text. (These are not used in // chromeos, so we may not need them) -// * Undo/Redo class NativeTextfieldViews : public View, public ContextMenuController, public DragController, diff --git a/views/controls/textfield/native_textfield_views_unittest.cc b/views/controls/textfield/native_textfield_views_unittest.cc index 11d6a5b..8456cf7 100644 --- a/views/controls/textfield/native_textfield_views_unittest.cc +++ b/views/controls/textfield/native_textfield_views_unittest.cc @@ -177,6 +177,7 @@ class NativeTextfieldViewsTest : public ViewsTestBase, DCHECK(textfield_view_); model_ = textfield_view_->model_.get(); + model_->ClearEditHistory(); input_method_ = new MockInputMethod(); widget_->native_widget()->ReplaceInputMethod(input_method_); @@ -946,5 +947,109 @@ TEST_F(NativeTextfieldViewsTest, TextInputClientTest) { EXPECT_TRUE(textfield_->GetTextInputClient()); } +TEST_F(NativeTextfieldViewsTest, UndoRedoTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + + // AppendText + textfield_->AppendText(ASCIIToUTF16("b")); + last_contents_.clear(); // AppendText doesn't call ContentsChanged. + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + + // SetText + SendKeyEvent(ui::VKEY_C); + // Undo'ing append moves the cursor to the end for now. + // no-op SetText won't add new edit. See TextfieldViewsModel::SetText + // description. + EXPECT_STR_EQ("abc", textfield_->text()); + textfield_->SetText(ASCIIToUTF16("abc")); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + textfield_->SetText(ASCIIToUTF16("123")); + textfield_->SetText(ASCIIToUTF16("123")); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_END, false, false); + SendKeyEvent(ui::VKEY_4, false, false); + EXPECT_STR_EQ("1234", textfield_->text()); + last_contents_.clear(); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("1234", textfield_->text()); + + // Undoing to the same text shouldn't call ContentsChanged. + SendKeyEvent(ui::VKEY_A, false, true); // select all + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_B); + SendKeyEvent(ui::VKEY_C); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("1234", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + + // Delete/Backspace + SendKeyEvent(ui::VKEY_BACK); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_HOME); + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("", textfield_->text()); + + // Insert + textfield_->SetText(ASCIIToUTF16("123")); + SendKeyEvent(ui::VKEY_INSERT); + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a23", textfield_->text()); + SendKeyEvent(ui::VKEY_B); + EXPECT_STR_EQ("ab3", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab3", textfield_->text()); +} } // namespace views diff --git a/views/controls/textfield/textfield_views_model.cc b/views/controls/textfield/textfield_views_model.cc index 583dc20..96b4fb2 100644 --- a/views/controls/textfield/textfield_views_model.cc +++ b/views/controls/textfield/textfield_views_model.cc @@ -8,6 +8,7 @@ #include "base/i18n/break_iterator.h" #include "base/logging.h" +#include "base/stl_util-inl.h" #include "base/utf_string_conversions.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" @@ -18,6 +19,271 @@ namespace views { +namespace internal { + +// An edit object holds enough information/state to undo/redo the +// change. Two edits are merged when possible, for example, when +// you type new characters in sequence. |Commit()| can be used to +// mark an edit as an independent edit and it shouldn't be merged. +// (For example, when you did undo/redo, or a text is appended via +// API) +// TODO(oshima): Try to consolidate edit classes as one Edit. At least, +// there should be one method for each undo and redo. +class Edit { + public: + virtual ~Edit() {} + + enum Type { + INSERT_EDIT, + DELETE_EDIT, + REPLACE_EDIT + }; + + // Apply undo operation to the given |model|. + virtual void Undo(TextfieldViewsModel* model) = 0; + + // Apply redo operation to the given |model|. + virtual void Redo(TextfieldViewsModel* model) = 0; + + // Tries to merge the given edit to itself. Returns false + // if merge fails. + virtual bool Merge(Edit* edit) = 0; + + Type type() { return type_; } + + // Can this edit be merged? + bool mergeable() { return mergeable_; } + + // Commits the edit and marks as un-mergeable. + void Commit() { mergeable_ = false; } + + protected: + Edit(Type type, bool mergeable) + : mergeable_(mergeable), + type_(type) {} + + // True if the edit can be marged. + bool mergeable_; + + private: + Type type_; + + DISALLOW_COPY_AND_ASSIGN(Edit); +}; + +} // namespace internal + +namespace { + +// An edit to insert text. Insertion edits can be merged only if +// next insertion starts with the last insertion point (continuous). +class InsertEdit : public internal::Edit { + public: + InsertEdit(bool mergeable, size_t cursor_pos, const string16& new_text) + : Edit(INSERT_EDIT, mergeable), + cursor_pos_(cursor_pos), + new_text_(new_text) { + } + virtual ~InsertEdit() {} + + // internal::Edit implementation: + virtual void Undo(TextfieldViewsModel* model) OVERRIDE { + if (new_text_.empty()) + return; + model->SelectRange( + ui::Range(cursor_pos_, cursor_pos_ + new_text_.length())); + model->DeleteSelection(false); + } + + virtual void Redo(TextfieldViewsModel* model) OVERRIDE { + model->MoveCursorTo(cursor_pos_, false); + if (new_text_.empty()) + return; + model->InsertText(new_text_); + } + + virtual bool Merge(Edit* edit) OVERRIDE { + if (edit->type() != INSERT_EDIT || !mergeable_ || !edit->mergeable()) + return false; + + InsertEdit* insert_edit = static_cast<InsertEdit*>(edit); + // If continuous edit, merge it. + if (cursor_pos_ + new_text_.length() != insert_edit->cursor_pos_) + return false; + // TODO(oshima): gtk splits edits between whitespace. Find out what + // we want to here and implement if necessary. + new_text_ += insert_edit->new_text_; + return true; + } + + private: + friend class ReplaceEdit; + // A cursor position at which the new_text_ is added. + size_t cursor_pos_; + string16 new_text_; + + DISALLOW_COPY_AND_ASSIGN(InsertEdit); +}; + +// An edit to replace text. ReplaceEdit can be merged with either InsertEdit or +// ReplaceEdit when they're continuous. +class ReplaceEdit : public internal::Edit { + public: + ReplaceEdit(bool mergeable, + size_t cursor_pos, + int deleted_size, + const string16& old_text, + const string16& new_text) + : Edit(REPLACE_EDIT, mergeable), + cursor_pos_(cursor_pos), + deleted_size_(deleted_size), + old_text_(old_text), + new_text_(new_text) { + } + + virtual ~ReplaceEdit() { + } + + // internal::Edit implementation: + virtual void Undo(TextfieldViewsModel* model) OVERRIDE { + size_t cursor_pos = std::min(cursor_pos_, + cursor_pos_ + deleted_size_); + model->SelectRange( + ui::Range(cursor_pos, cursor_pos + new_text_.length())); + model->DeleteSelection(false); + model->InsertText(old_text_); + model->MoveCursorTo(cursor_pos_, false); + } + + virtual void Redo(TextfieldViewsModel* model) OVERRIDE { + size_t cursor_pos = std::min(cursor_pos_, + cursor_pos_ + deleted_size_); + model->SelectRange( + ui::Range(cursor_pos, cursor_pos + old_text_.length())); + model->DeleteSelection(false); + model->InsertText(new_text_); + } + + virtual bool Merge(Edit* edit) OVERRIDE { + if (edit->type() == DELETE_EDIT || !mergeable_ || !edit->mergeable()) + return false; + if (edit->type() == INSERT_EDIT) { + return MergeInsert(static_cast<InsertEdit*>(edit)); + } else { + DCHECK_EQ(REPLACE_EDIT, edit->type()); + return MergeReplace(static_cast<ReplaceEdit*>(edit)); + } + } + + private: + bool MergeInsert(InsertEdit* insert_edit) { + if (deleted_size_ < 0) { + if (cursor_pos_ + deleted_size_ + new_text_.length() + != insert_edit->cursor_pos_) + return false; + } else { + if (cursor_pos_ + new_text_.length() != insert_edit->cursor_pos_) + return false; + } + new_text_ += insert_edit->new_text_; + return true; + } + + bool MergeReplace(ReplaceEdit* replace_edit) { + if (deleted_size_ > 0) { + if (cursor_pos_ + new_text_.length() != replace_edit->cursor_pos_) + return false; + } else { + if (cursor_pos_ + deleted_size_ + new_text_.length() + != replace_edit->cursor_pos_) + return false; + } + old_text_ += replace_edit->old_text_; + new_text_ += replace_edit->new_text_; + return true; + } + // A cursor position of the selected text that was replaced. + size_t cursor_pos_; + // Index difference of the selected text that was replaced. + // This is negative if the selection is made backward. + int deleted_size_; + string16 old_text_; + string16 new_text_; + + DISALLOW_COPY_AND_ASSIGN(ReplaceEdit); +}; + +// An edit for deletion. Delete edits can be merged only if two +// deletions have the same direction, and are continuous. +class DeleteEdit : public internal::Edit { + public: + DeleteEdit(bool mergeable, + size_t cursor_pos, int size, + const string16& text) + : Edit(DELETE_EDIT, mergeable), + cursor_pos_(cursor_pos), + size_(size), + old_text_(text) { + } + virtual ~DeleteEdit() { + } + + // internal::Edit implementation: + virtual void Undo(TextfieldViewsModel* model) OVERRIDE { + size_t cursor_pos = std::min(cursor_pos_, + cursor_pos_ + size_); + model->MoveCursorTo(cursor_pos, false); + model->InsertText(old_text_); + model->MoveCursorTo(cursor_pos_, false); + } + + virtual void Redo(TextfieldViewsModel* model) OVERRIDE { + model->SelectRange( + ui::Range(cursor_pos_, cursor_pos_ + size_)); + model->DeleteSelection(false); + } + + virtual bool Merge(Edit* edit) OVERRIDE { + if (edit->type() != DELETE_EDIT || !mergeable_ || !edit->mergeable()) + return false; + + DeleteEdit* delete_edit = static_cast<DeleteEdit*>(edit); + if (size_ < 0) { + // backspace can be merged only with backspace at the + // same position. + if (delete_edit->size_ > 0 || + cursor_pos_ + size_ != delete_edit->cursor_pos_) + return false; + old_text_ = delete_edit->old_text_ + old_text_; + } else { + // delete can be merged only with delete at the same + // position. + if (delete_edit->size_ < 0 || + cursor_pos_ != delete_edit->cursor_pos_) + return false; + old_text_ += delete_edit->old_text_; + } + size_ += delete_edit->size_; + return true; + } + + private: + // A cursor position where the deletion started. + size_t cursor_pos_; + // Number of characters deleted. This is positive for delete and + // negative for backspace operation. + int size_; + // Deleted text. + string16 old_text_; + + DISALLOW_COPY_AND_ASSIGN(DeleteEdit); +}; + +} // namespace + +///////////////////////////////////////////////////////////////// +// TextfieldViewsModel: public + TextfieldViewsModel::Delegate::~Delegate() { } @@ -27,10 +293,13 @@ TextfieldViewsModel::TextfieldViewsModel(Delegate* delegate) selection_start_(0), composition_start_(0), composition_end_(0), - is_password_(false) { + is_password_(false), + current_edit_(edit_history_.end()), + update_history_(true) { } TextfieldViewsModel::~TextfieldViewsModel() { + ClearEditHistory(); } void TextfieldViewsModel::GetFragments(TextFragments* fragments) const { @@ -145,50 +414,38 @@ bool TextfieldViewsModel::SetText(const string16& text) { changed = true; } if (text_ != text) { - text_ = text; - if (cursor_pos_ > text.length()) { - cursor_pos_ = text.length(); - } - changed = true; + if (changed) // no need to remember composition. + Undo(); + SelectAll(); + InsertTextInternal(text, false); + cursor_pos_ = 0; } ClearSelection(); return changed; } -void TextfieldViewsModel::InsertText(const string16& text) { - if (HasCompositionText()) - ClearCompositionText(); - else if (HasSelection()) - DeleteSelection(); - text_.insert(cursor_pos_, text); - cursor_pos_ += text.size(); - ClearSelection(); -} - -void TextfieldViewsModel::ReplaceText(const string16& text) { - if (HasCompositionText()) - ClearCompositionText(); - else if (!HasSelection()) - SelectRange(ui::Range(cursor_pos_, cursor_pos_ + text.length())); - InsertText(text); -} - void TextfieldViewsModel::Append(const string16& text) { if (HasCompositionText()) ConfirmCompositionText(); - text_ += text; + size_t save = cursor_pos_; + MoveCursorToEnd(false); + InsertText(text); + cursor_pos_ = save; + ClearSelection(); } bool TextfieldViewsModel::Delete() { if (HasCompositionText()) { + // No undo/redo for composition text. ClearCompositionText(); return true; } if (HasSelection()) { - DeleteSelection(); + DeleteSelection(true); return true; } if (text_.length() > cursor_pos_) { + RecordDelete(cursor_pos_, 1, true); text_.erase(cursor_pos_, 1); return true; } @@ -197,14 +454,16 @@ bool TextfieldViewsModel::Delete() { bool TextfieldViewsModel::Backspace() { if (HasCompositionText()) { + // No undo/redo for composition text. ClearCompositionText(); return true; } if (HasSelection()) { - DeleteSelection(); + DeleteSelection(true); return true; } if (cursor_pos_ > 0) { + RecordDelete(cursor_pos_, -1, true); cursor_pos_--; text_.erase(cursor_pos_, 1); ClearSelection(); @@ -399,11 +658,63 @@ void TextfieldViewsModel::ClearSelection() { selection_start_ = cursor_pos_; } +bool TextfieldViewsModel::CanUndo() { + return edit_history_.size() && current_edit_ != edit_history_.end(); +} + +bool TextfieldViewsModel::CanRedo() { + if (!edit_history_.size()) + return false; + // There is no redo iff the current edit is the last element + // in the history. + EditHistory::iterator iter = current_edit_; + return iter == edit_history_.end() || // at the top. + ++iter != edit_history_.end(); +} + +bool TextfieldViewsModel::Undo() { + if (!CanUndo()) + return false; + DCHECK(!HasCompositionText()); + if (HasCompositionText()) // safe guard for release build. + ClearCompositionText(); + + string16 old = text_; + update_history_ = false; + (*current_edit_)->Commit(); + (*current_edit_)->Undo(this); + update_history_ = true; + + if (current_edit_ == edit_history_.begin()) + current_edit_ = edit_history_.end(); + else + current_edit_--; + return old != text_; +} + +bool TextfieldViewsModel::Redo() { + if (!CanRedo()) + return false; + DCHECK(!HasCompositionText()); + if (HasCompositionText()) // safe guard for release build. + ClearCompositionText(); + + if (current_edit_ == edit_history_.end()) + current_edit_ = edit_history_.begin(); + else + current_edit_ ++; + string16 old = text_; + update_history_ = false; + (*current_edit_)->Redo(this); + update_history_ = true; + return old != text_; +} + bool TextfieldViewsModel::Cut() { if (!HasCompositionText() && HasSelection()) { ui::ScopedClipboardWriter(views::ViewsDelegate::views_delegate ->GetClipboard()).WriteText(GetSelectedText()); - DeleteSelection(); + DeleteSelection(true); return true; } return false; @@ -421,13 +732,7 @@ bool TextfieldViewsModel::Paste() { views::ViewsDelegate::views_delegate->GetClipboard() ->ReadText(ui::Clipboard::BUFFER_STANDARD, &result); if (!result.empty()) { - if (HasCompositionText()) - ConfirmCompositionText(); - else if (HasSelection()) - DeleteSelection(); - text_.insert(cursor_pos_, result); - cursor_pos_ += result.length(); - ClearSelection(); + InsertTextInternal(result, false); return true; } return false; @@ -437,11 +742,13 @@ bool TextfieldViewsModel::HasSelection() const { return selection_start_ != cursor_pos_; } -void TextfieldViewsModel::DeleteSelection() { +void TextfieldViewsModel::DeleteSelection(bool record_edit_history) { DCHECK(!HasCompositionText()); DCHECK(HasSelection()); size_t n = std::abs(static_cast<long>(cursor_pos_ - selection_start_)); size_t begin = std::min(cursor_pos_, selection_start_); + if (record_edit_history) + RecordDelete(cursor_pos_, selection_start_ - cursor_pos_, false); text_.erase(begin, n); cursor_pos_ = begin; ClearSelection(); @@ -462,7 +769,7 @@ void TextfieldViewsModel::SetCompositionText( if (HasCompositionText()) ClearCompositionText(); else if (HasSelection()) - DeleteSelection(); + DeleteSelection(true); if (composition.text.empty()) return; @@ -488,6 +795,11 @@ void TextfieldViewsModel::SetCompositionText( void TextfieldViewsModel::ConfirmCompositionText() { DCHECK(HasCompositionText()); + string16 new_text = + text_.substr(composition_start_, composition_end_ - composition_start_); + // TODO(oshima): current behavior on ChromeOS is a bit weird and not + // sure exactly how this should work. Find out and fix if necessary. + AddEditHistory(new InsertEdit(false, composition_start_, new_text)); cursor_pos_ = composition_end_; composition_start_ = composition_end_ = string16::npos; composition_underlines_.clear(); @@ -518,6 +830,9 @@ bool TextfieldViewsModel::HasCompositionText() const { return composition_start_ != composition_end_; } +///////////////////////////////////////////////////////////////// +// TextfieldViewsModel: private + string16 TextfieldViewsModel::GetVisibleText(size_t begin, size_t end) const { DCHECK(end >= begin); if (is_password_) @@ -537,4 +852,103 @@ size_t TextfieldViewsModel::GetSafePosition(size_t position) const { return position; } +void TextfieldViewsModel::InsertTextInternal(const string16& text, + bool mergeable) { + // TODO(oshima): Simplify the implementation by using an edit TO + // modify the text here as well, instead of create an edit AND + // modify the text. + if (HasCompositionText()) { + ClearCompositionText(); + RecordInsert(text, mergeable); + } else if (HasSelection()) { + RecordReplace(text, mergeable); + DeleteSelection(false); + } else { + RecordInsert(text, mergeable); + } + text_.insert(cursor_pos_, text); + cursor_pos_ += text.size(); + ClearSelection(); +} + +void TextfieldViewsModel::ReplaceTextInternal(const string16& text, + bool mergeable) { + if (HasCompositionText()) + ClearCompositionText(); + else if (!HasSelection()) + SelectRange(ui::Range(cursor_pos_ + text.length(), cursor_pos_)); + // Edit history is recorded in InsertText. + InsertTextInternal(text, mergeable); +} + +void TextfieldViewsModel::ClearEditHistory() { + STLDeleteContainerPointers(edit_history_.begin(), + edit_history_.end()); + edit_history_.clear(); + current_edit_ = edit_history_.end(); +} + +void TextfieldViewsModel::ClearRedoHistory() { + if (edit_history_.begin() == edit_history_.end()) + return; + if (current_edit_ == edit_history_.end()) { + ClearEditHistory(); + return; + } + EditHistory::iterator delete_start = current_edit_; + delete_start++; + STLDeleteContainerPointers(delete_start, + edit_history_.end()); + edit_history_.erase(delete_start, edit_history_.end()); +} + +void TextfieldViewsModel::RecordDelete(size_t cursor, int size, + bool mergeable) { + if (!update_history_) + return; + ClearRedoHistory(); + int c = std::min(cursor, cursor + size); + const string16 text = text_.substr(c, std::abs(size)); + AddEditHistory(new DeleteEdit(mergeable, cursor, size, text)); +} + +void TextfieldViewsModel::RecordReplace(const string16& text, + bool mergeable) { + if (!update_history_) + return; + ClearRedoHistory(); + AddEditHistory(new ReplaceEdit(mergeable, + cursor_pos_, + selection_start_ - cursor_pos_, + GetSelectedText(), + text)); +} + +void TextfieldViewsModel::RecordInsert(const string16& text, + bool mergeable) { + if (!update_history_) + return; + ClearRedoHistory(); + AddEditHistory(new InsertEdit(mergeable, cursor_pos_, text)); +} + +void TextfieldViewsModel::AddEditHistory(internal::Edit* edit) { + DCHECK(update_history_); + if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) { + // If a current edit exists and has been merged with a new edit, + // delete that edit. + delete edit; + return; + } + edit_history_.push_back(edit); + if (current_edit_ == edit_history_.end()) { + // If there is no redoable edit, this is the 1st edit because + // RedoHistory has been already deleted. + DCHECK_EQ(1u, edit_history_.size()); + current_edit_ = edit_history_.begin(); + } else { + current_edit_++; + } +} + } // namespace views diff --git a/views/controls/textfield/textfield_views_model.h b/views/controls/textfield/textfield_views_model.h index 260d493..55fd6e2 100644 --- a/views/controls/textfield/textfield_views_model.h +++ b/views/controls/textfield/textfield_views_model.h @@ -6,8 +6,10 @@ #define VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ #pragma once +#include <list> #include <vector> +#include "base/gtest_prod_util.h" #include "base/string16.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/ime/composition_text.h" @@ -23,6 +25,11 @@ class Range; namespace views { +namespace internal { +// Internal Edit class that keeps track of edits for undo/redo. +class Edit; +} // namespace internal + // A model that represents a text content for TextfieldViews. // It supports editing, selection and cursor manipulation. class TextfieldViewsModel { @@ -70,26 +77,34 @@ class TextfieldViewsModel { // Edit related methods. - // Sest the text. Returns true if the text has been modified. - // The current composition text will be confirmed first. + // Sest the text. Returns true if the text has been modified. The + // current composition text will be confirmed first. Setting + // the same text will not add edit history because it's not user + // visible change nor user-initiated change. This allow a client + // code to set the same text multiple times without worrying about + // messing edit history. bool SetText(const string16& text); // Inserts given |text| at the current cursor position. // The current composition text will be cleared. - void InsertText(const string16& text); + void InsertText(const string16& text) { + InsertTextInternal(text, false); + } // Inserts a character at the current cursor position. void InsertChar(char16 c) { - InsertText(string16(&c, 1)); + InsertTextInternal(string16(&c, 1), true); } // Replaces characters at the current position with characters in given text. // The current composition text will be cleared. - void ReplaceText(const string16& text); + void ReplaceText(const string16& text) { + ReplaceTextInternal(text, false); + } // Replaces the char at the current position with given character. void ReplaceChar(char16 c) { - ReplaceText(string16(&c, 1)); + ReplaceTextInternal(string16(&c, 1), true); } // Appends the text. @@ -159,7 +174,10 @@ class TextfieldViewsModel { void GetSelectedRange(ui::Range* range) const; - // The current composition text will be confirmed. + // The current composition text will be confirmed. The + // selection starts with the range's start position, + // and ends with the range's end position, therefore + // the cursor position becomes the end position. void SelectRange(const ui::Range& range); // Selects all text. @@ -174,6 +192,18 @@ class TextfieldViewsModel { // The current composition text will be confirmed. void ClearSelection(); + // Returns true if there is an undoable edit. + bool CanUndo(); + + // Returns true if there is an redoable edit. + bool CanRedo(); + + // Undo edit. Returns true if undo changed the text. + bool Undo(); + + // Redo edit. Returns true if redo changed the text. + bool Redo(); + // Returns visible text. If the field is password, it returns the // sequence of "*". string16 GetVisibleText() const { @@ -196,8 +226,9 @@ class TextfieldViewsModel { bool HasSelection() const; // Deletes the selected text. This method shouldn't be called with - // composition text. - void DeleteSelection(); + // composition text. If |record_edit_history| is true, the deletion + // will be recorded so that it can be undone or redone. + void DeleteSelection(bool record_edit_history); // Retrieves the text content in a given range. string16 GetTextFromRange(const ui::Range& range) const; @@ -227,6 +258,16 @@ class TextfieldViewsModel { private: friend class NativeTextfieldViews; + friend class NativeTextfieldViewsTest; + friend class TextfieldViewsModelTest; + friend class UndoRedo_BasicTest; + friend class UndoRedo_CutCopyPasteTest; + friend class UndoRedo_ReplaceTest; + friend class internal::Edit; + + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_BasicTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_CutCopyPasteTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_ReplaceTest); // Returns the visible text given |start| and |end|. string16 GetVisibleText(size_t start, size_t end) const; @@ -238,6 +279,29 @@ class TextfieldViewsModel { // text length. size_t GetSafePosition(size_t position) const; + // Insert the given |text|. |mergeable| indicates if this insert + // operation can be merged to previous edit in the edit history. + void InsertTextInternal(const string16& text, bool mergeable); + + // Replace the current text with the given |text|. |mergeable| + // indicates if this replace operation can be merged to previous + // edit in the edit history. + void ReplaceTextInternal(const string16& text, bool mergeable); + + // Clears all edit history. + void ClearEditHistory(); + + // Clears redo history. + void ClearRedoHistory(); + + // Records edit operations. + void RecordDelete(size_t cursor_pos, int size, bool mergeable); + void RecordReplace(const string16& text, bool mergeable); + void RecordInsert(const string16& text, bool mergeable); + + // Adds edit into edit history. + void AddEditHistory(internal::Edit* edit); + // Pointer to a TextfieldViewsModel::Delegate instance, should be provided by // the View object. Delegate* delegate_; @@ -261,6 +325,26 @@ class TextfieldViewsModel { // True if the text is the password. bool is_password_; + typedef std::list<internal::Edit*> EditHistory; + EditHistory edit_history_; + + // An iterator that points to the current edit that can be undone. + // This iterator moves from the |end()|, meaining no edit to undo, + // to the last element (one before |end()|), meaning no edit to redo. + // There is no edit to undo (== end()) when: + // 1) in initial state. (nothing to undo) + // 2) very 1st edit is undone. + // 3) all edit history is removed. + // There is no edit to redo (== last element or no element) when: + // 1) in initial state. (nothing to redo) + // 2) new edit is added. (redo history is cleared) + // 3) redone all undone edits. + EditHistory::iterator current_edit_; + + // A guard variable to prevent edit history from being updated while + // performing undo/redo. + bool update_history_; + DISALLOW_COPY_AND_ASSIGN(TextfieldViewsModel); }; diff --git a/views/controls/textfield/textfield_views_model_unittest.cc b/views/controls/textfield/textfield_views_model_unittest.cc index 7271adc..d9c1e11 100644 --- a/views/controls/textfield/textfield_views_model_unittest.cc +++ b/views/controls/textfield/textfield_views_model_unittest.cc @@ -33,6 +33,11 @@ class TextfieldViewsModelTest : public ViewsTestBase, } protected: + void ResetModel(TextfieldViewsModel* model) const { + model->SetText(ASCIIToUTF16("")); + model->ClearEditHistory(); + } + bool composition_text_confirmed_or_cleared_; private: @@ -302,17 +307,14 @@ TEST_F(TextfieldViewsModelTest, SetText) { model.MoveCursorToEnd(false); model.SetText(ASCIIToUTF16("GOODBYE")); EXPECT_STR_EQ("GOODBYE", model.text()); - EXPECT_EQ(5U, model.cursor_pos()); + EXPECT_EQ(0U, model.cursor_pos()); model.SelectAll(); EXPECT_STR_EQ("GOODBYE", model.GetSelectedText()); - // Selection move the current pos to the end. - EXPECT_EQ(7U, model.cursor_pos()); - model.MoveCursorToHome(false); - EXPECT_EQ(0U, model.cursor_pos()); model.MoveCursorToEnd(false); + EXPECT_EQ(7U, model.cursor_pos()); model.SetText(ASCIIToUTF16("BYE")); - EXPECT_EQ(3U, model.cursor_pos()); + EXPECT_EQ(0U, model.cursor_pos()); EXPECT_EQ(string16(), model.GetSelectedText()); model.SetText(ASCIIToUTF16("")); EXPECT_EQ(0U, model.cursor_pos()); @@ -705,4 +707,381 @@ TEST_F(TextfieldViewsModelTest, CompositionTextTest) { EXPECT_FALSE(composition_text_confirmed_or_cleared_); } +TEST_F(TextfieldViewsModelTest, UndoRedo_BasicTest) { + TextfieldViewsModel model(NULL); + model.InsertChar('a'); + EXPECT_FALSE(model.Redo()); // nothing to redo + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a", model.text()); + + // Continuous inserts are treated as one edit. + model.InsertChar('b'); + model.InsertChar('c'); + EXPECT_STR_EQ("abc", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("a", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + + // Undoing further shouldn't change the text. + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + + // Redoing to the latest text. + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abc", model.text()); + + // Backspace =============================== + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("ab", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abc", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ab", model.text()); + // Continous backspaces are treated as one edit. + EXPECT_TRUE(model.Backspace()); + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("", model.text()); + // Extra backspace shouldn't affect the history. + EXPECT_FALSE(model.Backspace()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ab", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abc", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("a", model.text()); + + // Clear history + model.ClearEditHistory(); + EXPECT_FALSE(model.Undo()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("a", model.text()); + + // Delete =============================== + model.SetText(ASCIIToUTF16("ABCDE")); + model.ClearEditHistory(); + model.MoveCursorTo(2, false); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("ABDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABDE", model.text()); + // Continous deletes are treated as one edit. + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("AB", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("AB", model.text()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_CutCopyPasteTest) { + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("ABCDE")); + EXPECT_FALSE(model.Redo()); // nothing to redo + // Cut + model.MoveCursorTo(1, false); + model.MoveCursorTo(3, true); + model.Cut(); + EXPECT_STR_EQ("ADE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); // no more undo + EXPECT_STR_EQ("", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ADE", model.text()); + EXPECT_FALSE(model.Redo()); // no more redo + EXPECT_STR_EQ("ADE", model.text()); + + model.Paste(); + model.Paste(); + model.Paste(); + EXPECT_STR_EQ("ABCBCBCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ADE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.text()); + // Redo + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ADE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCBCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCBCBCDE", model.text()); + EXPECT_FALSE(model.Redo()); + + // with SelectRange + model.SelectRange(ui::Range(1, 3)); + EXPECT_TRUE(model.Cut()); + EXPECT_STR_EQ("ABCBCDE", model.text()); + model.SelectRange(ui::Range(1, 1)); + EXPECT_FALSE(model.Cut()); + model.MoveCursorToEnd(false); + EXPECT_TRUE(model.Paste()); + EXPECT_STR_EQ("ABCBCDEBC", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCDE", model.text()); + // empty cut shouldn't create an edit. + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCBCDE", model.text()); + + // Copy + ResetModel(&model); + model.SetText(ASCIIToUTF16("12345")); + EXPECT_STR_EQ("12345", model.text()); + model.MoveCursorTo(1, false); + model.MoveCursorTo(3, true); + model.Copy(); + EXPECT_STR_EQ("12345", model.text()); + model.Paste(); + EXPECT_STR_EQ("12345", model.text()); + model.Paste(); + EXPECT_STR_EQ("1232345", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("12345", model.text()); + EXPECT_FALSE(model.Undo()); // no text change + EXPECT_STR_EQ("12345", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + // Redo + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("12345", model.text()); + EXPECT_FALSE(model.Redo()); // no text change. + EXPECT_STR_EQ("12345", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("1232345", model.text()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("1232345", model.text()); + + // with SelectRange + model.SelectRange(ui::Range(1, 3)); + model.Copy(); + EXPECT_STR_EQ("1232345", model.text()); + model.MoveCursorToEnd(false); + EXPECT_TRUE(model.Paste()); + EXPECT_STR_EQ("123234523", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("1232345", model.text()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_CursorTest) { + TextfieldViewsModel model(NULL); + model.InsertChar('a'); + model.MoveCursorLeft(false); + model.MoveCursorRight(false); + model.InsertChar('b'); + // Moving cursor shoudln't create a new edit. + EXPECT_STR_EQ("ab", model.text()); + EXPECT_FALSE(model.Redo()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ab", model.text()); + EXPECT_FALSE(model.Redo()); +} + +void RunInsertReplaceTest(TextfieldViewsModel& model) { + model.InsertChar('1'); + model.InsertChar('2'); + model.InsertChar('3'); + EXPECT_STR_EQ("a123d", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcd", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abcd", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a123d", model.text()); + EXPECT_FALSE(model.Redo()); +} + +void RunOverwriteReplaceTest(TextfieldViewsModel& model) { + model.ReplaceChar('1'); + model.ReplaceChar('2'); + model.ReplaceChar('3'); + model.ReplaceChar('4'); + EXPECT_STR_EQ("a1234", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcd", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_FALSE(model.Undo()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abcd", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a1234", model.text()); + EXPECT_FALSE(model.Redo()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_ReplaceTest) { + // By Cursor + { + SCOPED_TRACE("forward & insert by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.MoveCursorTo(1, false); + model.MoveCursorTo(3, true); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("backward & insert by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.MoveCursorTo(3, false); + model.MoveCursorTo(1, true); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("forward & overwrite by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.MoveCursorTo(1, false); + model.MoveCursorTo(3, true); + RunOverwriteReplaceTest(model); + } + { + SCOPED_TRACE("backward & overwrite by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.MoveCursorTo(3, false); + model.MoveCursorTo(1, true); + RunOverwriteReplaceTest(model); + } + // By SelectRange API + { + SCOPED_TRACE("forward & insert by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("backward & insert by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("forward & overwrite by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunOverwriteReplaceTest(model); + } + { + SCOPED_TRACE("backward & overwrite by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunOverwriteReplaceTest(model); + } +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_CompositionText) { + TextfieldViewsModel model(NULL); + + ui::CompositionText composition; + composition.text = ASCIIToUTF16("abc"); + composition.underlines.push_back(ui::CompositionUnderline(0, 3, 0, false)); + composition.selection = ui::Range(2, 3); + + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorToEnd(false); + model.SetCompositionText(composition); + EXPECT_TRUE(model.HasCompositionText()); + EXPECT_TRUE(model.HasSelection()); + EXPECT_STR_EQ("ABCDEabc", model.text()); + + // Accepting composition + model.ConfirmCompositionText(); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_FALSE(model.Redo()); + + // Canceling composition + model.MoveCursorToHome(false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("abcABCDEabc", model.text()); + model.ClearCompositionText(); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_FALSE(model.Redo()); + + // SetText with the same text as the result. + ResetModel(&model); + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorToEnd(false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("ABCDEabc", model.text()); + model.SetText(ASCIIToUTF16("ABCDEabc")); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.text()); + EXPECT_FALSE(model.Redo()); + + // SetText with the different text than the result + // should not remember composition text. + ResetModel(&model); + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorToEnd(false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("ABCDEabc", model.text()); + model.SetText(ASCIIToUTF16("1234")); + EXPECT_STR_EQ("1234", model.text()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.text()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("1234", model.text()); + EXPECT_FALSE(model.Redo()); + + // TODO(oshima): undo/redo while compositing text should to be + // handled by IME layer. Figure out how to test. +} + +// TODO(oshima): UndoRedo with Drag and Drop + } // namespace views diff --git a/views/examples/examples_main.cc b/views/examples/examples_main.cc index d46fb72..e61f1b2 100644 --- a/views/examples/examples_main.cc +++ b/views/examples/examples_main.cc @@ -9,6 +9,7 @@ #include "base/command_line.h" #include "base/i18n/icu_util.h" #include "base/process_util.h" +#include "base/scoped_ptr.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/ui_base_paths.h" #include "views/controls/button/text_button.h" @@ -31,6 +32,7 @@ #include "views/examples/widget_example.h" #include "views/focus/accelerator_handler.h" #include "views/layout/grid_layout.h" +#include "views/views_delegate.h" #include "views/widget/root_view.h" #include "views/window/window.h" #include "views/test/test_views_delegate.h" @@ -186,6 +188,8 @@ int main(int argc, char** argv) { #endif TestViewsDelegate delegate; + // The delegate needs to be set before any UI is created so that windows + // display the correct icon. CommandLine::Init(argc, argv); // We do not this header: chrome/common/chrome_switches.h diff --git a/views/examples/textfield_example.cc b/views/examples/textfield_example.cc index 61a3175..26c732d 100644 --- a/views/examples/textfield_example.cc +++ b/views/examples/textfield_example.cc @@ -30,6 +30,7 @@ void TextfieldExample::CreateExampleView(views::View* container) { show_password_ = new views::TextButton(this, L"Show password"); clear_all_ = new views::TextButton(this, L"Clear All"); append_ = new views::TextButton(this, L"Append"); + set_ = new views::TextButton(this, L"Set"); name_->SetController(this); password_->SetController(this); @@ -53,6 +54,8 @@ void TextfieldExample::CreateExampleView(views::View* container) { layout->AddView(clear_all_); layout->StartRow(0, 0); layout->AddView(append_); + layout->StartRow(0, 0); + layout->AddView(set_); } void TextfieldExample::ContentsChanged(views::Textfield* sender, @@ -79,6 +82,8 @@ void TextfieldExample::ButtonPressed(views::Button* sender, password_->SetText(empty); } else if (sender == append_) { name_->AppendText(WideToUTF16(L"[append]")); + } else if (sender == set_) { + name_->SetText(WideToUTF16(L"[set]")); } } diff --git a/views/examples/textfield_example.h b/views/examples/textfield_example.h index 1535efb..dc29842 100644 --- a/views/examples/textfield_example.h +++ b/views/examples/textfield_example.h @@ -48,6 +48,7 @@ class TextfieldExample : public ExampleBase, views::TextButton* show_password_; views::TextButton* clear_all_; views::TextButton* append_; + views::TextButton* set_; DISALLOW_COPY_AND_ASSIGN(TextfieldExample); }; |