| 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 | |