Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(309)

Unified Diff: third_party/WebKit/Source/core/editing/suggestion/TextSuggestionController.cpp

Issue 2931443003: Add support for Android spellcheck menu in Chrome/WebViews (Closed)
Patch Set: Use correct base commit Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: third_party/WebKit/Source/core/editing/suggestion/TextSuggestionController.cpp
diff --git a/third_party/WebKit/Source/core/editing/suggestion/TextSuggestionController.cpp b/third_party/WebKit/Source/core/editing/suggestion/TextSuggestionController.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e745601cc29b3eb0945215427a4f852615c031c5
--- /dev/null
+++ b/third_party/WebKit/Source/core/editing/suggestion/TextSuggestionController.cpp
@@ -0,0 +1,352 @@
+// Copyright 2017 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 "core/editing/suggestion/TextSuggestionController.h"
+
+#include "core/editing/EditingUtilities.h"
+#include "core/editing/Editor.h"
+#include "core/editing/FrameSelection.h"
+#include "core/editing/PlainTextRange.h"
+#include "core/editing/Position.h"
+#include "core/editing/markers/DocumentMarkerController.h"
+#include "core/editing/markers/SpellCheckMarker.h"
+#include "core/editing/spellcheck/SpellChecker.h"
+#include "core/frame/FrameView.h"
+#include "core/frame/LocalFrame.h"
+#include "public/platform/InterfaceProvider.h"
+
+namespace blink {
+
+namespace {
+
+bool ShouldDeleteNextCharacter(const Node& marker_text_node,
+ const DocumentMarker& marker) {
+ // If the character immediately following the range to be deleted is a space,
+ // delete it if either of these conditions holds:
+ // - We're deleting at the beginning of the editable text (to avoid ending up
+ // with a space at the beginning)
+ // - The character immediately before the range being deleted is also a space
+ // (to avoid ending up with two adjacent spaces)
+ const EphemeralRange next_character_range =
+ PlainTextRange(marker.EndOffset(), marker.EndOffset() + 1)
+ .CreateRange(*marker_text_node.parentNode());
+ // No character immediately following the range (so it can't be a space)
+ if (next_character_range.IsNull())
+ return false;
+
+ const String next_character_str =
+ PlainText(next_character_range, TextIteratorBehavior::Builder().Build());
+ const UChar next_character = next_character_str[0];
+ // Character immediately following the range is not a space
+ if (next_character != kSpaceCharacter &&
+ next_character != kNoBreakSpaceCharacter)
+ return false;
+
+ // First case: we're deleting at the beginning of the editable text
+ if (marker.StartOffset() == 0)
+ return true;
+
+ const EphemeralRange prev_character_range =
+ PlainTextRange(marker.StartOffset() - 1, marker.StartOffset())
+ .CreateRange(*marker_text_node.parentNode());
+ // Not at beginning, but there's no character immediately before the range
+ // being deleted (so it can't be a space)
+ if (prev_character_range.IsNull())
+ return false;
+
+ const String prev_character_str =
+ PlainText(prev_character_range, TextIteratorBehavior::Builder().Build());
+ // Return true if the character immediately before the range is a space, false
+ // otherwise
+ const UChar prev_character = prev_character_str[0];
+ return prev_character == kSpaceCharacter ||
+ prev_character == kNoBreakSpaceCharacter;
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy> ComputeRangeSurroundingCaret(
+ const VisiblePositionTemplate<Strategy>& caret_visible_position) {
+ const PositionTemplate<Strategy>& caret_position =
+ caret_visible_position.DeepEquivalent();
+
+ const Node* const position_node = caret_position.ComputeContainerNode();
+ const bool is_text_node = position_node->IsTextNode();
+ const unsigned position_offset_in_node =
+ caret_position.ComputeOffsetInContainerNode();
+
+ // If we're in the interior of a text node, we can avoid calling
+ // PreviousPositionOf/NextPositionOf for better efficiency.
+ if (is_text_node && position_offset_in_node != 0 &&
+ position_offset_in_node !=
+ static_cast<unsigned>(position_node->MaxCharacterOffset())) {
+ return EphemeralRangeTemplate<Strategy>(
+ PositionTemplate<Strategy>(position_node, position_offset_in_node - 1),
+ PositionTemplate<Strategy>(position_node, position_offset_in_node + 1));
+ }
+
+ const PositionTemplate<Strategy>& previous_position =
+ caret_visible_position.DeepEquivalent();
+
+ const PositionTemplate<Strategy>& next_position =
+ caret_visible_position.DeepEquivalent();
+
+ return EphemeralRangeTemplate<Strategy>(
+ previous_position.IsNull() ? caret_position : previous_position,
+ next_position.IsNull() ? caret_position : next_position);
+}
+
+} // namespace
+
+TextSuggestionController::TextSuggestionController(LocalFrame& frame)
+ : is_suggestion_menu_open_(false), frame_(&frame) {}
+
+void TextSuggestionController::DocumentAttached(Document* document) {
+ DCHECK(document);
+ SetContext(document);
+}
+
+bool TextSuggestionController::IsMenuOpen() const {
+ return is_suggestion_menu_open_;
+}
+
+void TextSuggestionController::HandlePotentialMisspelledWordTap(
+ const VisiblePositionInFlatTree& caret_visible_position) {
+ const EphemeralRangeInFlatTree& range_to_check =
+ ComputeRangeSurroundingCaret(caret_visible_position);
+
+ HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::MisspellingMarkers());
+ if (node_marker_pairs.IsEmpty())
+ return;
+
+ if (!text_suggestion_host_) {
+ GetFrame().GetInterfaceProvider()->GetInterface(
+ mojo::MakeRequest(&text_suggestion_host_));
+ }
+
+ text_suggestion_host_->StartSpellCheckMenuTimer();
+}
+
+DEFINE_TRACE(TextSuggestionController) {
+ visitor->Trace(frame_);
+ DocumentShutdownObserver::Trace(visitor);
+}
+
+void TextSuggestionController::ReplaceSpellingMarkerTouchingSelectionWithText(
+ const String& suggestion) {
+ const VisibleSelection& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return;
+
+ const EphemeralRange& range_to_check =
+ selection.IsRange()
+ ? FirstEphemeralRangeOf(selection)
+ : ComputeRangeSurroundingCaret(selection.VisibleStart());
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::MisspellingMarkers());
+
+ if (node_marker_pairs.IsEmpty())
+ return;
+
+ Node* const container_node = node_marker_pairs.front().first;
+ const DocumentMarker* const marker = node_marker_pairs.front().second;
+
+ GetFrame().Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(container_node, marker->StartOffset()))
+ .Extend(Position(container_node, marker->EndOffset()))
+ .Build());
+
+ Document& current_document = *GetFrame().GetDocument();
+
+ // Dispatch 'beforeinput'.
+ Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection();
+ DataTransfer* const data_transfer = DataTransfer::Create(
+ DataTransfer::DataTransferType::kInsertReplacementText,
+ DataTransferAccessPolicy::kDataTransferReadable,
+ DataObject::CreateFromString(suggestion));
+
+ const bool cancel = DispatchBeforeInputDataTransfer(
+ target, InputEvent::InputType::kInsertReplacementText,
+ data_transfer) != DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy target frame.
+ if (current_document != GetFrame().GetDocument())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (cancel)
+ return;
+ GetFrame().GetEditor().ReplaceSelectionWithText(
+ suggestion, false, false, InputEvent::InputType::kInsertReplacementText);
+}
+
+void TextSuggestionController::ApplySpellCheckSuggestion(
+ const String& suggestion) {
+ ReplaceSpellingMarkerTouchingSelectionWithText(suggestion);
+ SuggestionMenuClosed();
+}
+
+void TextSuggestionController::DeleteActiveSuggestionRange() {
+ const VisibleSelection& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return;
+
+ const EphemeralRange& range_to_check =
+ selection.IsRange()
+ ? FirstEphemeralRangeOf(selection)
+ : ComputeRangeSurroundingCaret(selection.VisibleStart());
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::kActiveSuggestion);
+
+ if (node_marker_pairs.IsEmpty())
+ return;
+
+ Node* const marker_text_node = node_marker_pairs.front().first;
+ const DocumentMarker* const marker = node_marker_pairs.front().second;
+
+ const bool delete_next_char =
+ ShouldDeleteNextCharacter(*marker_text_node, *marker);
+
+ const EphemeralRange range_to_delete = EphemeralRange(
+ Position(marker_text_node, marker->StartOffset()),
+ Position(marker_text_node, marker->EndOffset() + delete_next_char));
+
+ GetFrame().Selection().SetSelection(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(range_to_delete).Build());
+
+ // FrameSelection::SetSelection() may destroy the frame.
+ if (!IsAvailable())
+ return;
+
+ // Dispatch 'beforeinput'.
+ Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection();
+ DataTransfer* const data_transfer = DataTransfer::Create(
+ DataTransfer::DataTransferType::kInsertReplacementText,
+ DataTransferAccessPolicy::kDataTransferReadable,
+ DataObject::CreateFromString(""));
+
+ const bool is_canceled =
+ DispatchBeforeInputDataTransfer(
+ target, InputEvent::InputType::kInsertReplacementText,
+ data_transfer) != DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy target frame.
+ if (!IsAvailable())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (is_canceled)
+ return;
+ GetFrame().GetEditor().ReplaceSelectionWithText(
+ "", false, false, InputEvent::InputType::kInsertReplacementText);
+
+ SuggestionMenuClosed();
+}
+
+void TextSuggestionController::NewWordAddedToDictionary(const String& word) {
+ // Android pops up a dialog to let the user confirm they actually want to add
+ // the word to the dictionary; this method gets called as soon as the dialog
+ // is shown. So the word isn't actually in the dictionary here, even if the
+ // user will end up confirming the dialog, and we shouldn't try to re-run
+ // spellcheck here.
+
+ // Note: this actually matches the behavior in native Android text boxes
+ GetDocument().Markers().RemoveSpellingMarkersUnderWords(
+ Vector<String>({word}));
+ SuggestionMenuClosed();
+}
+
+void TextSuggestionController::SpellCheckMenuTimeoutCallback() {
+ const VisibleSelection& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return;
+
+ const EphemeralRange& range_to_check =
+ selection.IsRange()
+ ? FirstEphemeralRangeOf(selection)
+ : ComputeRangeSurroundingCaret(selection.VisibleStart());
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::MisspellingMarkers());
+ if (node_marker_pairs.IsEmpty())
+ return;
+
+ Node* const container_node = node_marker_pairs.front().first;
+ SpellCheckMarker* const marker =
+ ToSpellCheckMarker(node_marker_pairs.front().second);
+
+ const EphemeralRange marker_range =
+ EphemeralRange(Position(container_node, marker->StartOffset()),
+ Position(container_node, marker->EndOffset()));
+ const String& misspelled_word = PlainText(marker_range);
+ const String& description = marker->Description();
+
+ is_suggestion_menu_open_ = true;
+ GetFrame().Selection().SetCaretVisible(false);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, SK_ColorTRANSPARENT, StyleableMarker::Thickness::kThin,
+ LayoutTheme::GetTheme().PlatformActiveSpellingMarkerHighlightColor());
+
+ Vector<String> suggestions;
+ description.Split('\n', suggestions);
+
+ Vector<mojom::blink::SpellCheckSuggestionPtr> suggestion_ptrs;
+ for (const String& suggestion : suggestions) {
+ mojom::blink::SpellCheckSuggestionPtr info_ptr(
+ mojom::blink::SpellCheckSuggestion::New());
+ info_ptr->suggestion = suggestion;
+ suggestion_ptrs.push_back(std::move(info_ptr));
+ }
+
+ const IntRect& absolute_bounds = GetFrame().Selection().AbsoluteCaretBounds();
+ const IntRect& viewport_bounds =
+ GetFrame().View()->ContentsToViewport(absolute_bounds);
+
+ text_suggestion_host_->ShowSpellCheckSuggestionMenu(
+ viewport_bounds.X(), viewport_bounds.MaxY(), std::move(misspelled_word),
+ std::move(suggestion_ptrs));
+}
+
+void TextSuggestionController::SuggestionMenuClosed() {
+ if (!IsAvailable())
+ return;
+
+ GetDocument().Markers().RemoveMarkersOfTypes(
+ DocumentMarker::kActiveSuggestion);
+ GetFrame().Selection().SetCaretVisible(true);
+ is_suggestion_menu_open_ = false;
+}
+
+Document& TextSuggestionController::GetDocument() const {
+ DCHECK(IsAvailable());
+ return *LifecycleContext();
+}
+
+bool TextSuggestionController::IsAvailable() const {
+ return LifecycleContext();
+}
+
+LocalFrame& TextSuggestionController::GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+}
+
+} // namespace blink

Powered by Google App Engine
This is Rietveld 408576698