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. 4 5#include "ui/views/controls/textfield/textfield_model.h" 6 7#include <algorithm> 8 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" 15 16namespace views { 17 18namespace internal { 19 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. 23class Edit { 24 public: 25 enum Type { 26 INSERT_EDIT, 27 DELETE_EDIT, 28 REPLACE_EDIT, 29 }; 30 31 virtual ~Edit() {} 32 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_, 37 old_cursor_pos_); 38 } 39 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_, 44 new_cursor_pos_); 45 } 46 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()) { 55 MergeReplace(edit); 56 return true; 57 } 58 return mergeable() && edit->mergeable() && DoMerge(edit); 59 } 60 61 // Commits the edit and marks as un-mergeable. 62 void Commit() { merge_type_ = DO_NOT_MERGE; } 63 64 private: 65 friend class InsertEdit; 66 friend class ReplaceEdit; 67 friend class DeleteEdit; 68 69 Edit(Type type, 70 MergeType merge_type, 71 size_t old_cursor_pos, 72 const base::string16& old_text, 73 size_t old_text_start, 74 bool delete_backward, 75 size_t new_cursor_pos, 76 const base::string16& new_text, 77 size_t new_text_start) 78 : type_(type), 79 merge_type_(merge_type), 80 old_cursor_pos_(old_cursor_pos), 81 old_text_(old_text), 82 old_text_start_(old_text_start), 83 delete_backward_(delete_backward), 84 new_cursor_pos_(new_cursor_pos), 85 new_text_(new_text), 86 new_text_start_(new_text_start) { 87 } 88 89 // Each type of edit provides its own specific merge implementation. 90 virtual bool DoMerge(const Edit* edit) = 0; 91 92 Type type() const { return type_; } 93 94 // Can this edit be merged? 95 bool mergeable() const { return merge_type_ == MERGEABLE; } 96 97 // Should this edit be forcibly merged with the previous edit? 98 bool force_merge() const { return merge_type_ == FORCE_MERGE; } 99 100 // Returns the end index of the |old_text_|. 101 size_t old_text_end() const { return old_text_start_ + old_text_.length(); } 102 103 // Returns the end index of the |new_text_|. 104 size_t new_text_end() const { return new_text_start_ + new_text_.length(); } 105 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; 120 121 new_text_ = edit->new_text_; 122 new_text_start_ = edit->new_text_start_; 123 merge_type_ = DO_NOT_MERGE; 124 } 125 126 Type type_; 127 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_; 140 // Added text. 141 base::string16 new_text_; 142 // The index of |new_text_| 143 size_t new_text_start_; 144 145 DISALLOW_COPY_AND_ASSIGN(Edit); 146}; 147 148class InsertEdit : public Edit { 149 public: 150 InsertEdit(bool mergeable, const base::string16& new_text, size_t at) 151 : Edit(INSERT_EDIT, 152 mergeable ? MERGEABLE : DO_NOT_MERGE, 153 at /* old cursor */, 154 base::string16(), 155 at, 156 false /* N/A */, 157 at + new_text.length() /* new cursor */, 158 new_text, 159 at) { 160 } 161 162 // Edit implementation. 163 virtual bool DoMerge(const Edit* edit) OVERRIDE { 164 if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_) 165 return false; 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_; 171 return true; 172 } 173}; 174 175class ReplaceEdit : public Edit { 176 public: 177 ReplaceEdit(MergeType merge_type, 178 const base::string16& old_text, 179 size_t old_cursor_pos, 180 size_t old_text_start, 181 bool backward, 182 size_t new_cursor_pos, 183 const base::string16& new_text, 184 size_t new_text_start) 185 : Edit(REPLACE_EDIT, merge_type, 186 old_cursor_pos, 187 old_text, 188 old_text_start, 189 backward, 190 new_cursor_pos, 191 new_text, 192 new_text_start) { 193 } 194 195 // Edit implementation. 196 virtual 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_) 200 return false; 201 old_text_ += edit->old_text_; 202 new_text_ += edit->new_text_; 203 new_cursor_pos_ = edit->new_cursor_pos_; 204 return true; 205 } 206}; 207 208class DeleteEdit : public Edit { 209 public: 210 DeleteEdit(bool mergeable, 211 const base::string16& text, 212 size_t text_start, 213 bool backward) 214 : Edit(DELETE_EDIT, 215 mergeable ? MERGEABLE : DO_NOT_MERGE, 216 (backward ? text_start + text.length() : text_start), 217 text, 218 text_start, 219 backward, 220 text_start, 221 base::string16(), 222 text_start) { 223 } 224 225 // Edit implementation. 226 virtual bool DoMerge(const Edit* edit) OVERRIDE { 227 if (edit->type() != DELETE_EDIT) 228 return false; 229 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()) 233 return false; 234 old_text_start_ = edit->old_text_start_; 235 old_text_ = edit->old_text_ + old_text_; 236 new_cursor_pos_ = edit->new_cursor_pos_; 237 } else { 238 // delete can be merged only with delete at the same position. 239 if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_) 240 return false; 241 old_text_ += edit->old_text_; 242 } 243 return true; 244 } 245}; 246 247} // namespace internal 248 249namespace { 250 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. 254gfx::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]; 257 if (underline.thick) 258 return gfx::Range(underline.start_offset, underline.end_offset); 259 } 260 return gfx::Range::InvalidRange(); 261} 262 263} // namespace 264 265using internal::Edit; 266using internal::DeleteEdit; 267using internal::InsertEdit; 268using internal::ReplaceEdit; 269using internal::MergeType; 270using internal::DO_NOT_MERGE; 271using internal::FORCE_MERGE; 272using internal::MERGEABLE; 273 274///////////////////////////////////////////////////////////////// 275// TextfieldModel: public 276 277TextfieldModel::Delegate::~Delegate() {} 278 279TextfieldModel::TextfieldModel(Delegate* delegate) 280 : delegate_(delegate), 281 render_text_(gfx::RenderText::CreateInstance()), 282 current_edit_(edit_history_.end()) { 283} 284 285TextfieldModel::~TextfieldModel() { 286 ClearEditHistory(); 287 ClearComposition(); 288} 289 290bool TextfieldModel::SetText(const base::string16& new_text) { 291 bool changed = false; 292 if (HasCompositionText()) { 293 ConfirmCompositionText(); 294 changed = true; 295 } 296 if (text() != new_text) { 297 if (changed) // No need to remember composition. 298 Undo(); 299 size_t old_cursor = GetCursorPosition(); 300 // SetText moves the cursor to the end. 301 size_t new_cursor = new_text.length(); 302 SelectAll(false); 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); 308 } 309 ClearSelection(); 310 return changed; 311} 312 313void 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(), 319 false); 320 InsertText(new_text); 321 render_text_->SetCursorPosition(save); 322 ClearSelection(); 323} 324 325bool TextfieldModel::Delete() { 326 if (HasCompositionText()) { 327 // No undo/redo for composition text. 328 CancelCompositionText(); 329 return true; 330 } 331 if (HasSelection()) { 332 DeleteSelection(); 333 return true; 334 } 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), 340 true); 341 return true; 342 } 343 return false; 344} 345 346bool TextfieldModel::Backspace() { 347 if (HasCompositionText()) { 348 // No undo/redo for composition text. 349 CancelCompositionText(); 350 return true; 351 } 352 if (HasSelection()) { 353 DeleteSelection(); 354 return true; 355 } 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); 361 return true; 362 } 363 return false; 364} 365 366size_t TextfieldModel::GetCursorPosition() const { 367 return render_text_->cursor_position(); 368} 369 370void TextfieldModel::MoveCursor(gfx::BreakType break_type, 371 gfx::VisualCursorDirection direction, 372 bool select) { 373 if (HasCompositionText()) 374 ConfirmCompositionText(); 375 render_text_->MoveCursor(break_type, direction, select); 376} 377 378bool 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())); 388 } 389 return render_text_->MoveCursorTo(cursor); 390} 391 392bool TextfieldModel::MoveCursorTo(const gfx::Point& point, bool select) { 393 if (HasCompositionText()) 394 ConfirmCompositionText(); 395 gfx::SelectionModel cursor = render_text_->FindCursorPosition(point); 396 if (select) 397 cursor.set_selection_start(render_text_->selection().start()); 398 return render_text_->MoveCursorTo(cursor); 399} 400 401base::string16 TextfieldModel::GetSelectedText() const { 402 return text().substr(render_text_->selection().GetMin(), 403 render_text_->selection().length()); 404} 405 406void TextfieldModel::SelectRange(const gfx::Range& range) { 407 if (HasCompositionText()) 408 ConfirmCompositionText(); 409 render_text_->SelectRange(range); 410} 411 412void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel& sel) { 413 if (HasCompositionText()) 414 ConfirmCompositionText(); 415 render_text_->MoveCursorTo(sel); 416} 417 418void TextfieldModel::SelectAll(bool reversed) { 419 if (HasCompositionText()) 420 ConfirmCompositionText(); 421 render_text_->SelectAll(reversed); 422} 423 424void TextfieldModel::SelectWord() { 425 if (HasCompositionText()) 426 ConfirmCompositionText(); 427 render_text_->SelectWord(); 428} 429 430void TextfieldModel::ClearSelection() { 431 if (HasCompositionText()) 432 ConfirmCompositionText(); 433 render_text_->ClearSelection(); 434} 435 436bool TextfieldModel::CanUndo() { 437 return edit_history_.size() && current_edit_ != edit_history_.end(); 438} 439 440bool TextfieldModel::CanRedo() { 441 if (!edit_history_.size()) 442 return false; 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(); 447} 448 449bool TextfieldModel::Undo() { 450 if (!CanUndo()) 451 return false; 452 DCHECK(!HasCompositionText()); 453 if (HasCompositionText()) 454 CancelCompositionText(); 455 456 base::string16 old = text(); 457 size_t old_cursor = GetCursorPosition(); 458 (*current_edit_)->Commit(); 459 (*current_edit_)->Undo(this); 460 461 if (current_edit_ == edit_history_.begin()) 462 current_edit_ = edit_history_.end(); 463 else 464 current_edit_--; 465 return old != text() || old_cursor != GetCursorPosition(); 466} 467 468bool TextfieldModel::Redo() { 469 if (!CanRedo()) 470 return false; 471 DCHECK(!HasCompositionText()); 472 if (HasCompositionText()) 473 CancelCompositionText(); 474 475 if (current_edit_ == edit_history_.end()) 476 current_edit_ = edit_history_.begin(); 477 else 478 current_edit_ ++; 479 base::string16 old = text(); 480 size_t old_cursor = GetCursorPosition(); 481 (*current_edit_)->Redo(this); 482 return old != text() || old_cursor != GetCursorPosition(); 483} 484 485bool 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())); 496 DeleteSelection(); 497 return true; 498 } 499 return false; 500} 501 502bool TextfieldModel::Copy() { 503 if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) { 504 ui::ScopedClipboardWriter( 505 ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText()); 506 return true; 507 } 508 return false; 509} 510 511bool TextfieldModel::Paste() { 512 base::string16 result; 513 ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, 514 &result); 515 if (result.empty()) 516 return false; 517 518 InsertTextInternal(result, false); 519 return true; 520} 521 522bool TextfieldModel::HasSelection() const { 523 return !render_text_->selection().is_empty(); 524} 525 526void TextfieldModel::DeleteSelection() { 527 DCHECK(!HasCompositionText()); 528 DCHECK(HasSelection()); 529 ExecuteAndRecordDelete(render_text_->selection(), false); 530} 531 532void TextfieldModel::DeleteSelectionAndInsertTextAt( 533 const base::string16& new_text, 534 size_t position) { 535 if (HasCompositionText()) 536 CancelCompositionText(); 537 ExecuteAndRecordReplace(DO_NOT_MERGE, 538 GetCursorPosition(), 539 position + new_text.length(), 540 new_text, 541 position); 542} 543 544base::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(); 548} 549 550void TextfieldModel::GetTextRange(gfx::Range* range) const { 551 *range = gfx::Range(0, text().length()); 552} 553 554void TextfieldModel::SetCompositionText( 555 const ui::CompositionText& composition) { 556 if (HasCompositionText()) 557 CancelCompositionText(); 558 else if (HasSelection()) 559 DeleteSelection(); 560 561 if (composition.text.empty()) 562 return; 563 564 size_t cursor = GetCursorPosition(); 565 base::string16 new_text = text(); 566 render_text_->SetText(new_text.insert(cursor, composition.text)); 567 gfx::Range range(cursor, cursor + composition.text.length()); 568 render_text_->SetCompositionRange(range); 569 gfx::Range emphasized_range = GetFirstEmphasizedRange(composition); 570 if (emphasized_range.IsValid()) { 571 // This is a workaround due to the lack of support in RenderText to draw 572 // a thick underline. In a composition returned from an IME, the segment 573 // emphasized by a thick underline usually represents the target clause. 574 // Because the target clause is more important than the actual selection 575 // range (or caret position) in the composition here we use a selection-like 576 // marker instead to show this range. 577 // TODO(yukawa, msw): Support thick underlines and remove this workaround. 578 render_text_->SelectRange(gfx::Range( 579 cursor + emphasized_range.GetMin(), 580 cursor + emphasized_range.GetMax())); 581 } else if (!composition.selection.is_empty()) { 582 render_text_->SelectRange(gfx::Range( 583 cursor + composition.selection.GetMin(), 584 cursor + composition.selection.GetMax())); 585 } else { 586 render_text_->SetCursorPosition(cursor + composition.selection.end()); 587 } 588} 589 590void TextfieldModel::ConfirmCompositionText() { 591 DCHECK(HasCompositionText()); 592 gfx::Range range = render_text_->GetCompositionRange(); 593 base::string16 composition = text().substr(range.start(), range.length()); 594 // TODO(oshima): current behavior on ChromeOS is a bit weird and not 595 // sure exactly how this should work. Find out and fix if necessary. 596 AddOrMergeEditHistory(new InsertEdit(false, composition, range.start())); 597 render_text_->SetCursorPosition(range.end()); 598 ClearComposition(); 599 if (delegate_) 600 delegate_->OnCompositionTextConfirmedOrCleared(); 601} 602 603void TextfieldModel::CancelCompositionText() { 604 DCHECK(HasCompositionText()); 605 gfx::Range range = render_text_->GetCompositionRange(); 606 ClearComposition(); 607 base::string16 new_text = text(); 608 render_text_->SetText(new_text.erase(range.start(), range.length())); 609 render_text_->SetCursorPosition(range.start()); 610 if (delegate_) 611 delegate_->OnCompositionTextConfirmedOrCleared(); 612} 613 614void TextfieldModel::ClearComposition() { 615 render_text_->SetCompositionRange(gfx::Range::InvalidRange()); 616} 617 618void TextfieldModel::GetCompositionTextRange(gfx::Range* range) const { 619 *range = gfx::Range(render_text_->GetCompositionRange()); 620} 621 622bool TextfieldModel::HasCompositionText() const { 623 return !render_text_->GetCompositionRange().is_empty(); 624} 625 626void TextfieldModel::ClearEditHistory() { 627 STLDeleteElements(&edit_history_); 628 current_edit_ = edit_history_.end(); 629} 630 631///////////////////////////////////////////////////////////////// 632// TextfieldModel: private 633 634void TextfieldModel::InsertTextInternal(const base::string16& new_text, 635 bool mergeable) { 636 if (HasCompositionText()) { 637 CancelCompositionText(); 638 ExecuteAndRecordInsert(new_text, mergeable); 639 } else if (HasSelection()) { 640 ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE, 641 new_text); 642 } else { 643 ExecuteAndRecordInsert(new_text, mergeable); 644 } 645} 646 647void TextfieldModel::ReplaceTextInternal(const base::string16& new_text, 648 bool mergeable) { 649 if (HasCompositionText()) { 650 CancelCompositionText(); 651 } else if (!HasSelection()) { 652 size_t cursor = GetCursorPosition(); 653 const gfx::SelectionModel& model = render_text_->selection_model(); 654 // When there is no selection, the default is to replace the next grapheme 655 // with |new_text|. So, need to find the index of next grapheme first. 656 size_t next = 657 render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD); 658 if (next == model.caret_pos()) 659 render_text_->MoveCursorTo(model); 660 else 661 render_text_->SelectRange(gfx::Range(next, model.caret_pos())); 662 } 663 // Edit history is recorded in InsertText. 664 InsertTextInternal(new_text, mergeable); 665} 666 667void TextfieldModel::ClearRedoHistory() { 668 if (edit_history_.begin() == edit_history_.end()) 669 return; 670 if (current_edit_ == edit_history_.end()) { 671 ClearEditHistory(); 672 return; 673 } 674 EditHistory::iterator delete_start = current_edit_; 675 delete_start++; 676 STLDeleteContainerPointers(delete_start, edit_history_.end()); 677 edit_history_.erase(delete_start, edit_history_.end()); 678} 679 680void TextfieldModel::ExecuteAndRecordDelete(gfx::Range range, bool mergeable) { 681 size_t old_text_start = range.GetMin(); 682 const base::string16 old_text = text().substr(old_text_start, range.length()); 683 bool backward = range.is_reversed(); 684 Edit* edit = new DeleteEdit(mergeable, old_text, old_text_start, backward); 685 bool delete_edit = AddOrMergeEditHistory(edit); 686 edit->Redo(this); 687 if (delete_edit) 688 delete edit; 689} 690 691void TextfieldModel::ExecuteAndRecordReplaceSelection( 692 MergeType merge_type, 693 const base::string16& new_text) { 694 size_t new_text_start = render_text_->selection().GetMin(); 695 size_t new_cursor_pos = new_text_start + new_text.length(); 696 ExecuteAndRecordReplace(merge_type, 697 GetCursorPosition(), 698 new_cursor_pos, 699 new_text, 700 new_text_start); 701} 702 703void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type, 704 size_t old_cursor_pos, 705 size_t new_cursor_pos, 706 const base::string16& new_text, 707 size_t new_text_start) { 708 size_t old_text_start = render_text_->selection().GetMin(); 709 bool backward = render_text_->selection().is_reversed(); 710 Edit* edit = new ReplaceEdit(merge_type, 711 GetSelectedText(), 712 old_cursor_pos, 713 old_text_start, 714 backward, 715 new_cursor_pos, 716 new_text, 717 new_text_start); 718 bool delete_edit = AddOrMergeEditHistory(edit); 719 edit->Redo(this); 720 if (delete_edit) 721 delete edit; 722} 723 724void TextfieldModel::ExecuteAndRecordInsert(const base::string16& new_text, 725 bool mergeable) { 726 Edit* edit = new InsertEdit(mergeable, new_text, GetCursorPosition()); 727 bool delete_edit = AddOrMergeEditHistory(edit); 728 edit->Redo(this); 729 if (delete_edit) 730 delete edit; 731} 732 733bool TextfieldModel::AddOrMergeEditHistory(Edit* edit) { 734 ClearRedoHistory(); 735 736 if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) { 737 // If a current edit exists and has been merged with a new edit, don't add 738 // to the history, and return true to delete |edit| after redo. 739 return true; 740 } 741 edit_history_.push_back(edit); 742 if (current_edit_ == edit_history_.end()) { 743 // If there is no redoable edit, this is the 1st edit because RedoHistory 744 // has been already deleted. 745 DCHECK_EQ(1u, edit_history_.size()); 746 current_edit_ = edit_history_.begin(); 747 } else { 748 current_edit_++; 749 } 750 return false; 751} 752 753void TextfieldModel::ModifyText(size_t delete_from, 754 size_t delete_to, 755 const base::string16& new_text, 756 size_t new_text_insert_at, 757 size_t new_cursor_pos) { 758 DCHECK_LE(delete_from, delete_to); 759 base::string16 old_text = text(); 760 ClearComposition(); 761 if (delete_from != delete_to) 762 render_text_->SetText(old_text.erase(delete_from, delete_to - delete_from)); 763 if (!new_text.empty()) 764 render_text_->SetText(old_text.insert(new_text_insert_at, new_text)); 765 render_text_->SetCursorPosition(new_cursor_pos); 766 // TODO(oshima): Select text that was just undone, like Mac (but not GTK). 767} 768 769} // namespace views 770