1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/views/controls/textfield/textfield_views_model.h"
9 #include "base/i18n/break_iterator.h"
10 #include "base/logging.h"
11 #include "base/stl_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "ui/base/clipboard/clipboard.h"
14 #include "ui/base/clipboard/scoped_clipboard_writer.h"
15 #include "ui/gfx/canvas.h"
16 #include "ui/gfx/font.h"
17 #include "ui/gfx/range/range.h"
18 #include "ui/gfx/render_text.h"
19 #include "ui/gfx/text_constants.h"
20 #include "ui/gfx/utf16_indexing.h"
21 #include "ui/views/controls/textfield/textfield.h"
27 // An edit object holds enough information/state to undo/redo the
28 // change. Two edits are merged when possible, for example, when
29 // you type new characters in sequence. |Commit()| can be used to
30 // mark an edit as an independent edit and it shouldn't be merged.
31 // (For example, when you did undo/redo, or a text is appended via
44 // Revert the change made by this edit in |model|.
45 void Undo(TextfieldViewsModel* model) {
46 model->ModifyText(new_text_start_, new_text_end(),
47 old_text_, old_text_start_,
51 // Apply the change of this edit to the |model|.
52 void Redo(TextfieldViewsModel* model) {
53 model->ModifyText(old_text_start_, old_text_end(),
54 new_text_, new_text_start_,
58 // Try to merge the |edit| into this edit. Returns true if merge was
59 // successful, or false otherwise. Merged edit will be deleted after
60 // redo and should not be reused.
61 bool Merge(const Edit* edit) {
62 // Don't merge if previous edit is DELETE. This happens when a
63 // user deletes characters then hits return. In this case, the
64 // delete should be treated as separate edit that can be undone
65 // and should not be merged with the replace edit.
66 if (type_ != DELETE_EDIT && edit->merge_with_previous()) {
70 return mergeable() && edit->mergeable() && DoMerge(edit);
73 // Commits the edit and marks as un-mergeable.
74 void Commit() { merge_type_ = DO_NOT_MERGE; }
77 friend class InsertEdit;
78 friend class ReplaceEdit;
79 friend class DeleteEdit;
83 size_t old_cursor_pos,
84 const string16& old_text,
85 size_t old_text_start,
87 size_t new_cursor_pos,
88 const string16& new_text,
89 size_t new_text_start)
91 merge_type_(merge_type),
92 old_cursor_pos_(old_cursor_pos),
94 old_text_start_(old_text_start),
95 delete_backward_(delete_backward),
96 new_cursor_pos_(new_cursor_pos),
98 new_text_start_(new_text_start) {
101 // A template method pattern that provides specific merge
102 // implementation for each type of edit.
103 virtual bool DoMerge(const Edit* edit) = 0;
105 Type type() const { return type_; }
107 // Can this edit be merged?
108 bool mergeable() const { return merge_type_ == MERGEABLE; }
110 // Should this edit be forcibly merged with the previous edit?
111 bool merge_with_previous() const {
112 return merge_type_ == MERGE_WITH_PREVIOUS;
115 // Returns the end index of the |old_text_|.
116 size_t old_text_end() const { return old_text_start_ + old_text_.length(); }
118 // Returns the end index of the |new_text_|.
119 size_t new_text_end() const { return new_text_start_ + new_text_.length(); }
121 // Merge the replace edit into the current edit. This is a special case to
122 // handle an omnibox setting autocomplete string after new character is
124 void MergeReplace(const Edit* edit) {
125 CHECK_EQ(REPLACE_EDIT, edit->type_);
126 CHECK_EQ(0U, edit->old_text_start_);
127 CHECK_EQ(0U, edit->new_text_start_);
128 string16 old_text = edit->old_text_;
129 old_text.erase(new_text_start_, new_text_.length());
130 old_text.insert(old_text_start_, old_text_);
131 // SetText() replaces entire text. Set |old_text_| to the entire
132 // replaced text with |this| edit undone.
133 old_text_ = old_text;
134 old_text_start_ = edit->old_text_start_;
135 delete_backward_ = false;
137 new_text_ = edit->new_text_;
138 new_text_start_ = edit->new_text_start_;
139 merge_type_ = DO_NOT_MERGE;
144 // True if the edit can be marged.
145 MergeType merge_type_;
146 // Old cursor position.
147 size_t old_cursor_pos_;
148 // Deleted text by this edit.
150 // The index of |old_text_|.
151 size_t old_text_start_;
152 // True if the deletion is made backward.
153 bool delete_backward_;
154 // New cursor position.
155 size_t new_cursor_pos_;
158 // The index of |new_text_|
159 size_t new_text_start_;
161 DISALLOW_COPY_AND_ASSIGN(Edit);
164 class InsertEdit : public Edit {
166 InsertEdit(bool mergeable, const string16& new_text, size_t at)
168 mergeable ? MERGEABLE : DO_NOT_MERGE,
173 at + new_text.length() /* new cursor */,
178 // Edit implementation.
179 virtual bool DoMerge(const Edit* edit) OVERRIDE {
180 if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_)
182 // If continuous edit, merge it.
183 // TODO(oshima): gtk splits edits between whitespace. Find out what
184 // we want to here and implement if necessary.
185 new_text_ += edit->new_text_;
186 new_cursor_pos_ = edit->new_cursor_pos_;
191 class ReplaceEdit : public Edit {
193 ReplaceEdit(MergeType merge_type,
194 const string16& old_text,
195 size_t old_cursor_pos,
196 size_t old_text_start,
198 size_t new_cursor_pos,
199 const string16& new_text,
200 size_t new_text_start)
201 : Edit(REPLACE_EDIT, merge_type,
211 // Edit implementation.
212 virtual bool DoMerge(const Edit* edit) OVERRIDE {
213 if (edit->type() == DELETE_EDIT ||
214 new_text_end() != edit->old_text_start_ ||
215 edit->old_text_start_ != edit->new_text_start_)
217 old_text_ += edit->old_text_;
218 new_text_ += edit->new_text_;
219 new_cursor_pos_ = edit->new_cursor_pos_;
224 class DeleteEdit : public Edit {
226 DeleteEdit(bool mergeable,
227 const string16& text,
231 mergeable ? MERGEABLE : DO_NOT_MERGE,
232 (backward ? text_start + text.length() : text_start),
241 // Edit implementation.
242 virtual bool DoMerge(const Edit* edit) OVERRIDE {
243 if (edit->type() != DELETE_EDIT)
246 if (delete_backward_) {
247 // backspace can be merged only with backspace at the
249 if (!edit->delete_backward_ || old_text_start_ != edit->old_text_end())
251 old_text_start_ = edit->old_text_start_;
252 old_text_ = edit->old_text_ + old_text_;
253 new_cursor_pos_ = edit->new_cursor_pos_;
255 // delete can be merged only with delete at the same
257 if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_)
259 old_text_ += edit->old_text_;
265 } // namespace internal
269 // Returns the first segment that is visually emphasized. Usually it's used for
270 // representing the target clause (on Windows). Returns an invalid range if
271 // there is no such a range.
272 gfx::Range GetFirstEmphasizedRange(const ui::CompositionText& composition) {
273 for (size_t i = 0; i < composition.underlines.size(); ++i) {
274 const ui::CompositionUnderline& underline = composition.underlines[i];
276 return gfx::Range(underline.start_offset, underline.end_offset);
278 return gfx::Range::InvalidRange();
283 using internal::Edit;
284 using internal::DeleteEdit;
285 using internal::InsertEdit;
286 using internal::ReplaceEdit;
287 using internal::MergeType;
288 using internal::DO_NOT_MERGE;
289 using internal::MERGE_WITH_PREVIOUS;
290 using internal::MERGEABLE;
292 /////////////////////////////////////////////////////////////////
293 // TextfieldViewsModel: public
295 TextfieldViewsModel::Delegate::~Delegate() {
298 TextfieldViewsModel::TextfieldViewsModel(Delegate* delegate)
299 : delegate_(delegate),
300 render_text_(gfx::RenderText::CreateInstance()),
301 current_edit_(edit_history_.end()) {
304 TextfieldViewsModel::~TextfieldViewsModel() {
309 const string16& TextfieldViewsModel::GetText() const {
310 return render_text_->text();
313 bool TextfieldViewsModel::SetText(const string16& text) {
314 bool changed = false;
315 if (HasCompositionText()) {
316 ConfirmCompositionText();
319 if (GetText() != text) {
320 if (changed) // No need to remember composition.
322 size_t old_cursor = GetCursorPosition();
323 // SetText moves the cursor to the end.
324 size_t new_cursor = text.length();
326 // If there is a composition text, don't merge with previous edit.
327 // Otherwise, force merge the edits.
328 ExecuteAndRecordReplace(
329 changed ? DO_NOT_MERGE : MERGE_WITH_PREVIOUS,
334 render_text_->SetCursorPosition(new_cursor);
340 void TextfieldViewsModel::Append(const string16& text) {
341 if (HasCompositionText())
342 ConfirmCompositionText();
343 size_t save = GetCursorPosition();
344 MoveCursor(gfx::LINE_BREAK,
345 render_text_->GetVisualDirectionOfLogicalEnd(),
348 render_text_->SetCursorPosition(save);
352 bool TextfieldViewsModel::Delete() {
353 if (HasCompositionText()) {
354 // No undo/redo for composition text.
355 CancelCompositionText();
358 if (HasSelection()) {
362 if (GetText().length() > GetCursorPosition()) {
363 size_t cursor_position = GetCursorPosition();
364 size_t next_grapheme_index = render_text_->IndexOfAdjacentGrapheme(
365 cursor_position, gfx::CURSOR_FORWARD);
366 ExecuteAndRecordDelete(gfx::Range(cursor_position, next_grapheme_index),
373 bool TextfieldViewsModel::Backspace() {
374 if (HasCompositionText()) {
375 // No undo/redo for composition text.
376 CancelCompositionText();
379 if (HasSelection()) {
383 size_t cursor_position = GetCursorPosition();
384 if (cursor_position > 0) {
385 // Delete one code point, which may be two UTF-16 words.
386 size_t previous_char =
387 gfx::UTF16OffsetToIndex(GetText(), cursor_position, -1);
388 ExecuteAndRecordDelete(gfx::Range(cursor_position, previous_char), true);
394 size_t TextfieldViewsModel::GetCursorPosition() const {
395 return render_text_->cursor_position();
398 void TextfieldViewsModel::MoveCursor(gfx::BreakType break_type,
399 gfx::VisualCursorDirection direction,
401 if (HasCompositionText())
402 ConfirmCompositionText();
403 render_text_->MoveCursor(break_type, direction, select);
406 bool TextfieldViewsModel::MoveCursorTo(const gfx::SelectionModel& model) {
407 if (HasCompositionText()) {
408 ConfirmCompositionText();
409 // ConfirmCompositionText() updates cursor position. Need to reflect it in
410 // the SelectionModel parameter of MoveCursorTo().
411 gfx::Range range(render_text_->selection().start(), model.caret_pos());
412 if (!range.is_empty())
413 return render_text_->SelectRange(range);
414 return render_text_->MoveCursorTo(
415 gfx::SelectionModel(model.caret_pos(), model.caret_affinity()));
417 return render_text_->MoveCursorTo(model);
420 bool TextfieldViewsModel::MoveCursorTo(const gfx::Point& point, bool select) {
421 if (HasCompositionText())
422 ConfirmCompositionText();
423 return render_text_->MoveCursorTo(point, select);
426 string16 TextfieldViewsModel::GetSelectedText() const {
427 return GetText().substr(render_text_->selection().GetMin(),
428 render_text_->selection().length());
431 void TextfieldViewsModel::SelectRange(const gfx::Range& range) {
432 if (HasCompositionText())
433 ConfirmCompositionText();
434 render_text_->SelectRange(range);
437 void TextfieldViewsModel::SelectSelectionModel(const gfx::SelectionModel& sel) {
438 if (HasCompositionText())
439 ConfirmCompositionText();
440 render_text_->MoveCursorTo(sel);
443 void TextfieldViewsModel::SelectAll(bool reversed) {
444 if (HasCompositionText())
445 ConfirmCompositionText();
446 render_text_->SelectAll(reversed);
449 void TextfieldViewsModel::SelectWord() {
450 if (HasCompositionText())
451 ConfirmCompositionText();
452 render_text_->SelectWord();
455 void TextfieldViewsModel::ClearSelection() {
456 if (HasCompositionText())
457 ConfirmCompositionText();
458 render_text_->ClearSelection();
461 bool TextfieldViewsModel::CanUndo() {
462 return edit_history_.size() && current_edit_ != edit_history_.end();
465 bool TextfieldViewsModel::CanRedo() {
466 if (!edit_history_.size())
468 // There is no redo iff the current edit is the last element
470 EditHistory::iterator iter = current_edit_;
471 return iter == edit_history_.end() || // at the top.
472 ++iter != edit_history_.end();
475 bool TextfieldViewsModel::Undo() {
478 DCHECK(!HasCompositionText());
479 if (HasCompositionText()) // safe guard for release build.
480 CancelCompositionText();
482 string16 old = GetText();
483 size_t old_cursor = GetCursorPosition();
484 (*current_edit_)->Commit();
485 (*current_edit_)->Undo(this);
487 if (current_edit_ == edit_history_.begin())
488 current_edit_ = edit_history_.end();
491 return old != GetText() || old_cursor != GetCursorPosition();
494 bool TextfieldViewsModel::Redo() {
497 DCHECK(!HasCompositionText());
498 if (HasCompositionText()) // safe guard for release build.
499 CancelCompositionText();
501 if (current_edit_ == edit_history_.end())
502 current_edit_ = edit_history_.begin();
505 string16 old = GetText();
506 size_t old_cursor = GetCursorPosition();
507 (*current_edit_)->Redo(this);
508 return old != GetText() || old_cursor != GetCursorPosition();
511 bool TextfieldViewsModel::Cut() {
512 if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
513 ui::ScopedClipboardWriter(
514 ui::Clipboard::GetForCurrentThread(),
515 ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText());
516 // A trick to let undo/redo handle cursor correctly.
517 // Undoing CUT moves the cursor to the end of the change rather
518 // than beginning, unlike Delete/Backspace.
519 // TODO(oshima): Change Delete/Backspace to use DeleteSelection,
520 // update DeleteEdit and remove this trick.
521 const gfx::Range& selection = render_text_->selection();
522 render_text_->SelectRange(gfx::Range(selection.end(), selection.start()));
529 bool TextfieldViewsModel::Copy() {
530 if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
531 ui::ScopedClipboardWriter(
532 ui::Clipboard::GetForCurrentThread(),
533 ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText());
539 bool TextfieldViewsModel::Paste() {
541 ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE,
543 if (!result.empty()) {
544 InsertTextInternal(result, false);
550 bool TextfieldViewsModel::HasSelection() const {
551 return !render_text_->selection().is_empty();
554 void TextfieldViewsModel::DeleteSelection() {
555 DCHECK(!HasCompositionText());
556 DCHECK(HasSelection());
557 ExecuteAndRecordDelete(render_text_->selection(), false);
560 void TextfieldViewsModel::DeleteSelectionAndInsertTextAt(
561 const string16& text, size_t position) {
562 if (HasCompositionText())
563 CancelCompositionText();
564 ExecuteAndRecordReplace(DO_NOT_MERGE,
566 position + text.length(),
571 string16 TextfieldViewsModel::GetTextFromRange(const gfx::Range& range) const {
572 if (range.IsValid() && range.GetMin() < GetText().length())
573 return GetText().substr(range.GetMin(), range.length());
577 void TextfieldViewsModel::GetTextRange(gfx::Range* range) const {
578 *range = gfx::Range(0, GetText().length());
581 void TextfieldViewsModel::SetCompositionText(
582 const ui::CompositionText& composition) {
583 if (HasCompositionText())
584 CancelCompositionText();
585 else if (HasSelection())
588 if (composition.text.empty())
591 size_t cursor = GetCursorPosition();
592 string16 new_text = GetText();
593 render_text_->SetText(new_text.insert(cursor, composition.text));
594 gfx::Range range(cursor, cursor + composition.text.length());
595 render_text_->SetCompositionRange(range);
596 gfx::Range emphasized_range = GetFirstEmphasizedRange(composition);
597 if (emphasized_range.IsValid()) {
598 // This is a workaround due to the lack of support in RenderText to draw
599 // a thick underline. In a composition returned from an IME, the segment
600 // emphasized by a thick underline usually represents the target clause.
601 // Because the target clause is more important than the actual selection
602 // range (or caret position) in the composition here we use a selection-like
603 // marker instead to show this range.
604 // TODO(yukawa, msw): Support thick underline in RenderText and remove
606 render_text_->SelectRange(gfx::Range(
607 cursor + emphasized_range.GetMin(),
608 cursor + emphasized_range.GetMax()));
609 } else if (!composition.selection.is_empty()) {
610 render_text_->SelectRange(gfx::Range(
611 cursor + composition.selection.GetMin(),
612 cursor + composition.selection.GetMax()));
614 render_text_->SetCursorPosition(cursor + composition.selection.end());
618 void TextfieldViewsModel::ConfirmCompositionText() {
619 DCHECK(HasCompositionText());
620 gfx::Range range = render_text_->GetCompositionRange();
621 string16 text = GetText().substr(range.start(), range.length());
622 // TODO(oshima): current behavior on ChromeOS is a bit weird and not
623 // sure exactly how this should work. Find out and fix if necessary.
624 AddOrMergeEditHistory(new InsertEdit(false, text, range.start()));
625 render_text_->SetCursorPosition(range.end());
628 delegate_->OnCompositionTextConfirmedOrCleared();
631 void TextfieldViewsModel::CancelCompositionText() {
632 DCHECK(HasCompositionText());
633 gfx::Range range = render_text_->GetCompositionRange();
635 string16 new_text = GetText();
636 render_text_->SetText(new_text.erase(range.start(), range.length()));
637 render_text_->SetCursorPosition(range.start());
639 delegate_->OnCompositionTextConfirmedOrCleared();
642 void TextfieldViewsModel::ClearComposition() {
643 render_text_->SetCompositionRange(gfx::Range::InvalidRange());
646 void TextfieldViewsModel::GetCompositionTextRange(gfx::Range* range) const {
647 *range = gfx::Range(render_text_->GetCompositionRange());
650 bool TextfieldViewsModel::HasCompositionText() const {
651 return !render_text_->GetCompositionRange().is_empty();
654 /////////////////////////////////////////////////////////////////
655 // TextfieldViewsModel: private
657 void TextfieldViewsModel::InsertTextInternal(const string16& text,
659 if (HasCompositionText()) {
660 CancelCompositionText();
661 ExecuteAndRecordInsert(text, mergeable);
662 } else if (HasSelection()) {
663 ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE,
666 ExecuteAndRecordInsert(text, mergeable);
670 void TextfieldViewsModel::ReplaceTextInternal(const string16& text,
672 if (HasCompositionText()) {
673 CancelCompositionText();
674 } else if (!HasSelection()) {
675 size_t cursor = GetCursorPosition();
676 const gfx::SelectionModel& model = render_text_->selection_model();
677 // When there is no selection, the default is to replace the next grapheme
678 // with |text|. So, need to find the index of next grapheme first.
680 render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD);
681 if (next == model.caret_pos())
682 render_text_->MoveCursorTo(model);
684 render_text_->SelectRange(gfx::Range(next, model.caret_pos()));
686 // Edit history is recorded in InsertText.
687 InsertTextInternal(text, mergeable);
690 void TextfieldViewsModel::ClearEditHistory() {
691 STLDeleteElements(&edit_history_);
692 current_edit_ = edit_history_.end();
695 void TextfieldViewsModel::ClearRedoHistory() {
696 if (edit_history_.begin() == edit_history_.end())
698 if (current_edit_ == edit_history_.end()) {
702 EditHistory::iterator delete_start = current_edit_;
704 STLDeleteContainerPointers(delete_start, edit_history_.end());
705 edit_history_.erase(delete_start, edit_history_.end());
708 void TextfieldViewsModel::ExecuteAndRecordDelete(gfx::Range range,
710 size_t old_text_start = range.GetMin();
711 const string16 text = GetText().substr(old_text_start, range.length());
712 bool backward = range.is_reversed();
713 Edit* edit = new DeleteEdit(mergeable, text, old_text_start, backward);
714 bool delete_edit = AddOrMergeEditHistory(edit);
720 void TextfieldViewsModel::ExecuteAndRecordReplaceSelection(
721 MergeType merge_type, const string16& new_text) {
722 size_t new_text_start = render_text_->selection().GetMin();
723 size_t new_cursor_pos = new_text_start + new_text.length();
724 ExecuteAndRecordReplace(merge_type,
731 void TextfieldViewsModel::ExecuteAndRecordReplace(MergeType merge_type,
732 size_t old_cursor_pos,
733 size_t new_cursor_pos,
734 const string16& new_text,
735 size_t new_text_start) {
736 size_t old_text_start = render_text_->selection().GetMin();
737 bool backward = render_text_->selection().is_reversed();
738 Edit* edit = new ReplaceEdit(merge_type,
746 bool delete_edit = AddOrMergeEditHistory(edit);
752 void TextfieldViewsModel::ExecuteAndRecordInsert(const string16& text,
754 Edit* edit = new InsertEdit(mergeable, text, GetCursorPosition());
755 bool delete_edit = AddOrMergeEditHistory(edit);
761 bool TextfieldViewsModel::AddOrMergeEditHistory(Edit* edit) {
764 if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) {
765 // If a current edit exists and has been merged with a new edit,
766 // don't add to the history, and return true to delete |edit| after
770 edit_history_.push_back(edit);
771 if (current_edit_ == edit_history_.end()) {
772 // If there is no redoable edit, this is the 1st edit because
773 // RedoHistory has been already deleted.
774 DCHECK_EQ(1u, edit_history_.size());
775 current_edit_ = edit_history_.begin();
782 void TextfieldViewsModel::ModifyText(size_t delete_from,
784 const string16& new_text,
785 size_t new_text_insert_at,
786 size_t new_cursor_pos) {
787 DCHECK_LE(delete_from, delete_to);
788 string16 text = GetText();
790 if (delete_from != delete_to)
791 render_text_->SetText(text.erase(delete_from, delete_to - delete_from));
792 if (!new_text.empty())
793 render_text_->SetText(text.insert(new_text_insert_at, new_text));
794 render_text_->SetCursorPosition(new_cursor_pos);
795 // TODO(oshima): mac selects the text that is just undone (but gtk doesn't).
796 // This looks fine feature and we may want to do the same.