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
48namespace WebCore {
49
50#if USE(AUTOCORRECTION_PANEL)
51
52static inline OptionSet<DocumentMarker::MarkerType> markerTypesForAutocorrection()
53{
54 return { DocumentMarker::Autocorrected, DocumentMarker::CorrectionIndicator, DocumentMarker::Replacement, DocumentMarker::SpellCheckingExemption };
55}
56
57static inline OptionSet<DocumentMarker::MarkerType> markerTypesForReplacement()
58{
59 return { DocumentMarker::Replacement, DocumentMarker::SpellCheckingExemption };
60}
61
62static inline OptionSet<DocumentMarker::MarkerType> markerTypesForAppliedDictationAlternative()
63{
64 return DocumentMarker::SpellCheckingExemption;
65}
66
67static 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
80AlternativeTextController::AlternativeTextController(Frame& frame)
81 : m_timer(*this, &AlternativeTextController::timerFired)
82 , m_frame(frame)
83{
84}
85
86AlternativeTextController::~AlternativeTextController()
87{
88 dismiss(ReasonForDismissingAlternativeTextIgnored);
89}
90
91void 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
104void AlternativeTextController::stopAlternativeTextUITimer()
105{
106 m_timer.stop();
107 m_rangeWithAlternative = nullptr;
108}
109
110void 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
121void 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
138bool AlternativeTextController::hasPendingCorrection() const
139{
140 return m_rangeWithAlternative;
141}
142
143bool AlternativeTextController::isSpellingMarkerAllowed(Range& misspellingRange) const
144{
145 return !m_frame.document()->markers().hasMarkers(misspellingRange, DocumentMarker::SpellCheckingExemption);
146}
147
148void 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
161void AlternativeTextController::handleCancelOperation()
162{
163 if (!m_isActive)
164 return;
165 m_isActive = false;
166 dismiss(ReasonForDismissingAlternativeTextCancelled);
167}
168
169void 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
179String 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
190void 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, &paragraphRangeContainingCorrection->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
240bool 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
261void 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
277void 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
345void 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
379bool AlternativeTextController::isAutomaticSpellingCorrectionEnabled()
380{
381 return editorClient() && editorClient()->isAutomaticSpellingCorrectionEnabled();
382}
383
384FloatRect 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
397void 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
431void 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
442void 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
452AlternativeTextClient* AlternativeTextController::alternativeTextClient()
453{
454 return m_frame.page() ? m_frame.page()->alternativeTextClient() : nullptr;
455}
456
457EditorClient* AlternativeTextController::editorClient()
458{
459 return m_frame.page() ? &m_frame.page()->editorClient() : nullptr;
460}
461
462TextCheckerClient* AlternativeTextController::textChecker()
463{
464 if (EditorClient* owner = editorClient())
465 return owner->textChecker();
466 return nullptr;
467}
468
469void AlternativeTextController::recordAutocorrectionResponse(AutocorrectionResponse response, const String& replacedString, Range* replacementRange)
470{
471 if (auto client = alternativeTextClient())
472 client->recordAutocorrectionResponse(response, replacedString, plainText(replacementRange));
473}
474
475void 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
481void 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
492void 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
511void AlternativeTextController::deletedAutocorrectionAtPosition(const Position& position, const String& originalString)
512{
513 m_originalStringForLastDeletedAutocorrection = originalString;
514 m_positionForLastDeletedAutocorrection = position;
515}
516
517void 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
538bool 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
566bool 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
571bool 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
610String 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
620bool 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
640void 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
651Vector<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
664void 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