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 16 namespace views { 17 18 namespace 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. 23 class 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 148 class 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 175 class 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 208 class 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 249 namespace { 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. 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]; 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 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; 273 274 ///////////////////////////////////////////////////////////////// 275 // TextfieldModel: public 276 277 TextfieldModel::Delegate::~Delegate() {} 278 279 TextfieldModel::TextfieldModel(Delegate* delegate) 280 : delegate_(delegate), 281 render_text_(gfx::RenderText::CreateInstance()), 282 current_edit_(edit_history_.end()) { 283 } 284 285 TextfieldModel::~TextfieldModel() { 286 ClearEditHistory(); 287 ClearComposition(); 288 } 289 290 bool 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 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(), 319 false); 320 InsertText(new_text); 321 render_text_->SetCursorPosition(save); 322 ClearSelection(); 323 } 324 325 bool 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 346 bool 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 366 size_t TextfieldModel::GetCursorPosition() const { 367 return render_text_->cursor_position(); 368 } 369 370 void 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 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())); 388 } 389 return render_text_->MoveCursorTo(cursor); 390 } 391 392 bool 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 401 base::string16 TextfieldModel::GetSelectedText() const { 402 return text().substr(render_text_->selection().GetMin(), 403 render_text_->selection().length()); 404 } 405 406 void TextfieldModel::SelectRange(const gfx::Range& range) { 407 if (HasCompositionText()) 408 ConfirmCompositionText(); 409 render_text_->SelectRange(range); 410 } 411 412 void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel& sel) { 413 if (HasCompositionText()) 414 ConfirmCompositionText(); 415 render_text_->MoveCursorTo(sel); 416 } 417 418 void TextfieldModel::SelectAll(bool reversed) { 419 if (HasCompositionText()) 420 ConfirmCompositionText(); 421 render_text_->SelectAll(reversed); 422 } 423 424 void TextfieldModel::SelectWord() { 425 if (HasCompositionText()) 426 ConfirmCompositionText(); 427 render_text_->SelectWord(); 428 } 429 430 void TextfieldModel::ClearSelection() { 431 if (HasCompositionText()) 432 ConfirmCompositionText(); 433 render_text_->ClearSelection(); 434 } 435 436 bool TextfieldModel::CanUndo() { 437 return edit_history_.size() && current_edit_ != edit_history_.end(); 438 } 439 440 bool 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 449 bool 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 468 bool 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 485 bool TextfieldModel::Cut() { 486 if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) { 487 ui::ScopedClipboardWriter( 488 ui::Clipboard::GetForCurrentThread(), 489 ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText()); 490 // A trick to let undo/redo handle cursor correctly. 491 // Undoing CUT moves the cursor to the end of the change rather 492 // than beginning, unlike Delete/Backspace. 493 // TODO(oshima): Change Delete/Backspace to use DeleteSelection, 494 // update DeleteEdit and remove this trick. 495 const gfx::Range& selection = render_text_->selection(); 496 render_text_->SelectRange(gfx::Range(selection.end(), selection.start())); 497 DeleteSelection(); 498 return true; 499 } 500 return false; 501 } 502 503 bool TextfieldModel::Copy() { 504 if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) { 505 ui::ScopedClipboardWriter( 506 ui::Clipboard::GetForCurrentThread(), 507 ui::CLIPBOARD_TYPE_COPY_PASTE).WriteText(GetSelectedText()); 508 return true; 509 } 510 return false; 511 } 512 513 bool TextfieldModel::Paste() { 514 base::string16 result; 515 ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, 516 &result); 517 if (result.empty()) 518 return false; 519 520 InsertTextInternal(result, false); 521 return true; 522 } 523 524 bool TextfieldModel::HasSelection() const { 525 return !render_text_->selection().is_empty(); 526 } 527 528 void TextfieldModel::DeleteSelection() { 529 DCHECK(!HasCompositionText()); 530 DCHECK(HasSelection()); 531 ExecuteAndRecordDelete(render_text_->selection(), false); 532 } 533 534 void TextfieldModel::DeleteSelectionAndInsertTextAt( 535 const base::string16& new_text, 536 size_t position) { 537 if (HasCompositionText()) 538 CancelCompositionText(); 539 ExecuteAndRecordReplace(DO_NOT_MERGE, 540 GetCursorPosition(), 541 position + new_text.length(), 542 new_text, 543 position); 544 } 545 546 base::string16 TextfieldModel::GetTextFromRange(const gfx::Range& range) const { 547 if (range.IsValid() && range.GetMin() < text().length()) 548 return text().substr(range.GetMin(), range.length()); 549 return base::string16(); 550 } 551 552 void TextfieldModel::GetTextRange(gfx::Range* range) const { 553 *range = gfx::Range(0, text().length()); 554 } 555 556 void TextfieldModel::SetCompositionText( 557 const ui::CompositionText& composition) { 558 if (HasCompositionText()) 559 CancelCompositionText(); 560 else if (HasSelection()) 561 DeleteSelection(); 562 563 if (composition.text.empty()) 564 return; 565 566 size_t cursor = GetCursorPosition(); 567 base::string16 new_text = text(); 568 render_text_->SetText(new_text.insert(cursor, composition.text)); 569 gfx::Range range(cursor, cursor + composition.text.length()); 570 render_text_->SetCompositionRange(range); 571 gfx::Range emphasized_range = GetFirstEmphasizedRange(composition); 572 if (emphasized_range.IsValid()) { 573 // This is a workaround due to the lack of support in RenderText to draw 574 // a thick underline. In a composition returned from an IME, the segment 575 // emphasized by a thick underline usually represents the target clause. 576 // Because the target clause is more important than the actual selection 577 // range (or caret position) in the composition here we use a selection-like 578 // marker instead to show this range. 579 // TODO(yukawa, msw): Support thick underlines and remove this workaround. 580 render_text_->SelectRange(gfx::Range( 581 cursor + emphasized_range.GetMin(), 582 cursor + emphasized_range.GetMax())); 583 } else if (!composition.selection.is_empty()) { 584 render_text_->SelectRange(gfx::Range( 585 cursor + composition.selection.GetMin(), 586 cursor + composition.selection.GetMax())); 587 } else { 588 render_text_->SetCursorPosition(cursor + composition.selection.end()); 589 } 590 } 591 592 void TextfieldModel::ConfirmCompositionText() { 593 DCHECK(HasCompositionText()); 594 gfx::Range range = render_text_->GetCompositionRange(); 595 base::string16 composition = text().substr(range.start(), range.length()); 596 // TODO(oshima): current behavior on ChromeOS is a bit weird and not 597 // sure exactly how this should work. Find out and fix if necessary. 598 AddOrMergeEditHistory(new InsertEdit(false, composition, range.start())); 599 render_text_->SetCursorPosition(range.end()); 600 ClearComposition(); 601 if (delegate_) 602 delegate_->OnCompositionTextConfirmedOrCleared(); 603 } 604 605 void TextfieldModel::CancelCompositionText() { 606 DCHECK(HasCompositionText()); 607 gfx::Range range = render_text_->GetCompositionRange(); 608 ClearComposition(); 609 base::string16 new_text = text(); 610 render_text_->SetText(new_text.erase(range.start(), range.length())); 611 render_text_->SetCursorPosition(range.start()); 612 if (delegate_) 613 delegate_->OnCompositionTextConfirmedOrCleared(); 614 } 615 616 void TextfieldModel::ClearComposition() { 617 render_text_->SetCompositionRange(gfx::Range::InvalidRange()); 618 } 619 620 void TextfieldModel::GetCompositionTextRange(gfx::Range* range) const { 621 *range = gfx::Range(render_text_->GetCompositionRange()); 622 } 623 624 bool TextfieldModel::HasCompositionText() const { 625 return !render_text_->GetCompositionRange().is_empty(); 626 } 627 628 void TextfieldModel::ClearEditHistory() { 629 STLDeleteElements(&edit_history_); 630 current_edit_ = edit_history_.end(); 631 } 632 633 ///////////////////////////////////////////////////////////////// 634 // TextfieldModel: private 635 636 void TextfieldModel::InsertTextInternal(const base::string16& new_text, 637 bool mergeable) { 638 if (HasCompositionText()) { 639 CancelCompositionText(); 640 ExecuteAndRecordInsert(new_text, mergeable); 641 } else if (HasSelection()) { 642 ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE, 643 new_text); 644 } else { 645 ExecuteAndRecordInsert(new_text, mergeable); 646 } 647 } 648 649 void TextfieldModel::ReplaceTextInternal(const base::string16& new_text, 650 bool mergeable) { 651 if (HasCompositionText()) { 652 CancelCompositionText(); 653 } else if (!HasSelection()) { 654 size_t cursor = GetCursorPosition(); 655 const gfx::SelectionModel& model = render_text_->selection_model(); 656 // When there is no selection, the default is to replace the next grapheme 657 // with |new_text|. So, need to find the index of next grapheme first. 658 size_t next = 659 render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD); 660 if (next == model.caret_pos()) 661 render_text_->MoveCursorTo(model); 662 else 663 render_text_->SelectRange(gfx::Range(next, model.caret_pos())); 664 } 665 // Edit history is recorded in InsertText. 666 InsertTextInternal(new_text, mergeable); 667 } 668 669 void TextfieldModel::ClearRedoHistory() { 670 if (edit_history_.begin() == edit_history_.end()) 671 return; 672 if (current_edit_ == edit_history_.end()) { 673 ClearEditHistory(); 674 return; 675 } 676 EditHistory::iterator delete_start = current_edit_; 677 delete_start++; 678 STLDeleteContainerPointers(delete_start, edit_history_.end()); 679 edit_history_.erase(delete_start, edit_history_.end()); 680 } 681 682 void TextfieldModel::ExecuteAndRecordDelete(gfx::Range range, bool mergeable) { 683 size_t old_text_start = range.GetMin(); 684 const base::string16 old_text = text().substr(old_text_start, range.length()); 685 bool backward = range.is_reversed(); 686 Edit* edit = new DeleteEdit(mergeable, old_text, old_text_start, backward); 687 bool delete_edit = AddOrMergeEditHistory(edit); 688 edit->Redo(this); 689 if (delete_edit) 690 delete edit; 691 } 692 693 void TextfieldModel::ExecuteAndRecordReplaceSelection( 694 MergeType merge_type, 695 const base::string16& new_text) { 696 size_t new_text_start = render_text_->selection().GetMin(); 697 size_t new_cursor_pos = new_text_start + new_text.length(); 698 ExecuteAndRecordReplace(merge_type, 699 GetCursorPosition(), 700 new_cursor_pos, 701 new_text, 702 new_text_start); 703 } 704 705 void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type, 706 size_t old_cursor_pos, 707 size_t new_cursor_pos, 708 const base::string16& new_text, 709 size_t new_text_start) { 710 size_t old_text_start = render_text_->selection().GetMin(); 711 bool backward = render_text_->selection().is_reversed(); 712 Edit* edit = new ReplaceEdit(merge_type, 713 GetSelectedText(), 714 old_cursor_pos, 715 old_text_start, 716 backward, 717 new_cursor_pos, 718 new_text, 719 new_text_start); 720 bool delete_edit = AddOrMergeEditHistory(edit); 721 edit->Redo(this); 722 if (delete_edit) 723 delete edit; 724 } 725 726 void TextfieldModel::ExecuteAndRecordInsert(const base::string16& new_text, 727 bool mergeable) { 728 Edit* edit = new InsertEdit(mergeable, new_text, GetCursorPosition()); 729 bool delete_edit = AddOrMergeEditHistory(edit); 730 edit->Redo(this); 731 if (delete_edit) 732 delete edit; 733 } 734 735 bool TextfieldModel::AddOrMergeEditHistory(Edit* edit) { 736 ClearRedoHistory(); 737 738 if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) { 739 // If a current edit exists and has been merged with a new edit, don't add 740 // to the history, and return true to delete |edit| after redo. 741 return true; 742 } 743 edit_history_.push_back(edit); 744 if (current_edit_ == edit_history_.end()) { 745 // If there is no redoable edit, this is the 1st edit because RedoHistory 746 // has been already deleted. 747 DCHECK_EQ(1u, edit_history_.size()); 748 current_edit_ = edit_history_.begin(); 749 } else { 750 current_edit_++; 751 } 752 return false; 753 } 754 755 void TextfieldModel::ModifyText(size_t delete_from, 756 size_t delete_to, 757 const base::string16& new_text, 758 size_t new_text_insert_at, 759 size_t new_cursor_pos) { 760 DCHECK_LE(delete_from, delete_to); 761 base::string16 old_text = text(); 762 ClearComposition(); 763 if (delete_from != delete_to) 764 render_text_->SetText(old_text.erase(delete_from, delete_to - delete_from)); 765 if (!new_text.empty()) 766 render_text_->SetText(old_text.insert(new_text_insert_at, new_text)); 767 render_text_->SetCursorPosition(new_cursor_pos); 768 // TODO(oshima): Select text that was just undone, like Mac (but not GTK). 769 } 770 771 } // namespace views 772