summaryrefslogtreecommitdiffstats
path: root/views/controls/textfield
diff options
context:
space:
mode:
authoroshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-05-11 01:24:09 +0000
committeroshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-05-11 01:24:09 +0000
commit1cbb4c4a87c0e1e87defd6ab919217280d275b3f (patch)
tree6f8d1bc47b919807cae1876053e3050797ce1e6d /views/controls/textfield
parentc9d88a05388dfc5702aa018fc06d800741a6a385 (diff)
downloadchromium_src-1cbb4c4a87c0e1e87defd6ab919217280d275b3f.zip
chromium_src-1cbb4c4a87c0e1e87defd6ab919217280d275b3f.tar.gz
chromium_src-1cbb4c4a87c0e1e87defd6ab919217280d275b3f.tar.bz2
Undo Redo for Textfield Views
Added Edit and its subclass that represents a change that can be undone/redone. Set TestViewsDelegates in examples_main.cc to make cut&copy work in views_examples. I'll refactor this soon so that text change is always done by an edit object. BUG=none TEST=unit tests for undo/redo Review URL: http://codereview.chromium.org/6937002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@84902 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views/controls/textfield')
-rw-r--r--views/controls/textfield/native_textfield_views.cc24
-rw-r--r--views/controls/textfield/native_textfield_views.h5
-rw-r--r--views/controls/textfield/native_textfield_views_unittest.cc105
-rw-r--r--views/controls/textfield/textfield_views_model.cc488
-rw-r--r--views/controls/textfield/textfield_views_model.h102
-rw-r--r--views/controls/textfield/textfield_views_model_unittest.cc391
6 files changed, 1055 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