1 // Copyright 2014 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_model.h"
9 #include "base/logging.h"
10 #include "base/stl_util.h"
11 #include "ui/base/clipboard/clipboard.h"
12 #include "ui/base/clipboard/scoped_clipboard_writer.h"
13 #include "ui/gfx/range/range.h"
14 #include "ui/gfx/utf16_indexing.h"
20 // Edit holds state information to undo/redo editing changes. Editing operations
21 // are merged when possible, like when characters are typed in sequence. Calling
22 // Commit() marks an edit as an independent operation that shouldn't be merged.
33 // Revert the change made by this edit in |model|.
34 void Undo(TextfieldModel
* model
) {
35 model
->ModifyText(new_text_start_
, new_text_end(),
36 old_text_
, old_text_start_
,
40 // Apply the change of this edit to the |model|.
41 void Redo(TextfieldModel
* model
) {
42 model
->ModifyText(old_text_start_
, old_text_end(),
43 new_text_
, new_text_start_
,
47 // Try to merge the |edit| into this edit and returns true on success. The
48 // merged edit will be deleted after redo and should not be reused.
49 bool Merge(const Edit
* edit
) {
50 // Don't merge if previous edit is DELETE. This happens when a
51 // user deletes characters then hits return. In this case, the
52 // delete should be treated as separate edit that can be undone
53 // and should not be merged with the replace edit.
54 if (type_
!= DELETE_EDIT
&& edit
->force_merge()) {
58 return mergeable() && edit
->mergeable() && DoMerge(edit
);
61 // Commits the edit and marks as un-mergeable.
62 void Commit() { merge_type_
= DO_NOT_MERGE
; }
65 friend class InsertEdit
;
66 friend class ReplaceEdit
;
67 friend class DeleteEdit
;
71 size_t old_cursor_pos
,
72 const base::string16
& old_text
,
73 size_t old_text_start
,
75 size_t new_cursor_pos
,
76 const base::string16
& new_text
,
77 size_t new_text_start
)
79 merge_type_(merge_type
),
80 old_cursor_pos_(old_cursor_pos
),
82 old_text_start_(old_text_start
),
83 delete_backward_(delete_backward
),
84 new_cursor_pos_(new_cursor_pos
),
86 new_text_start_(new_text_start
) {
89 // Each type of edit provides its own specific merge implementation.
90 virtual bool DoMerge(const Edit
* edit
) = 0;
92 Type
type() const { return type_
; }
94 // Can this edit be merged?
95 bool mergeable() const { return merge_type_
== MERGEABLE
; }
97 // Should this edit be forcibly merged with the previous edit?
98 bool force_merge() const { return merge_type_
== FORCE_MERGE
; }
100 // Returns the end index of the |old_text_|.
101 size_t old_text_end() const { return old_text_start_
+ old_text_
.length(); }
103 // Returns the end index of the |new_text_|.
104 size_t new_text_end() const { return new_text_start_
+ new_text_
.length(); }
106 // Merge the replace edit into the current edit. This handles the special case
107 // where an omnibox autocomplete string is set after a new character is typed.
108 void MergeReplace(const Edit
* edit
) {
109 CHECK_EQ(REPLACE_EDIT
, edit
->type_
);
110 CHECK_EQ(0U, edit
->old_text_start_
);
111 CHECK_EQ(0U, edit
->new_text_start_
);
112 base::string16 old_text
= edit
->old_text_
;
113 old_text
.erase(new_text_start_
, new_text_
.length());
114 old_text
.insert(old_text_start_
, old_text_
);
115 // SetText() replaces entire text. Set |old_text_| to the entire
116 // replaced text with |this| edit undone.
117 old_text_
= old_text
;
118 old_text_start_
= edit
->old_text_start_
;
119 delete_backward_
= false;
121 new_text_
= edit
->new_text_
;
122 new_text_start_
= edit
->new_text_start_
;
123 merge_type_
= DO_NOT_MERGE
;
128 // True if the edit can be marged.
129 MergeType merge_type_
;
130 // Old cursor position.
131 size_t old_cursor_pos_
;
132 // Deleted text by this edit.
133 base::string16 old_text_
;
134 // The index of |old_text_|.
135 size_t old_text_start_
;
136 // True if the deletion is made backward.
137 bool delete_backward_
;
138 // New cursor position.
139 size_t new_cursor_pos_
;
141 base::string16 new_text_
;
142 // The index of |new_text_|
143 size_t new_text_start_
;
145 DISALLOW_COPY_AND_ASSIGN(Edit
);
148 class InsertEdit
: public Edit
{
150 InsertEdit(bool mergeable
, const base::string16
& new_text
, size_t at
)
152 mergeable
? MERGEABLE
: DO_NOT_MERGE
,
157 at
+ new_text
.length() /* new cursor */,
162 // Edit implementation.
163 bool DoMerge(const Edit
* edit
) override
{
164 if (edit
->type() != INSERT_EDIT
|| new_text_end() != edit
->new_text_start_
)
166 // If continuous edit, merge it.
167 // TODO(oshima): gtk splits edits between whitespace. Find out what
168 // we want to here and implement if necessary.
169 new_text_
+= edit
->new_text_
;
170 new_cursor_pos_
= edit
->new_cursor_pos_
;
175 class ReplaceEdit
: public Edit
{
177 ReplaceEdit(MergeType merge_type
,
178 const base::string16
& old_text
,
179 size_t old_cursor_pos
,
180 size_t old_text_start
,
182 size_t new_cursor_pos
,
183 const base::string16
& new_text
,
184 size_t new_text_start
)
185 : Edit(REPLACE_EDIT
, merge_type
,
195 // Edit implementation.
196 bool DoMerge(const Edit
* edit
) override
{
197 if (edit
->type() == DELETE_EDIT
||
198 new_text_end() != edit
->old_text_start_
||
199 edit
->old_text_start_
!= edit
->new_text_start_
)
201 old_text_
+= edit
->old_text_
;
202 new_text_
+= edit
->new_text_
;
203 new_cursor_pos_
= edit
->new_cursor_pos_
;
208 class DeleteEdit
: public Edit
{
210 DeleteEdit(bool mergeable
,
211 const base::string16
& text
,
215 mergeable
? MERGEABLE
: DO_NOT_MERGE
,
216 (backward
? text_start
+ text
.length() : text_start
),
225 // Edit implementation.
226 bool DoMerge(const Edit
* edit
) override
{
227 if (edit
->type() != DELETE_EDIT
)
230 if (delete_backward_
) {
231 // backspace can be merged only with backspace at the same position.
232 if (!edit
->delete_backward_
|| old_text_start_
!= edit
->old_text_end())
234 old_text_start_
= edit
->old_text_start_
;
235 old_text_
= edit
->old_text_
+ old_text_
;
236 new_cursor_pos_
= edit
->new_cursor_pos_
;
238 // delete can be merged only with delete at the same position.
239 if (edit
->delete_backward_
|| old_text_start_
!= edit
->old_text_start_
)
241 old_text_
+= edit
->old_text_
;
247 } // namespace internal
251 // Returns the first segment that is visually emphasized. Usually it's used for
252 // representing the target clause (on Windows). Returns an invalid range if
253 // there is no such a range.
254 gfx::Range
GetFirstEmphasizedRange(const ui::CompositionText
& composition
) {
255 for (size_t i
= 0; i
< composition
.underlines
.size(); ++i
) {
256 const ui::CompositionUnderline
& underline
= composition
.underlines
[i
];
258 return gfx::Range(underline
.start_offset
, underline
.end_offset
);
260 return gfx::Range::InvalidRange();
265 using internal::Edit
;
266 using internal::DeleteEdit
;
267 using internal::InsertEdit
;
268 using internal::ReplaceEdit
;
269 using internal::MergeType
;
270 using internal::DO_NOT_MERGE
;
271 using internal::FORCE_MERGE
;
272 using internal::MERGEABLE
;
274 /////////////////////////////////////////////////////////////////
275 // TextfieldModel: public
277 TextfieldModel::Delegate::~Delegate() {}
279 TextfieldModel::TextfieldModel(Delegate
* delegate
)
280 : delegate_(delegate
),
281 render_text_(gfx::RenderText::CreateInstanceForEditing()),
282 current_edit_(edit_history_
.end()) {
285 TextfieldModel::~TextfieldModel() {
290 bool TextfieldModel::SetText(const base::string16
& new_text
) {
291 bool changed
= false;
292 if (HasCompositionText()) {
293 ConfirmCompositionText();
296 if (text() != new_text
) {
297 if (changed
) // No need to remember composition.
299 size_t old_cursor
= GetCursorPosition();
300 // SetText moves the cursor to the end.
301 size_t new_cursor
= new_text
.length();
303 // If there is a composition text, don't merge with previous edit.
304 // Otherwise, force merge the edits.
305 ExecuteAndRecordReplace(changed
? DO_NOT_MERGE
: FORCE_MERGE
,
306 old_cursor
, new_cursor
, new_text
, 0U);
307 render_text_
->SetCursorPosition(new_cursor
);
313 void TextfieldModel::Append(const base::string16
& new_text
) {
314 if (HasCompositionText())
315 ConfirmCompositionText();
316 size_t save
= GetCursorPosition();
317 MoveCursor(gfx::LINE_BREAK
,
318 render_text_
->GetVisualDirectionOfLogicalEnd(),
320 InsertText(new_text
);
321 render_text_
->SetCursorPosition(save
);
325 bool TextfieldModel::Delete() {
326 if (HasCompositionText()) {
327 // No undo/redo for composition text.
328 CancelCompositionText();
331 if (HasSelection()) {
335 if (text().length() > GetCursorPosition()) {
336 size_t cursor_position
= GetCursorPosition();
337 size_t next_grapheme_index
= render_text_
->IndexOfAdjacentGrapheme(
338 cursor_position
, gfx::CURSOR_FORWARD
);
339 ExecuteAndRecordDelete(gfx::Range(cursor_position
, next_grapheme_index
),
346 bool TextfieldModel::Backspace() {
347 if (HasCompositionText()) {
348 // No undo/redo for composition text.
349 CancelCompositionText();
352 if (HasSelection()) {
356 size_t cursor_position
= GetCursorPosition();
357 if (cursor_position
> 0) {
358 // Delete one code point, which may be two UTF-16 words.
359 size_t previous_char
= gfx::UTF16OffsetToIndex(text(), cursor_position
, -1);
360 ExecuteAndRecordDelete(gfx::Range(cursor_position
, previous_char
), true);
366 size_t TextfieldModel::GetCursorPosition() const {
367 return render_text_
->cursor_position();
370 void TextfieldModel::MoveCursor(gfx::BreakType break_type
,
371 gfx::VisualCursorDirection direction
,
373 if (HasCompositionText())
374 ConfirmCompositionText();
375 render_text_
->MoveCursor(break_type
, direction
, select
);
378 bool TextfieldModel::MoveCursorTo(const gfx::SelectionModel
& cursor
) {
379 if (HasCompositionText()) {
380 ConfirmCompositionText();
381 // ConfirmCompositionText() updates cursor position. Need to reflect it in
382 // the SelectionModel parameter of MoveCursorTo().
383 gfx::Range
range(render_text_
->selection().start(), cursor
.caret_pos());
384 if (!range
.is_empty())
385 return render_text_
->SelectRange(range
);
386 return render_text_
->MoveCursorTo(
387 gfx::SelectionModel(cursor
.caret_pos(), cursor
.caret_affinity()));
389 return render_text_
->MoveCursorTo(cursor
);
392 bool TextfieldModel::MoveCursorTo(const gfx::Point
& point
, bool select
) {
393 if (HasCompositionText())
394 ConfirmCompositionText();
395 gfx::SelectionModel cursor
= render_text_
->FindCursorPosition(point
);
397 cursor
.set_selection_start(render_text_
->selection().start());
398 return render_text_
->MoveCursorTo(cursor
);
401 base::string16
TextfieldModel::GetSelectedText() const {
402 return text().substr(render_text_
->selection().GetMin(),
403 render_text_
->selection().length());
406 void TextfieldModel::SelectRange(const gfx::Range
& range
) {
407 if (HasCompositionText())
408 ConfirmCompositionText();
409 render_text_
->SelectRange(range
);
412 void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel
& sel
) {
413 if (HasCompositionText())
414 ConfirmCompositionText();
415 render_text_
->MoveCursorTo(sel
);
418 void TextfieldModel::SelectAll(bool reversed
) {
419 if (HasCompositionText())
420 ConfirmCompositionText();
421 render_text_
->SelectAll(reversed
);
424 void TextfieldModel::SelectWord() {
425 if (HasCompositionText())
426 ConfirmCompositionText();
427 render_text_
->SelectWord();
430 void TextfieldModel::ClearSelection() {
431 if (HasCompositionText())
432 ConfirmCompositionText();
433 render_text_
->ClearSelection();
436 bool TextfieldModel::CanUndo() {
437 return edit_history_
.size() && current_edit_
!= edit_history_
.end();
440 bool TextfieldModel::CanRedo() {
441 if (!edit_history_
.size())
443 // There is no redo iff the current edit is the last element in the history.
444 EditHistory::iterator iter
= current_edit_
;
445 return iter
== edit_history_
.end() || // at the top.
446 ++iter
!= edit_history_
.end();
449 bool TextfieldModel::Undo() {
452 DCHECK(!HasCompositionText());
453 if (HasCompositionText())
454 CancelCompositionText();
456 base::string16 old
= text();
457 size_t old_cursor
= GetCursorPosition();
458 (*current_edit_
)->Commit();
459 (*current_edit_
)->Undo(this);
461 if (current_edit_
== edit_history_
.begin())
462 current_edit_
= edit_history_
.end();
465 return old
!= text() || old_cursor
!= GetCursorPosition();
468 bool TextfieldModel::Redo() {
471 DCHECK(!HasCompositionText());
472 if (HasCompositionText())
473 CancelCompositionText();
475 if (current_edit_
== edit_history_
.end())
476 current_edit_
= edit_history_
.begin();
479 base::string16 old
= text();
480 size_t old_cursor
= GetCursorPosition();
481 (*current_edit_
)->Redo(this);
482 return old
!= text() || old_cursor
!= GetCursorPosition();
485 bool TextfieldModel::Cut() {
486 if (!HasCompositionText() && HasSelection() && !render_text_
->obscured()) {
487 ui::ScopedClipboardWriter(
488 ui::CLIPBOARD_TYPE_COPY_PASTE
).WriteText(GetSelectedText());
489 // A trick to let undo/redo handle cursor correctly.
490 // Undoing CUT moves the cursor to the end of the change rather
491 // than beginning, unlike Delete/Backspace.
492 // TODO(oshima): Change Delete/Backspace to use DeleteSelection,
493 // update DeleteEdit and remove this trick.
494 const gfx::Range
& selection
= render_text_
->selection();
495 render_text_
->SelectRange(gfx::Range(selection
.end(), selection
.start()));
502 bool TextfieldModel::Copy() {
503 if (!HasCompositionText() && HasSelection() && !render_text_
->obscured()) {
504 ui::ScopedClipboardWriter(
505 ui::CLIPBOARD_TYPE_COPY_PASTE
).WriteText(GetSelectedText());
511 bool TextfieldModel::Paste() {
512 base::string16 result
;
513 ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE
,
518 InsertTextInternal(result
, false);
522 bool TextfieldModel::HasSelection() const {
523 return !render_text_
->selection().is_empty();
526 void TextfieldModel::DeleteSelection() {
527 DCHECK(!HasCompositionText());
528 DCHECK(HasSelection());
529 ExecuteAndRecordDelete(render_text_
->selection(), false);
532 void TextfieldModel::DeleteSelectionAndInsertTextAt(
533 const base::string16
& new_text
,
535 if (HasCompositionText())
536 CancelCompositionText();
537 ExecuteAndRecordReplace(DO_NOT_MERGE
,
539 position
+ new_text
.length(),
544 base::string16
TextfieldModel::GetTextFromRange(const gfx::Range
& range
) const {
545 if (range
.IsValid() && range
.GetMin() < text().length())
546 return text().substr(range
.GetMin(), range
.length());
547 return base::string16();
550 void TextfieldModel::GetTextRange(gfx::Range
* range
) const {
551 *range
= gfx::Range(0, text().length());
554 void TextfieldModel::SetCompositionText(
555 const ui::CompositionText
& composition
) {
556 if (HasCompositionText())
557 CancelCompositionText();
558 else if (HasSelection())
561 if (composition
.text
.empty())
564 size_t cursor
= GetCursorPosition();
565 base::string16 new_text
= text();
566 render_text_
->SetText(new_text
.insert(cursor
, composition
.text
));
567 composition_range_
= gfx::Range(cursor
, cursor
+ composition
.text
.length());
568 // Don't render transparent composition underlines.
569 if (composition
.underlines
.size() > 0 && composition
.underlines
[0].color
!= 0)
570 render_text_
->SetCompositionRange(composition_range_
);
572 render_text_
->SetCompositionRange(gfx::Range::InvalidRange());
573 gfx::Range emphasized_range
= GetFirstEmphasizedRange(composition
);
574 if (emphasized_range
.IsValid()) {
575 // This is a workaround due to the lack of support in RenderText to draw
576 // a thick underline. In a composition returned from an IME, the segment
577 // emphasized by a thick underline usually represents the target clause.
578 // Because the target clause is more important than the actual selection
579 // range (or caret position) in the composition here we use a selection-like
580 // marker instead to show this range.
581 // TODO(yukawa, msw): Support thick underlines and remove this workaround.
582 render_text_
->SelectRange(gfx::Range(
583 cursor
+ emphasized_range
.GetMin(),
584 cursor
+ emphasized_range
.GetMax()));
585 } else if (!composition
.selection
.is_empty()) {
586 render_text_
->SelectRange(gfx::Range(
587 cursor
+ composition
.selection
.GetMin(),
588 cursor
+ composition
.selection
.GetMax()));
590 render_text_
->SetCursorPosition(cursor
+ composition
.selection
.end());
594 void TextfieldModel::ConfirmCompositionText() {
595 DCHECK(HasCompositionText());
596 base::string16 composition
= text().substr(
597 composition_range_
.start(), composition_range_
.length());
598 // TODO(oshima): current behavior on ChromeOS is a bit weird and not
599 // sure exactly how this should work. Find out and fix if necessary.
600 AddOrMergeEditHistory(
601 new InsertEdit(false, composition
, composition_range_
.start()));
602 render_text_
->SetCursorPosition(composition_range_
.end());
605 delegate_
->OnCompositionTextConfirmedOrCleared();
608 void TextfieldModel::CancelCompositionText() {
609 DCHECK(HasCompositionText());
610 gfx::Range range
= composition_range_
;
612 base::string16 new_text
= text();
613 render_text_
->SetText(new_text
.erase(range
.start(), range
.length()));
614 render_text_
->SetCursorPosition(range
.start());
616 delegate_
->OnCompositionTextConfirmedOrCleared();
619 void TextfieldModel::ClearComposition() {
620 composition_range_
= gfx::Range::InvalidRange();
621 render_text_
->SetCompositionRange(composition_range_
);
624 void TextfieldModel::GetCompositionTextRange(gfx::Range
* range
) const {
625 *range
= composition_range_
;
628 bool TextfieldModel::HasCompositionText() const {
629 return !composition_range_
.is_empty();
632 void TextfieldModel::ClearEditHistory() {
633 STLDeleteElements(&edit_history_
);
634 current_edit_
= edit_history_
.end();
637 /////////////////////////////////////////////////////////////////
638 // TextfieldModel: private
640 void TextfieldModel::InsertTextInternal(const base::string16
& new_text
,
642 if (HasCompositionText()) {
643 CancelCompositionText();
644 ExecuteAndRecordInsert(new_text
, mergeable
);
645 } else if (HasSelection()) {
646 ExecuteAndRecordReplaceSelection(mergeable
? MERGEABLE
: DO_NOT_MERGE
,
649 ExecuteAndRecordInsert(new_text
, mergeable
);
653 void TextfieldModel::ReplaceTextInternal(const base::string16
& new_text
,
655 if (HasCompositionText()) {
656 CancelCompositionText();
657 } else if (!HasSelection()) {
658 size_t cursor
= GetCursorPosition();
659 const gfx::SelectionModel
& model
= render_text_
->selection_model();
660 // When there is no selection, the default is to replace the next grapheme
661 // with |new_text|. So, need to find the index of next grapheme first.
663 render_text_
->IndexOfAdjacentGrapheme(cursor
, gfx::CURSOR_FORWARD
);
664 if (next
== model
.caret_pos())
665 render_text_
->MoveCursorTo(model
);
667 render_text_
->SelectRange(gfx::Range(next
, model
.caret_pos()));
669 // Edit history is recorded in InsertText.
670 InsertTextInternal(new_text
, mergeable
);
673 void TextfieldModel::ClearRedoHistory() {
674 if (edit_history_
.begin() == edit_history_
.end())
676 if (current_edit_
== edit_history_
.end()) {
680 EditHistory::iterator delete_start
= current_edit_
;
682 STLDeleteContainerPointers(delete_start
, edit_history_
.end());
683 edit_history_
.erase(delete_start
, edit_history_
.end());
686 void TextfieldModel::ExecuteAndRecordDelete(gfx::Range range
, bool mergeable
) {
687 size_t old_text_start
= range
.GetMin();
688 const base::string16 old_text
= text().substr(old_text_start
, range
.length());
689 bool backward
= range
.is_reversed();
690 Edit
* edit
= new DeleteEdit(mergeable
, old_text
, old_text_start
, backward
);
691 bool delete_edit
= AddOrMergeEditHistory(edit
);
697 void TextfieldModel::ExecuteAndRecordReplaceSelection(
698 MergeType merge_type
,
699 const base::string16
& new_text
) {
700 size_t new_text_start
= render_text_
->selection().GetMin();
701 size_t new_cursor_pos
= new_text_start
+ new_text
.length();
702 ExecuteAndRecordReplace(merge_type
,
709 void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type
,
710 size_t old_cursor_pos
,
711 size_t new_cursor_pos
,
712 const base::string16
& new_text
,
713 size_t new_text_start
) {
714 size_t old_text_start
= render_text_
->selection().GetMin();
715 bool backward
= render_text_
->selection().is_reversed();
716 Edit
* edit
= new ReplaceEdit(merge_type
,
724 bool delete_edit
= AddOrMergeEditHistory(edit
);
730 void TextfieldModel::ExecuteAndRecordInsert(const base::string16
& new_text
,
732 Edit
* edit
= new InsertEdit(mergeable
, new_text
, GetCursorPosition());
733 bool delete_edit
= AddOrMergeEditHistory(edit
);
739 bool TextfieldModel::AddOrMergeEditHistory(Edit
* edit
) {
742 if (current_edit_
!= edit_history_
.end() && (*current_edit_
)->Merge(edit
)) {
743 // If a current edit exists and has been merged with a new edit, don't add
744 // to the history, and return true to delete |edit| after redo.
747 edit_history_
.push_back(edit
);
748 if (current_edit_
== edit_history_
.end()) {
749 // If there is no redoable edit, this is the 1st edit because RedoHistory
750 // has been already deleted.
751 DCHECK_EQ(1u, edit_history_
.size());
752 current_edit_
= edit_history_
.begin();
759 void TextfieldModel::ModifyText(size_t delete_from
,
761 const base::string16
& new_text
,
762 size_t new_text_insert_at
,
763 size_t new_cursor_pos
) {
764 DCHECK_LE(delete_from
, delete_to
);
765 base::string16 old_text
= text();
767 if (delete_from
!= delete_to
)
768 render_text_
->SetText(old_text
.erase(delete_from
, delete_to
- delete_from
));
769 if (!new_text
.empty())
770 render_text_
->SetText(old_text
.insert(new_text_insert_at
, new_text
));
771 render_text_
->SetCursorPosition(new_cursor_pos
);
772 // TODO(oshima): Select text that was just undone, like Mac (but not GTK).