blob: d3df8334a98d172897847ae0085c41cee7e647e3 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/textfield/textfield_model.h"
#include <algorithm>
#include <utility>
#include "base/check_op.h"
#include "base/macros.h"
#include "base/message_loop/message_loop_current.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/utf16_indexing.h"
#include "ui/views/style/platform_style.h"
namespace {
// Orders ranges decreasing with respect to their min index. This is useful for
// applying text edits such that an edit doesn't offset the positions of later
// edits. It should be reversed when undoing edits.
void order_ranges(std::vector<gfx::Range>* ranges) {
std::sort(ranges->begin(), ranges->end(), [](const auto& r1, const auto& r2) {
return r1.GetMin() > r2.GetMin();
});
}
// Adjusts |position| for the deletion of |ranges|. E.g., if |position| is 10,
// and |ranges| is {{1, 3}, {15, 18}, and {6, 13}}, this will return 4,
// subtracting 2 (3-1), 0 (15>10), and 4 (10-6) for each range respectively.
size_t adjust_position_for_removals(size_t position,
std::vector<gfx::Range> ranges) {
size_t adjustment = 0;
for (auto range : ranges)
adjustment += range.Intersect(gfx::Range(0, position)).length();
return position - adjustment;
}
} // namespace
namespace views {
namespace internal {
// Edit holds state information to undo/redo editing changes. Editing operations
// are merged when possible, like when characters are typed in sequence. Calling
// Commit() marks an edit as an independent operation that shouldn't be merged.
class Edit {
public:
enum class Type {
kInsert,
kDelete,
kReplace,
};
virtual ~Edit() = default;
// Revert the change made by this edit in |model|.
void Undo(TextfieldModel* model) {
// Insertions must be applied in order of increasing indices since |Redo|
// applies them in decreasing order.
auto insertion_texts = old_texts_;
std::reverse(insertion_texts.begin(), insertion_texts.end());
auto insertion_text_starts = old_text_starts_;
std::reverse(insertion_text_starts.begin(), insertion_text_starts.end());
model->ModifyText({{new_text_start_, new_text_end()}}, insertion_texts,
insertion_text_starts, old_primary_selection_,
old_secondary_selections_);
}
// Apply the change of this edit to the |model|.
void Redo(TextfieldModel* model) {
std::vector<gfx::Range> deletions;
for (size_t i = 0; i < old_texts_.size(); ++i) {
deletions.emplace_back(old_text_starts_[i],
old_text_starts_[i] + old_texts_[i].length());
}
model->ModifyText(deletions, {new_text_}, {new_text_start_},
{new_cursor_pos_, new_cursor_pos_}, {});
}
// Try to merge the |edit| into this edit and returns true on success. The
// merged edit will be deleted after redo and should not be reused.
bool Merge(const Edit* edit) {
// Don't merge if previous edit is DELETE. This happens when a
// user deletes characters then hits return. In this case, the
// delete should be treated as separate edit that can be undone
// and should not be merged with the replace edit.
if (type_ != Type::kDelete && edit->force_merge()) {
MergeReplace(edit);
return true;
}
return mergeable() && edit->mergeable() && DoMerge(edit);
}
// Commits the edit and marks as un-mergeable.
void Commit() { merge_type_ = MergeType::kDoNotMerge; }
private:
friend class InsertEdit;
friend class ReplaceEdit;
friend class DeleteEdit;
Edit(Type type,
MergeType merge_type,
std::vector<base::string16> old_texts,
std::vector<size_t> old_text_starts,
gfx::Range old_primary_selection,
std::vector<gfx::Range> old_secondary_selections,
bool delete_backward,
size_t new_cursor_pos,
const base::string16& new_text,
size_t new_text_start)
: type_(type),
merge_type_(merge_type),
old_texts_(old_texts),
old_text_starts_(old_text_starts),
old_primary_selection_(old_primary_selection),
old_secondary_selections_(old_secondary_selections),
delete_backward_(delete_backward),
new_cursor_pos_(new_cursor_pos),
new_text_(new_text),
new_text_start_(new_text_start) {}
// Each type of edit provides its own specific merge implementation. Assumes
// |edit| occurs after |this|.
virtual bool DoMerge(const Edit* edit) = 0;
Type type() const { return type_; }
// Can this edit be merged?
bool mergeable() const { return merge_type_ == MergeType::kMergeable; }
// Should this edit be forcibly merged with the previous edit?
bool force_merge() const { return merge_type_ == MergeType::kForceMerge; }
// Returns the end index of the |new_text_|.
size_t new_text_end() const { return new_text_start_ + new_text_.length(); }
// Merge the replace edit into the current edit. This handles the special case
// where an omnibox autocomplete string is set after a new character is typed.
void MergeReplace(const Edit* edit) {
CHECK_EQ(Type::kReplace, edit->type_);
CHECK_EQ(1U, edit->old_text_starts_.size());
CHECK_EQ(0U, edit->old_text_starts_[0]);
CHECK_EQ(0U, edit->new_text_start_);
// We need to compute the merged edit's |old_texts_| by undoing this edit.
// Otherwise, |old_texts_| would be the autocompleted text following the
// user input. E.g., given goo|[gle.com], when the user types 'g', the text
// updates to goog|[le.com]. If we leave old_texts_ unchanged as 'gle.com',
// then undoing will result in 'gle.com' instead of 'goo|[gle.com]'
base::string16 old_texts = edit->old_texts_[0];
// Remove |new_text_|.
old_texts.erase(new_text_start_, new_text_.length());
// Add |old_texts_| in reverse order since we're undoing an edit.
for (size_t i = old_texts_.size(); i != 0; i--)
old_texts.insert(old_text_starts_[i - 1], old_texts_[i - 1]);
merge_type_ = MergeType::kDoNotMerge;
old_texts_ = {old_texts};
old_text_starts_ = {0};
delete_backward_ = false;
new_cursor_pos_ = edit->new_cursor_pos_;
new_text_ = edit->new_text_;
new_text_start_ = 0;
}
Type type_;
// The type of merging allowed.
MergeType merge_type_;
// Deleted texts ordered with decreasing indices.
std::vector<base::string16> old_texts_;
// The indices of |old_texts_|.
std::vector<size_t> old_text_starts_;
// The text selection ranges prior to the edit. |old_primary_selection_|
// represents the selection associated with the cursor.
gfx::Range old_primary_selection_;
std::vector<gfx::Range> old_secondary_selections_;
// True if the deletion is made backward.
bool delete_backward_;
// New cursor position.
size_t new_cursor_pos_;
// Added text.
base::string16 new_text_;
// The index of |new_text_|
size_t new_text_start_;
DISALLOW_COPY_AND_ASSIGN(Edit);
};
// Insert text at a given position. Assumes 1) no previous selection and 2) the
// insertion is at the cursor, which will advance by the insertion length.
class InsertEdit : public Edit {
public:
InsertEdit(bool mergeable, const base::string16& new_text, size_t at)
: Edit(Type::kInsert,
mergeable ? MergeType::kMergeable : MergeType::kDoNotMerge,
{} /* old_texts */,
{} /* old_text_starts */,
{gfx::Range(at, at)} /* old_primary_selection */,
{} /* old_secondary_selections */,
false /* delete_backward */,
at + new_text.length() /* new_cursor_pos */,
new_text /* new_text */,
at /* new_text_start */) {}
// Merge if |edit| is an insertion continuing forward where |this| ended. E.g.
// If |this| changed "ab|c" to "abX|c", an edit to "abXY|c" can be merged.
bool DoMerge(const Edit* edit) override {
// Reject other edit types, and inserts starting somewhere other than where
// this insert ended.
if (edit->type() != Type::kInsert ||
new_text_end() != edit->new_text_start_)
return false;
new_text_ += edit->new_text_;
new_cursor_pos_ = edit->new_cursor_pos_;
return true;
}
};
// Delete one or more ranges and do a single insertion. The insertion need not
// be adjacent to the deletions (e.g. drag & drop).
class ReplaceEdit : public Edit {
public:
ReplaceEdit(MergeType merge_type,
std::vector<base::string16> old_texts,
std::vector<size_t> old_text_starts,
gfx::Range old_primary_selection,
std::vector<gfx::Range> old_secondary_selections,
bool backward,
size_t new_cursor_pos,
const base::string16& new_text,
size_t new_text_start)
: Edit(Type::kReplace,
merge_type,
old_texts,
old_text_starts,
old_primary_selection,
old_secondary_selections,
backward,
new_cursor_pos,
new_text,
new_text_start) {}
// Merge if |edit| is an insertion or replacement continuing forward where
// |this| ended. E.g. If |this| changed "a|bc" to "aX|c", edits to "aXY|" or
// "aXYc" can be merged. Drag and drops are marked kDoNotMerge and should not
// get here.
bool DoMerge(const Edit* edit) override {
// Reject deletions, replacements deleting multiple ranges, and edits
// inserting or deleting text somewhere other than where this edit ended.
if (edit->type() == Type::kDelete || edit->old_texts_.size() > 1 ||
new_text_end() != edit->new_text_start_ ||
(!edit->old_text_starts_.empty() &&
new_text_end() != edit->old_text_starts_[0]))
return false;
if (edit->old_texts_.size() == 1)
old_texts_[0] += edit->old_texts_[0];
new_text_ += edit->new_text_;
new_cursor_pos_ = edit->new_cursor_pos_;
return true;
}
};
// Delete possibly multiple texts.
class DeleteEdit : public Edit {
public:
DeleteEdit(bool mergeable,
std::vector<base::string16> texts,
std::vector<size_t> text_starts,
gfx::Range old_primary_selection,
std::vector<gfx::Range> old_secondary_selections,
bool backward,
size_t new_cursor_pos)
: Edit(Type::kDelete,
mergeable ? MergeType::kMergeable : MergeType::kDoNotMerge,
texts,
text_starts,
old_primary_selection,
old_secondary_selections,
backward,
new_cursor_pos,
base::string16() /* new_text */,
0 /* new_text_start */) {}
// Merge if |edit| is a deletion continuing in the same direction and position
// where |this| ended. E.g. If |this| changed "ab|c" to "a|c" an edit to "|c"
// can be merged.
bool DoMerge(const Edit* edit) override {
if (edit->type() != Type::kDelete)
return false;
// Deletions with selections are marked kDoNotMerge and should not get here.
DCHECK(old_secondary_selections_.empty());
DCHECK(old_primary_selection_.is_empty());
DCHECK(edit->old_secondary_selections_.empty());
DCHECK(edit->old_primary_selection_.is_empty());
if (delete_backward_) {
// Backspace can be merged only with backspace at the same position.
if (!edit->delete_backward_ ||
old_text_starts_[0] !=
edit->old_text_starts_[0] + edit->old_texts_[0].length())
return false;
old_text_starts_[0] = edit->old_text_starts_[0];
old_texts_[0] = edit->old_texts_[0] + old_texts_[0];
new_cursor_pos_ = edit->new_cursor_pos_;
} else {
// Delete can be merged only with delete at the same position.
if (edit->delete_backward_ ||
old_text_starts_[0] != edit->old_text_starts_[0])
return false;
old_texts_[0] += edit->old_texts_[0];
}
return true;
}
};
} // namespace internal
namespace {
// Returns the first segment that is visually emphasized. Usually it's used for
// representing the target clause (on Windows). Returns an invalid range if
// there is no such a range.
gfx::Range GetFirstEmphasizedRange(const ui::CompositionText& composition) {
for (const auto& underline : composition.ime_text_spans) {
if (underline.thickness == ui::ImeTextSpan::Thickness::kThick)
return gfx::Range(underline.start_offset, underline.end_offset);
}
return gfx::Range::InvalidRange();
}
// Returns a pointer to the kill buffer which holds the text to be inserted on
// executing yank command. Singleton since it needs to be persisted across
// multiple textfields.
// On Mac, the size of the kill ring (no. of buffers) is controlled by
// NSTextKillRingSize, a text system default. However to keep things simple,
// the default kill ring size of 1 (i.e. a single buffer) is assumed.
base::string16* GetKillBuffer() {
static base::NoDestructor<base::string16> kill_buffer;
DCHECK(base::MessageLoopCurrentForUI::IsSet());
return kill_buffer.get();
}
// Helper method to set the kill buffer.
void SetKillBuffer(const base::string16& buffer) {
base::string16* kill_buffer = GetKillBuffer();
*kill_buffer = buffer;
}
void SelectRangeInCompositionText(gfx::RenderText* render_text,
size_t cursor,
const gfx::Range& range) {
DCHECK(render_text);
DCHECK(range.IsValid());
uint32_t start = range.GetMin();
uint32_t end = range.GetMax();
#if defined(OS_CHROMEOS)
// Swap |start| and |end| so that GetCaretBounds() can always return the same
// value during conversion.
// TODO(yusukes): Check if this works for other platforms. If it is, use this
// on all platforms.
std::swap(start, end);
#endif
render_text->SelectRange(gfx::Range(cursor + start, cursor + end));
}
} // namespace
/////////////////////////////////////////////////////////////////
// TextfieldModel: public
TextfieldModel::Delegate::~Delegate() = default;
TextfieldModel::TextfieldModel(Delegate* delegate)
: delegate_(delegate),
render_text_(gfx::RenderText::CreateRenderText()),
current_edit_(edit_history_.end()) {}
TextfieldModel::~TextfieldModel() {
ClearEditHistory();
ClearComposition();
}
bool TextfieldModel::SetText(const base::string16& new_text,
size_t cursor_position) {
using MergeType = internal::MergeType;
bool changed = false;
if (HasCompositionText()) {
ConfirmCompositionText();
changed = true;
}
if (text() != new_text) {
if (changed) // No need to remember composition.
Undo();
// If there is a composition text, don't merge with previous edit.
// Otherwise, force merge the edits.
ExecuteAndRecordReplace(
changed ? MergeType::kDoNotMerge : MergeType::kForceMerge,
{gfx::Range(0, text().length())}, cursor_position, new_text, 0U);
}
ClearSelection();
return changed;
}
void TextfieldModel::Append(const base::string16& new_text) {
if (HasCompositionText())
ConfirmCompositionText();
size_t save = GetCursorPosition();
MoveCursor(gfx::LINE_BREAK, render_text_->GetVisualDirectionOfLogicalEnd(),
gfx::SELECTION_NONE);
InsertText(new_text);
render_text_->SetCursorPosition(save);
ClearSelection();
}
bool TextfieldModel::Delete(bool add_to_kill_buffer) {
// |add_to_kill_buffer| should never be true for an obscured textfield.
DCHECK(!add_to_kill_buffer || !render_text_->obscured());
if (HasCompositionText()) {
// No undo/redo for composition text.
CancelCompositionText();
return true;
}
if (HasSelection()) {
if (add_to_kill_buffer)
SetKillBuffer(GetSelectedText());
DeleteSelection();
return true;
}
const size_t cursor_position = GetCursorPosition();
if (cursor_position < text().length()) {
size_t next_grapheme_index = render_text_->IndexOfAdjacentGrapheme(
cursor_position, gfx::CURSOR_FORWARD);
gfx::Range range_to_delete(cursor_position, next_grapheme_index);
if (add_to_kill_buffer)
SetKillBuffer(GetTextFromRange(range_to_delete));
ExecuteAndRecordDelete({range_to_delete}, true);
return true;
}
return false;
}
bool TextfieldModel::Backspace(bool add_to_kill_buffer) {
// |add_to_kill_buffer| should never be true for an obscured textfield.
DCHECK(!add_to_kill_buffer || !render_text_->obscured());
if (HasCompositionText()) {
// No undo/redo for composition text.
CancelCompositionText();
return true;
}
if (HasSelection()) {
if (add_to_kill_buffer)
SetKillBuffer(GetSelectedText());
DeleteSelection();
return true;
}
const size_t cursor_position = GetCursorPosition();
if (cursor_position > 0) {
gfx::Range range_to_delete(
PlatformStyle::RangeToDeleteBackwards(text(), cursor_position));
if (add_to_kill_buffer)
SetKillBuffer(GetTextFromRange(range_to_delete));
ExecuteAndRecordDelete({range_to_delete}, true);
return true;
}
return false;
}
size_t TextfieldModel::GetCursorPosition() const {
return render_text_->cursor_position();
}
void TextfieldModel::MoveCursor(gfx::BreakType break_type,
gfx::VisualCursorDirection direction,
gfx::SelectionBehavior selection_behavior) {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->MoveCursor(break_type, direction, selection_behavior);
}
bool TextfieldModel::MoveCursorTo(const gfx::SelectionModel& cursor) {
if (HasCompositionText()) {
ConfirmCompositionText();
// ConfirmCompositionText() updates cursor position. Need to reflect it in
// the SelectionModel parameter of MoveCursorTo().
gfx::Range range(render_text_->selection().start(), cursor.caret_pos());
if (!range.is_empty())
return render_text_->SelectRange(range);
return render_text_->SetSelection(
gfx::SelectionModel(cursor.caret_pos(), cursor.caret_affinity()));
}
return render_text_->SetSelection(cursor);
}
bool TextfieldModel::MoveCursorTo(size_t pos) {
return MoveCursorTo(gfx::SelectionModel(pos, gfx::CURSOR_FORWARD));
}
bool TextfieldModel::MoveCursorTo(const gfx::Point& point, bool select) {
if (HasCompositionText())
ConfirmCompositionText();
return render_text_->MoveCursorToPoint(point, select);
}
base::string16 TextfieldModel::GetSelectedText() const {
return GetTextFromRange(render_text_->selection());
}
void TextfieldModel::SelectRange(const gfx::Range& range, bool primary) {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->SelectRange(range, primary);
}
void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel& sel) {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->SetSelection(sel);
}
void TextfieldModel::SelectAll(bool reversed) {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->SelectAll(reversed);
}
void TextfieldModel::SelectWord() {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->SelectWord();
}
void TextfieldModel::ClearSelection() {
if (HasCompositionText())
ConfirmCompositionText();
render_text_->ClearSelection();
}
bool TextfieldModel::CanUndo() {
return edit_history_.size() && current_edit_ != edit_history_.end();
}
bool TextfieldModel::CanRedo() {
if (edit_history_.empty())
return false;
// There is no redo iff the current edit is the last element in the history.
auto iter = current_edit_;
return iter == edit_history_.end() || // at the top.
++iter != edit_history_.end();
}
bool TextfieldModel::Undo() {
if (!CanUndo())
return false;
DCHECK(!HasCompositionText());
if (HasCompositionText())
CancelCompositionText();
base::string16 old = text();
size_t old_cursor = GetCursorPosition();
(*current_edit_)->Commit();
(*current_edit_)->Undo(this);
if (current_edit_ == edit_history_.begin())
current_edit_ = edit_history_.end();
else
--current_edit_;
return old != text() || old_cursor != GetCursorPosition();
}
bool TextfieldModel::Redo() {
if (!CanRedo())
return false;
DCHECK(!HasCompositionText());
if (HasCompositionText())
CancelCompositionText();
if (current_edit_ == edit_history_.end())
current_edit_ = edit_history_.begin();
else
++current_edit_;
base::string16 old = text();
size_t old_cursor = GetCursorPosition();
(*current_edit_)->Redo(this);
return old != text() || old_cursor != GetCursorPosition();
}
bool TextfieldModel::Cut() {
if (!HasCompositionText() && HasSelection(true) &&
!render_text_->obscured()) {
ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste)
.WriteText(GetSelectedText());
DeleteSelection();
return true;
}
return false;
}
bool TextfieldModel::Copy() {
if (!HasCompositionText() && HasSelection(true) &&
!render_text_->obscured()) {
ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste)
.WriteText(GetSelectedText());
return true;
}
return false;
}
bool TextfieldModel::Paste() {
base::string16 text;
ui::Clipboard::GetForCurrentThread()->ReadText(
ui::ClipboardBuffer::kCopyPaste, &text);
if (text.empty())
return false;
// Leading/trailing whitespace is often selected accidentally, and is rarely
// critical to include (e.g. when pasting into a find bar). Trim it. By
// contrast, whitespace in the middle of the string may need exact
// preservation to avoid changing the effect (e.g. converting a full-width
// space to a regular space), so don't call a more aggressive function like
// CollapseWhitespace().
base::TrimWhitespace(text, base::TRIM_ALL, &text);
// If the clipboard contains all whitespace then paste a single space.
if (text.empty())
text = base::ASCIIToUTF16(" ");
InsertTextInternal(text, false);
return true;
}
bool TextfieldModel::Transpose() {
if (HasCompositionText() || HasSelection())
return false;
size_t cur = GetCursorPosition();
size_t next = render_text_->IndexOfAdjacentGrapheme(cur, gfx::CURSOR_FORWARD);
size_t prev =
render_text_->IndexOfAdjacentGrapheme(cur, gfx::CURSOR_BACKWARD);
// At the end of the line, the last two characters should be transposed.
if (cur == text().length()) {
DCHECK_EQ(cur, next);
cur = prev;
prev = render_text_->IndexOfAdjacentGrapheme(prev, gfx::CURSOR_BACKWARD);
}
// This happens at the beginning of the line or when the line has less than
// two graphemes.
if (gfx::UTF16IndexToOffset(text(), prev, next) != 2)
return false;
SelectRange(gfx::Range(prev, next));
base::string16 text = GetSelectedText();
base::string16 transposed_text =
text.substr(cur - prev) + text.substr(0, cur - prev);
InsertTextInternal(transposed_text, false);
return true;
}
bool TextfieldModel::Yank() {
const base::string16* kill_buffer = GetKillBuffer();
if (!kill_buffer->empty() || HasSelection()) {
InsertTextInternal(*kill_buffer, false);
return true;
}
return false;
}
bool TextfieldModel::HasSelection(bool primary_only) const {
if (primary_only)
return !render_text_->selection().is_empty();
auto selections = render_text_->GetAllSelections();
return std::any_of(
selections.begin(), selections.end(),
[](const auto& selection) { return !selection.is_empty(); });
}
void TextfieldModel::DeleteSelection() {
DCHECK(!HasCompositionText());
DCHECK(HasSelection());
ExecuteAndRecordDelete(render_text_->GetAllSelections(), false);
}
void TextfieldModel::DeletePrimarySelectionAndInsertTextAt(
const base::string16& new_text,
size_t position) {
using MergeType = internal::MergeType;
if (HasCompositionText())
CancelCompositionText();
// We don't use |ExecuteAndRecordReplaceSelection| because that assumes the
// insertion occurs at the cursor.
ExecuteAndRecordReplace(MergeType::kDoNotMerge, {render_text_->selection()},
position + new_text.length(), new_text, position);
}
base::string16 TextfieldModel::GetTextFromRange(const gfx::Range& range) const {
return render_text_->GetTextFromRange(range);
}
void TextfieldModel::GetTextRange(gfx::Range* range) const {
*range = gfx::Range(0, text().length());
}
void TextfieldModel::SetCompositionText(
const ui::CompositionText& composition) {
if (HasCompositionText())
CancelCompositionText();
else if (HasSelection())
DeleteSelection();
if (composition.text.empty())
return;
size_t cursor = GetCursorPosition();
base::string16 new_text = text();
SetRenderTextText(new_text.insert(cursor, composition.text));
composition_range_ = gfx::Range(cursor, cursor + composition.text.length());
// Don't render IME spans with thickness "kNone".
if (composition.ime_text_spans.size() > 0 &&
composition.ime_text_spans[0].thickness !=
ui::ImeTextSpan::Thickness::kNone)
render_text_->SetCompositionRange(composition_range_);
else
render_text_->SetCompositionRange(gfx::Range::InvalidRange());
gfx::Range emphasized_range = GetFirstEmphasizedRange(composition);
if (emphasized_range.IsValid()) {
// This is a workaround due to the lack of support in RenderText to draw
// a thick underline. In a composition returned from an IME, the segment
// emphasized by a thick underline usually represents the target clause.
// Because the target clause is more important than the actual selection
// range (or caret position) in the composition here we use a selection-like
// marker instead to show this range.
// TODO(yukawa, msw): Support thick underlines and remove this workaround.
SelectRangeInCompositionText(render_text_.get(), cursor, emphasized_range);
} else if (!composition.selection.is_empty()) {
SelectRangeInCompositionText(render_text_.get(), cursor,
composition.selection);
} else {
render_text_->SetCursorPosition(cursor + composition.selection.end());
}
}
void TextfieldModel::SetCompositionFromExistingText(const gfx::Range& range) {
if (range.is_empty() || !gfx::Range(0, text().length()).Contains(range)) {
ClearComposition();
return;
}
composition_range_ = range;
render_text_->SetCompositionRange(range);
}
void TextfieldModel::ConfirmCompositionText() {
DCHECK(HasCompositionText());
base::string16 composition =
text().substr(composition_range_.start(), composition_range_.length());
// TODO(oshima): current behavior on ChromeOS is a bit weird and not
// sure exactly how this should work. Find out and fix if necessary.
AddOrMergeEditHistory(std::make_unique<internal::InsertEdit>(
false, composition, composition_range_.start()));
render_text_->SetCursorPosition(composition_range_.end());
ClearComposition();
if (delegate_)
delegate_->OnCompositionTextConfirmedOrCleared();
}
void TextfieldModel::CancelCompositionText() {
DCHECK(HasCompositionText());
gfx::Range range = composition_range_;
ClearComposition();
base::string16 new_text = text();
SetRenderTextText(new_text.erase(range.start(), range.length()));
render_text_->SetCursorPosition(range.start());
if (delegate_)
delegate_->OnCompositionTextConfirmedOrCleared();
}
void TextfieldModel::ClearComposition() {
composition_range_ = gfx::Range::InvalidRange();
render_text_->SetCompositionRange(composition_range_);
}
void TextfieldModel::GetCompositionTextRange(gfx::Range* range) const {
*range = composition_range_;
}
bool TextfieldModel::HasCompositionText() const {
return !composition_range_.is_empty();
}
void TextfieldModel::ClearEditHistory() {
edit_history_.clear();
current_edit_ = edit_history_.end();
}
/////////////////////////////////////////////////////////////////
// TextfieldModel: private
void TextfieldModel::InsertTextInternal(const base::string16& new_text,
bool mergeable) {
using MergeType = internal::MergeType;
if (HasCompositionText()) {
CancelCompositionText();
ExecuteAndRecordInsert(new_text, mergeable);
} else if (HasSelection()) {
ExecuteAndRecordReplaceSelection(
mergeable ? MergeType::kMergeable : MergeType::kDoNotMerge, new_text);
} else {
ExecuteAndRecordInsert(new_text, mergeable);
}
}
void TextfieldModel::ReplaceTextInternal(const base::string16& new_text,
bool mergeable) {
if (HasCompositionText()) {
CancelCompositionText();
} else if (!HasSelection()) {
size_t cursor = GetCursorPosition();
const gfx::SelectionModel& model = render_text_->selection_model();
// When there is no selection, the default is to replace the next grapheme
// with |new_text|. So, need to find the index of next grapheme first.
size_t next =
render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD);
if (next == model.caret_pos())
render_text_->SetSelection(model);
else
render_text_->SelectRange(gfx::Range(next, model.caret_pos()));
}
// Edit history is recorded in InsertText.
InsertTextInternal(new_text, mergeable);
}
void TextfieldModel::ClearRedoHistory() {
if (edit_history_.begin() == edit_history_.end())
return;
if (current_edit_ == edit_history_.end()) {
ClearEditHistory();
return;
}
auto delete_start = current_edit_;
++delete_start;
edit_history_.erase(delete_start, edit_history_.end());
}
void TextfieldModel::ExecuteAndRecordDelete(std::vector<gfx::Range> ranges,
bool mergeable) {
// We need only check replacement_ranges[0] as |delete_backwards_| is
// irrelevant for multi-range deletions which can't be merged anyways.
const bool backward = ranges[0].is_reversed();
order_ranges(&ranges);
std::vector<base::string16> old_texts;
std::vector<size_t> old_text_starts;
for (const auto& range : ranges) {
old_texts.push_back(GetTextFromRange(range));
old_text_starts.push_back(range.GetMin());
}
size_t cursor_pos = adjust_position_for_removals(GetCursorPosition(), ranges);
auto edit = std::make_unique<internal::DeleteEdit>(
mergeable, old_texts, old_text_starts, render_text_->selection(),
render_text_->secondary_selections(), backward, cursor_pos);
edit->Redo(this);
AddOrMergeEditHistory(std::move(edit));
}
void TextfieldModel::ExecuteAndRecordReplaceSelection(
internal::MergeType merge_type,
const base::string16& new_text) {
auto replacement_ranges = render_text_->GetAllSelections();
size_t new_text_start =
adjust_position_for_removals(GetCursorPosition(), replacement_ranges);
size_t new_cursor_pos = new_text_start + new_text.length();
ExecuteAndRecordReplace(merge_type, replacement_ranges, new_cursor_pos,
new_text, new_text_start);
}
void TextfieldModel::ExecuteAndRecordReplace(
internal::MergeType merge_type,
std::vector<gfx::Range> replacement_ranges,
size_t new_cursor_pos,
const base::string16& new_text,
size_t new_text_start) {
// We need only check replacement_ranges[0] as |delete_backwards_| is
// irrelevant for multi-range deletions which can't be merged anyways.
const bool backward = replacement_ranges[0].is_reversed();
order_ranges(&replacement_ranges);
std::vector<base::string16> old_texts;
std::vector<size_t> old_text_starts;
for (const auto& range : replacement_ranges) {
old_texts.push_back(GetTextFromRange(range));
old_text_starts.push_back(range.GetMin());
}
auto edit = std::make_unique<internal::ReplaceEdit>(
merge_type, old_texts, old_text_starts, render_text_->selection(),
render_text_->secondary_selections(), backward, new_cursor_pos, new_text,
new_text_start);
edit->Redo(this);
AddOrMergeEditHistory(std::move(edit));
}
void TextfieldModel::ExecuteAndRecordInsert(const base::string16& new_text,
bool mergeable) {
auto edit = std::make_unique<internal::InsertEdit>(mergeable, new_text,
GetCursorPosition());
edit->Redo(this);
AddOrMergeEditHistory(std::move(edit));
}
void TextfieldModel::AddOrMergeEditHistory(
std::unique_ptr<internal::Edit> edit) {
ClearRedoHistory();
if (current_edit_ != edit_history_.end() &&
(*current_edit_)->Merge(edit.get())) {
// If the new edit was successfully merged with an old one, don't add it to
// the history.
return;
}
edit_history_.push_back(std::move(edit));
if (current_edit_ == edit_history_.end()) {
// If there is no redoable edit, this is the 1st edit because RedoHistory
// has been already deleted.
DCHECK_EQ(1u, edit_history_.size());
current_edit_ = edit_history_.begin();
} else {
++current_edit_;
}
}
void TextfieldModel::ModifyText(
const std::vector<gfx::Range>& deletions,
const std::vector<base::string16>& insertion_texts,
const std::vector<size_t>& insertion_positions,
const gfx::Range& primary_selection,
const std::vector<gfx::Range>& secondary_selections) {
DCHECK_EQ(insertion_texts.size(), insertion_positions.size());
base::string16 old_text = text();
ClearComposition();
for (auto deletion : deletions)
old_text.erase(deletion.start(), deletion.length());
for (size_t i = 0; i < insertion_texts.size(); ++i)
old_text.insert(insertion_positions[i], insertion_texts[i]);
SetRenderTextText(old_text);
if (primary_selection.start() == primary_selection.end())
render_text_->SetCursorPosition(primary_selection.start());
else
render_text_->SelectRange(primary_selection);
for (auto secondary_selection : secondary_selections)
render_text_->SelectRange(secondary_selection, false);
}
void TextfieldModel::SetRenderTextText(const base::string16& text) {
render_text_->SetText(text);
if (delegate_)
delegate_->OnTextChanged();
}
// static
void TextfieldModel::ClearKillBuffer() {
SetKillBuffer(base::string16());
}
} // namespace views