| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 // Copyright 2017 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 "core/editing/suggestion/TextSuggestionController.h" | 
|  | 6 | 
|  | 7 #include "core/editing/EditingUtilities.h" | 
|  | 8 #include "core/editing/Editor.h" | 
|  | 9 #include "core/editing/FrameSelection.h" | 
|  | 10 #include "core/editing/PlainTextRange.h" | 
|  | 11 #include "core/editing/Position.h" | 
|  | 12 #include "core/editing/markers/DocumentMarkerController.h" | 
|  | 13 #include "core/editing/markers/SpellCheckMarker.h" | 
|  | 14 #include "core/editing/spellcheck/SpellChecker.h" | 
|  | 15 #include "core/frame/FrameView.h" | 
|  | 16 #include "core/frame/LocalFrame.h" | 
|  | 17 #include "core/layout/LayoutTheme.h" | 
|  | 18 #include "services/service_manager/public/cpp/interface_provider.h" | 
|  | 19 | 
|  | 20 namespace blink { | 
|  | 21 | 
|  | 22 namespace { | 
|  | 23 | 
|  | 24 bool ShouldDeleteNextCharacter(const Node& marker_text_node, | 
|  | 25                                const DocumentMarker& marker) { | 
|  | 26   // If the character immediately following the range to be deleted is a space, | 
|  | 27   // delete it if either of these conditions holds: | 
|  | 28   // - We're deleting at the beginning of the editable text (to avoid ending up | 
|  | 29   //   with a space at the beginning) | 
|  | 30   // - The character immediately before the range being deleted is also a space | 
|  | 31   //   (to avoid ending up with two adjacent spaces) | 
|  | 32   const EphemeralRange next_character_range = | 
|  | 33       PlainTextRange(marker.EndOffset(), marker.EndOffset() + 1) | 
|  | 34           .CreateRange(*marker_text_node.parentNode()); | 
|  | 35   // No character immediately following the range (so it can't be a space) | 
|  | 36   if (next_character_range.IsNull()) | 
|  | 37     return false; | 
|  | 38 | 
|  | 39   const String next_character_str = | 
|  | 40       PlainText(next_character_range, TextIteratorBehavior::Builder().Build()); | 
|  | 41   const UChar next_character = next_character_str[0]; | 
|  | 42   // Character immediately following the range is not a space | 
|  | 43   if (next_character != kSpaceCharacter && | 
|  | 44       next_character != kNoBreakSpaceCharacter) | 
|  | 45     return false; | 
|  | 46 | 
|  | 47   // First case: we're deleting at the beginning of the editable text | 
|  | 48   if (marker.StartOffset() == 0) | 
|  | 49     return true; | 
|  | 50 | 
|  | 51   const EphemeralRange prev_character_range = | 
|  | 52       PlainTextRange(marker.StartOffset() - 1, marker.StartOffset()) | 
|  | 53           .CreateRange(*marker_text_node.parentNode()); | 
|  | 54   // Not at beginning, but there's no character immediately before the range | 
|  | 55   // being deleted (so it can't be a space) | 
|  | 56   if (prev_character_range.IsNull()) | 
|  | 57     return false; | 
|  | 58 | 
|  | 59   const String prev_character_str = | 
|  | 60       PlainText(prev_character_range, TextIteratorBehavior::Builder().Build()); | 
|  | 61   // Return true if the character immediately before the range is a space, false | 
|  | 62   // otherwise | 
|  | 63   const UChar prev_character = prev_character_str[0]; | 
|  | 64   return prev_character == kSpaceCharacter || | 
|  | 65          prev_character == kNoBreakSpaceCharacter; | 
|  | 66 } | 
|  | 67 | 
|  | 68 EphemeralRangeInFlatTree ComputeRangeSurroundingCaret( | 
|  | 69     const VisiblePositionInFlatTree& caret_visible_position) { | 
|  | 70   const PositionInFlatTree& caret_position = | 
|  | 71       caret_visible_position.DeepEquivalent(); | 
|  | 72 | 
|  | 73   const Node* const position_node = caret_position.ComputeContainerNode(); | 
|  | 74   const bool is_text_node = position_node->IsTextNode(); | 
|  | 75   const unsigned position_offset_in_node = | 
|  | 76       caret_position.ComputeOffsetInContainerNode(); | 
|  | 77 | 
|  | 78   // If we're in the interior of a text node, we can avoid calling | 
|  | 79   // PreviousPositionOf/NextPositionOf for better efficiency. | 
|  | 80   if (is_text_node && position_offset_in_node != 0 && | 
|  | 81       position_offset_in_node != | 
|  | 82           static_cast<unsigned>(position_node->MaxCharacterOffset())) { | 
|  | 83     return EphemeralRangeInFlatTree( | 
|  | 84         PositionInFlatTree(position_node, position_offset_in_node - 1), | 
|  | 85         PositionInFlatTree(position_node, position_offset_in_node + 1)); | 
|  | 86   } | 
|  | 87 | 
|  | 88   const PositionInFlatTree& previous_position = | 
|  | 89       PreviousPositionOf(caret_visible_position).DeepEquivalent(); | 
|  | 90 | 
|  | 91   const PositionInFlatTree& next_position = | 
|  | 92       NextPositionOf(caret_visible_position).DeepEquivalent(); | 
|  | 93 | 
|  | 94   return EphemeralRangeInFlatTree( | 
|  | 95       previous_position.IsNull() ? caret_position : previous_position, | 
|  | 96       next_position.IsNull() ? caret_position : next_position); | 
|  | 97 } | 
|  | 98 | 
|  | 99 }  // namespace | 
|  | 100 | 
|  | 101 TextSuggestionController::TextSuggestionController(LocalFrame& frame) | 
|  | 102     : is_suggestion_menu_open_(false), frame_(&frame) {} | 
|  | 103 | 
|  | 104 void TextSuggestionController::DocumentAttached(Document* document) { | 
|  | 105   DCHECK(document); | 
|  | 106   SetContext(document); | 
|  | 107 } | 
|  | 108 | 
|  | 109 bool TextSuggestionController::IsMenuOpen() const { | 
|  | 110   return is_suggestion_menu_open_; | 
|  | 111 } | 
|  | 112 | 
|  | 113 void TextSuggestionController::HandlePotentialMisspelledWordTap( | 
|  | 114     const VisiblePositionInFlatTree& caret_visible_position) { | 
|  | 115   const EphemeralRangeInFlatTree& range_to_check = | 
|  | 116       ComputeRangeSurroundingCaret(caret_visible_position); | 
|  | 117 | 
|  | 118   const Optional<std::pair<const Node*, const DocumentMarker*>>& | 
|  | 119       node_and_marker = FirstMarkerIntersectingRange( | 
|  | 120           range_to_check, DocumentMarker::MisspellingMarkers()); | 
|  | 121   if (!node_and_marker) | 
|  | 122     return; | 
|  | 123 | 
|  | 124   if (!text_suggestion_host_) { | 
|  | 125     GetFrame().GetInterfaceProvider().GetInterface( | 
|  | 126         mojo::MakeRequest(&text_suggestion_host_)); | 
|  | 127   } | 
|  | 128 | 
|  | 129   text_suggestion_host_->StartSpellCheckMenuTimer(); | 
|  | 130 } | 
|  | 131 | 
|  | 132 DEFINE_TRACE(TextSuggestionController) { | 
|  | 133   visitor->Trace(frame_); | 
|  | 134   DocumentShutdownObserver::Trace(visitor); | 
|  | 135 } | 
|  | 136 | 
|  | 137 void TextSuggestionController::ApplySpellCheckSuggestion( | 
|  | 138     const String& suggestion) { | 
|  | 139   ReplaceSpellingMarkerTouchingSelectionWithText(suggestion); | 
|  | 140   SuggestionMenuClosed(); | 
|  | 141 } | 
|  | 142 | 
|  | 143 void TextSuggestionController::DeleteActiveSuggestionRange() { | 
|  | 144   AttemptToDeleteActiveSuggestionRange(); | 
|  | 145   SuggestionMenuClosed(); | 
|  | 146 } | 
|  | 147 | 
|  | 148 void TextSuggestionController::NewWordAddedToDictionary(const String& word) { | 
|  | 149   // Android pops up a dialog to let the user confirm they actually want to add | 
|  | 150   // the word to the dictionary; this method gets called as soon as the dialog | 
|  | 151   // is shown. So the word isn't actually in the dictionary here, even if the | 
|  | 152   // user will end up confirming the dialog, and we shouldn't try to re-run | 
|  | 153   // spellcheck here. | 
|  | 154 | 
|  | 155   // Note: this actually matches the behavior in native Android text boxes | 
|  | 156   GetDocument().Markers().RemoveSpellingMarkersUnderWords( | 
|  | 157       Vector<String>({word})); | 
|  | 158   SuggestionMenuClosed(); | 
|  | 159 } | 
|  | 160 | 
|  | 161 void TextSuggestionController::SpellCheckMenuTimeoutCallback() { | 
|  | 162   const Optional<std::pair<const Node*, const DocumentMarker*>>& | 
|  | 163       node_and_marker = | 
|  | 164           FirstMarkerTouchingSelection(DocumentMarker::MisspellingMarkers()); | 
|  | 165   if (!node_and_marker) | 
|  | 166     return; | 
|  | 167 | 
|  | 168   const Node* const marker_text_node = node_and_marker.value().first; | 
|  | 169   const SpellCheckMarker* const marker = | 
|  | 170       ToSpellCheckMarker(node_and_marker.value().second); | 
|  | 171 | 
|  | 172   const EphemeralRange marker_range = | 
|  | 173       EphemeralRange(Position(marker_text_node, marker->StartOffset()), | 
|  | 174                      Position(marker_text_node, marker->EndOffset())); | 
|  | 175   const String& misspelled_word = PlainText(marker_range); | 
|  | 176   const String& description = marker->Description(); | 
|  | 177 | 
|  | 178   is_suggestion_menu_open_ = true; | 
|  | 179   GetFrame().Selection().SetCaretVisible(false); | 
|  | 180   GetDocument().Markers().AddActiveSuggestionMarker( | 
|  | 181       marker_range, SK_ColorTRANSPARENT, StyleableMarker::Thickness::kThin, | 
|  | 182       LayoutTheme::GetTheme().PlatformActiveSpellingMarkerHighlightColor()); | 
|  | 183 | 
|  | 184   Vector<String> suggestions; | 
|  | 185   description.Split('\n', suggestions); | 
|  | 186 | 
|  | 187   Vector<mojom::blink::SpellCheckSuggestionPtr> suggestion_ptrs; | 
|  | 188   for (const String& suggestion : suggestions) { | 
|  | 189     mojom::blink::SpellCheckSuggestionPtr info_ptr( | 
|  | 190         mojom::blink::SpellCheckSuggestion::New()); | 
|  | 191     info_ptr->suggestion = suggestion; | 
|  | 192     suggestion_ptrs.push_back(std::move(info_ptr)); | 
|  | 193   } | 
|  | 194 | 
|  | 195   const IntRect& absolute_bounds = GetFrame().Selection().AbsoluteCaretBounds(); | 
|  | 196   const IntRect& viewport_bounds = | 
|  | 197       GetFrame().View()->ContentsToViewport(absolute_bounds); | 
|  | 198 | 
|  | 199   text_suggestion_host_->ShowSpellCheckSuggestionMenu( | 
|  | 200       viewport_bounds.X(), viewport_bounds.MaxY(), std::move(misspelled_word), | 
|  | 201       std::move(suggestion_ptrs)); | 
|  | 202 } | 
|  | 203 | 
|  | 204 void TextSuggestionController::SuggestionMenuClosed() { | 
|  | 205   if (!IsAvailable()) | 
|  | 206     return; | 
|  | 207 | 
|  | 208   GetDocument().Markers().RemoveMarkersOfTypes( | 
|  | 209       DocumentMarker::kActiveSuggestion); | 
|  | 210   GetFrame().Selection().SetCaretVisible(true); | 
|  | 211   is_suggestion_menu_open_ = false; | 
|  | 212 } | 
|  | 213 | 
|  | 214 Document& TextSuggestionController::GetDocument() const { | 
|  | 215   DCHECK(IsAvailable()); | 
|  | 216   return *LifecycleContext(); | 
|  | 217 } | 
|  | 218 | 
|  | 219 bool TextSuggestionController::IsAvailable() const { | 
|  | 220   return LifecycleContext(); | 
|  | 221 } | 
|  | 222 | 
|  | 223 LocalFrame& TextSuggestionController::GetFrame() const { | 
|  | 224   DCHECK(frame_); | 
|  | 225   return *frame_; | 
|  | 226 } | 
|  | 227 | 
|  | 228 Optional<std::pair<const Node*, const DocumentMarker*>> | 
|  | 229 TextSuggestionController::FirstMarkerIntersectingRange( | 
|  | 230     const EphemeralRangeInFlatTree& range, | 
|  | 231     DocumentMarker::MarkerTypes types) const { | 
|  | 232   const Node* const range_start_container = | 
|  | 233       range.StartPosition().ComputeContainerNode(); | 
|  | 234   const unsigned range_start_offset = | 
|  | 235       range.StartPosition().ComputeOffsetInContainerNode(); | 
|  | 236   const Node* const range_end_container = | 
|  | 237       range.EndPosition().ComputeContainerNode(); | 
|  | 238   const unsigned range_end_offset = | 
|  | 239       range.EndPosition().ComputeOffsetInContainerNode(); | 
|  | 240 | 
|  | 241   for (const Node& node : range.Nodes()) { | 
|  | 242     if (!node.IsTextNode()) | 
|  | 243       continue; | 
|  | 244 | 
|  | 245     const unsigned start_offset = | 
|  | 246         node == range_start_container ? range_start_offset : 0; | 
|  | 247     const unsigned end_offset = node == range_end_container | 
|  | 248                                     ? range_end_offset | 
|  | 249                                     : node.MaxCharacterOffset(); | 
|  | 250 | 
|  | 251     const DocumentMarker* const found_marker = | 
|  | 252         GetFrame().GetDocument()->Markers().FirstMarkerIntersectingOffsetRange( | 
|  | 253             ToText(node), start_offset, end_offset, types); | 
|  | 254     if (found_marker) | 
|  | 255       return std::make_pair(&node, found_marker); | 
|  | 256   } | 
|  | 257 | 
|  | 258   return Optional<std::pair<const Node*, const DocumentMarker*>>(); | 
|  | 259 } | 
|  | 260 | 
|  | 261 Optional<std::pair<const Node*, const DocumentMarker*>> | 
|  | 262 TextSuggestionController::FirstMarkerTouchingSelection( | 
|  | 263     DocumentMarker::MarkerTypes types) const { | 
|  | 264   const VisibleSelectionInFlatTree& selection = | 
|  | 265       GetFrame().Selection().ComputeVisibleSelectionInFlatTree(); | 
|  | 266   if (selection.IsNone()) | 
|  | 267     return Optional<std::pair<const Node*, const DocumentMarker*>>(); | 
|  | 268 | 
|  | 269   const EphemeralRangeInFlatTree& range_to_check = | 
|  | 270       selection.IsRange() | 
|  | 271           ? selection.ToNormalizedEphemeralRange() | 
|  | 272           : ComputeRangeSurroundingCaret(selection.VisibleStart()); | 
|  | 273 | 
|  | 274   return FirstMarkerIntersectingRange(range_to_check, types); | 
|  | 275 } | 
|  | 276 | 
|  | 277 void TextSuggestionController::AttemptToDeleteActiveSuggestionRange() { | 
|  | 278   const Optional<std::pair<const Node*, const DocumentMarker*>>& | 
|  | 279       node_and_marker = | 
|  | 280           FirstMarkerTouchingSelection(DocumentMarker::kActiveSuggestion); | 
|  | 281   if (!node_and_marker) | 
|  | 282     return; | 
|  | 283 | 
|  | 284   const Node* const marker_text_node = node_and_marker.value().first; | 
|  | 285   const DocumentMarker* const marker = node_and_marker.value().second; | 
|  | 286 | 
|  | 287   const bool delete_next_char = | 
|  | 288       ShouldDeleteNextCharacter(*marker_text_node, *marker); | 
|  | 289 | 
|  | 290   const EphemeralRange range_to_delete = EphemeralRange( | 
|  | 291       Position(marker_text_node, marker->StartOffset()), | 
|  | 292       Position(marker_text_node, marker->EndOffset() + delete_next_char)); | 
|  | 293   ReplaceRangeWithText(range_to_delete, ""); | 
|  | 294 } | 
|  | 295 | 
|  | 296 void TextSuggestionController::ReplaceSpellingMarkerTouchingSelectionWithText( | 
|  | 297     const String& suggestion) { | 
|  | 298   const Optional<std::pair<const Node*, const DocumentMarker*>>& | 
|  | 299       node_and_marker = | 
|  | 300           FirstMarkerTouchingSelection(DocumentMarker::MisspellingMarkers()); | 
|  | 301   if (!node_and_marker) | 
|  | 302     return; | 
|  | 303 | 
|  | 304   const Node* const marker_text_node = node_and_marker.value().first; | 
|  | 305   const DocumentMarker* const marker = node_and_marker.value().second; | 
|  | 306 | 
|  | 307   const EphemeralRange range_to_replace( | 
|  | 308       Position(marker_text_node, marker->StartOffset()), | 
|  | 309       Position(marker_text_node, marker->EndOffset())); | 
|  | 310   ReplaceRangeWithText(range_to_replace, suggestion); | 
|  | 311 } | 
|  | 312 | 
|  | 313 void TextSuggestionController::ReplaceRangeWithText(const EphemeralRange& range, | 
|  | 314                                                     const String& replacement) { | 
|  | 315   GetFrame().Selection().SetSelection( | 
|  | 316       SelectionInDOMTree::Builder().SetBaseAndExtent(range).Build()); | 
|  | 317 | 
|  | 318   // Dispatch 'beforeinput'. | 
|  | 319   Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection(); | 
|  | 320   DataTransfer* const data_transfer = DataTransfer::Create( | 
|  | 321       DataTransfer::DataTransferType::kInsertReplacementText, | 
|  | 322       DataTransferAccessPolicy::kDataTransferReadable, | 
|  | 323       DataObject::CreateFromString(replacement)); | 
|  | 324 | 
|  | 325   const bool is_canceled = | 
|  | 326       DispatchBeforeInputDataTransfer( | 
|  | 327           target, InputEvent::InputType::kInsertReplacementText, | 
|  | 328           data_transfer) != DispatchEventResult::kNotCanceled; | 
|  | 329 | 
|  | 330   // 'beforeinput' event handler may destroy target frame. | 
|  | 331   if (!IsAvailable()) | 
|  | 332     return; | 
|  | 333 | 
|  | 334   // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets | 
|  | 335   // needs to be audited.  See http://crbug.com/590369 for more details. | 
|  | 336   GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets(); | 
|  | 337 | 
|  | 338   if (is_canceled) | 
|  | 339     return; | 
|  | 340   GetFrame().GetEditor().ReplaceSelectionWithText( | 
|  | 341       replacement, false, false, InputEvent::InputType::kInsertReplacementText); | 
|  | 342 } | 
|  | 343 | 
|  | 344 }  // namespace blink | 
| OLD | NEW | 
|---|