diff options
Diffstat (limited to 'chromium/ui/views/controls/textfield/textfield_model.cc')
-rw-r--r-- | chromium/ui/views/controls/textfield/textfield_model.cc | 771 |
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 |