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

Side by Side Diff: third_party/WebKit/Source/core/editing/TextSuggestionController.cpp

Issue 2650113004: [WIP] Add support for Android SuggestionSpans when editing text (Closed)
Patch Set: Uploading the latest version from my repo so I can reference it Created 3 years, 7 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/TextSuggestionController.h"
6
7 #include "core/dom/Document.h"
8 #include "core/editing/Editor.h"
9 #include "core/editing/FrameSelection.h"
10 #include "core/editing/InputMethodController.h"
11 #include "core/editing/PlainTextRange.h"
12 #include "core/editing/TextSuggestionInfo.h"
13 #include "core/editing/TextSuggestionList.h"
14 #include "core/editing/markers/DocumentMarker.h"
15 #include "core/editing/markers/DocumentMarkerController.h"
16 #include "core/frame/LocalFrame.h"
17 #include "core/layout/LayoutTheme.h"
18 #include "public/platform/InterfaceProvider.h"
19 #include "public/platform/InterfaceRegistry.h"
20 #include "wtf/Vector.h"
21
22 namespace blink {
23
24 namespace {
25
26 const int kMaxNumberSuggestions = 5;
27 const double kSuggestionUnderlineAlphaMultiplier = 0.4;
28
29 } // anonymous namespace
30
31 TextSuggestionController* TextSuggestionController::create(LocalFrame& frame) {
32 return new TextSuggestionController(frame);
33 }
34
35 TextSuggestionController::TextSuggestionController(LocalFrame& frame)
36 : m_suggestionMenuIsOpen(false), m_frame(&frame) {}
37
38 bool TextSuggestionController::suggestionMenuIsOpen() const {
39 return m_suggestionMenuIsOpen;
40 }
41
42 void TextSuggestionController::applySuggestionReplacement(
43 int suggestionID,
44 int suggestionIndex) {
45 suggestionMenuClosed();
46
47 Element* rootEditableElement =
48 frame()
49 .selection()
50 .computeVisibleSelectionInDOMTreeDeprecated()
51 .rootEditableElement();
52
53 DocumentMarkerVector suggestionMarkers =
54 rootEditableElement->document().markers().markersInRangeInclusive(
55 firstEphemeralRangeOf(
56 m_frame->selection()
57 .computeVisibleSelectionInDOMTreeDeprecated()),
58 DocumentMarker::Suggestion);
59
60 SuggestionMarker* suggestionMarkerPtr = nullptr;
61 for (Member<DocumentMarker>& marker : suggestionMarkers) {
62 SuggestionMarker& suggestionMarker = toSuggestionMarker(*marker.get());
63 if (suggestionMarker.suggestionID() == suggestionID) {
64 suggestionMarkerPtr = &suggestionMarker;
65 break;
66 }
67 }
68
69 // TODO: replace ALL suggestion markers with ID (could span multiple nodes)
70 // We should always be able to find the marker we're replacing unless we have
71 // a bug
72 DCHECK(suggestionMarkerPtr);
73 if (!suggestionMarkerPtr)
74 return;
75
76 const EphemeralRange rangeToReplace =
77 PlainTextRange(suggestionMarkerPtr->startOffset(), suggestionMarkerPtr->en dOffset())
78 .createRange(*rootEditableElement);
79
80 // The entry in the suggestion drop-down that the user tapped on is replaced
81 // by the text they just overwrote
82 String replacement =
83 String::fromUTF8(suggestionMarkerPtr->suggestions()[suggestionIndex].utf8( ));
84 String newSuggestion;
85 {
86 DocumentLifecycle::DisallowTransitionScope disallowTransition(
87 document().lifecycle());
88 newSuggestion =
89 plainText(rangeToReplace, TextIteratorBehavior::Builder().build());
90 }
91
92 DCHECK(!rangeToReplace.isNull());
93 if (rangeToReplace.isNull())
94 return;
95
96 frame().selection().setSelection(
97 SelectionInDOMTree::Builder().setBaseAndExtent(rangeToReplace).build(),
98 0);
99 // Intentionally avoid sending JavaScript composition events
100 frame().editor().replaceSelectionWithText(
101 replacement, false, false, InputEvent::InputType::InsertReplacementText);
102
103 suggestionMarkerPtr->replaceSuggestion(suggestionIndex, newSuggestion.utf8().d ata());
104 }
105
106 void TextSuggestionController::deleteSuggestionHighlight() {
107 // We *do* want to remove markers touching the selection for this operation
108 m_suggestionMenuIsOpen = false;
109
110 Element* rootEditableElement =
111 frame()
112 .selection()
113 .computeVisibleSelectionInDOMTreeDeprecated()
114 .rootEditableElement();
115 DocumentMarkerVector suggestionHighlightMarkers =
116 rootEditableElement->document().markers().markersInRangeInclusive(
117 firstEphemeralRangeOf(
118 m_frame->selection()
119 .computeVisibleSelectionInDOMTreeDeprecated()),
120 DocumentMarker::SuggestionHighlight);
121
122 DCHECK(!suggestionHighlightMarkers.isEmpty());
123 if (suggestionHighlightMarkers.isEmpty())
124 return;
125
126 const DocumentMarker* suggestionHighlightMarkerPtr =
127 suggestionHighlightMarkers[0].get();
128
129 // If the character immediately following the range to be deleted is a space,
130 // delete it if either of these conditions holds:
131 // - We're deleting at the beginning of the editable text (to avoid ending up
132 // with a space at the beginning)
133 // - The character immediately before the range being deleted is also a space
134 // (to avoid ending up with two adjacent spaces)
135
136 bool deleteNextChar = false;
137
138 const PlainTextRange nextCharacterRange(
139 suggestionHighlightMarkerPtr->endOffset(),
140 suggestionHighlightMarkerPtr->endOffset() + 1);
141 const EphemeralRange& nextCharacterEphemeralRange =
142 nextCharacterRange.createRange(*rootEditableElement);
143 if (!nextCharacterEphemeralRange.isNull()) {
144 String nextCharacterStr = plainText(
145 nextCharacterEphemeralRange,
146 TextIteratorBehavior::Builder().setEmitsSpaceForNbsp(true).build());
147
148 if (WTF::isASCIISpace(nextCharacterStr[0])) {
149 if (suggestionHighlightMarkerPtr->startOffset() == 0) {
150 deleteNextChar = true;
151 } else {
152 const PlainTextRange prevCharacterRange(
153 suggestionHighlightMarkerPtr->startOffset() - 1,
154 suggestionHighlightMarkerPtr->startOffset());
155 const EphemeralRange prevCharacterEphemeralRange =
156 prevCharacterRange.createRange(*rootEditableElement);
157 if (!prevCharacterEphemeralRange.isNull()) {
158 String prevCharacterStr = plainText(prevCharacterEphemeralRange,
159 TextIteratorBehavior::Builder()
160 .setEmitsSpaceForNbsp(true)
161 .build());
162 if (WTF::isASCIISpace(prevCharacterStr[0])) {
163 deleteNextChar = true;
164 }
165 }
166 }
167 }
168 }
169
170 const EphemeralRange highlightRange =
171 PlainTextRange(suggestionHighlightMarkerPtr->startOffset(),
172 suggestionHighlightMarkerPtr->endOffset() + deleteNextChar)
173 .createRange(*rootEditableElement);
174
175 frame().selection().setSelection(
176 SelectionInDOMTree::Builder().setBaseAndExtent(highlightRange).build(),
177 0);
178 // Don't want to send JavaScript composition events here
179 frame().editor().replaceSelectionWithText(
180 "", false, false, InputEvent::InputType::InsertReplacementText);
181
182 suggestionMenuClosed();
183 }
184
185 void TextSuggestionController::suggestionMenuClosed() {
186 document().markers().removeMarkers(DocumentMarker::SuggestionHighlight);
187 frame().selection().setCaretVisible(true);
188 m_suggestionMenuIsOpen = false;
189 }
190
191 void TextSuggestionController::handlePotentialTextSuggestionTap() {
192 Vector<blink::TextSuggestionInfo> suggestion_infos =
193 getTextSuggestionInfosUnderCaretAndAddHighlight();
194 if (suggestion_infos.size() == 0)
195 return;
196
197 // The composition is now on the suggestion range highlight
198 // TODO: how to do this?
199 // UpdateCompositionInfo(false /* not an immediate request */);
200 prepareForTextSuggestionMenuToBeShown();
201
202 if (!text_suggestion_host_) {
203 frame().interfaceProvider()->getInterface(
204 mojo::MakeRequest(&text_suggestion_host_));
205 }
206
207 Vector<mojom::blink::TextSuggestionInfoPtr> suggestion_info_ptrs;
208 for (const blink::TextSuggestionInfo& info : suggestion_infos) {
209 mojom::blink::TextSuggestionInfoPtr info_ptr(
210 mojom::blink::TextSuggestionInfo::New());
211 info_ptr->suggestionID = info.suggestionID;
212 info_ptr->suggestionIndex = info.suggestionIndex;
213 info_ptr->prefix = info.prefix;
214 info_ptr->suggestion = info.suggestion;
215 info_ptr->suffix = info.suffix;
216
217 suggestion_info_ptrs.push_back(std::move(info_ptr));
218 }
219
220 text_suggestion_host_->ShowTextSuggestionMenu(
221 std::move(suggestion_info_ptrs));
222 }
223
224 void TextSuggestionController::prepareForTextSuggestionMenuToBeShown() {
225 m_suggestionMenuIsOpen = true;
226 frame().selection().setCaretVisible(false);
227 }
228
229 void TextSuggestionController::removeSuggestionMarkersAffectedByEditing(
230 bool doNotRemoveIfSelectionAtWordBoundary) {
231 // Don't remove suggestion markers when we're doing a replace operation
232 if (suggestionMenuIsOpen())
233 return;
234
235 document().markers().removeMarkersForWordsAffectedByEditing(
236 DocumentMarker::Suggestion, doNotRemoveIfSelectionAtWordBoundary);
237 }
238
239 bool TextSuggestionController::isAvailable() const {
240 return frame().document();
241 }
242
243 Document& TextSuggestionController::document() const {
244 DCHECK(isAvailable());
245 return *frame().document();
246 }
247
248 Vector<blink::TextSuggestionInfo>
249 TextSuggestionController::getTextSuggestionInfosUnderCaretAndAddHighlight()
250 const {
251 Element* rootEditableElement =
252 frame()
253 .selection()
254 .computeVisibleSelectionInDOMTreeDeprecated()
255 .rootEditableElement();
256 if (!rootEditableElement)
257 return Vector<blink::TextSuggestionInfo>();
258
259 DocumentMarkerVector suggestionMarkers =
260 rootEditableElement->document().markers().markersInRangeInclusive(
261 firstEphemeralRangeOf(
262 m_frame->selection()
263 .computeVisibleSelectionInDOMTreeDeprecated()),
264 DocumentMarker::Suggestion);
265
266 Vector<TextSuggestionList> suggestionSpans;
267 for (const Member<DocumentMarker>& marker : suggestionMarkers) {
268 // ignore ranges that have been collapsed as a result of editing
269 // operations
270 if (marker->endOffset() == marker->startOffset())
271 continue;
272
273 const SuggestionMarker& suggestionMarker = toSuggestionMarker(*marker);
274
275 TextSuggestionList suggestionList;
276 suggestionList.suggestionID = suggestionMarker.suggestionID();
277 suggestionList.start = suggestionMarker.startOffset();
278 suggestionList.end = suggestionMarker.endOffset();
279 suggestionList.suggestions = suggestionMarker.suggestions();
280 suggestionSpans.push_back(suggestionList);
281 }
282 std::sort(suggestionSpans.begin(), suggestionSpans.end());
283
284 Vector<TextSuggestionInfo> suggestionInfos;
285
286 for (const TextSuggestionList& suggestionList : suggestionSpans) {
287 if (suggestionInfos.size() == kMaxNumberSuggestions)
288 break;
289
290 for (size_t suggestionIndex = 0;
291 suggestionIndex < suggestionList.suggestions.size();
292 suggestionIndex++) {
293 const String& suggestion = suggestionList.suggestions[suggestionIndex];
294 bool isDupe = false;
295 for (size_t i = 0; i < suggestionInfos.size(); i++) {
296 const TextSuggestionInfo& otherSuggestionInfo = suggestionInfos[i];
297 if (otherSuggestionInfo.suggestion == suggestion) {
298 if (suggestionList.start == otherSuggestionInfo.spanStart &&
299 suggestionList.end == otherSuggestionInfo.spanEnd) {
300 isDupe = true;
301 break;
302 }
303 }
304 }
305
306 if (isDupe)
307 continue;
308
309 TextSuggestionInfo suggestionInfo;
310 suggestionInfo.suggestionID = suggestionList.suggestionID;
311 suggestionInfo.suggestionStart = 0;
312 suggestionInfo.suggestionEnd = suggestion.length();
313 suggestionInfo.spanStart = suggestionList.start;
314 suggestionInfo.spanEnd = suggestionList.end;
315 suggestionInfo.suggestionIndex = suggestionIndex;
316 suggestionInfo.suggestion = suggestion;
317 suggestionInfos.push_back(suggestionInfo);
318 if (suggestionInfos.size() == kMaxNumberSuggestions)
319 break;
320 }
321 }
322
323 if (suggestionInfos.size() == 0)
324 return Vector<blink::TextSuggestionInfo>();
325
326 int spanUnionStart = suggestionInfos[0].spanStart;
327 int spanUnionEnd = suggestionInfos[0].spanEnd;
328
329 for (size_t i = 1; i < suggestionInfos.size(); i++) {
330 spanUnionStart = std::min(spanUnionStart, suggestionInfos[i].spanStart);
331 spanUnionEnd = std::max(spanUnionEnd, suggestionInfos[i].spanEnd);
332 }
333
334 for (TextSuggestionInfo& info : suggestionInfos) {
335 const EphemeralRange& prefixRange =
336 PlainTextRange(spanUnionStart, info.spanStart)
337 .createRange(*rootEditableElement);
338 String prefix =
339 plainText(prefixRange, TextIteratorBehavior::Builder().build());
340
341 const EphemeralRange& suffixRange =
342 PlainTextRange(info.spanEnd, spanUnionEnd)
343 .createRange(*rootEditableElement);
344 String suffix =
345 plainText(suffixRange, TextIteratorBehavior::Builder().build());
346
347 info.prefix = prefix.utf8().data();
348 info.suffix = suffix.utf8().data();
349 }
350
351 Color highlightColor;
352 const SuggestionMarker& firstSuggestionMarker = *toSuggestionMarker(suggestion Markers[0]);
353 if (firstSuggestionMarker.underlineColor() != 0) {
354 highlightColor = firstSuggestionMarker.underlineColor().combineWithAlpha(
355 kSuggestionUnderlineAlphaMultiplier);
356 } else {
357 highlightColor = LayoutTheme::tapHighlightColor();
358 }
359
360 EphemeralRange backgroundHighlightRange =
361 PlainTextRange(spanUnionStart, spanUnionEnd)
362 .createRange(*rootEditableElement);
363 document().markers().addSuggestionHighlightMarker(
364 backgroundHighlightRange,
365 Color::black, false,
366 highlightColor);
367
368 return suggestionInfos;
369 }
370
371 DEFINE_TRACE(TextSuggestionController) {
372 visitor->trace(m_frame);
373 }
374
375 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698