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. 4 5 #include "ui/views/controls/textfield/textfield_views_model.h" 6 7 #include <algorithm> 8 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" 22 23 namespace views { 24 25 namespace internal { 26 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 32 // API) 33 class Edit { 34 public: 35 enum Type { 36 INSERT_EDIT, 37 DELETE_EDIT, 38 REPLACE_EDIT 39 }; 40 41 virtual ~Edit() { 42 } 43 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_, 48 old_cursor_pos_); 49 } 50 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_, 55 new_cursor_pos_); 56 } 57 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()) { 67 MergeReplace(edit); 68 return true; 69 } 70 return mergeable() && edit->mergeable() && DoMerge(edit); 71 } 72 73 // Commits the edit and marks as un-mergeable. 74 void Commit() { merge_type_ = DO_NOT_MERGE; } 75 76 private: 77 friend class InsertEdit; 78 friend class ReplaceEdit; 79 friend class DeleteEdit; 80 81 Edit(Type type, 82 MergeType merge_type, 83 size_t old_cursor_pos, 84 const string16& old_text, 85 size_t old_text_start, 86 bool delete_backward, 87 size_t new_cursor_pos, 88 const string16& new_text, 89 size_t new_text_start) 90 : type_(type), 91 merge_type_(merge_type), 92 old_cursor_pos_(old_cursor_pos), 93 old_text_(old_text), 94 old_text_start_(old_text_start), 95 delete_backward_(delete_backward), 96 new_cursor_pos_(new_cursor_pos), 97 new_text_(new_text), 98 new_text_start_(new_text_start) { 99 } 100 101 // A template method pattern that provides specific merge 102 // implementation for each type of edit. 103 virtual bool DoMerge(const Edit* edit) = 0; 104 105 Type type() const { return type_; } 106 107 // Can this edit be merged? 108 bool mergeable() const { return merge_type_ == MERGEABLE; } 109 110 // Should this edit be forcibly merged with the previous edit? 111 bool merge_with_previous() const { 112 return merge_type_ == MERGE_WITH_PREVIOUS; 113 } 114 115 // Returns the end index of the |old_text_|. 116 size_t old_text_end() const { return old_text_start_ + old_text_.length(); } 117 118 // Returns the end index of the |new_text_|. 119 size_t new_text_end() const { return new_text_start_ + new_text_.length(); } 120 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 123 // typed in. 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; 136 137 new_text_ = edit->new_text_; 138 new_text_start_ = edit->new_text_start_; 139 merge_type_ = DO_NOT_MERGE; 140 } 141 142 Type type_; 143 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. 149 string16 old_text_; 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_; 156 // Added text. 157 string16 new_text_; 158 // The index of |new_text_| 159 size_t new_text_start_; 160 161 DISALLOW_COPY_AND_ASSIGN(Edit); 162 }; 163 164 class InsertEdit : public Edit { 165 public: 166 InsertEdit(bool mergeable, const string16& new_text, size_t at) 167 : Edit(INSERT_EDIT, 168 mergeable ? MERGEABLE : DO_NOT_MERGE, 169 at /* old cursor */, 170 string16(), 171 at, 172 false /* N/A */, 173 at + new_text.length() /* new cursor */, 174 new_text, 175 at) { 176 } 177 178 // Edit implementation. 179 virtual bool DoMerge(const Edit* edit) OVERRIDE { 180 if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_) 181 return false; 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_; 187 return true; 188 } 189 }; 190 191 class ReplaceEdit : public Edit { 192 public: 193 ReplaceEdit(MergeType merge_type, 194 const string16& old_text, 195 size_t old_cursor_pos, 196 size_t old_text_start, 197 bool backward, 198 size_t new_cursor_pos, 199 const string16& new_text, 200 size_t new_text_start) 201 : Edit(REPLACE_EDIT, merge_type, 202 old_cursor_pos, 203 old_text, 204 old_text_start, 205 backward, 206 new_cursor_pos, 207 new_text, 208 new_text_start) { 209 } 210 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_) 216 return false; 217 old_text_ += edit->old_text_; 218 new_text_ += edit->new_text_; 219 new_cursor_pos_ = edit->new_cursor_pos_; 220 return true; 221 } 222 }; 223 224 class DeleteEdit : public Edit { 225 public: 226 DeleteEdit(bool mergeable, 227 const string16& text, 228 size_t text_start, 229 bool backward) 230 : Edit(DELETE_EDIT, 231 mergeable ? MERGEABLE : DO_NOT_MERGE, 232 (backward ? text_start + text.length() : text_start), 233 text, 234 text_start, 235 backward, 236 text_start, 237 string16(), 238 text_start) { 239 } 240 241 // Edit implementation. 242 virtual bool DoMerge(const Edit* edit) OVERRIDE { 243 if (edit->type() != DELETE_EDIT) 244 return false; 245 246 if (delete_backward_) { 247 // backspace can be merged only with backspace at the 248 // same position. 249 if (!edit->delete_backward_ || old_text_start_ != edit->old_text_end()) 250 return false; 251 old_text_start_ = edit->old_text_start_; 252 old_text_ = edit->old_text_ + old_text_; 253 new_cursor_pos_ = edit->new_cursor_pos_; 254 } else { 255 // delete can be merged only with delete at the same 256 // position. 257 if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_) 258 return false; 259 old_text_ += edit->old_text_; 260 } 261 return true; 262 } 263 }; 264 265 } // namespace internal 266 267 namespace { 268 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]; 275 if (underline.thick) 276 return gfx::Range(underline.start_offset, underline.end_offset); 277 } 278 return gfx::Range::InvalidRange(); 279 } 280 281 } // namespace 282 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; 291 292 ///////////////////////////////////////////////////////////////// 293 // TextfieldViewsModel: public 294 295 TextfieldViewsModel::Delegate::~Delegate() { 296 } 297 298 TextfieldViewsModel::TextfieldViewsModel(Delegate* delegate) 299 : delegate_(delegate), 300 render_text_(gfx::RenderText::CreateInstance()), 301 current_edit_(edit_history_.end()) { 302 } 303 304 TextfieldViewsModel::~TextfieldViewsModel() { 305 ClearEditHistory(); 306 ClearComposition(); 307 } 308 309 const string16& TextfieldViewsModel::GetText() const { 310 return render_text_->text(); 311 } 312 313 bool TextfieldViewsModel::SetText(const string16& text) { 314 bool changed = false; 315 if (HasCompositionText()) { 316 ConfirmCompositionText(); 317 changed = true; 318 } 319 if (GetText() != text) { 320 if (changed) // No need to remember composition. 321 Undo(); 322 size_t old_cursor = GetCursorPosition(); 323 // SetText moves the cursor to the end. 324 size_t new_cursor = text.length(); 325 SelectAll(false); 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, 330 old_cursor, 331 new_cursor, 332 text, 333 0U); 334 render_text_->SetCursorPosition(new_cursor); 335 } 336 ClearSelection(); 337 return changed; 338 } 339 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(), 346 false); 347 InsertText(text); 348 render_text_->SetCursorPosition(save); 349 ClearSelection(); 350 } 351 352 bool TextfieldViewsModel::Delete() { 353 if (HasCompositionText()) { 354 // No undo/redo for composition text. 355 CancelCompositionText(); 356 return true; 357 } 358 if (HasSelection()) { 359 DeleteSelection(); 360 return true; 361 } 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), 367 true); 368 return true; 369 } 370 return false; 371 } 372 373 bool TextfieldViewsModel::Backspace() { 374 if (HasCompositionText()) { 375 // No undo/redo for composition text. 376 CancelCompositionText(); 377 return true; 378 } 379 if (HasSelection()) { 380 DeleteSelection(); 381 return true; 382 } 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); 389 return true; 390 } 391 return false; 392 } 393 394 size_t TextfieldViewsModel::GetCursorPosition() const { 395 return render_text_->cursor_position(); 396 } 397 398 void TextfieldViewsModel::MoveCursor(gfx::BreakType break_type, 399 gfx::VisualCursorDirection direction, 400 bool select) { 401 if (HasCompositionText()) 402 ConfirmCompositionText(); 403 render_text_->MoveCursor(break_type, direction, select); 404 } 405 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())); 416 } 417 return render_text_->MoveCursorTo(model); 418 } 419 420 bool TextfieldViewsModel::MoveCursorTo(const gfx::Point& point, bool select) { 421 if (HasCompositionText()) 422 ConfirmCompositionText(); 423 return render_text_->MoveCursorTo(point, select); 424 } 425 426 string16 TextfieldViewsModel::GetSelectedText() const { 427 return GetText().substr(render_text_->selection().GetMin(), 428 render_text_->selection().length()); 429 } 430 431 void TextfieldViewsModel::SelectRange(const gfx::Range& range) { 432 if (HasCompositionText()) 433 ConfirmCompositionText(); 434 render_text_->SelectRange(range); 435 } 436 437 void TextfieldViewsModel::SelectSelectionModel(const gfx::SelectionModel& sel) { 438 if (HasCompositionText()) 439 ConfirmCompositionText(); 440 render_text_->MoveCursorTo(sel); 441 } 442 443 void TextfieldViewsModel::SelectAll(bool reversed) { 444 if (HasCompositionText()) 445 ConfirmCompositionText(); 446 render_text_->SelectAll(reversed); 447 } 448 449 void TextfieldViewsModel::SelectWord() { 450 if (HasCompositionText()) 451 ConfirmCompositionText(); 452 render_text_->SelectWord(); 453 } 454 455 void TextfieldViewsModel::ClearSelection() { 456 if (HasCompositionText()) 457 ConfirmCompositionText(); 458 render_text_->ClearSelection(); 459 } 460 461 bool TextfieldViewsModel::CanUndo() { 462 return edit_history_.size() && current_edit_ != edit_history_.end(); 463 } 464 465 bool TextfieldViewsModel::CanRedo() { 466 if (!edit_history_.size()) 467 return false; 468 // There is no redo iff the current edit is the last element 469 // in the history. 470 EditHistory::iterator iter = current_edit_; 471 return iter == edit_history_.end() || // at the top. 472 ++iter != edit_history_.end(); 473 } 474 475 bool TextfieldViewsModel::Undo() { 476 if (!CanUndo()) 477 return false; 478 DCHECK(!HasCompositionText()); 479 if (HasCompositionText()) // safe guard for release build. 480 CancelCompositionText(); 481 482 string16 old = GetText(); 483 size_t old_cursor = GetCursorPosition(); 484 (*current_edit_)->Commit(); 485 (*current_edit_)->Undo(this); 486 487 if (current_edit_ == edit_history_.begin()) 488 current_edit_ = edit_history_.end(); 489 else 490 current_edit_--; 491 return old != GetText() || old_cursor != GetCursorPosition(); 492 } 493 494 bool TextfieldViewsModel::Redo() { 495 if (!CanRedo()) 496 return false; 497 DCHECK(!HasCompositionText()); 498 if (HasCompositionText()) // safe guard for release build. 499 CancelCompositionText(); 500 501 if (current_edit_ == edit_history_.end()) 502 current_edit_ = edit_history_.begin(); 503 else 504 current_edit_ ++; 505 string16 old = GetText(); 506 size_t old_cursor = GetCursorPosition(); 507 (*current_edit_)->Redo(this); 508 return old != GetText() || old_cursor != GetCursorPosition(); 509 } 510 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())); 523 DeleteSelection(); 524 return true; 525 } 526 return false; 527 } 528 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()); 534 return true; 535 } 536 return false; 537 } 538 539 bool TextfieldViewsModel::Paste() { 540 string16 result; 541 ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, 542 &result); 543 if (!result.empty()) { 544 InsertTextInternal(result, false); 545 return true; 546 } 547 return false; 548 } 549 550 bool TextfieldViewsModel::HasSelection() const { 551 return !render_text_->selection().is_empty(); 552 } 553 554 void TextfieldViewsModel::DeleteSelection() { 555 DCHECK(!HasCompositionText()); 556 DCHECK(HasSelection()); 557 ExecuteAndRecordDelete(render_text_->selection(), false); 558 } 559 560 void TextfieldViewsModel::DeleteSelectionAndInsertTextAt( 561 const string16& text, size_t position) { 562 if (HasCompositionText()) 563 CancelCompositionText(); 564 ExecuteAndRecordReplace(DO_NOT_MERGE, 565 GetCursorPosition(), 566 position + text.length(), 567 text, 568 position); 569 } 570 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()); 574 return string16(); 575 } 576 577 void TextfieldViewsModel::GetTextRange(gfx::Range* range) const { 578 *range = gfx::Range(0, GetText().length()); 579 } 580 581 void TextfieldViewsModel::SetCompositionText( 582 const ui::CompositionText& composition) { 583 if (HasCompositionText()) 584 CancelCompositionText(); 585 else if (HasSelection()) 586 DeleteSelection(); 587 588 if (composition.text.empty()) 589 return; 590 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 605 // this workaround. 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())); 613 } else { 614 render_text_->SetCursorPosition(cursor + composition.selection.end()); 615 } 616 } 617 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()); 626 ClearComposition(); 627 if (delegate_) 628 delegate_->OnCompositionTextConfirmedOrCleared(); 629 } 630 631 void TextfieldViewsModel::CancelCompositionText() { 632 DCHECK(HasCompositionText()); 633 gfx::Range range = render_text_->GetCompositionRange(); 634 ClearComposition(); 635 string16 new_text = GetText(); 636 render_text_->SetText(new_text.erase(range.start(), range.length())); 637 render_text_->SetCursorPosition(range.start()); 638 if (delegate_) 639 delegate_->OnCompositionTextConfirmedOrCleared(); 640 } 641 642 void TextfieldViewsModel::ClearComposition() { 643 render_text_->SetCompositionRange(gfx::Range::InvalidRange()); 644 } 645 646 void TextfieldViewsModel::GetCompositionTextRange(gfx::Range* range) const { 647 *range = gfx::Range(render_text_->GetCompositionRange()); 648 } 649 650 bool TextfieldViewsModel::HasCompositionText() const { 651 return !render_text_->GetCompositionRange().is_empty(); 652 } 653 654 ///////////////////////////////////////////////////////////////// 655 // TextfieldViewsModel: private 656 657 void TextfieldViewsModel::InsertTextInternal(const string16& text, 658 bool mergeable) { 659 if (HasCompositionText()) { 660 CancelCompositionText(); 661 ExecuteAndRecordInsert(text, mergeable); 662 } else if (HasSelection()) { 663 ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE, 664 text); 665 } else { 666 ExecuteAndRecordInsert(text, mergeable); 667 } 668 } 669 670 void TextfieldViewsModel::ReplaceTextInternal(const string16& text, 671 bool mergeable) { 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. 679 size_t next = 680 render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD); 681 if (next == model.caret_pos()) 682 render_text_->MoveCursorTo(model); 683 else 684 render_text_->SelectRange(gfx::Range(next, model.caret_pos())); 685 } 686 // Edit history is recorded in InsertText. 687 InsertTextInternal(text, mergeable); 688 } 689 690 void TextfieldViewsModel::ClearEditHistory() { 691 STLDeleteElements(&edit_history_); 692 current_edit_ = edit_history_.end(); 693 } 694 695 void TextfieldViewsModel::ClearRedoHistory() { 696 if (edit_history_.begin() == edit_history_.end()) 697 return; 698 if (current_edit_ == edit_history_.end()) { 699 ClearEditHistory(); 700 return; 701 } 702 EditHistory::iterator delete_start = current_edit_; 703 delete_start++; 704 STLDeleteContainerPointers(delete_start, edit_history_.end()); 705 edit_history_.erase(delete_start, edit_history_.end()); 706 } 707 708 void TextfieldViewsModel::ExecuteAndRecordDelete(gfx::Range range, 709 bool mergeable) { 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); 715 edit->Redo(this); 716 if (delete_edit) 717 delete edit; 718 } 719 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, 725 GetCursorPosition(), 726 new_cursor_pos, 727 new_text, 728 new_text_start); 729 } 730 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, 739 GetSelectedText(), 740 old_cursor_pos, 741 old_text_start, 742 backward, 743 new_cursor_pos, 744 new_text, 745 new_text_start); 746 bool delete_edit = AddOrMergeEditHistory(edit); 747 edit->Redo(this); 748 if (delete_edit) 749 delete edit; 750 } 751 752 void TextfieldViewsModel::ExecuteAndRecordInsert(const string16& text, 753 bool mergeable) { 754 Edit* edit = new InsertEdit(mergeable, text, GetCursorPosition()); 755 bool delete_edit = AddOrMergeEditHistory(edit); 756 edit->Redo(this); 757 if (delete_edit) 758 delete edit; 759 } 760 761 bool TextfieldViewsModel::AddOrMergeEditHistory(Edit* edit) { 762 ClearRedoHistory(); 763 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 767 // redo. 768 return true; 769 } 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(); 776 } else { 777 current_edit_++; 778 } 779 return false; 780 } 781 782 void TextfieldViewsModel::ModifyText(size_t delete_from, 783 size_t delete_to, 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(); 789 ClearComposition(); 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. 797 } 798 799 } // namespace views 800