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

Side by Side 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, 5 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 unified diff | Download patch
OLDNEW
(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 "public/platform/InterfaceProvider.h"
18
19 namespace blink {
20
21 namespace {
22
23 bool ShouldDeleteNextCharacter(const Node& marker_text_node,
24 const DocumentMarker& marker) {
25 // If the character immediately following the range to be deleted is a space,
26 // delete it if either of these conditions holds:
27 // - We're deleting at the beginning of the editable text (to avoid ending up
28 // with a space at the beginning)
29 // - The character immediately before the range being deleted is also a space
30 // (to avoid ending up with two adjacent spaces)
31 const EphemeralRange next_character_range =
32 PlainTextRange(marker.EndOffset(), marker.EndOffset() + 1)
33 .CreateRange(*marker_text_node.parentNode());
34 // No character immediately following the range (so it can't be a space)
35 if (next_character_range.IsNull())
36 return false;
37
38 const String next_character_str =
39 PlainText(next_character_range, TextIteratorBehavior::Builder().Build());
40 const UChar next_character = next_character_str[0];
41 // Character immediately following the range is not a space
42 if (next_character != kSpaceCharacter &&
43 next_character != kNoBreakSpaceCharacter)
44 return false;
45
46 // First case: we're deleting at the beginning of the editable text
47 if (marker.StartOffset() == 0)
48 return true;
49
50 const EphemeralRange prev_character_range =
51 PlainTextRange(marker.StartOffset() - 1, marker.StartOffset())
52 .CreateRange(*marker_text_node.parentNode());
53 // Not at beginning, but there's no character immediately before the range
54 // being deleted (so it can't be a space)
55 if (prev_character_range.IsNull())
56 return false;
57
58 const String prev_character_str =
59 PlainText(prev_character_range, TextIteratorBehavior::Builder().Build());
60 // Return true if the character immediately before the range is a space, false
61 // otherwise
62 const UChar prev_character = prev_character_str[0];
63 return prev_character == kSpaceCharacter ||
64 prev_character == kNoBreakSpaceCharacter;
65 }
66
67 template <typename Strategy>
68 EphemeralRangeTemplate<Strategy> ComputeRangeSurroundingCaret(
69 const VisiblePositionTemplate<Strategy>& caret_visible_position) {
70 const PositionTemplate<Strategy>& 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 EphemeralRangeTemplate<Strategy>(
84 PositionTemplate<Strategy>(position_node, position_offset_in_node - 1),
85 PositionTemplate<Strategy>(position_node, position_offset_in_node + 1));
86 }
87
88 const PositionTemplate<Strategy>& previous_position =
89 caret_visible_position.DeepEquivalent();
90
91 const PositionTemplate<Strategy>& next_position =
92 caret_visible_position.DeepEquivalent();
93
94 return EphemeralRangeTemplate<Strategy>(
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 HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
119 node_marker_pairs =
120 GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
121 range_to_check, DocumentMarker::MisspellingMarkers());
122 if (node_marker_pairs.IsEmpty())
123 return;
124
125 if (!text_suggestion_host_) {
126 GetFrame().GetInterfaceProvider()->GetInterface(
127 mojo::MakeRequest(&text_suggestion_host_));
128 }
129
130 text_suggestion_host_->StartSpellCheckMenuTimer();
131 }
132
133 DEFINE_TRACE(TextSuggestionController) {
134 visitor->Trace(frame_);
135 DocumentShutdownObserver::Trace(visitor);
136 }
137
138 void TextSuggestionController::ReplaceSpellingMarkerTouchingSelectionWithText(
139 const String& suggestion) {
140 const VisibleSelection& selection =
141 GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
142 if (selection.IsNone())
143 return;
144
145 const EphemeralRange& range_to_check =
146 selection.IsRange()
147 ? FirstEphemeralRangeOf(selection)
148 : ComputeRangeSurroundingCaret(selection.VisibleStart());
149 const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
150 node_marker_pairs =
151 GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
152 range_to_check, DocumentMarker::MisspellingMarkers());
153
154 if (node_marker_pairs.IsEmpty())
155 return;
156
157 Node* const container_node = node_marker_pairs.front().first;
158 const DocumentMarker* const marker = node_marker_pairs.front().second;
159
160 GetFrame().Selection().SetSelection(
161 SelectionInDOMTree::Builder()
162 .Collapse(Position(container_node, marker->StartOffset()))
163 .Extend(Position(container_node, marker->EndOffset()))
164 .Build());
165
166 Document& current_document = *GetFrame().GetDocument();
167
168 // Dispatch 'beforeinput'.
169 Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection();
170 DataTransfer* const data_transfer = DataTransfer::Create(
171 DataTransfer::DataTransferType::kInsertReplacementText,
172 DataTransferAccessPolicy::kDataTransferReadable,
173 DataObject::CreateFromString(suggestion));
174
175 const bool cancel = DispatchBeforeInputDataTransfer(
176 target, InputEvent::InputType::kInsertReplacementText,
177 data_transfer) != DispatchEventResult::kNotCanceled;
178
179 // 'beforeinput' event handler may destroy target frame.
180 if (current_document != GetFrame().GetDocument())
181 return;
182
183 // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
184 // needs to be audited. See http://crbug.com/590369 for more details.
185 GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
186
187 if (cancel)
188 return;
189 GetFrame().GetEditor().ReplaceSelectionWithText(
190 suggestion, false, false, InputEvent::InputType::kInsertReplacementText);
191 }
192
193 void TextSuggestionController::ApplySpellCheckSuggestion(
194 const String& suggestion) {
195 ReplaceSpellingMarkerTouchingSelectionWithText(suggestion);
196 SuggestionMenuClosed();
197 }
198
199 void TextSuggestionController::DeleteActiveSuggestionRange() {
200 const VisibleSelection& selection =
201 GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
202 if (selection.IsNone())
203 return;
204
205 const EphemeralRange& range_to_check =
206 selection.IsRange()
207 ? FirstEphemeralRangeOf(selection)
208 : ComputeRangeSurroundingCaret(selection.VisibleStart());
209 const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
210 node_marker_pairs =
211 GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
212 range_to_check, DocumentMarker::kActiveSuggestion);
213
214 if (node_marker_pairs.IsEmpty())
215 return;
216
217 Node* const marker_text_node = node_marker_pairs.front().first;
218 const DocumentMarker* const marker = node_marker_pairs.front().second;
219
220 const bool delete_next_char =
221 ShouldDeleteNextCharacter(*marker_text_node, *marker);
222
223 const EphemeralRange range_to_delete = EphemeralRange(
224 Position(marker_text_node, marker->StartOffset()),
225 Position(marker_text_node, marker->EndOffset() + delete_next_char));
226
227 GetFrame().Selection().SetSelection(
228 SelectionInDOMTree::Builder().SetBaseAndExtent(range_to_delete).Build());
229
230 // FrameSelection::SetSelection() may destroy the frame.
231 if (!IsAvailable())
232 return;
233
234 // Dispatch 'beforeinput'.
235 Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection();
236 DataTransfer* const data_transfer = DataTransfer::Create(
237 DataTransfer::DataTransferType::kInsertReplacementText,
238 DataTransferAccessPolicy::kDataTransferReadable,
239 DataObject::CreateFromString(""));
240
241 const bool is_canceled =
242 DispatchBeforeInputDataTransfer(
243 target, InputEvent::InputType::kInsertReplacementText,
244 data_transfer) != DispatchEventResult::kNotCanceled;
245
246 // 'beforeinput' event handler may destroy target frame.
247 if (!IsAvailable())
248 return;
249
250 // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
251 // needs to be audited. See http://crbug.com/590369 for more details.
252 GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
253
254 if (is_canceled)
255 return;
256 GetFrame().GetEditor().ReplaceSelectionWithText(
257 "", false, false, InputEvent::InputType::kInsertReplacementText);
258
259 SuggestionMenuClosed();
260 }
261
262 void TextSuggestionController::NewWordAddedToDictionary(const String& word) {
263 // Android pops up a dialog to let the user confirm they actually want to add
264 // the word to the dictionary; this method gets called as soon as the dialog
265 // is shown. So the word isn't actually in the dictionary here, even if the
266 // user will end up confirming the dialog, and we shouldn't try to re-run
267 // spellcheck here.
268
269 // Note: this actually matches the behavior in native Android text boxes
270 GetDocument().Markers().RemoveSpellingMarkersUnderWords(
271 Vector<String>({word}));
272 SuggestionMenuClosed();
273 }
274
275 void TextSuggestionController::SpellCheckMenuTimeoutCallback() {
276 const VisibleSelection& selection =
277 GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
278 if (selection.IsNone())
279 return;
280
281 const EphemeralRange& range_to_check =
282 selection.IsRange()
283 ? FirstEphemeralRangeOf(selection)
284 : ComputeRangeSurroundingCaret(selection.VisibleStart());
285 const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
286 node_marker_pairs =
287 GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
288 range_to_check, DocumentMarker::MisspellingMarkers());
289 if (node_marker_pairs.IsEmpty())
290 return;
291
292 Node* const container_node = node_marker_pairs.front().first;
293 SpellCheckMarker* const marker =
294 ToSpellCheckMarker(node_marker_pairs.front().second);
295
296 const EphemeralRange marker_range =
297 EphemeralRange(Position(container_node, marker->StartOffset()),
298 Position(container_node, marker->EndOffset()));
299 const String& misspelled_word = PlainText(marker_range);
300 const String& description = marker->Description();
301
302 is_suggestion_menu_open_ = true;
303 GetFrame().Selection().SetCaretVisible(false);
304 GetDocument().Markers().AddActiveSuggestionMarker(
305 marker_range, SK_ColorTRANSPARENT, StyleableMarker::Thickness::kThin,
306 LayoutTheme::GetTheme().PlatformActiveSpellingMarkerHighlightColor());
307
308 Vector<String> suggestions;
309 description.Split('\n', suggestions);
310
311 Vector<mojom::blink::SpellCheckSuggestionPtr> suggestion_ptrs;
312 for (const String& suggestion : suggestions) {
313 mojom::blink::SpellCheckSuggestionPtr info_ptr(
314 mojom::blink::SpellCheckSuggestion::New());
315 info_ptr->suggestion = suggestion;
316 suggestion_ptrs.push_back(std::move(info_ptr));
317 }
318
319 const IntRect& absolute_bounds = GetFrame().Selection().AbsoluteCaretBounds();
320 const IntRect& viewport_bounds =
321 GetFrame().View()->ContentsToViewport(absolute_bounds);
322
323 text_suggestion_host_->ShowSpellCheckSuggestionMenu(
324 viewport_bounds.X(), viewport_bounds.MaxY(), std::move(misspelled_word),
325 std::move(suggestion_ptrs));
326 }
327
328 void TextSuggestionController::SuggestionMenuClosed() {
329 if (!IsAvailable())
330 return;
331
332 GetDocument().Markers().RemoveMarkersOfTypes(
333 DocumentMarker::kActiveSuggestion);
334 GetFrame().Selection().SetCaretVisible(true);
335 is_suggestion_menu_open_ = false;
336 }
337
338 Document& TextSuggestionController::GetDocument() const {
339 DCHECK(IsAvailable());
340 return *LifecycleContext();
341 }
342
343 bool TextSuggestionController::IsAvailable() const {
344 return LifecycleContext();
345 }
346
347 LocalFrame& TextSuggestionController::GetFrame() const {
348 DCHECK(frame_);
349 return *frame_;
350 }
351
352 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698