1 | /* |
2 | * Copyright (C) 2006-2017 Apple Inc. All rights reserved. |
3 | * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions |
7 | * are met: |
8 | * 1. Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * 2. Redistributions in binary form must reproduce the above copyright |
11 | * notice, this list of conditions and the following disclaimer in the |
12 | * documentation and/or other materials provided with the distribution. |
13 | * |
14 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
15 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
17 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
18 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
19 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
20 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
21 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
22 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
25 | */ |
26 | |
27 | #include "config.h" |
28 | #include "AlternativeTextController.h" |
29 | |
30 | #include "Document.h" |
31 | #include "DocumentMarkerController.h" |
32 | #include "Editing.h" |
33 | #include "Editor.h" |
34 | #include "Element.h" |
35 | #include "FloatQuad.h" |
36 | #include "Frame.h" |
37 | #include "FrameView.h" |
38 | #include "Page.h" |
39 | #include "RenderedDocumentMarker.h" |
40 | #include "SpellingCorrectionCommand.h" |
41 | #include "TextCheckerClient.h" |
42 | #include "TextCheckingHelper.h" |
43 | #include "TextEvent.h" |
44 | #include "TextIterator.h" |
45 | #include "VisibleUnits.h" |
46 | #include "markup.h" |
47 | |
48 | namespace WebCore { |
49 | |
50 | #if USE(AUTOCORRECTION_PANEL) |
51 | |
52 | static inline OptionSet<DocumentMarker::MarkerType> markerTypesForAutocorrection() |
53 | { |
54 | return { DocumentMarker::Autocorrected, DocumentMarker::CorrectionIndicator, DocumentMarker::Replacement, DocumentMarker::SpellCheckingExemption }; |
55 | } |
56 | |
57 | static inline OptionSet<DocumentMarker::MarkerType> markerTypesForReplacement() |
58 | { |
59 | return { DocumentMarker::Replacement, DocumentMarker::SpellCheckingExemption }; |
60 | } |
61 | |
62 | static inline OptionSet<DocumentMarker::MarkerType> markerTypesForAppliedDictationAlternative() |
63 | { |
64 | return DocumentMarker::SpellCheckingExemption; |
65 | } |
66 | |
67 | static bool markersHaveIdenticalDescription(const Vector<RenderedDocumentMarker*>& markers) |
68 | { |
69 | if (markers.isEmpty()) |
70 | return true; |
71 | |
72 | const String& description = markers[0]->description(); |
73 | for (size_t i = 1; i < markers.size(); ++i) { |
74 | if (description != markers[i]->description()) |
75 | return false; |
76 | } |
77 | return true; |
78 | } |
79 | |
80 | AlternativeTextController::AlternativeTextController(Frame& frame) |
81 | : m_timer(*this, &AlternativeTextController::timerFired) |
82 | , m_frame(frame) |
83 | { |
84 | } |
85 | |
86 | AlternativeTextController::~AlternativeTextController() |
87 | { |
88 | dismiss(ReasonForDismissingAlternativeTextIgnored); |
89 | } |
90 | |
91 | void AlternativeTextController::startAlternativeTextUITimer(AlternativeTextType type) |
92 | { |
93 | const Seconds correctionPanelTimerInterval { 300_ms }; |
94 | if (!isAutomaticSpellingCorrectionEnabled()) |
95 | return; |
96 | |
97 | // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. |
98 | if (type == AlternativeTextTypeCorrection) |
99 | m_rangeWithAlternative = nullptr; |
100 | m_type = type; |
101 | m_timer.startOneShot(correctionPanelTimerInterval); |
102 | } |
103 | |
104 | void AlternativeTextController::stopAlternativeTextUITimer() |
105 | { |
106 | m_timer.stop(); |
107 | m_rangeWithAlternative = nullptr; |
108 | } |
109 | |
110 | void AlternativeTextController::stopPendingCorrection(const VisibleSelection& oldSelection) |
111 | { |
112 | // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. |
113 | VisibleSelection currentSelection(m_frame.selection().selection()); |
114 | if (currentSelection == oldSelection) |
115 | return; |
116 | |
117 | stopAlternativeTextUITimer(); |
118 | dismiss(ReasonForDismissingAlternativeTextIgnored); |
119 | } |
120 | |
121 | void AlternativeTextController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping) |
122 | { |
123 | // Apply pending autocorrection before next round of spell checking. |
124 | bool doApplyCorrection = true; |
125 | VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); |
126 | VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); |
127 | if (currentWord.visibleEnd() == startOfSelection) { |
128 | String wordText = plainText(currentWord.toNormalizedRange().get()); |
129 | if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) |
130 | doApplyCorrection = false; |
131 | } |
132 | if (doApplyCorrection) |
133 | handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted)); |
134 | else |
135 | m_rangeWithAlternative = nullptr; |
136 | } |
137 | |
138 | bool AlternativeTextController::hasPendingCorrection() const |
139 | { |
140 | return m_rangeWithAlternative; |
141 | } |
142 | |
143 | bool AlternativeTextController::isSpellingMarkerAllowed(Range& misspellingRange) const |
144 | { |
145 | return !m_frame.document()->markers().hasMarkers(misspellingRange, DocumentMarker::SpellCheckingExemption); |
146 | } |
147 | |
148 | void AlternativeTextController::show(Range& rangeToReplace, const String& replacement) |
149 | { |
150 | FloatRect boundingBox = rootViewRectForRange(&rangeToReplace); |
151 | if (boundingBox.isEmpty()) |
152 | return; |
153 | m_originalText = plainText(&rangeToReplace); |
154 | m_rangeWithAlternative = &rangeToReplace; |
155 | m_details = replacement; |
156 | m_isActive = true; |
157 | if (AlternativeTextClient* client = alternativeTextClient()) |
158 | client->showCorrectionAlternative(m_type, boundingBox, m_originalText, replacement, { }); |
159 | } |
160 | |
161 | void AlternativeTextController::handleCancelOperation() |
162 | { |
163 | if (!m_isActive) |
164 | return; |
165 | m_isActive = false; |
166 | dismiss(ReasonForDismissingAlternativeTextCancelled); |
167 | } |
168 | |
169 | void AlternativeTextController::dismiss(ReasonForDismissingAlternativeText reasonForDismissing) |
170 | { |
171 | if (!m_isActive) |
172 | return; |
173 | m_isActive = false; |
174 | m_isDismissedByEditing = true; |
175 | if (AlternativeTextClient* client = alternativeTextClient()) |
176 | client->dismissAlternative(reasonForDismissing); |
177 | } |
178 | |
179 | String AlternativeTextController::dismissSoon(ReasonForDismissingAlternativeText reasonForDismissing) |
180 | { |
181 | if (!m_isActive) |
182 | return String(); |
183 | m_isActive = false; |
184 | m_isDismissedByEditing = true; |
185 | if (AlternativeTextClient* client = alternativeTextClient()) |
186 | return client->dismissAlternativeSoon(reasonForDismissing); |
187 | return String(); |
188 | } |
189 | |
190 | void AlternativeTextController::applyAlternativeTextToRange(const Range& range, const String& alternative, AlternativeTextType alternativeType, OptionSet<DocumentMarker::MarkerType> markerTypesToAdd) |
191 | { |
192 | auto paragraphRangeContainingCorrection = range.cloneRange(); |
193 | |
194 | setStart(paragraphRangeContainingCorrection.ptr(), startOfParagraph(range.startPosition())); |
195 | setEnd(paragraphRangeContainingCorrection.ptr(), endOfParagraph(range.endPosition())); |
196 | |
197 | // After we replace the word at range rangeWithAlternative, we need to add markers to that range. |
198 | // However, once the replacement took place, the value of rangeWithAlternative is not valid anymore. |
199 | // So before we carry out the replacement, we need to store the start position of rangeWithAlternative |
200 | // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph |
201 | // to store this value. In order to obtain this offset, we need to first create a range |
202 | // which spans from the start of paragraph to the start position of rangeWithAlternative. |
203 | auto correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer().document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); |
204 | |
205 | Position startPositionOfRangeWithAlternative = range.startPosition(); |
206 | if (!startPositionOfRangeWithAlternative.containerNode()) |
207 | return; |
208 | auto setEndResult = correctionStartOffsetInParagraphAsRange->setEnd(*startPositionOfRangeWithAlternative.containerNode(), startPositionOfRangeWithAlternative.computeOffsetInContainerNode()); |
209 | if (setEndResult.hasException()) |
210 | return; |
211 | |
212 | // Take note of the location of autocorrection so that we can add marker after the replacement took place. |
213 | int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.ptr()); |
214 | |
215 | // Clone the range, since the caller of this method may want to keep the original range around. |
216 | auto rangeWithAlternative = range.cloneRange(); |
217 | |
218 | ContainerNode& rootNode = paragraphRangeContainingCorrection->startContainer().treeScope().rootNode(); |
219 | int paragraphStartIndex = TextIterator::rangeLength(Range::create(rootNode.document(), &rootNode, 0, ¶graphRangeContainingCorrection->startContainer(), paragraphRangeContainingCorrection->startOffset()).ptr()); |
220 | SpellingCorrectionCommand::create(rangeWithAlternative, alternative)->apply(); |
221 | // Recalculate pragraphRangeContainingCorrection, since SpellingCorrectionCommand modified the DOM, such that the original paragraphRangeContainingCorrection is no longer valid. Radar: 10305315 Bugzilla: 89526 |
222 | auto updatedParagraphRangeContainingCorrection = TextIterator::rangeFromLocationAndLength(&rootNode, paragraphStartIndex, correctionStartOffsetInParagraph + alternative.length()); |
223 | if (!updatedParagraphRangeContainingCorrection) |
224 | return; |
225 | |
226 | setEnd(updatedParagraphRangeContainingCorrection.get(), m_frame.selection().selection().start()); |
227 | RefPtr<Range> replacementRange = TextIterator::subrange(*updatedParagraphRangeContainingCorrection, correctionStartOffsetInParagraph, alternative.length()); |
228 | String newText = plainText(replacementRange.get()); |
229 | |
230 | // Check to see if replacement succeeded. |
231 | if (newText != alternative) |
232 | return; |
233 | |
234 | DocumentMarkerController& markers = replacementRange->startContainer().document().markers(); |
235 | |
236 | for (auto markerType : markerTypesToAdd) |
237 | markers.addMarker(*replacementRange, markerType, markerDescriptionForAppliedAlternativeText(alternativeType, markerType)); |
238 | } |
239 | |
240 | bool AlternativeTextController::applyAutocorrectionBeforeTypingIfAppropriate() |
241 | { |
242 | if (!m_rangeWithAlternative || !m_isActive) |
243 | return false; |
244 | |
245 | if (m_type != AlternativeTextTypeCorrection) |
246 | return false; |
247 | |
248 | Position caretPosition = m_frame.selection().selection().start(); |
249 | |
250 | if (m_rangeWithAlternative->endPosition() == caretPosition) { |
251 | handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted)); |
252 | return true; |
253 | } |
254 | |
255 | // Pending correction should always be where caret is. But in case this is not always true, we still want to dismiss the panel without accepting the correction. |
256 | ASSERT(m_rangeWithAlternative->endPosition() == caretPosition); |
257 | dismiss(ReasonForDismissingAlternativeTextIgnored); |
258 | return false; |
259 | } |
260 | |
261 | void AlternativeTextController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) |
262 | { |
263 | if (AlternativeTextClient* client = alternativeTextClient()) |
264 | client->recordAutocorrectionResponse(AutocorrectionResponse::Reverted, corrected, correction); |
265 | |
266 | Ref<Frame> protector(m_frame); |
267 | m_frame.document()->updateLayout(); |
268 | m_frame.selection().setSelection(selectionOfCorrected, FrameSelection::defaultSetSelectionOptions() | FrameSelection::SpellCorrectionTriggered); |
269 | auto range = Range::create(*m_frame.document(), m_frame.selection().selection().start(), m_frame.selection().selection().end()); |
270 | |
271 | auto& markers = m_frame.document()->markers(); |
272 | markers.removeMarkers(range, OptionSet<DocumentMarker::MarkerType> { DocumentMarker::Spelling, DocumentMarker::Autocorrected }, DocumentMarkerController::RemovePartiallyOverlappingMarker); |
273 | markers.addMarker(range, DocumentMarker::Replacement); |
274 | markers.addMarker(range, DocumentMarker::SpellCheckingExemption); |
275 | } |
276 | |
277 | void AlternativeTextController::timerFired() |
278 | { |
279 | m_isDismissedByEditing = false; |
280 | switch (m_type) { |
281 | case AlternativeTextTypeCorrection: { |
282 | VisibleSelection selection(m_frame.selection().selection()); |
283 | VisiblePosition start(selection.start(), selection.affinity()); |
284 | VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); |
285 | VisibleSelection adjacentWords = VisibleSelection(p, start); |
286 | auto adjacentWordRange = adjacentWords.toNormalizedRange(); |
287 | m_frame.editor().markAllMisspellingsAndBadGrammarInRanges({ TextCheckingType::Spelling, TextCheckingType::Replacement, TextCheckingType::ShowCorrectionPanel }, adjacentWordRange.copyRef(), adjacentWordRange.copyRef(), nullptr); |
288 | } |
289 | break; |
290 | case AlternativeTextTypeReversion: { |
291 | if (!m_rangeWithAlternative) |
292 | break; |
293 | String replacementString = WTF::get<AutocorrectionReplacement>(m_details); |
294 | if (replacementString.isEmpty()) |
295 | break; |
296 | m_isActive = true; |
297 | m_originalText = plainText(m_rangeWithAlternative.get()); |
298 | FloatRect boundingBox = rootViewRectForRange(m_rangeWithAlternative.get()); |
299 | if (!boundingBox.isEmpty()) { |
300 | if (AlternativeTextClient* client = alternativeTextClient()) |
301 | client->showCorrectionAlternative(m_type, boundingBox, m_originalText, replacementString, { }); |
302 | } |
303 | } |
304 | break; |
305 | case AlternativeTextTypeSpellingSuggestions: { |
306 | if (!m_rangeWithAlternative || plainText(m_rangeWithAlternative.get()) != m_originalText) |
307 | break; |
308 | String paragraphText = plainText(&TextCheckingParagraph(*m_rangeWithAlternative).paragraphRange()); |
309 | Vector<String> suggestions; |
310 | textChecker()->getGuessesForWord(m_originalText, paragraphText, m_frame.selection().selection(), suggestions); |
311 | if (suggestions.isEmpty()) { |
312 | m_rangeWithAlternative = nullptr; |
313 | break; |
314 | } |
315 | String topSuggestion = suggestions.first(); |
316 | suggestions.remove(0); |
317 | m_isActive = true; |
318 | FloatRect boundingBox = rootViewRectForRange(m_rangeWithAlternative.get()); |
319 | if (!boundingBox.isEmpty()) { |
320 | if (AlternativeTextClient* client = alternativeTextClient()) |
321 | client->showCorrectionAlternative(m_type, boundingBox, m_originalText, topSuggestion, suggestions); |
322 | } |
323 | } |
324 | break; |
325 | case AlternativeTextTypeDictationAlternatives: |
326 | { |
327 | #if USE(DICTATION_ALTERNATIVES) |
328 | if (!m_rangeWithAlternative) |
329 | return; |
330 | uint64_t dictationContext = WTF::get<AlternativeDictationContext>(m_details); |
331 | if (!dictationContext) |
332 | return; |
333 | FloatRect boundingBox = rootViewRectForRange(m_rangeWithAlternative.get()); |
334 | m_isActive = true; |
335 | if (!boundingBox.isEmpty()) { |
336 | if (AlternativeTextClient* client = alternativeTextClient()) |
337 | client->showDictationAlternativeUI(boundingBox, dictationContext); |
338 | } |
339 | #endif |
340 | } |
341 | break; |
342 | } |
343 | } |
344 | |
345 | void AlternativeTextController::handleAlternativeTextUIResult(const String& result) |
346 | { |
347 | Range* rangeWithAlternative = m_rangeWithAlternative.get(); |
348 | if (!rangeWithAlternative || m_frame.document() != &rangeWithAlternative->ownerDocument()) |
349 | return; |
350 | |
351 | String currentWord = plainText(rangeWithAlternative); |
352 | // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. |
353 | if (currentWord != m_originalText) |
354 | return; |
355 | |
356 | m_isActive = false; |
357 | |
358 | switch (m_type) { |
359 | case AlternativeTextTypeCorrection: |
360 | if (result.length()) |
361 | applyAlternativeTextToRange(*rangeWithAlternative, result, m_type, markerTypesForAutocorrection()); |
362 | else if (!m_isDismissedByEditing) |
363 | rangeWithAlternative->startContainer().document().markers().addMarker(*rangeWithAlternative, DocumentMarker::RejectedCorrection, m_originalText); |
364 | break; |
365 | case AlternativeTextTypeReversion: |
366 | case AlternativeTextTypeSpellingSuggestions: |
367 | if (result.length()) |
368 | applyAlternativeTextToRange(*rangeWithAlternative, result, m_type, markerTypesForReplacement()); |
369 | break; |
370 | case AlternativeTextTypeDictationAlternatives: |
371 | if (result.length()) |
372 | applyAlternativeTextToRange(*rangeWithAlternative, result, m_type, markerTypesForAppliedDictationAlternative()); |
373 | break; |
374 | } |
375 | |
376 | m_rangeWithAlternative = nullptr; |
377 | } |
378 | |
379 | bool AlternativeTextController::isAutomaticSpellingCorrectionEnabled() |
380 | { |
381 | return editorClient() && editorClient()->isAutomaticSpellingCorrectionEnabled(); |
382 | } |
383 | |
384 | FloatRect AlternativeTextController::rootViewRectForRange(const Range* range) const |
385 | { |
386 | FrameView* view = m_frame.view(); |
387 | if (!view) |
388 | return FloatRect(); |
389 | Vector<FloatQuad> textQuads; |
390 | range->absoluteTextQuads(textQuads); |
391 | FloatRect boundingRect; |
392 | for (auto& textQuad : textQuads) |
393 | boundingRect.unite(textQuad.boundingBox()); |
394 | return view->contentsToRootView(IntRect(boundingRect)); |
395 | } |
396 | |
397 | void AlternativeTextController::respondToChangedSelection(const VisibleSelection& oldSelection) |
398 | { |
399 | VisibleSelection currentSelection(m_frame.selection().selection()); |
400 | // When user moves caret to the end of autocorrected word and pauses, we show the panel |
401 | // containing the original pre-correction word so that user can quickly revert the |
402 | // undesired autocorrection. Here, we start correction panel timer once we confirm that |
403 | // the new caret position is at the end of a word. |
404 | if (!currentSelection.isCaret() || currentSelection == oldSelection || !currentSelection.isContentEditable()) |
405 | return; |
406 | |
407 | VisiblePosition selectionPosition = currentSelection.start(); |
408 | |
409 | // Creating a Visible position triggers a layout and there is no |
410 | // guarantee that the selection is still valid. |
411 | if (selectionPosition.isNull()) |
412 | return; |
413 | |
414 | VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); |
415 | if (selectionPosition != endPositionOfWord) |
416 | return; |
417 | |
418 | Position position = endPositionOfWord.deepEquivalent(); |
419 | if (position.anchorType() != Position::PositionIsOffsetInAnchor) |
420 | return; |
421 | |
422 | Node* node = position.containerNode(); |
423 | ASSERT(node); |
424 | for (auto* marker : node->document().markers().markersFor(*node)) { |
425 | ASSERT(marker); |
426 | if (respondToMarkerAtEndOfWord(*marker, position)) |
427 | break; |
428 | } |
429 | } |
430 | |
431 | void AlternativeTextController::respondToAppliedEditing(CompositeEditCommand* command) |
432 | { |
433 | if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) |
434 | m_frame.document()->markers().removeMarkers(DocumentMarker::CorrectionIndicator); |
435 | |
436 | markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(command); |
437 | m_originalStringForLastDeletedAutocorrection = String(); |
438 | |
439 | dismiss(ReasonForDismissingAlternativeTextIgnored); |
440 | } |
441 | |
442 | void AlternativeTextController::respondToUnappliedEditing(EditCommandComposition* command) |
443 | { |
444 | if (!command->wasCreateLinkCommand()) |
445 | return; |
446 | auto range = Range::create(*m_frame.document(), command->startingSelection().start(), command->startingSelection().end()); |
447 | auto& markers = m_frame.document()->markers(); |
448 | markers.addMarker(range, DocumentMarker::Replacement); |
449 | markers.addMarker(range, DocumentMarker::SpellCheckingExemption); |
450 | } |
451 | |
452 | AlternativeTextClient* AlternativeTextController::alternativeTextClient() |
453 | { |
454 | return m_frame.page() ? m_frame.page()->alternativeTextClient() : nullptr; |
455 | } |
456 | |
457 | EditorClient* AlternativeTextController::editorClient() |
458 | { |
459 | return m_frame.page() ? &m_frame.page()->editorClient() : nullptr; |
460 | } |
461 | |
462 | TextCheckerClient* AlternativeTextController::textChecker() |
463 | { |
464 | if (EditorClient* owner = editorClient()) |
465 | return owner->textChecker(); |
466 | return nullptr; |
467 | } |
468 | |
469 | void AlternativeTextController::recordAutocorrectionResponse(AutocorrectionResponse response, const String& replacedString, Range* replacementRange) |
470 | { |
471 | if (auto client = alternativeTextClient()) |
472 | client->recordAutocorrectionResponse(response, replacedString, plainText(replacementRange)); |
473 | } |
474 | |
475 | void AlternativeTextController::markReversed(Range& changedRange) |
476 | { |
477 | changedRange.startContainer().document().markers().removeMarkers(changedRange, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); |
478 | changedRange.startContainer().document().markers().addMarker(changedRange, DocumentMarker::SpellCheckingExemption); |
479 | } |
480 | |
481 | void AlternativeTextController::markCorrection(Range& replacedRange, const String& replacedString) |
482 | { |
483 | DocumentMarkerController& markers = replacedRange.startContainer().document().markers(); |
484 | for (auto markerType : markerTypesForAutocorrection()) { |
485 | if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected) |
486 | markers.addMarker(replacedRange, markerType, replacedString); |
487 | else |
488 | markers.addMarker(replacedRange, markerType); |
489 | } |
490 | } |
491 | |
492 | void AlternativeTextController::recordSpellcheckerResponseForModifiedCorrection(Range& rangeOfCorrection, const String& corrected, const String& correction) |
493 | { |
494 | DocumentMarkerController& markers = rangeOfCorrection.startContainer().document().markers(); |
495 | Vector<RenderedDocumentMarker*> correctedOnceMarkers = markers.markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected); |
496 | if (correctedOnceMarkers.isEmpty()) |
497 | return; |
498 | |
499 | if (AlternativeTextClient* client = alternativeTextClient()) { |
500 | // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or |
501 | // edited it to something else, and notify spellchecker accordingly. |
502 | if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0]->description() == corrected) |
503 | client->recordAutocorrectionResponse(AutocorrectionResponse::Reverted, corrected, correction); |
504 | else |
505 | client->recordAutocorrectionResponse(AutocorrectionResponse::Edited, corrected, correction); |
506 | } |
507 | |
508 | markers.removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); |
509 | } |
510 | |
511 | void AlternativeTextController::deletedAutocorrectionAtPosition(const Position& position, const String& originalString) |
512 | { |
513 | m_originalStringForLastDeletedAutocorrection = originalString; |
514 | m_positionForLastDeletedAutocorrection = position; |
515 | } |
516 | |
517 | void AlternativeTextController::markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(EditCommand* command) |
518 | { |
519 | Position endOfSelection = command->endingSelection().end(); |
520 | if (endOfSelection != m_positionForLastDeletedAutocorrection) |
521 | return; |
522 | |
523 | Position precedingCharacterPosition = endOfSelection.previous(); |
524 | if (endOfSelection == precedingCharacterPosition) |
525 | return; |
526 | |
527 | auto precedingCharacterRange = Range::create(*m_frame.document(), precedingCharacterPosition, endOfSelection); |
528 | String string = plainText(precedingCharacterRange.ptr()); |
529 | if (string.isEmpty() || !deprecatedIsEditingWhitespace(string[string.length() - 1])) |
530 | return; |
531 | |
532 | // Mark this whitespace to indicate we have deleted an autocorrection following this |
533 | // whitespace. So if the user types the same original word again at this position, we |
534 | // won't autocorrect it again. |
535 | m_frame.document()->markers().addMarker(precedingCharacterRange, DocumentMarker::DeletedAutocorrection, m_originalStringForLastDeletedAutocorrection); |
536 | } |
537 | |
538 | bool AlternativeTextController::processMarkersOnTextToBeReplacedByResult(const TextCheckingResult& result, Range& rangeWithAlternative, const String& stringToBeReplaced) |
539 | { |
540 | DocumentMarkerController& markerController = m_frame.document()->markers(); |
541 | if (markerController.hasMarkers(rangeWithAlternative, DocumentMarker::Replacement)) { |
542 | if (result.type == TextCheckingType::Correction) |
543 | recordSpellcheckerResponseForModifiedCorrection(rangeWithAlternative, stringToBeReplaced, result.replacement); |
544 | return false; |
545 | } |
546 | |
547 | if (markerController.hasMarkers(rangeWithAlternative, DocumentMarker::RejectedCorrection)) |
548 | return false; |
549 | |
550 | if (markerController.hasMarkers(rangeWithAlternative, DocumentMarker::AcceptedCandidate)) |
551 | return false; |
552 | |
553 | Position beginningOfRange = rangeWithAlternative.startPosition(); |
554 | Position precedingCharacterPosition = beginningOfRange.previous(); |
555 | auto precedingCharacterRange = Range::create(*m_frame.document(), precedingCharacterPosition, beginningOfRange); |
556 | |
557 | Vector<RenderedDocumentMarker*> markers = markerController.markersInRange(precedingCharacterRange, DocumentMarker::DeletedAutocorrection); |
558 | for (const auto* marker : markers) { |
559 | if (marker->description() == stringToBeReplaced) |
560 | return false; |
561 | } |
562 | |
563 | return true; |
564 | } |
565 | |
566 | bool AlternativeTextController::shouldStartTimerFor(const WebCore::DocumentMarker &marker, int endOffset) const |
567 | { |
568 | return (((marker.type() == DocumentMarker::Replacement && !marker.description().isNull()) || marker.type() == DocumentMarker::Spelling || marker.type() == DocumentMarker::DictationAlternatives) && static_cast<int>(marker.endOffset()) == endOffset); |
569 | } |
570 | |
571 | bool AlternativeTextController::respondToMarkerAtEndOfWord(const DocumentMarker& marker, const Position& endOfWordPosition) |
572 | { |
573 | if (!shouldStartTimerFor(marker, endOfWordPosition.offsetInContainerNode())) |
574 | return false; |
575 | Node* node = endOfWordPosition.containerNode(); |
576 | auto wordRange = Range::create(*m_frame.document(), node, marker.startOffset(), node, marker.endOffset()); |
577 | String currentWord = plainText(wordRange.ptr()); |
578 | if (!currentWord.length()) |
579 | return false; |
580 | m_originalText = currentWord; |
581 | switch (marker.type()) { |
582 | case DocumentMarker::Spelling: |
583 | m_rangeWithAlternative = WTFMove(wordRange); |
584 | m_details = emptyString(); |
585 | startAlternativeTextUITimer(AlternativeTextTypeSpellingSuggestions); |
586 | break; |
587 | case DocumentMarker::Replacement: |
588 | m_rangeWithAlternative = WTFMove(wordRange); |
589 | m_details = marker.description(); |
590 | startAlternativeTextUITimer(AlternativeTextTypeReversion); |
591 | break; |
592 | case DocumentMarker::DictationAlternatives: { |
593 | if (!WTF::holds_alternative<DocumentMarker::DictationData>(marker.data())) |
594 | return false; |
595 | auto& markerData = WTF::get<DocumentMarker::DictationData>(marker.data()); |
596 | if (currentWord != markerData.originalText) |
597 | return false; |
598 | m_rangeWithAlternative = WTFMove(wordRange); |
599 | m_details = markerData.context; |
600 | startAlternativeTextUITimer(AlternativeTextTypeDictationAlternatives); |
601 | } |
602 | break; |
603 | default: |
604 | ASSERT_NOT_REACHED(); |
605 | break; |
606 | } |
607 | return true; |
608 | } |
609 | |
610 | String AlternativeTextController::markerDescriptionForAppliedAlternativeText(AlternativeTextType alternativeTextType, DocumentMarker::MarkerType markerType) |
611 | { |
612 | |
613 | if (alternativeTextType != AlternativeTextTypeReversion && alternativeTextType != AlternativeTextTypeDictationAlternatives && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)) |
614 | return m_originalText; |
615 | return emptyString(); |
616 | } |
617 | |
618 | #endif |
619 | |
620 | bool AlternativeTextController::insertDictatedText(const String& text, const Vector<DictationAlternative>& dictationAlternatives, Event* triggeringEvent) |
621 | { |
622 | EventTarget* target; |
623 | if (triggeringEvent) |
624 | target = triggeringEvent->target(); |
625 | else |
626 | target = eventTargetElementForDocument(m_frame.document()); |
627 | if (!target) |
628 | return false; |
629 | |
630 | if (FrameView* view = m_frame.view()) |
631 | view->disableLayerFlushThrottlingTemporarilyForInteraction(); |
632 | |
633 | auto event = TextEvent::createForDictation(&m_frame.windowProxy(), text, dictationAlternatives); |
634 | event->setUnderlyingEvent(triggeringEvent); |
635 | |
636 | target->dispatchEvent(event); |
637 | return event->defaultHandled(); |
638 | } |
639 | |
640 | void AlternativeTextController::removeDictationAlternativesForMarker(const DocumentMarker& marker) |
641 | { |
642 | #if USE(DICTATION_ALTERNATIVES) |
643 | ASSERT(WTF::holds_alternative<DocumentMarker::DictationData>(marker.data())); |
644 | if (auto* client = alternativeTextClient()) |
645 | client->removeDictationAlternatives(WTF::get<DocumentMarker::DictationData>(marker.data()).context); |
646 | #else |
647 | UNUSED_PARAM(marker); |
648 | #endif |
649 | } |
650 | |
651 | Vector<String> AlternativeTextController::dictationAlternativesForMarker(const DocumentMarker& marker) |
652 | { |
653 | #if USE(DICTATION_ALTERNATIVES) |
654 | ASSERT(marker.type() == DocumentMarker::DictationAlternatives); |
655 | if (auto* client = alternativeTextClient()) |
656 | return client->dictationAlternatives(WTF::get<DocumentMarker::DictationData>(marker.data()).context); |
657 | return Vector<String>(); |
658 | #else |
659 | UNUSED_PARAM(marker); |
660 | return Vector<String>(); |
661 | #endif |
662 | } |
663 | |
664 | void AlternativeTextController::applyDictationAlternative(const String& alternativeString) |
665 | { |
666 | #if USE(DICTATION_ALTERNATIVES) |
667 | Editor& editor = m_frame.editor(); |
668 | RefPtr<Range> selection = editor.selectedRange(); |
669 | if (!selection || !editor.shouldInsertText(alternativeString, selection.get(), EditorInsertAction::Pasted)) |
670 | return; |
671 | DocumentMarkerController& markers = selection->startContainer().document().markers(); |
672 | Vector<RenderedDocumentMarker*> dictationAlternativesMarkers = markers.markersInRange(*selection, DocumentMarker::DictationAlternatives); |
673 | for (auto* marker : dictationAlternativesMarkers) |
674 | removeDictationAlternativesForMarker(*marker); |
675 | |
676 | applyAlternativeTextToRange(*selection, alternativeString, AlternativeTextTypeDictationAlternatives, markerTypesForAppliedDictationAlternative()); |
677 | #else |
678 | UNUSED_PARAM(alternativeString); |
679 | #endif |
680 | } |
681 | |
682 | } // namespace WebCore |
683 | |