| // Copyright 2017 The PDFium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com | |
| #include "xfa/fde/cfde_texteditengine.h" | |
| #include <algorithm> | |
| #include <utility> | |
| #include "core/fxcrt/check.h" | |
| #include "core/fxcrt/check_op.h" | |
| #include "core/fxcrt/compiler_specific.h" | |
| #include "core/fxcrt/fx_extension.h" | |
| #include "core/fxcrt/numerics/safe_conversions.h" | |
| #include "core/fxcrt/stl_util.h" | |
| #include "core/fxge/text_char_pos.h" | |
| #include "xfa/fde/cfde_textout.h" | |
| #include "xfa/fde/cfde_wordbreak_data.h" | |
| #include "xfa/fgas/font/cfgas_gefont.h" | |
| namespace pdfium { | |
| namespace { | |
| class InsertOperation final : public CFDE_TextEditEngine::Operation { | |
| public: | |
| InsertOperation(CFDE_TextEditEngine* engine, | |
| size_t start_idx, | |
| const WideString& added_text) | |
| : engine_(engine), start_idx_(start_idx), added_text_(added_text) {} | |
| ~InsertOperation() override = default; | |
| void Redo() const override { | |
| engine_->Insert(start_idx_, added_text_, | |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); | |
| } | |
| void Undo() const override { | |
| engine_->Delete(start_idx_, added_text_.GetLength(), | |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); | |
| } | |
| private: | |
| UnownedPtr<CFDE_TextEditEngine> engine_; | |
| size_t start_idx_; | |
| WideString added_text_; | |
| }; | |
| class DeleteOperation final : public CFDE_TextEditEngine::Operation { | |
| public: | |
| DeleteOperation(CFDE_TextEditEngine* engine, | |
| size_t start_idx, | |
| const WideString& removed_text) | |
| : engine_(engine), start_idx_(start_idx), removed_text_(removed_text) {} | |
| ~DeleteOperation() override = default; | |
| void Redo() const override { | |
| engine_->Delete(start_idx_, removed_text_.GetLength(), | |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); | |
| } | |
| void Undo() const override { | |
| engine_->Insert(start_idx_, removed_text_, | |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); | |
| } | |
| private: | |
| UnownedPtr<CFDE_TextEditEngine> engine_; | |
| size_t start_idx_; | |
| WideString removed_text_; | |
| }; | |
| class ReplaceOperation final : public CFDE_TextEditEngine::Operation { | |
| public: | |
| ReplaceOperation(CFDE_TextEditEngine* engine, | |
| size_t start_idx, | |
| const WideString& removed_text, | |
| const WideString& added_text) | |
| : insert_op_(engine, start_idx, added_text), | |
| delete_op_(engine, start_idx, removed_text) {} | |
| ~ReplaceOperation() override = default; | |
| void Redo() const override { | |
| delete_op_.Redo(); | |
| insert_op_.Redo(); | |
| } | |
| void Undo() const override { | |
| insert_op_.Undo(); | |
| delete_op_.Undo(); | |
| } | |
| private: | |
| InsertOperation insert_op_; | |
| DeleteOperation delete_op_; | |
| }; | |
| int GetBreakFlagsFor(WordBreakProperty current, WordBreakProperty next) { | |
| if (current == WordBreakProperty::kMidLetter) { | |
| if (next == WordBreakProperty::kALetter) { | |
| return 1; | |
| } | |
| } else if (current == WordBreakProperty::kMidNum) { | |
| if (next == WordBreakProperty::kNumeric) { | |
| return 2; | |
| } | |
| } else if (current == WordBreakProperty::kMidNumLet) { | |
| if (next == WordBreakProperty::kALetter) { | |
| return 1; | |
| } | |
| if (next == WordBreakProperty::kNumeric) { | |
| return 2; | |
| } | |
| } | |
| return 0; | |
| } | |
| bool BreakFlagsChanged(int flags, WordBreakProperty previous) { | |
| return (flags != 1 || previous != WordBreakProperty::kALetter) && | |
| (flags != 2 || previous != WordBreakProperty::kNumeric); | |
| } | |
| } // namespace | |
| CFDE_TextEditEngine::CFDE_TextEditEngine() { | |
| content_.resize(gap_size_); | |
| operation_buffer_.resize(max_edit_operations_); | |
| text_break_.SetFontSize(font_size_); | |
| text_break_.SetLineBreakTolerance(2.0f); | |
| text_break_.SetTabWidth(36); | |
| } | |
| CFDE_TextEditEngine::~CFDE_TextEditEngine() = default; | |
| void CFDE_TextEditEngine::Clear() { | |
| text_length_ = 0; | |
| gap_position_ = 0; | |
| gap_size_ = kGapSize; | |
| content_.clear(); | |
| content_.resize(gap_size_); | |
| ClearSelection(); | |
| ClearOperationRecords(); | |
| } | |
| void CFDE_TextEditEngine::SetMaxEditOperationsForTesting(size_t max) { | |
| max_edit_operations_ = max; | |
| operation_buffer_.resize(max); | |
| ClearOperationRecords(); | |
| } | |
| void CFDE_TextEditEngine::AdjustGap(size_t idx, size_t length) { | |
| static const size_t char_size = sizeof(WideString::CharType); | |
| // Move the gap, if necessary. | |
| if (idx < gap_position_) { | |
| UNSAFE_TODO(FXSYS_memmove(content_.data() + idx + gap_size_, | |
| content_.data() + idx, | |
| (gap_position_ - idx) * char_size)); | |
| gap_position_ = idx; | |
| } else if (idx > gap_position_) { | |
| UNSAFE_TODO(FXSYS_memmove(content_.data() + gap_position_, | |
| content_.data() + gap_position_ + gap_size_, | |
| (idx - gap_position_) * char_size)); | |
| gap_position_ = idx; | |
| } | |
| // If the gap is too small, make it bigger. | |
| if (length >= gap_size_) { | |
| size_t new_gap_size = length + kGapSize; | |
| content_.resize(text_length_ + new_gap_size); | |
| UNSAFE_TODO(FXSYS_memmove(content_.data() + gap_position_ + new_gap_size, | |
| content_.data() + gap_position_ + gap_size_, | |
| (text_length_ - gap_position_) * char_size)); | |
| gap_size_ = new_gap_size; | |
| } | |
| } | |
| size_t CFDE_TextEditEngine::CountCharsExceedingSize(const WideString& text, | |
| size_t num_to_check) { | |
| if (!limit_horizontal_area_ && !limit_vertical_area_) { | |
| return 0; | |
| } | |
| auto text_out = std::make_unique<CFDE_TextOut>(); | |
| text_out->SetLineSpace(line_spacing_); | |
| text_out->SetFont(font_); | |
| text_out->SetFontSize(font_size_); | |
| FDE_TextStyle style; | |
| style.single_line_ = !is_multiline_; | |
| CFX_RectF text_rect; | |
| if (is_linewrap_enabled_) { | |
| style.line_wrap_ = true; | |
| text_rect.width = available_width_; | |
| } else { | |
| text_rect.width = kPageWidthMax; | |
| } | |
| text_out->SetStyles(style); | |
| size_t length = text.GetLength(); | |
| WideStringView temp = text.AsStringView(); | |
| float vertical_height = line_spacing_ * visible_line_count_; | |
| size_t chars_exceeding_size = 0; | |
| // TODO(dsinclair): Can this get changed to a binary search? | |
| for (size_t i = 0; i < num_to_check; i++) { | |
| text_out->CalcLogicSize(temp, &text_rect); | |
| if (limit_horizontal_area_ && text_rect.width <= available_width_) { | |
| break; | |
| } | |
| if (limit_vertical_area_ && text_rect.height <= vertical_height) { | |
| break; | |
| } | |
| ++chars_exceeding_size; | |
| --length; | |
| temp = temp.First(length); | |
| } | |
| return chars_exceeding_size; | |
| } | |
| void CFDE_TextEditEngine::Insert(size_t idx, | |
| const WideString& request_text, | |
| RecordOperation add_operation) { | |
| WideString text = request_text; | |
| if (text.IsEmpty()) { | |
| return; | |
| } | |
| idx = std::min(idx, text_length_); | |
| TextChange change; | |
| change.selection_start = idx; | |
| change.selection_end = idx; | |
| change.text = text; | |
| change.previous_text = GetText(); | |
| change.cancelled = false; | |
| if (delegate_ && (add_operation != RecordOperation::kSkipRecord && | |
| add_operation != RecordOperation::kSkipNotify)) { | |
| delegate_->OnTextWillChange(&change); | |
| if (change.cancelled) { | |
| return; | |
| } | |
| text = change.text; | |
| idx = change.selection_start; | |
| // Delegate extended the selection, so delete it before we insert. | |
| if (change.selection_end != change.selection_start) { | |
| DeleteSelectedText(RecordOperation::kSkipRecord); | |
| } | |
| // Delegate may have changed text entirely, recheck. | |
| idx = std::min(idx, text_length_); | |
| } | |
| size_t length = text.GetLength(); | |
| if (length == 0) { | |
| return; | |
| } | |
| // If we're going to be too big we insert what we can and notify the | |
| // delegate we've filled the text after the insert is done. | |
| bool exceeded_limit = false; | |
| // Currently we allow inserting a number of characters over the text limit if | |
| // we're skipping notify. This means we're setting the formatted text into the | |
| // engine. Otherwise, if you enter 123456789 for an SSN into a field | |
| // with a 9 character limit and we reformat to 123-45-6789 we'll truncate | |
| // the 89 when inserting into the text edit. See https://crbug.com/pdfium/1089 | |
| if (has_character_limit_ && text_length_ + length > character_limit_) { | |
| if (add_operation == RecordOperation::kSkipNotify) { | |
| // Raise the limit to allow subsequent changes to expanded text. | |
| character_limit_ = text_length_ + length; | |
| } else { | |
| // Truncate the text to comply with the limit. | |
| CHECK(text_length_ <= character_limit_); | |
| length = character_limit_ - text_length_; | |
| exceeded_limit = true; | |
| } | |
| } | |
| AdjustGap(idx, length); | |
| if (validation_enabled_ || limit_horizontal_area_ || limit_vertical_area_) { | |
| WideString str; | |
| if (gap_position_ > 0) { | |
| str += WideStringView(pdfium::span(content_).first(gap_position_)); | |
| } | |
| str += text; | |
| if (text_length_ - gap_position_ > 0) { | |
| str += WideStringView(pdfium::span(content_).subspan( | |
| gap_position_ + gap_size_, text_length_ - gap_position_)); | |
| } | |
| if (validation_enabled_ && delegate_ && !delegate_->OnValidate(str)) { | |
| // TODO(dsinclair): Notify delegate of validation failure? | |
| return; | |
| } | |
| // Check if we've limited the horizontal/vertical area, and if so determine | |
| // how many of our characters would be outside the area. | |
| size_t chars_exceeding = CountCharsExceedingSize(str, length); | |
| if (chars_exceeding > 0) { | |
| // If none of the characters will fit, notify and exit. | |
| if (chars_exceeding == length) { | |
| if (delegate_) { | |
| delegate_->NotifyTextFull(); | |
| } | |
| return; | |
| } | |
| // Some, but not all, chars will fit, insert them and then notify | |
| // we're full. | |
| exceeded_limit = true; | |
| length -= chars_exceeding; | |
| } | |
| } | |
| if (add_operation == RecordOperation::kInsertRecord) { | |
| AddOperationRecord( | |
| std::make_unique<InsertOperation>(this, gap_position_, text)); | |
| } | |
| WideString previous_text; | |
| if (delegate_) { | |
| previous_text = GetText(); | |
| } | |
| // Copy the new text into the gap. | |
| fxcrt::Copy(text.span().first(length), | |
| pdfium::span(content_).subspan(gap_position_)); | |
| gap_position_ += length; | |
| gap_size_ -= length; | |
| text_length_ += length; | |
| is_dirty_ = true; | |
| // Inserting text resets the selection. | |
| ClearSelection(); | |
| if (delegate_) { | |
| if (exceeded_limit) { | |
| delegate_->NotifyTextFull(); | |
| } | |
| delegate_->OnTextChanged(); | |
| } | |
| } | |
| void CFDE_TextEditEngine::AddOperationRecord(std::unique_ptr<Operation> op) { | |
| size_t last_insert_position = next_operation_index_to_insert_ == 0 | |
| ? max_edit_operations_ - 1 | |
| : next_operation_index_to_insert_ - 1; | |
| // If our undo record is not the last thing we inserted then we need to | |
| // remove all the undo records between our insert position and the undo marker | |
| // and make that our new insert position. | |
| if (next_operation_index_to_undo_ != last_insert_position) { | |
| if (next_operation_index_to_undo_ > last_insert_position) { | |
| // Our Undo position is ahead of us, which means we need to clear out the | |
| // head of the queue. | |
| while (last_insert_position != 0) { | |
| operation_buffer_[last_insert_position].reset(); | |
| --last_insert_position; | |
| } | |
| operation_buffer_[0].reset(); | |
| // Moving this will let us then clear out the end, setting the undo | |
| // position to before the insert position. | |
| last_insert_position = max_edit_operations_ - 1; | |
| } | |
| // Clear out the vector from undo position to our set insert position. | |
| while (next_operation_index_to_undo_ != last_insert_position) { | |
| operation_buffer_[last_insert_position].reset(); | |
| --last_insert_position; | |
| } | |
| } | |
| // We're now pointing at the next thing we want to Undo, so insert at the | |
| // next position in the queue. | |
| ++last_insert_position; | |
| if (last_insert_position >= max_edit_operations_) { | |
| last_insert_position = 0; | |
| } | |
| operation_buffer_[last_insert_position] = std::move(op); | |
| next_operation_index_to_insert_ = | |
| (last_insert_position + 1) % max_edit_operations_; | |
| next_operation_index_to_undo_ = last_insert_position; | |
| } | |
| void CFDE_TextEditEngine::ClearOperationRecords() { | |
| for (auto& record : operation_buffer_) { | |
| record.reset(); | |
| } | |
| next_operation_index_to_undo_ = max_edit_operations_ - 1; | |
| next_operation_index_to_insert_ = 0; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexLeft(size_t pos) const { | |
| if (pos == 0) { | |
| return 0; | |
| } | |
| --pos; | |
| while (pos != 0) { | |
| // We want to be on the location just before the \r or \n | |
| wchar_t ch = GetChar(pos - 1); | |
| if (ch != '\r' && ch != '\n') { | |
| break; | |
| } | |
| --pos; | |
| } | |
| return pos; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexRight(size_t pos) const { | |
| if (pos >= text_length_) { | |
| return text_length_; | |
| } | |
| ++pos; | |
| wchar_t ch = GetChar(pos); | |
| // We want to be on the location after the \r\n. | |
| while (pos < text_length_ && (ch == '\r' || ch == '\n')) { | |
| ++pos; | |
| ch = GetChar(pos); | |
| } | |
| return pos; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexUp(size_t pos) const { | |
| size_t line_start = GetIndexAtStartOfLine(pos); | |
| if (line_start == 0) { | |
| return pos; | |
| } | |
| // Determine how far along the line we were. | |
| size_t dist = pos - line_start; | |
| // Move to the end of the preceding line. | |
| wchar_t ch; | |
| do { | |
| --line_start; | |
| ch = GetChar(line_start); | |
| } while (line_start != 0 && (ch == '\r' || ch == '\n')); | |
| if (line_start == 0) { | |
| return dist; | |
| } | |
| // Get the start of the line prior to the current line. | |
| size_t prior_start = GetIndexAtStartOfLine(line_start); | |
| // Prior line is shorter then next line, and we're past the end of that line | |
| // return the end of line. | |
| if (prior_start + dist > line_start) { | |
| return GetIndexAtEndOfLine(line_start); | |
| } | |
| return prior_start + dist; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexDown(size_t pos) const { | |
| size_t line_end = GetIndexAtEndOfLine(pos); | |
| if (line_end == text_length_) { | |
| return pos; | |
| } | |
| wchar_t ch; | |
| do { | |
| ++line_end; | |
| ch = GetChar(line_end); | |
| } while (line_end < text_length_ && (ch == '\r' || ch == '\n')); | |
| if (line_end == text_length_) { | |
| return line_end; | |
| } | |
| // Determine how far along the line we are. | |
| size_t dist = pos - GetIndexAtStartOfLine(pos); | |
| // Check if next line is shorter then current line. If so, return end | |
| // of next line. | |
| size_t next_line_end = GetIndexAtEndOfLine(line_end); | |
| if (line_end + dist > next_line_end) { | |
| return next_line_end; | |
| } | |
| return line_end + dist; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexAtStartOfLine(size_t pos) const { | |
| if (pos == 0) { | |
| return 0; | |
| } | |
| wchar_t ch = GetChar(pos); | |
| // What to do. | |
| if (ch == '\r' || ch == '\n') { | |
| return pos; | |
| } | |
| do { | |
| // We want to be on the location just after the \r\n | |
| ch = GetChar(pos - 1); | |
| if (ch == '\r' || ch == '\n') { | |
| break; | |
| } | |
| --pos; | |
| } while (pos > 0); | |
| return pos; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexAtEndOfLine(size_t pos) const { | |
| if (pos >= text_length_) { | |
| return text_length_; | |
| } | |
| wchar_t ch = GetChar(pos); | |
| // Not quite sure which way to go here? | |
| if (ch == '\r' || ch == '\n') { | |
| return pos; | |
| } | |
| // We want to be on the location of the first \r or \n. | |
| do { | |
| ++pos; | |
| ch = GetChar(pos); | |
| } while (pos < text_length_ && (ch != '\r' && ch != '\n')); | |
| return pos; | |
| } | |
| void CFDE_TextEditEngine::LimitHorizontalScroll(bool val) { | |
| ClearOperationRecords(); | |
| limit_horizontal_area_ = val; | |
| } | |
| void CFDE_TextEditEngine::LimitVerticalScroll(bool val) { | |
| ClearOperationRecords(); | |
| limit_vertical_area_ = val; | |
| } | |
| bool CFDE_TextEditEngine::CanUndo() const { | |
| return operation_buffer_[next_operation_index_to_undo_] != nullptr && | |
| next_operation_index_to_undo_ != next_operation_index_to_insert_; | |
| } | |
| bool CFDE_TextEditEngine::CanRedo() const { | |
| size_t idx = (next_operation_index_to_undo_ + 1) % max_edit_operations_; | |
| return idx != next_operation_index_to_insert_ && | |
| operation_buffer_[idx] != nullptr; | |
| } | |
| bool CFDE_TextEditEngine::Redo() { | |
| if (!CanRedo()) { | |
| return false; | |
| } | |
| next_operation_index_to_undo_ = | |
| (next_operation_index_to_undo_ + 1) % max_edit_operations_; | |
| operation_buffer_[next_operation_index_to_undo_]->Redo(); | |
| return true; | |
| } | |
| bool CFDE_TextEditEngine::Undo() { | |
| if (!CanUndo()) { | |
| return false; | |
| } | |
| operation_buffer_[next_operation_index_to_undo_]->Undo(); | |
| next_operation_index_to_undo_ = next_operation_index_to_undo_ == 0 | |
| ? max_edit_operations_ - 1 | |
| : next_operation_index_to_undo_ - 1; | |
| return true; | |
| } | |
| void CFDE_TextEditEngine::Layout() { | |
| if (!is_dirty_) { | |
| return; | |
| } | |
| is_dirty_ = false; | |
| RebuildPieces(); | |
| } | |
| CFX_RectF CFDE_TextEditEngine::GetContentsBoundingBox() { | |
| // Layout if necessary. | |
| Layout(); | |
| return contents_bounding_box_; | |
| } | |
| void CFDE_TextEditEngine::SetAvailableWidth(size_t width) { | |
| if (width == available_width_) { | |
| return; | |
| } | |
| ClearOperationRecords(); | |
| available_width_ = width; | |
| if (is_linewrap_enabled_) { | |
| text_break_.SetLineWidth(width); | |
| } | |
| if (is_comb_text_) { | |
| SetCombTextWidth(); | |
| } | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetHasCharacterLimit(bool limit) { | |
| if (has_character_limit_ == limit) { | |
| return; | |
| } | |
| has_character_limit_ = limit; | |
| character_limit_ = std::max(character_limit_, text_length_); | |
| if (is_comb_text_) { | |
| SetCombTextWidth(); | |
| } | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetCharacterLimit(size_t limit) { | |
| if (character_limit_ == limit) { | |
| return; | |
| } | |
| ClearOperationRecords(); | |
| character_limit_ = std::max(limit, text_length_); | |
| if (is_comb_text_) { | |
| SetCombTextWidth(); | |
| } | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetFont(RetainPtr<CFGAS_GEFont> font) { | |
| if (font_ == font) { | |
| return; | |
| } | |
| font_ = std::move(font); | |
| text_break_.SetFont(font_); | |
| is_dirty_ = true; | |
| } | |
| RetainPtr<CFGAS_GEFont> CFDE_TextEditEngine::GetFont() const { | |
| return font_; | |
| } | |
| void CFDE_TextEditEngine::SetFontSize(float size) { | |
| if (font_size_ == size) { | |
| return; | |
| } | |
| font_size_ = size; | |
| text_break_.SetFontSize(font_size_); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetTabWidth(float width) { | |
| int32_t old_tab_width = text_break_.GetTabWidth(); | |
| text_break_.SetTabWidth(width); | |
| if (old_tab_width == text_break_.GetTabWidth()) { | |
| return; | |
| } | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetAlignment(uint32_t alignment) { | |
| if (alignment == character_alignment_) { | |
| return; | |
| } | |
| character_alignment_ = alignment; | |
| text_break_.SetAlignment(alignment); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetVisibleLineCount(size_t count) { | |
| if (visible_line_count_ == count) { | |
| return; | |
| } | |
| visible_line_count_ = std::max(static_cast<size_t>(1), count); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::EnableMultiLine(bool val) { | |
| if (is_multiline_ == val) { | |
| return; | |
| } | |
| is_multiline_ = val; | |
| Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles(); | |
| if (is_multiline_) { | |
| style.Clear(CFGAS_Break::LayoutStyle::kSingleLine); | |
| } else { | |
| style |= CFGAS_Break::LayoutStyle::kSingleLine; | |
| } | |
| text_break_.SetLayoutStyles(style); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::EnableLineWrap(bool val) { | |
| if (is_linewrap_enabled_ == val) { | |
| return; | |
| } | |
| is_linewrap_enabled_ = val; | |
| text_break_.SetLineWidth(is_linewrap_enabled_ ? available_width_ | |
| : kPageWidthMax); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetCombText(bool enable) { | |
| if (is_comb_text_ == enable) { | |
| return; | |
| } | |
| is_comb_text_ = enable; | |
| Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles(); | |
| if (enable) { | |
| style |= CFGAS_Break::LayoutStyle::kCombText; | |
| SetCombTextWidth(); | |
| } else { | |
| style.Clear(CFGAS_Break::LayoutStyle::kCombText); | |
| } | |
| text_break_.SetLayoutStyles(style); | |
| is_dirty_ = true; | |
| } | |
| void CFDE_TextEditEngine::SetCombTextWidth() { | |
| size_t width = available_width_; | |
| if (has_character_limit_) { | |
| width /= character_limit_; | |
| } | |
| text_break_.SetCombWidth(width); | |
| } | |
| void CFDE_TextEditEngine::SelectAll() { | |
| if (text_length_ == 0) { | |
| return; | |
| } | |
| has_selection_ = true; | |
| selection_.start_idx = 0; | |
| selection_.count = text_length_; | |
| } | |
| void CFDE_TextEditEngine::ClearSelection() { | |
| has_selection_ = false; | |
| selection_.start_idx = 0; | |
| selection_.count = 0; | |
| } | |
| void CFDE_TextEditEngine::SetSelection(size_t start_idx, size_t count) { | |
| if (count == 0) { | |
| ClearSelection(); | |
| return; | |
| } | |
| if (start_idx > text_length_) { | |
| return; | |
| } | |
| if (start_idx + count > text_length_) { | |
| count = text_length_ - start_idx; | |
| } | |
| has_selection_ = true; | |
| selection_.start_idx = start_idx; | |
| selection_.count = count; | |
| } | |
| WideString CFDE_TextEditEngine::GetSelectedText() const { | |
| if (!has_selection_) { | |
| return WideString(); | |
| } | |
| WideString text; | |
| if (selection_.start_idx < gap_position_) { | |
| // Fully on left of gap. | |
| if (selection_.start_idx + selection_.count < gap_position_) { | |
| text += WideStringView(pdfium::span(content_).subspan( | |
| selection_.start_idx, selection_.count)); | |
| return text; | |
| } | |
| // Pre-gap text | |
| text += WideStringView(pdfium::span(content_).subspan( | |
| selection_.start_idx, gap_position_ - selection_.start_idx)); | |
| if (selection_.count - (gap_position_ - selection_.start_idx) > 0) { | |
| // Post-gap text | |
| text += WideStringView(pdfium::span(content_).subspan( | |
| gap_position_ + gap_size_, | |
| selection_.count - (gap_position_ - selection_.start_idx))); | |
| } | |
| return text; | |
| } | |
| // Fully right of gap | |
| text += WideStringView(pdfium::span(content_).subspan( | |
| gap_size_ + selection_.start_idx, selection_.count)); | |
| return text; | |
| } | |
| WideString CFDE_TextEditEngine::DeleteSelectedText( | |
| RecordOperation add_operation) { | |
| if (!has_selection_) { | |
| return WideString(); | |
| } | |
| return Delete(selection_.start_idx, selection_.count, add_operation); | |
| } | |
| WideString CFDE_TextEditEngine::Delete(size_t start_idx, | |
| size_t length, | |
| RecordOperation add_operation) { | |
| if (start_idx >= text_length_) { | |
| return WideString(); | |
| } | |
| TextChange change; | |
| change.text.clear(); | |
| change.cancelled = false; | |
| if (delegate_ && (add_operation != RecordOperation::kSkipRecord && | |
| add_operation != RecordOperation::kSkipNotify)) { | |
| change.previous_text = GetText(); | |
| change.selection_start = start_idx; | |
| change.selection_end = start_idx + length; | |
| delegate_->OnTextWillChange(&change); | |
| if (change.cancelled) { | |
| return WideString(); | |
| } | |
| // Delegate may have changed the selection range. | |
| start_idx = change.selection_start; | |
| length = change.selection_end - change.selection_start; | |
| // Delegate may have changed text entirely, recheck. | |
| if (start_idx >= text_length_) { | |
| return WideString(); | |
| } | |
| } | |
| length = std::min(length, text_length_ - start_idx); | |
| AdjustGap(start_idx + length, 0); | |
| WideString ret( | |
| WideStringView(pdfium::span(content_).subspan(start_idx, length))); | |
| if (add_operation == RecordOperation::kInsertRecord) { | |
| AddOperationRecord(std::make_unique<DeleteOperation>(this, start_idx, ret)); | |
| } | |
| WideString previous_text = GetText(); | |
| gap_position_ = start_idx; | |
| gap_size_ += length; | |
| text_length_ -= length; | |
| is_dirty_ = true; | |
| ClearSelection(); | |
| // The JS requested the insertion of text instead of just a deletion. | |
| if (!change.text.IsEmpty()) { | |
| Insert(start_idx, change.text, RecordOperation::kSkipRecord); | |
| } | |
| if (delegate_) { | |
| delegate_->OnTextChanged(); | |
| } | |
| return ret; | |
| } | |
| void CFDE_TextEditEngine::ReplaceSelectedText(const WideString& requested_rep) { | |
| WideString rep = requested_rep; | |
| if (delegate_) { | |
| TextChange change; | |
| change.selection_start = selection_.start_idx; | |
| change.selection_end = selection_.start_idx + selection_.count; | |
| change.text = rep; | |
| change.previous_text = GetText(); | |
| change.cancelled = false; | |
| delegate_->OnTextWillChange(&change); | |
| if (change.cancelled) { | |
| return; | |
| } | |
| rep = change.text; | |
| selection_.start_idx = change.selection_start; | |
| selection_.count = change.selection_end - change.selection_start; | |
| } | |
| size_t start_idx = selection_.start_idx; | |
| WideString txt = DeleteSelectedText(RecordOperation::kSkipRecord); | |
| Insert(gap_position_, rep, RecordOperation::kSkipRecord); | |
| AddOperationRecord( | |
| std::make_unique<ReplaceOperation>(this, start_idx, txt, rep)); | |
| } | |
| WideString CFDE_TextEditEngine::GetText() const { | |
| WideString str; | |
| if (gap_position_ > 0) { | |
| str += WideStringView(pdfium::span(content_).first(gap_position_)); | |
| } | |
| if (text_length_ - gap_position_ > 0) { | |
| str += WideStringView(pdfium::span(content_).subspan( | |
| gap_position_ + gap_size_, text_length_ - gap_position_)); | |
| } | |
| return str; | |
| } | |
| size_t CFDE_TextEditEngine::GetLength() const { | |
| return text_length_; | |
| } | |
| wchar_t CFDE_TextEditEngine::GetChar(size_t idx) const { | |
| if (idx >= text_length_) { | |
| return L'\0'; | |
| } | |
| if (password_mode_) { | |
| return password_alias_; | |
| } | |
| return idx < gap_position_ | |
| ? content_[idx] | |
| : content_[gap_position_ + gap_size_ + (idx - gap_position_)]; | |
| } | |
| int32_t CFDE_TextEditEngine::GetWidthOfChar(size_t idx) { | |
| // Recalculate the widths if necessary. | |
| Layout(); | |
| return idx < char_widths_.size() ? char_widths_[idx] : 0; | |
| } | |
| size_t CFDE_TextEditEngine::GetIndexForPoint(const CFX_PointF& point) { | |
| // Recalculate the widths if necessary. | |
| Layout(); | |
| auto start_it = text_piece_info_.begin(); | |
| for (; start_it < text_piece_info_.end(); ++start_it) { | |
| if (start_it->rtPiece.top <= point.y && | |
| point.y < start_it->rtPiece.bottom()) { | |
| break; | |
| } | |
| } | |
| // We didn't find the point before getting to the end of the text, return | |
| // end of text. | |
| if (start_it == text_piece_info_.end()) { | |
| return text_length_; | |
| } | |
| auto end_it = start_it; | |
| for (; end_it < text_piece_info_.end(); ++end_it) { | |
| // We've moved past where the point should be and didn't find anything. | |
| // Return the start of the current piece as the location. | |
| if (end_it->rtPiece.bottom() <= point.y || point.y < end_it->rtPiece.top) { | |
| break; | |
| } | |
| } | |
| // Make sure the end iterator is pointing to our text pieces. | |
| if (end_it == text_piece_info_.end()) { | |
| --end_it; | |
| } | |
| size_t start_it_idx = start_it->nStart; | |
| for (; start_it <= end_it; ++start_it) { | |
| bool piece_contains_point_vertically = | |
| (point.y >= start_it->rtPiece.top && | |
| point.y < start_it->rtPiece.bottom()); | |
| if (!piece_contains_point_vertically) { | |
| continue; | |
| } | |
| std::vector<CFX_RectF> rects = GetCharRects(*start_it); | |
| for (size_t i = 0; i < rects.size(); ++i) { | |
| bool character_contains_point_horizontally = | |
| (point.x >= rects[i].left && point.x < rects[i].right()); | |
| if (!character_contains_point_horizontally) { | |
| continue; | |
| } | |
| // When clicking on the left half of a character, the cursor should be | |
| // moved before it. If the click was on the right half of that character, | |
| // move the cursor after it. | |
| bool closer_to_left = | |
| (point.x - rects[i].left < rects[i].right() - point.x); | |
| size_t caret_pos = closer_to_left ? i : i + 1; | |
| size_t pos = start_it->nStart + caret_pos; | |
| if (pos >= text_length_) { | |
| return text_length_; | |
| } | |
| wchar_t wch = GetChar(pos); | |
| if (wch == L'\n' || wch == L'\r') { | |
| if (wch == L'\n' && pos > 0 && GetChar(pos - 1) == L'\r') { | |
| --pos; | |
| } | |
| return pos; | |
| } | |
| // TODO(dsinclair): Old code had a before flag set based on bidi? | |
| return pos; | |
| } | |
| // Point is not within the horizontal range of any characters, it's | |
| // afterwards. Return the position after the last character. | |
| // The last line has nCount equal to the number of characters + 1 (sentinel | |
| // character maybe?). Restrict to the text_length_ to account for that. | |
| size_t pos = std::min( | |
| static_cast<size_t>(start_it->nStart + start_it->nCount), text_length_); | |
| // If the line is not the last one and it was broken right after a breaking | |
| // whitespace (space or line break), the cursor should not be placed after | |
| // the whitespace, but before it. If the cursor is moved after the | |
| // whitespace, it goes to the beginning of the next line. | |
| bool is_last_line = (std::next(start_it) == text_piece_info_.end()); | |
| if (!is_last_line && pos > 0) { | |
| wchar_t previous_char = GetChar(pos - 1); | |
| if (previous_char == L' ' || previous_char == L'\n' || | |
| previous_char == L'\r') { | |
| --pos; | |
| } | |
| } | |
| return pos; | |
| } | |
| if (start_it == text_piece_info_.end()) { | |
| return start_it_idx; | |
| } | |
| if (start_it == end_it) { | |
| return start_it->nStart; | |
| } | |
| // We didn't find the point before going over all of the pieces, we want to | |
| // return the start of the piece after the point. | |
| return end_it->nStart; | |
| } | |
| std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharRects( | |
| const FDE_TEXTEDITPIECE& piece) { | |
| if (piece.nCount < 1) { | |
| return std::vector<CFX_RectF>(); | |
| } | |
| CFGAS_TxtBreak::Run tr; | |
| tr.pEdtEngine = this; | |
| tr.iStart = piece.nStart; | |
| tr.iLength = piece.nCount; | |
| tr.font = font_; | |
| tr.fFontSize = font_size_; | |
| tr.dwStyles = text_break_.GetLayoutStyles(); | |
| tr.dwCharStyles = piece.dwCharStyles; | |
| tr.pRect = &piece.rtPiece; | |
| return text_break_.GetCharRects(tr); | |
| } | |
| std::vector<TextCharPos> CFDE_TextEditEngine::GetDisplayPos( | |
| const FDE_TEXTEDITPIECE& piece) { | |
| if (piece.nCount < 1) { | |
| return std::vector<TextCharPos>(); | |
| } | |
| CFGAS_TxtBreak::Run tr; | |
| tr.pEdtEngine = this; | |
| tr.iStart = piece.nStart; | |
| tr.iLength = piece.nCount; | |
| tr.font = font_; | |
| tr.fFontSize = font_size_; | |
| tr.dwStyles = text_break_.GetLayoutStyles(); | |
| tr.dwCharStyles = piece.dwCharStyles; | |
| tr.pRect = &piece.rtPiece; | |
| std::vector<TextCharPos> data(text_break_.GetDisplayPos(tr, {})); | |
| text_break_.GetDisplayPos(tr, data); | |
| return data; | |
| } | |
| void CFDE_TextEditEngine::RebuildPieces() { | |
| text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph); | |
| text_break_.ClearBreakPieces(); | |
| char_widths_.clear(); | |
| text_piece_info_.clear(); | |
| // Must have a font set in order to break the text. | |
| if (!CanGenerateCharacterInfo()) { | |
| return; | |
| } | |
| bool initialized_bounding_box = false; | |
| contents_bounding_box_ = CFX_RectF(); | |
| size_t current_piece_start = 0; | |
| float current_line_start = 0; | |
| CFDE_TextEditEngine::Iterator iter(this); | |
| while (!iter.IsEOF(false)) { | |
| iter.Next(false); | |
| CFGAS_Char::BreakType break_status = text_break_.AppendChar( | |
| password_mode_ ? password_alias_ : iter.GetChar()); | |
| if (iter.IsEOF(false) && CFX_BreakTypeNoneOrPiece(break_status)) { | |
| break_status = text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph); | |
| } | |
| if (CFX_BreakTypeNoneOrPiece(break_status)) { | |
| continue; | |
| } | |
| int32_t piece_count = text_break_.CountBreakPieces(); | |
| for (int32_t i = 0; i < piece_count; ++i) { | |
| const CFGAS_BreakPiece* piece = text_break_.GetBreakPieceUnstable(i); | |
| FDE_TEXTEDITPIECE txtEdtPiece; | |
| txtEdtPiece.rtPiece.left = piece->GetStartPos() / 20000.0f; | |
| txtEdtPiece.rtPiece.top = current_line_start; | |
| txtEdtPiece.rtPiece.width = piece->GetWidth() / 20000.0f; | |
| txtEdtPiece.rtPiece.height = line_spacing_; | |
| txtEdtPiece.nStart = checked_cast<int32_t>(current_piece_start); | |
| txtEdtPiece.nCount = piece->GetCharCount(); | |
| txtEdtPiece.nBidiLevel = piece->GetBidiLevel(); | |
| txtEdtPiece.dwCharStyles = piece->GetCharStyles(); | |
| if (FX_IsOdd(piece->GetBidiLevel())) { | |
| txtEdtPiece.dwCharStyles |= FX_TXTCHARSTYLE_OddBidiLevel; | |
| } | |
| text_piece_info_.push_back(txtEdtPiece); | |
| if (initialized_bounding_box) { | |
| contents_bounding_box_.Union(txtEdtPiece.rtPiece); | |
| } else { | |
| contents_bounding_box_ = txtEdtPiece.rtPiece; | |
| initialized_bounding_box = true; | |
| } | |
| current_piece_start += txtEdtPiece.nCount; | |
| for (int32_t k = 0; k < txtEdtPiece.nCount; ++k) { | |
| char_widths_.push_back(piece->GetChar(k)->char_width_); | |
| } | |
| } | |
| current_line_start += line_spacing_; | |
| text_break_.ClearBreakPieces(); | |
| } | |
| float delta = 0.0; | |
| bool bounds_smaller = contents_bounding_box_.width < available_width_; | |
| if (IsAlignedRight() && bounds_smaller) { | |
| delta = available_width_ - contents_bounding_box_.width; | |
| } else if (IsAlignedCenter() && bounds_smaller) { | |
| delta = (available_width_ - contents_bounding_box_.width) / 2.0f; | |
| } | |
| if (delta != 0.0) { | |
| float offset = delta - contents_bounding_box_.left; | |
| for (auto& info : text_piece_info_) { | |
| info.rtPiece.Offset(offset, 0.0f); | |
| } | |
| contents_bounding_box_.Offset(offset, 0.0f); | |
| } | |
| // Shrink the last piece down to the font_size. | |
| contents_bounding_box_.height -= line_spacing_ - font_size_; | |
| text_piece_info_.back().rtPiece.height = font_size_; | |
| } | |
| std::pair<int32_t, CFX_RectF> CFDE_TextEditEngine::GetCharacterInfo( | |
| int32_t start_idx) { | |
| DCHECK(start_idx >= 0); | |
| DCHECK(static_cast<size_t>(start_idx) <= text_length_); | |
| // Make sure the current available data is fresh. | |
| Layout(); | |
| auto it = text_piece_info_.begin(); | |
| for (; it != text_piece_info_.end(); ++it) { | |
| if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount) { | |
| break; | |
| } | |
| } | |
| CHECK_NE(it, text_piece_info_.end()); | |
| return {it->nBidiLevel, GetCharRects(*it)[start_idx - it->nStart]}; | |
| } | |
| std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharacterRectsInRange( | |
| int32_t start_idx, | |
| int32_t count) { | |
| // Make sure the current available data is fresh. | |
| Layout(); | |
| auto it = text_piece_info_.begin(); | |
| for (; it != text_piece_info_.end(); ++it) { | |
| if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount) { | |
| break; | |
| } | |
| } | |
| if (it == text_piece_info_.end()) { | |
| return std::vector<CFX_RectF>(); | |
| } | |
| int32_t end_idx = start_idx + count - 1; | |
| std::vector<CFX_RectF> rects; | |
| while (it != text_piece_info_.end()) { | |
| // If we end inside the current piece, extract what we need and we're done. | |
| if (it->nStart <= end_idx && end_idx < it->nStart + it->nCount) { | |
| std::vector<CFX_RectF> arr = GetCharRects(*it); | |
| CFX_RectF piece = arr[0]; | |
| piece.Union(arr[end_idx - it->nStart]); | |
| rects.push_back(piece); | |
| break; | |
| } | |
| rects.push_back(it->rtPiece); | |
| ++it; | |
| } | |
| return rects; | |
| } | |
| std::pair<size_t, size_t> CFDE_TextEditEngine::BoundsForWordAt( | |
| size_t idx) const { | |
| if (idx > text_length_) { | |
| return {0, 0}; | |
| } | |
| CFDE_TextEditEngine::Iterator iter(this); | |
| iter.SetAt(idx); | |
| size_t start_idx = iter.FindNextBreakPos(true); | |
| size_t end_idx = iter.FindNextBreakPos(false); | |
| return {start_idx, end_idx - start_idx + 1}; | |
| } | |
| CFDE_TextEditEngine::Iterator::Iterator(const CFDE_TextEditEngine* engine) | |
| : engine_(engine) {} | |
| CFDE_TextEditEngine::Iterator::~Iterator() = default; | |
| void CFDE_TextEditEngine::Iterator::Next(bool bPrev) { | |
| if (bPrev && current_position_ == -1) { | |
| return; | |
| } | |
| if (!bPrev && current_position_ > -1 && | |
| static_cast<size_t>(current_position_) == engine_->GetLength()) { | |
| return; | |
| } | |
| if (bPrev) { | |
| --current_position_; | |
| } else { | |
| ++current_position_; | |
| } | |
| } | |
| wchar_t CFDE_TextEditEngine::Iterator::GetChar() const { | |
| return engine_->GetChar(current_position_); | |
| } | |
| void CFDE_TextEditEngine::Iterator::SetAt(size_t nIndex) { | |
| nIndex = std::min(nIndex, engine_->GetLength()); | |
| current_position_ = checked_cast<int32_t>(nIndex); | |
| } | |
| bool CFDE_TextEditEngine::Iterator::IsEOF(bool bPrev) const { | |
| return bPrev ? current_position_ == -1 | |
| : current_position_ > -1 && | |
| static_cast<size_t>(current_position_) == | |
| engine_->GetLength(); | |
| } | |
| size_t CFDE_TextEditEngine::Iterator::FindNextBreakPos(bool bPrev) { | |
| if (IsEOF(bPrev)) { | |
| return current_position_ > -1 ? current_position_ : 0; | |
| } | |
| WordBreakProperty ePreType = WordBreakProperty::kNone; | |
| if (!IsEOF(!bPrev)) { | |
| Next(!bPrev); | |
| ePreType = FX_GetWordBreakProperty(GetChar()); | |
| Next(bPrev); | |
| } | |
| WordBreakProperty eCurType = FX_GetWordBreakProperty(GetChar()); | |
| bool bFirst = true; | |
| while (!IsEOF(bPrev)) { | |
| Next(bPrev); | |
| WordBreakProperty eNextType = FX_GetWordBreakProperty(GetChar()); | |
| bool wBreak = FX_CheckStateChangeForWordBreak(eCurType, eNextType); | |
| if (wBreak) { | |
| if (IsEOF(bPrev)) { | |
| Next(!bPrev); | |
| break; | |
| } | |
| if (bFirst) { | |
| int32_t nFlags = GetBreakFlagsFor(eCurType, eNextType); | |
| if (nFlags > 0) { | |
| if (BreakFlagsChanged(nFlags, ePreType)) { | |
| Next(!bPrev); | |
| break; | |
| } | |
| Next(bPrev); | |
| wBreak = false; | |
| } | |
| } | |
| if (wBreak) { | |
| int32_t nFlags = GetBreakFlagsFor(eNextType, eCurType); | |
| if (nFlags <= 0) { | |
| Next(!bPrev); | |
| break; | |
| } | |
| Next(bPrev); | |
| eNextType = FX_GetWordBreakProperty(GetChar()); | |
| if (BreakFlagsChanged(nFlags, eNextType)) { | |
| Next(!bPrev); | |
| Next(!bPrev); | |
| break; | |
| } | |
| } | |
| } | |
| eCurType = eNextType; | |
| bFirst = false; | |
| } | |
| return current_position_ > -1 ? current_position_ : 0; | |
| } | |
| } // namespace pdfium |