summaryrefslogtreecommitdiffstats
path: root/chromium/ui/views/controls/textfield/textfield_model.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/ui/views/controls/textfield/textfield_model.cc')
-rw-r--r--chromium/ui/views/controls/textfield/textfield_model.cc771
1 files changed, 771 insertions, 0 deletions
diff --git a/chromium/ui/views/controls/textfield/textfield_model.cc b/chromium/ui/views/controls/textfield/textfield_model.cc
new file mode 100644
index 00000000000..db0dc73b1b0
--- /dev/null
+++ b/chromium/ui/views/controls/textfield/textfield_model.cc
@@ -0,0 +1,771 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/views/controls/textfield/textfield_model.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/stl_util.h"
+#include "ui/base/clipboard/clipboard.h"
+#include "ui/base/clipboard/scoped_clipboard_writer.h"
+#include "ui/gfx/range/range.h"
+#include "ui/gfx/utf16_indexing.h"
+
+namespace views {
+
+namespace internal {
+
+// Edit holds state information to undo/redo editing changes. Editing operations
+// are merged when possible, like when characters are typed in sequence. Calling
+// Commit() marks an edit as an independent operation that shouldn't be merged.
+class Edit {
+ public:
+ enum Type {
+ INSERT_EDIT,
+ DELETE_EDIT,
+ REPLACE_EDIT,
+ };
+
+ virtual ~Edit() {}
+
+ // Revert the change made by this edit in |model|.
+ void Undo(TextfieldModel* model) {
+ model->ModifyText(new_text_start_, new_text_end(),
+ old_text_, old_text_start_,
+ old_cursor_pos_);
+ }
+
+ // Apply the change of this edit to the |model|.
+ void Redo(TextfieldModel* model) {
+ model->ModifyText(old_text_start_, old_text_end(),
+ new_text_, new_text_start_,
+ new_cursor_pos_);
+ }
+
+ // Try to merge the |edit| into this edit and returns true on success. The
+ // merged edit will be deleted after redo and should not be reused.
+ bool Merge(const Edit* edit) {
+ // Don't merge if previous edit is DELETE. This happens when a
+ // user deletes characters then hits return. In this case, the
+ // delete should be treated as separate edit that can be undone
+ // and should not be merged with the replace edit.
+ if (type_ != DELETE_EDIT && edit->force_merge()) {
+ MergeReplace(edit);
+ return true;
+ }
+ return mergeable() && edit->mergeable() && DoMerge(edit);
+ }
+
+ // Commits the edit and marks as un-mergeable.
+ void Commit() { merge_type_ = DO_NOT_MERGE; }
+
+ private:
+ friend class InsertEdit;
+ friend class ReplaceEdit;
+ friend class DeleteEdit;
+
+ Edit(Type type,
+ MergeType merge_type,
+ size_t old_cursor_pos,
+ const base::string16& old_text,
+ size_t old_text_start,
+ bool delete_backward,
+ size_t new_cursor_pos,
+ const base::string16& new_text,
+ size_t new_text_start)
+ : type_(type),
+ merge_type_(merge_type),
+ old_cursor_pos_(old_cursor_pos),
+ old_text_(old_text),
+ old_text_start_(old_text_start),
+ delete_backward_(delete_backward),
+ new_cursor_pos_(new_cursor_pos),
+ new_text_(new_text),
+ new_text_start_(new_text_start) {
+ }
+
+ // Each type of edit provides its own specific merge implementation.
+ virtual bool DoMerge(const Edit* edit) = 0;
+
+ Type type() const { return type_; }
+
+ // Can this edit be merged?
+ bool mergeable() const { return merge_type_ == MERGEABLE; }
+
+ // Should this edit be forcibly merged with the previous edit?
+ bool force_merge() const { return merge_type_ == FORCE_MERGE; }
+
+ // Returns the end index of the |old_text_|.
+ size_t old_text_end() const { return old_text_start_ + old_text_.length(); }
+
+ // Returns the end index of the |new_text_|.
+ size_t new_text_end() const { return new_text_start_ + new_text_.length(); }
+
+ // Merge the replace edit into the current edit. This handles the special case
+ // where an omnibox autocomplete string is set after a new character is typed.
+ void MergeReplace(const Edit* edit) {
+ CHECK_EQ(REPLACE_EDIT, edit->type_);
+ CHECK_EQ(0U, edit->old_text_start_);
+ CHECK_EQ(0U, edit->new_text_start_);
+ base::string16 old_text = edit->old_text_;
+ old_text.erase(new_text_start_, new_text_.length());
+ old_text.insert(old_text_start_, old_text_);
+ // SetText() replaces entire text. Set |old_text_| to the entire
+ // replaced text with |this| edit undone.
+ old_text_ = old_text;
+ old_text_start_ = edit->old_text_start_;
+ delete_backward_ = false;
+
+ new_text_ = edit->new_text_;
+ new_text_start_ = edit->new_text_start_;
+ merge_type_ = DO_NOT_MERGE;
+ }
+
+ Type type_;
+
+ // True if the edit can be marged.
+ MergeType merge_type_;
+ // Old cursor position.
+ size_t old_cursor_pos_;
+ // Deleted text by this edit.
+ base::string16 old_text_;
+ // The index of |old_text_|.
+ size_t old_text_start_;
+ // True if the deletion is made backward.
+ bool delete_backward_;
+ // New cursor position.
+ size_t new_cursor_pos_;
+ // Added text.
+ base::string16 new_text_;
+ // The index of |new_text_|
+ size_t new_text_start_;
+
+ DISALLOW_COPY_AND_ASSIGN(Edit);
+};
+
+class InsertEdit : public Edit {
+ public:
+ InsertEdit(bool mergeable, const base::string16& new_text, size_t at)
+ : Edit(INSERT_EDIT,
+ mergeable ? MERGEABLE : DO_NOT_MERGE,
+ at /* old cursor */,
+ base::string16(),
+ at,
+ false /* N/A */,
+ at + new_text.length() /* new cursor */,
+ new_text,
+ at) {
+ }
+
+ // Edit implementation.
+ virtual bool DoMerge(const Edit* edit) OVERRIDE {
+ if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_)
+ return false;
+ // If continuous edit, merge it.
+ // TODO(oshima): gtk splits edits between whitespace. Find out what
+ // we want to here and implement if necessary.
+ new_text_ += edit->new_text_;
+ new_cursor_pos_ = edit->new_cursor_pos_;
+ return true;
+ }
+};
+
+class ReplaceEdit : public Edit {
+ public:
+ ReplaceEdit(MergeType merge_type,
+ const base::string16& old_text,
+ size_t old_cursor_pos,
+ size_t old_text_start,
+ bool backward,
+ size_t new_cursor_pos,
+ const base::string16& new_text,
+ size_t new_text_start)
+ : Edit(REPLACE_EDIT, merge_type,
+ old_cursor_pos,
+ old_text,
+ old_text_start,
+ backward,
+ new_cursor_pos,
+ new_text,
+ new_text_start) {
+ }
+
+ // Edit implementation.
+ virtual bool DoMerge(const Edit* edit) OVERRIDE {
+ if (edit->type() == DELETE_EDIT ||
+ new_text_end() != edit->old_text_start_ ||
+ edit->old_text_start_ != edit->new_text_start_)
+ return false;
+ old_text_ += edit->old_text_;
+ new_text_ += edit->new_text_;
+ new_cursor_pos_ = edit->new_cursor_pos_;
+ return true;
+ }
+};
+
+class DeleteEdit : public Edit {
+ public:
+ DeleteEdit(bool mergeable,
+ const base::string16& text,
+ size_t text_start,
+ bool backward)
+ : Edit(DELETE_EDIT,
+ mergeable ? MERGEABLE : DO_NOT_MERGE,
+ (backward ? text_start + text.length() : text_start),
+ text,
+ text_start,
+ backward,
+ text_start,
+ base::string16(),
+ text_start) {
+ }
+
+ // Edit implementation.
+ virtual bool DoMerge(const Edit* edit) OVERRIDE {
+ if (edit->type() != DELETE_EDIT)
+ return false;
+
+ if (delete_backward_) {
+ // backspace can be merged only with backspace at the same position.
+ if (!edit->delete_backward_ || old_text_start_ != edit->old_text_end())
+ return false;
+ old_text_start_ = edit->old_text_start_;
+ old_text_ = edit->old_text_ + old_text_;
+ new_cursor_pos_ = edit->new_cursor_pos_;
+ } else {
+ // delete can be merged only with delete at the same position.
+ if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_)
+ return false;
+ old_text_ += edit->old_text_;
+ }
+ return true;
+ }
+};
+
+} // namespace internal
+
+namespace {
+
+// Returns the first segment that is visually emphasized. Usually it's used for
+// representing the target clause (on Windows). Returns an invalid range if
+// there is no such a range.
+gfx::Range GetFirstEmphasizedRange(const ui::CompositionText& composition) {
+ for (size_t i = 0; i < composition.underlines.size(); ++i) {
+ const ui::CompositionUnderline& underline = composition.underlines[i];
+ if (underline.thick)
+ return gfx::Range(underline.start_offset, underline.end_offset);
+ }
+ return gfx::Range::InvalidRange();
+}
+
+} // namespace
+
+using internal::Edit;
+using internal::DeleteEdit;
+using internal::InsertEdit;
+using internal::ReplaceEdit;
+using internal::MergeType;
+using internal::DO_NOT_MERGE;
+using internal::FORCE_MERGE;
+using internal::MERGEABLE;
+
+/////////////////////////////////////////////////////////////////
+// TextfieldModel: public
+
+TextfieldModel::Delegate::~Delegate() {}
+
+TextfieldModel::TextfieldModel(Delegate* delegate)
+ : delegate_(delegate),
+ render_text_(gfx::RenderText::CreateInstance()),
+ current_edit_(edit_history_.end()) {
+}
+
+TextfieldModel::~TextfieldModel() {
+ ClearEditHistory();
+ ClearComposition();
+}
+
+bool TextfieldModel::SetText(const base::string16& new_text) {
+ bool changed = false;
+ if (HasCompositionText()) {
+ ConfirmCompositionText();
+ changed = true;
+ }
+ if (text() != new_text) {
+ if (changed) // No need to remember composition.
+ Undo();
+ size_t old_cursor = GetCursorPosition();
+ // SetText moves the cursor to the end.
+ size_t new_cursor = new_text.length();
+ SelectAll(false);
+ // If there is a composition text, don't merge with previous edit.
+ // Otherwise, force merge the edits.
+ ExecuteAndRecordReplace(changed ? DO_NOT_MERGE : FORCE_MERGE,
+ old_cursor, new_cursor, new_text, 0U);
+ render_text_->SetCursorPosition(new_cursor);
+ }
+ ClearSelection();
+ return changed;
+}
+
+void TextfieldModel::Append(const base::string16& new_text) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ size_t save = GetCursorPosition();
+ MoveCursor(gfx::LINE_BREAK,
+ render_text_->GetVisualDirectionOfLogicalEnd(),
+ false);
+ InsertText(new_text);
+ render_text_->SetCursorPosition(save);
+ ClearSelection();
+}
+
+bool TextfieldModel::Delete() {
+ if (HasCompositionText()) {
+ // No undo/redo for composition text.
+ CancelCompositionText();
+ return true;
+ }
+ if (HasSelection()) {
+ DeleteSelection();
+ return true;
+ }
+ if (text().length() > GetCursorPosition()) {
+ size_t cursor_position = GetCursorPosition();
+ size_t next_grapheme_index = render_text_->IndexOfAdjacentGrapheme(
+ cursor_position, gfx::CURSOR_FORWARD);
+ ExecuteAndRecordDelete(gfx::Range(cursor_position, next_grapheme_index),
+ true);
+ return true;
+ }
+ return false;
+}
+
+bool TextfieldModel::Backspace() {
+ if (HasCompositionText()) {
+ // No undo/redo for composition text.
+ CancelCompositionText();
+ return true;
+ }
+ if (HasSelection()) {
+ DeleteSelection();
+ return true;
+ }
+ size_t cursor_position = GetCursorPosition();
+ if (cursor_position > 0) {
+ // Delete one code point, which may be two UTF-16 words.
+ size_t previous_char = gfx::UTF16OffsetToIndex(text(), cursor_position, -1);
+ ExecuteAndRecordDelete(gfx::Range(cursor_position, previous_char), true);
+ return true;
+ }
+ return false;
+}
+
+size_t TextfieldModel::GetCursorPosition() const {
+ return render_text_->cursor_position();
+}
+
+void TextfieldModel::MoveCursor(gfx::BreakType break_type,
+ gfx::VisualCursorDirection direction,
+ bool select) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->MoveCursor(break_type, direction, select);
+}
+
+bool TextfieldModel::MoveCursorTo(const gfx::SelectionModel& cursor) {
+ if (HasCompositionText()) {
+ ConfirmCompositionText();
+ // ConfirmCompositionText() updates cursor position. Need to reflect it in
+ // the SelectionModel parameter of MoveCursorTo().
+ gfx::Range range(render_text_->selection().start(), cursor.caret_pos());
+ if (!range.is_empty())
+ return render_text_->SelectRange(range);
+ return render_text_->MoveCursorTo(
+ gfx::SelectionModel(cursor.caret_pos(), cursor.caret_affinity()));
+ }
+ return render_text_->MoveCursorTo(cursor);
+}
+
+bool TextfieldModel::MoveCursorTo(const gfx::Point& point, bool select) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ gfx::SelectionModel cursor = render_text_->FindCursorPosition(point);
+ if (select)
+ cursor.set_selection_start(render_text_->selection().start());
+ return render_text_->MoveCursorTo(cursor);
+}
+
+base::string16 TextfieldModel::GetSelectedText() const {
+ return text().substr(render_text_->selection().GetMin(),
+ render_text_->selection().length());
+}
+
+void TextfieldModel::SelectRange(const gfx::Range& range) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->SelectRange(range);
+}
+
+void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel& sel) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->MoveCursorTo(sel);
+}
+
+void TextfieldModel::SelectAll(bool reversed) {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->SelectAll(reversed);
+}
+
+void TextfieldModel::SelectWord() {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->SelectWord();
+}
+
+void TextfieldModel::ClearSelection() {
+ if (HasCompositionText())
+ ConfirmCompositionText();
+ render_text_->ClearSelection();
+}
+
+bool TextfieldModel::CanUndo() {
+ return edit_history_.size() && current_edit_ != edit_history_.end();
+}
+
+bool TextfieldModel::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 TextfieldModel::Undo() {
+ if (!CanUndo())
+ return false;
+ DCHECK(!HasCompositionText());
+ if (HasCompositionText())
+ CancelCompositionText();
+
+ base::string16 old = text();
+ size_t old_cursor = GetCursorPosition();
+ (*current_edit_)->Commit();
+ (*current_edit_)->Undo(this);
+
+ if (current_edit_ == edit_history_.begin())
+ current_edit_ = edit_history_.end();
+ else
+ current_edit_--;
+ return old != text() || old_cursor != GetCursorPosition();
+}
+
+bool TextfieldModel::Redo() {
+ if (!CanRedo())
+ return false;
+ DCHECK(!HasCompositionText());
+ if (HasCompositionText())
+ CancelCompositionText();
+
+ if (current_edit_ == edit_history_.end())
+ current_edit_ = edit_history_.begin();
+ else
+ current_edit_ ++;
+ base::string16 old = text();
+ size_t old_cursor = GetCursorPosition();
+ (*current_edit_)->Redo(this);
+ return old != text() || old_cursor != GetCursorPosition();
+}
+
+bool TextfieldModel::Cut() {
+ if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
+ ui::ScopedClipboardWriter(
+ ui::Clipboard::GetForCurrentThread(),
+ ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText());
+ // A trick to let undo/redo handle cursor correctly.
+ // Undoing CUT moves the cursor to the end of the change rather
+ // than beginning, unlike Delete/Backspace.
+ // TODO(oshima): Change Delete/Backspace to use DeleteSelection,
+ // update DeleteEdit and remove this trick.
+ const gfx::Range& selection = render_text_->selection();
+ render_text_->SelectRange(gfx::Range(selection.end(), selection.start()));
+ DeleteSelection();
+ return true;
+ }
+ return false;
+}
+
+bool TextfieldModel::Copy() {
+ if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
+ ui::ScopedClipboardWriter(
+ ui::Clipboard::GetForCurrentThread(),
+ ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText());
+ return true;
+ }
+ return false;
+}
+
+bool TextfieldModel::Paste() {
+ base::string16 result;
+ ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE,
+ &result);
+ if (result.empty())
+ return false;
+
+ InsertTextInternal(result, false);
+ return true;
+}
+
+bool TextfieldModel::HasSelection() const {
+ return !render_text_->selection().is_empty();
+}
+
+void TextfieldModel::DeleteSelection() {
+ DCHECK(!HasCompositionText());
+ DCHECK(HasSelection());
+ ExecuteAndRecordDelete(render_text_->selection(), false);
+}
+
+void TextfieldModel::DeleteSelectionAndInsertTextAt(
+ const base::string16& new_text,
+ size_t position) {
+ if (HasCompositionText())
+ CancelCompositionText();
+ ExecuteAndRecordReplace(DO_NOT_MERGE,
+ GetCursorPosition(),
+ position + new_text.length(),
+ new_text,
+ position);
+}
+
+base::string16 TextfieldModel::GetTextFromRange(const gfx::Range& range) const {
+ if (range.IsValid() && range.GetMin() < text().length())
+ return text().substr(range.GetMin(), range.length());
+ return base::string16();
+}
+
+void TextfieldModel::GetTextRange(gfx::Range* range) const {
+ *range = gfx::Range(0, text().length());
+}
+
+void TextfieldModel::SetCompositionText(
+ const ui::CompositionText& composition) {
+ if (HasCompositionText())
+ CancelCompositionText();
+ else if (HasSelection())
+ DeleteSelection();
+
+ if (composition.text.empty())
+ return;
+
+ size_t cursor = GetCursorPosition();
+ base::string16 new_text = text();
+ render_text_->SetText(new_text.insert(cursor, composition.text));
+ gfx::Range range(cursor, cursor + composition.text.length());
+ render_text_->SetCompositionRange(range);
+ gfx::Range emphasized_range = GetFirstEmphasizedRange(composition);
+ if (emphasized_range.IsValid()) {
+ // This is a workaround due to the lack of support in RenderText to draw
+ // a thick underline. In a composition returned from an IME, the segment
+ // emphasized by a thick underline usually represents the target clause.
+ // Because the target clause is more important than the actual selection
+ // range (or caret position) in the composition here we use a selection-like
+ // marker instead to show this range.
+ // TODO(yukawa, msw): Support thick underlines and remove this workaround.
+ render_text_->SelectRange(gfx::Range(
+ cursor + emphasized_range.GetMin(),
+ cursor + emphasized_range.GetMax()));
+ } else if (!composition.selection.is_empty()) {
+ render_text_->SelectRange(gfx::Range(
+ cursor + composition.selection.GetMin(),
+ cursor + composition.selection.GetMax()));
+ } else {
+ render_text_->SetCursorPosition(cursor + composition.selection.end());
+ }
+}
+
+void TextfieldModel::ConfirmCompositionText() {
+ DCHECK(HasCompositionText());
+ gfx::Range range = render_text_->GetCompositionRange();
+ base::string16 composition = text().substr(range.start(), range.length());
+ // TODO(oshima): current behavior on ChromeOS is a bit weird and not
+ // sure exactly how this should work. Find out and fix if necessary.
+ AddOrMergeEditHistory(new InsertEdit(false, composition, range.start()));
+ render_text_->SetCursorPosition(range.end());
+ ClearComposition();
+ if (delegate_)
+ delegate_->OnCompositionTextConfirmedOrCleared();
+}
+
+void TextfieldModel::CancelCompositionText() {
+ DCHECK(HasCompositionText());
+ gfx::Range range = render_text_->GetCompositionRange();
+ ClearComposition();
+ base::string16 new_text = text();
+ render_text_->SetText(new_text.erase(range.start(), range.length()));
+ render_text_->SetCursorPosition(range.start());
+ if (delegate_)
+ delegate_->OnCompositionTextConfirmedOrCleared();
+}
+
+void TextfieldModel::ClearComposition() {
+ render_text_->SetCompositionRange(gfx::Range::InvalidRange());
+}
+
+void TextfieldModel::GetCompositionTextRange(gfx::Range* range) const {
+ *range = gfx::Range(render_text_->GetCompositionRange());
+}
+
+bool TextfieldModel::HasCompositionText() const {
+ return !render_text_->GetCompositionRange().is_empty();
+}
+
+void TextfieldModel::ClearEditHistory() {
+ STLDeleteElements(&edit_history_);
+ current_edit_ = edit_history_.end();
+}
+
+/////////////////////////////////////////////////////////////////
+// TextfieldModel: private
+
+void TextfieldModel::InsertTextInternal(const base::string16& new_text,
+ bool mergeable) {
+ if (HasCompositionText()) {
+ CancelCompositionText();
+ ExecuteAndRecordInsert(new_text, mergeable);
+ } else if (HasSelection()) {
+ ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE,
+ new_text);
+ } else {
+ ExecuteAndRecordInsert(new_text, mergeable);
+ }
+}
+
+void TextfieldModel::ReplaceTextInternal(const base::string16& new_text,
+ bool mergeable) {
+ if (HasCompositionText()) {
+ CancelCompositionText();
+ } else if (!HasSelection()) {
+ size_t cursor = GetCursorPosition();
+ const gfx::SelectionModel& model = render_text_->selection_model();
+ // When there is no selection, the default is to replace the next grapheme
+ // with |new_text|. So, need to find the index of next grapheme first.
+ size_t next =
+ render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD);
+ if (next == model.caret_pos())
+ render_text_->MoveCursorTo(model);
+ else
+ render_text_->SelectRange(gfx::Range(next, model.caret_pos()));
+ }
+ // Edit history is recorded in InsertText.
+ InsertTextInternal(new_text, mergeable);
+}
+
+void TextfieldModel::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 TextfieldModel::ExecuteAndRecordDelete(gfx::Range range, bool mergeable) {
+ size_t old_text_start = range.GetMin();
+ const base::string16 old_text = text().substr(old_text_start, range.length());
+ bool backward = range.is_reversed();
+ Edit* edit = new DeleteEdit(mergeable, old_text, old_text_start, backward);
+ bool delete_edit = AddOrMergeEditHistory(edit);
+ edit->Redo(this);
+ if (delete_edit)
+ delete edit;
+}
+
+void TextfieldModel::ExecuteAndRecordReplaceSelection(
+ MergeType merge_type,
+ const base::string16& new_text) {
+ size_t new_text_start = render_text_->selection().GetMin();
+ size_t new_cursor_pos = new_text_start + new_text.length();
+ ExecuteAndRecordReplace(merge_type,
+ GetCursorPosition(),
+ new_cursor_pos,
+ new_text,
+ new_text_start);
+}
+
+void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type,
+ size_t old_cursor_pos,
+ size_t new_cursor_pos,
+ const base::string16& new_text,
+ size_t new_text_start) {
+ size_t old_text_start = render_text_->selection().GetMin();
+ bool backward = render_text_->selection().is_reversed();
+ Edit* edit = new ReplaceEdit(merge_type,
+ GetSelectedText(),
+ old_cursor_pos,
+ old_text_start,
+ backward,
+ new_cursor_pos,
+ new_text,
+ new_text_start);
+ bool delete_edit = AddOrMergeEditHistory(edit);
+ edit->Redo(this);
+ if (delete_edit)
+ delete edit;
+}
+
+void TextfieldModel::ExecuteAndRecordInsert(const base::string16& new_text,
+ bool mergeable) {
+ Edit* edit = new InsertEdit(mergeable, new_text, GetCursorPosition());
+ bool delete_edit = AddOrMergeEditHistory(edit);
+ edit->Redo(this);
+ if (delete_edit)
+ delete edit;
+}
+
+bool TextfieldModel::AddOrMergeEditHistory(Edit* edit) {
+ ClearRedoHistory();
+
+ if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) {
+ // If a current edit exists and has been merged with a new edit, don't add
+ // to the history, and return true to delete |edit| after redo.
+ return true;
+ }
+ 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_++;
+ }
+ return false;
+}
+
+void TextfieldModel::ModifyText(size_t delete_from,
+ size_t delete_to,
+ const base::string16& new_text,
+ size_t new_text_insert_at,
+ size_t new_cursor_pos) {
+ DCHECK_LE(delete_from, delete_to);
+ base::string16 old_text = text();
+ ClearComposition();
+ if (delete_from != delete_to)
+ render_text_->SetText(old_text.erase(delete_from, delete_to - delete_from));
+ if (!new_text.empty())
+ render_text_->SetText(old_text.insert(new_text_insert_at, new_text));
+ render_text_->SetCursorPosition(new_cursor_pos);
+ // TODO(oshima): Select text that was just undone, like Mac (but not GTK).
+}
+
+} // namespace views