1/* 2 * Copyright (C) 2006, 2007, 2008 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 "Editor.h" 33#include "Element.h" 34#include "Event.h" 35#include "ExceptionCodePlaceholder.h" 36#include "FloatQuad.h" 37#include "Frame.h" 38#include "FrameView.h" 39#include "Page.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 "htmlediting.h" 47#include "markup.h" 48#include <wtf/NeverDestroyed.h> 49 50namespace WebCore { 51 52class AutocorrectionAlternativeDetails : public AlternativeTextDetails { 53public: 54 static PassRefPtr<AutocorrectionAlternativeDetails> create(const String& replacementString) 55 { 56 return adoptRef(new AutocorrectionAlternativeDetails(replacementString)); 57 } 58 59 const String& replacementString() const { return m_replacementString; } 60private: 61 AutocorrectionAlternativeDetails(const String& replacementString) 62 : m_replacementString(replacementString) 63 { } 64 65 String m_replacementString; 66}; 67 68class DictationAlternativeDetails : public AlternativeTextDetails { 69public: 70 static PassRefPtr<DictationAlternativeDetails> create(uint64_t dictationContext) 71 { 72 return adoptRef(new DictationAlternativeDetails(dictationContext)); 73 } 74 75 uint64_t dictationContext() const { return m_dictationContext; } 76 77private: 78 DictationAlternativeDetails(uint64_t dictationContext) 79 : m_dictationContext(dictationContext) 80 { } 81 82 uint64_t m_dictationContext; 83}; 84 85#if USE(AUTOCORRECTION_PANEL) 86 87static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection() 88{ 89 static NeverDestroyed<Vector<DocumentMarker::MarkerType>> markerTypesForAutoCorrection; 90 if (markerTypesForAutoCorrection.get().isEmpty()) { 91 markerTypesForAutoCorrection.get().append(DocumentMarker::Replacement); 92 markerTypesForAutoCorrection.get().append(DocumentMarker::CorrectionIndicator); 93 markerTypesForAutoCorrection.get().append(DocumentMarker::SpellCheckingExemption); 94 markerTypesForAutoCorrection.get().append(DocumentMarker::Autocorrected); 95 } 96 return markerTypesForAutoCorrection; 97} 98 99static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement() 100{ 101 static NeverDestroyed<Vector<DocumentMarker::MarkerType>> markerTypesForReplacement; 102 if (markerTypesForReplacement.get().isEmpty()) { 103 markerTypesForReplacement.get().append(DocumentMarker::Replacement); 104 markerTypesForReplacement.get().append(DocumentMarker::SpellCheckingExemption); 105 } 106 return markerTypesForReplacement; 107} 108 109static const Vector<DocumentMarker::MarkerType>& markerTypesForAppliedDictationAlternative() 110{ 111 static NeverDestroyed<Vector<DocumentMarker::MarkerType>> markerTypesForAppliedDictationAlternative; 112 if (markerTypesForAppliedDictationAlternative.get().isEmpty()) 113 markerTypesForAppliedDictationAlternative.get().append(DocumentMarker::SpellCheckingExemption); 114 return markerTypesForAppliedDictationAlternative; 115} 116 117static bool markersHaveIdenticalDescription(const Vector<DocumentMarker*>& markers) 118{ 119 if (markers.isEmpty()) 120 return true; 121 122 const String& description = markers[0]->description(); 123 for (size_t i = 1; i < markers.size(); ++i) { 124 if (description != markers[i]->description()) 125 return false; 126 } 127 return true; 128} 129 130AlternativeTextController::AlternativeTextController(Frame& frame) 131 : m_timer(this, &AlternativeTextController::timerFired) 132 , m_frame(frame) 133{ 134} 135 136AlternativeTextController::~AlternativeTextController() 137{ 138 dismiss(ReasonForDismissingAlternativeTextIgnored); 139} 140 141void AlternativeTextController::startAlternativeTextUITimer(AlternativeTextType type) 142{ 143 const double correctionPanelTimerInterval = 0.3; 144 if (!isAutomaticSpellingCorrectionEnabled()) 145 return; 146 147 // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. 148 if (type == AlternativeTextTypeCorrection) 149 m_alternativeTextInfo.rangeWithAlternative.clear(); 150 m_alternativeTextInfo.type = type; 151 m_timer.startOneShot(correctionPanelTimerInterval); 152} 153 154void AlternativeTextController::stopAlternativeTextUITimer() 155{ 156 m_timer.stop(); 157 m_alternativeTextInfo.rangeWithAlternative.clear(); 158} 159 160void AlternativeTextController::stopPendingCorrection(const VisibleSelection& oldSelection) 161{ 162 // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. 163 VisibleSelection currentSelection(m_frame.selection().selection()); 164 if (currentSelection == oldSelection) 165 return; 166 167 stopAlternativeTextUITimer(); 168 dismiss(ReasonForDismissingAlternativeTextIgnored); 169} 170 171void AlternativeTextController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping) 172{ 173 // Apply pending autocorrection before next round of spell checking. 174 bool doApplyCorrection = true; 175 VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); 176 VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); 177 if (currentWord.visibleEnd() == startOfSelection) { 178 String wordText = plainText(currentWord.toNormalizedRange().get()); 179 if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) 180 doApplyCorrection = false; 181 } 182 if (doApplyCorrection) 183 handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted)); 184 else 185 m_alternativeTextInfo.rangeWithAlternative.clear(); 186} 187 188bool AlternativeTextController::hasPendingCorrection() const 189{ 190 return m_alternativeTextInfo.rangeWithAlternative; 191} 192 193bool AlternativeTextController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const 194{ 195 return !m_frame.document()->markers().hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption); 196} 197 198void AlternativeTextController::show(PassRefPtr<Range> rangeToReplace, const String& replacement) 199{ 200 FloatRect boundingBox = rootViewRectForRange(rangeToReplace.get()); 201 if (boundingBox.isEmpty()) 202 return; 203 m_alternativeTextInfo.originalText = plainText(rangeToReplace.get()); 204 m_alternativeTextInfo.rangeWithAlternative = rangeToReplace; 205 m_alternativeTextInfo.details = AutocorrectionAlternativeDetails::create(replacement); 206 m_alternativeTextInfo.isActive = true; 207 if (AlternativeTextClient* client = alternativeTextClient()) 208 client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, replacement, Vector<String>()); 209} 210 211void AlternativeTextController::handleCancelOperation() 212{ 213 if (!m_alternativeTextInfo.isActive) 214 return; 215 m_alternativeTextInfo.isActive = false; 216 dismiss(ReasonForDismissingAlternativeTextCancelled); 217} 218 219void AlternativeTextController::dismiss(ReasonForDismissingAlternativeText reasonForDismissing) 220{ 221 if (!m_alternativeTextInfo.isActive) 222 return; 223 m_alternativeTextInfo.isActive = false; 224 m_isDismissedByEditing = true; 225 if (AlternativeTextClient* client = alternativeTextClient()) 226 client->dismissAlternative(reasonForDismissing); 227} 228 229String AlternativeTextController::dismissSoon(ReasonForDismissingAlternativeText reasonForDismissing) 230{ 231 if (!m_alternativeTextInfo.isActive) 232 return String(); 233 m_alternativeTextInfo.isActive = false; 234 m_isDismissedByEditing = true; 235 if (AlternativeTextClient* client = alternativeTextClient()) 236 return client->dismissAlternativeSoon(reasonForDismissing); 237 return String(); 238} 239 240void AlternativeTextController::applyAlternativeTextToRange(const Range* range, const String& alternative, AlternativeTextType alternativeType, const Vector<DocumentMarker::MarkerType>& markerTypesToAdd) 241{ 242 if (!range) 243 return; 244 245 ExceptionCode ec = 0; 246 RefPtr<Range> paragraphRangeContainingCorrection = range->cloneRange(ec); 247 if (ec) 248 return; 249 250 setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(range->startPosition())); 251 setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(range->endPosition())); 252 253 // After we replace the word at range rangeWithAlternative, we need to add markers to that range. 254 // However, once the replacement took place, the value of rangeWithAlternative is not valid anymore. 255 // So before we carry out the replacement, we need to store the start position of rangeWithAlternative 256 // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph 257 // to store this value. In order to obtain this offset, we need to first create a range 258 // which spans from the start of paragraph to the start position of rangeWithAlternative. 259 RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); 260 if (ec) 261 return; 262 263 Position startPositionOfRangeWithAlternative = range->startPosition(); 264 correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeWithAlternative.containerNode(), startPositionOfRangeWithAlternative.computeOffsetInContainerNode(), ec); 265 if (ec) 266 return; 267 268 // Take note of the location of autocorrection so that we can add marker after the replacement took place. 269 int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); 270 271 // Clone the range, since the caller of this method may want to keep the original range around. 272 RefPtr<Range> rangeWithAlternative = range->cloneRange(ec); 273 274 ContainerNode& rootNode = paragraphRangeContainingCorrection.get()->startContainer()->treeScope().rootNode(); 275 int paragraphStartIndex = TextIterator::rangeLength(Range::create(rootNode.document(), &rootNode, 0, paragraphRangeContainingCorrection.get()->startContainer(), paragraphRangeContainingCorrection.get()->startOffset()).get()); 276 applyCommand(SpellingCorrectionCommand::create(rangeWithAlternative, alternative)); 277 // Recalculate pragraphRangeContainingCorrection, since SpellingCorrectionCommand modified the DOM, such that the original paragraphRangeContainingCorrection is no longer valid. Radar: 10305315 Bugzilla: 89526 278 paragraphRangeContainingCorrection = TextIterator::rangeFromLocationAndLength(&rootNode, paragraphStartIndex, correctionStartOffsetInParagraph + alternative.length()); 279 280 setEnd(paragraphRangeContainingCorrection.get(), m_frame.selection().selection().start()); 281 RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, alternative.length()); 282 String newText = plainText(replacementRange.get()); 283 284 // Check to see if replacement succeeded. 285 if (newText != alternative) 286 return; 287 288 DocumentMarkerController& markers = replacementRange->startContainer()->document().markers(); 289 size_t size = markerTypesToAdd.size(); 290 for (size_t i = 0; i < size; ++i) 291 markers.addMarker(replacementRange.get(), markerTypesToAdd[i], markerDescriptionForAppliedAlternativeText(alternativeType, markerTypesToAdd[i])); 292} 293 294bool AlternativeTextController::applyAutocorrectionBeforeTypingIfAppropriate() 295{ 296 if (!m_alternativeTextInfo.rangeWithAlternative || !m_alternativeTextInfo.isActive) 297 return false; 298 299 if (m_alternativeTextInfo.type != AlternativeTextTypeCorrection) 300 return false; 301 302 Position caretPosition = m_frame.selection().selection().start(); 303 304 if (m_alternativeTextInfo.rangeWithAlternative->endPosition() == caretPosition) { 305 handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted)); 306 return true; 307 } 308 309 // 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. 310 ASSERT(m_alternativeTextInfo.rangeWithAlternative->endPosition() == caretPosition); 311 dismiss(ReasonForDismissingAlternativeTextIgnored); 312 return false; 313} 314 315void AlternativeTextController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) 316{ 317 if (AlternativeTextClient* client = alternativeTextClient()) 318 client->recordAutocorrectionResponse(AutocorrectionReverted, corrected, correction); 319 m_frame.document()->updateLayout(); 320 m_frame.selection().setSelection(selectionOfCorrected, FrameSelection::defaultSetSelectionOptions() | FrameSelection::SpellCorrectionTriggered); 321 RefPtr<Range> range = Range::create(*m_frame.document(), m_frame.selection().selection().start(), m_frame.selection().selection().end()); 322 323 DocumentMarkerController& markers = m_frame.document()->markers(); 324 markers.removeMarkers(range.get(), DocumentMarker::Spelling | DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 325 markers.addMarker(range.get(), DocumentMarker::Replacement); 326 markers.addMarker(range.get(), DocumentMarker::SpellCheckingExemption); 327} 328 329void AlternativeTextController::timerFired(Timer<AlternativeTextController>&) 330{ 331 m_isDismissedByEditing = false; 332 switch (m_alternativeTextInfo.type) { 333 case AlternativeTextTypeCorrection: { 334 VisibleSelection selection(m_frame.selection().selection()); 335 VisiblePosition start(selection.start(), selection.affinity()); 336 VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); 337 VisibleSelection adjacentWords = VisibleSelection(p, start); 338 m_frame.editor().markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeSpelling | TextCheckingTypeReplacement | TextCheckingTypeShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); 339 } 340 break; 341 case AlternativeTextTypeReversion: { 342 if (!m_alternativeTextInfo.rangeWithAlternative) 343 break; 344 m_alternativeTextInfo.isActive = true; 345 m_alternativeTextInfo.originalText = plainText(m_alternativeTextInfo.rangeWithAlternative.get()); 346 FloatRect boundingBox = rootViewRectForRange(m_alternativeTextInfo.rangeWithAlternative.get()); 347 if (!boundingBox.isEmpty()) { 348 const AutocorrectionAlternativeDetails* details = static_cast<const AutocorrectionAlternativeDetails*>(m_alternativeTextInfo.details.get()); 349 if (AlternativeTextClient* client = alternativeTextClient()) 350 client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, details->replacementString(), Vector<String>()); 351 } 352 } 353 break; 354 case AlternativeTextTypeSpellingSuggestions: { 355 if (!m_alternativeTextInfo.rangeWithAlternative || plainText(m_alternativeTextInfo.rangeWithAlternative.get()) != m_alternativeTextInfo.originalText) 356 break; 357 String paragraphText = plainText(TextCheckingParagraph(m_alternativeTextInfo.rangeWithAlternative).paragraphRange().get()); 358 Vector<String> suggestions; 359 textChecker()->getGuessesForWord(m_alternativeTextInfo.originalText, paragraphText, suggestions); 360 if (suggestions.isEmpty()) { 361 m_alternativeTextInfo.rangeWithAlternative.clear(); 362 break; 363 } 364 String topSuggestion = suggestions.first(); 365 suggestions.remove(0); 366 m_alternativeTextInfo.isActive = true; 367 FloatRect boundingBox = rootViewRectForRange(m_alternativeTextInfo.rangeWithAlternative.get()); 368 if (!boundingBox.isEmpty()) { 369 if (AlternativeTextClient* client = alternativeTextClient()) 370 client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, topSuggestion, suggestions); 371 } 372 } 373 break; 374 case AlternativeTextTypeDictationAlternatives: 375 { 376#if USE(DICTATION_ALTERNATIVES) 377 const Range* rangeWithAlternative = m_alternativeTextInfo.rangeWithAlternative.get(); 378 const DictationAlternativeDetails* details = static_cast<const DictationAlternativeDetails*>(m_alternativeTextInfo.details.get()); 379 if (!rangeWithAlternative || !details || !details->dictationContext()) 380 return; 381 FloatRect boundingBox = rootViewRectForRange(rangeWithAlternative); 382 m_alternativeTextInfo.isActive = true; 383 if (!boundingBox.isEmpty()) { 384 if (AlternativeTextClient* client = alternativeTextClient()) 385 client->showDictationAlternativeUI(boundingBox, details->dictationContext()); 386 } 387#endif 388 } 389 break; 390 } 391} 392 393void AlternativeTextController::handleAlternativeTextUIResult(const String& result) 394{ 395 Range* rangeWithAlternative = m_alternativeTextInfo.rangeWithAlternative.get(); 396 if (!rangeWithAlternative || m_frame.document() != &rangeWithAlternative->ownerDocument()) 397 return; 398 399 String currentWord = plainText(rangeWithAlternative); 400 // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. 401 if (currentWord != m_alternativeTextInfo.originalText) 402 return; 403 404 m_alternativeTextInfo.isActive = false; 405 406 switch (m_alternativeTextInfo.type) { 407 case AlternativeTextTypeCorrection: 408 if (result.length()) 409 applyAlternativeTextToRange(rangeWithAlternative, result, m_alternativeTextInfo.type, markerTypesForAutocorrection()); 410 else if (!m_isDismissedByEditing) 411 rangeWithAlternative->startContainer()->document().markers().addMarker(rangeWithAlternative, DocumentMarker::RejectedCorrection, m_alternativeTextInfo.originalText); 412 break; 413 case AlternativeTextTypeReversion: 414 case AlternativeTextTypeSpellingSuggestions: 415 if (result.length()) 416 applyAlternativeTextToRange(rangeWithAlternative, result, m_alternativeTextInfo.type, markerTypesForReplacement()); 417 break; 418 case AlternativeTextTypeDictationAlternatives: 419 if (result.length()) 420 applyAlternativeTextToRange(rangeWithAlternative, result, m_alternativeTextInfo.type, markerTypesForAppliedDictationAlternative()); 421 break; 422 } 423 424 m_alternativeTextInfo.rangeWithAlternative.clear(); 425} 426 427bool AlternativeTextController::isAutomaticSpellingCorrectionEnabled() 428{ 429 return editorClient() && editorClient()->isAutomaticSpellingCorrectionEnabled(); 430} 431 432FloatRect AlternativeTextController::rootViewRectForRange(const Range* range) const 433{ 434 FrameView* view = m_frame.view(); 435 if (!view) 436 return FloatRect(); 437 Vector<FloatQuad> textQuads; 438 range->textQuads(textQuads); 439 FloatRect boundingRect; 440 size_t size = textQuads.size(); 441 for (size_t i = 0; i < size; ++i) 442 boundingRect.unite(textQuads[i].boundingBox()); 443 return view->contentsToRootView(IntRect(boundingRect)); 444} 445 446void AlternativeTextController::respondToChangedSelection(const VisibleSelection& oldSelection) 447{ 448 VisibleSelection currentSelection(m_frame.selection().selection()); 449 // When user moves caret to the end of autocorrected word and pauses, we show the panel 450 // containing the original pre-correction word so that user can quickly revert the 451 // undesired autocorrection. Here, we start correction panel timer once we confirm that 452 // the new caret position is at the end of a word. 453 if (!currentSelection.isCaret() || currentSelection == oldSelection || !currentSelection.isContentEditable()) 454 return; 455 456 VisiblePosition selectionPosition = currentSelection.start(); 457 458 // Creating a Visible position triggers a layout and there is no 459 // guarantee that the selection is still valid. 460 if (selectionPosition.isNull()) 461 return; 462 463 VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); 464 if (selectionPosition != endPositionOfWord) 465 return; 466 467 Position position = endPositionOfWord.deepEquivalent(); 468 if (position.anchorType() != Position::PositionIsOffsetInAnchor) 469 return; 470 471 Node* node = position.containerNode(); 472 Vector<DocumentMarker*> markers = node->document().markers().markersFor(node); 473 size_t markerCount = markers.size(); 474 for (size_t i = 0; i < markerCount; ++i) { 475 const DocumentMarker* marker = markers[i]; 476 if (!marker) 477 continue; 478 479 if (respondToMarkerAtEndOfWord(*marker, position)) 480 break; 481 } 482} 483 484void AlternativeTextController::respondToAppliedEditing(CompositeEditCommand* command) 485{ 486 if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) 487 m_frame.document()->markers().removeMarkers(DocumentMarker::CorrectionIndicator); 488 489 markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(command); 490 m_originalStringForLastDeletedAutocorrection = String(); 491} 492 493void AlternativeTextController::respondToUnappliedEditing(EditCommandComposition* command) 494{ 495 if (!command->wasCreateLinkCommand()) 496 return; 497 RefPtr<Range> range = Range::create(*m_frame.document(), command->startingSelection().start(), command->startingSelection().end()); 498 if (!range) 499 return; 500 DocumentMarkerController& markers = m_frame.document()->markers(); 501 markers.addMarker(range.get(), DocumentMarker::Replacement); 502 markers.addMarker(range.get(), DocumentMarker::SpellCheckingExemption); 503} 504 505AlternativeTextClient* AlternativeTextController::alternativeTextClient() 506{ 507 return m_frame.page() ? m_frame.page()->alternativeTextClient() : 0; 508} 509 510EditorClient* AlternativeTextController::editorClient() 511{ 512 return m_frame.page() ? m_frame.page()->editorClient() : 0; 513} 514 515TextCheckerClient* AlternativeTextController::textChecker() 516{ 517 if (EditorClient* owner = editorClient()) 518 return owner->textChecker(); 519 return 0; 520} 521 522void AlternativeTextController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString) 523{ 524 if (AlternativeTextClient* client = alternativeTextClient()) 525 client->recordAutocorrectionResponse(AutocorrectionReverted, replacedString, replacementString); 526} 527 528void AlternativeTextController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange) 529{ 530 recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get())); 531} 532 533void AlternativeTextController::markReversed(PassRefPtr<Range> changedRange) 534{ 535 changedRange->startContainer()->document().markers().removeMarkers(changedRange.get(), DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 536 changedRange->startContainer()->document().markers().addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption); 537} 538 539void AlternativeTextController::markCorrection(PassRefPtr<Range> replacedRange, const String& replacedString) 540{ 541 Vector<DocumentMarker::MarkerType> markerTypesToAdd = markerTypesForAutocorrection(); 542 DocumentMarkerController& markers = replacedRange->startContainer()->document().markers(); 543 for (size_t i = 0; i < markerTypesToAdd.size(); ++i) { 544 DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; 545 if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected) 546 markers.addMarker(replacedRange.get(), markerType, replacedString); 547 else 548 markers.addMarker(replacedRange.get(), markerType); 549 } 550} 551 552void AlternativeTextController::recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction) 553{ 554 if (!rangeOfCorrection) 555 return; 556 DocumentMarkerController& markers = rangeOfCorrection->startContainer()->document().markers(); 557 Vector<DocumentMarker*> correctedOnceMarkers = markers.markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected); 558 if (correctedOnceMarkers.isEmpty()) 559 return; 560 561 if (AlternativeTextClient* client = alternativeTextClient()) { 562 // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or 563 // edited it to something else, and notify spellchecker accordingly. 564 if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0]->description() == corrected) 565 client->recordAutocorrectionResponse(AutocorrectionReverted, corrected, correction); 566 else 567 client->recordAutocorrectionResponse(AutocorrectionEdited, corrected, correction); 568 } 569 570 markers.removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 571} 572 573void AlternativeTextController::deletedAutocorrectionAtPosition(const Position& position, const String& originalString) 574{ 575 m_originalStringForLastDeletedAutocorrection = originalString; 576 m_positionForLastDeletedAutocorrection = position; 577} 578 579void AlternativeTextController::markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(EditCommand* command) 580{ 581 Position endOfSelection = command->endingSelection().end(); 582 if (endOfSelection != m_positionForLastDeletedAutocorrection) 583 return; 584 585 Position precedingCharacterPosition = endOfSelection.previous(); 586 if (endOfSelection == precedingCharacterPosition) 587 return; 588 589 RefPtr<Range> precedingCharacterRange = Range::create(*m_frame.document(), precedingCharacterPosition, endOfSelection); 590 String string = plainText(precedingCharacterRange.get()); 591 if (string.isEmpty() || !isWhitespace(string[string.length() - 1])) 592 return; 593 594 // Mark this whitespace to indicate we have deleted an autocorrection following this 595 // whitespace. So if the user types the same original word again at this position, we 596 // won't autocorrect it again. 597 m_frame.document()->markers().addMarker(precedingCharacterRange.get(), DocumentMarker::DeletedAutocorrection, m_originalStringForLastDeletedAutocorrection); 598} 599 600bool AlternativeTextController::processMarkersOnTextToBeReplacedByResult(const TextCheckingResult* result, Range* rangeWithAlternative, const String& stringToBeReplaced) 601{ 602 DocumentMarkerController& markerController = m_frame.document()->markers(); 603 if (markerController.hasMarkers(rangeWithAlternative, DocumentMarker::Replacement)) { 604 if (result->type == TextCheckingTypeCorrection) 605 recordSpellcheckerResponseForModifiedCorrection(rangeWithAlternative, stringToBeReplaced, result->replacement); 606 return false; 607 } 608 609 if (markerController.hasMarkers(rangeWithAlternative, DocumentMarker::RejectedCorrection)) 610 return false; 611 612 Position beginningOfRange = rangeWithAlternative->startPosition(); 613 Position precedingCharacterPosition = beginningOfRange.previous(); 614 RefPtr<Range> precedingCharacterRange = Range::create(*m_frame.document(), precedingCharacterPosition, beginningOfRange); 615 616 Vector<DocumentMarker*> markers = markerController.markersInRange(precedingCharacterRange.get(), DocumentMarker::DeletedAutocorrection); 617 618 for (size_t i = 0; i < markers.size(); ++i) { 619 if (markers[i]->description() == stringToBeReplaced) 620 return false; 621 } 622 623 return true; 624} 625 626bool AlternativeTextController::shouldStartTimerFor(const WebCore::DocumentMarker &marker, int endOffset) const 627{ 628 return (((marker.type() == DocumentMarker::Replacement && !marker.description().isNull()) || marker.type() == DocumentMarker::Spelling || marker.type() == DocumentMarker::DictationAlternatives) && static_cast<int>(marker.endOffset()) == endOffset); 629} 630 631bool AlternativeTextController::respondToMarkerAtEndOfWord(const DocumentMarker& marker, const Position& endOfWordPosition) 632{ 633 if (!shouldStartTimerFor(marker, endOfWordPosition.offsetInContainerNode())) 634 return false; 635 Node* node = endOfWordPosition.containerNode(); 636 RefPtr<Range> wordRange = Range::create(*m_frame.document(), node, marker.startOffset(), node, marker.endOffset()); 637 if (!wordRange) 638 return false; 639 String currentWord = plainText(wordRange.get()); 640 if (!currentWord.length()) 641 return false; 642 m_alternativeTextInfo.originalText = currentWord; 643 switch (marker.type()) { 644 case DocumentMarker::Spelling: 645 m_alternativeTextInfo.rangeWithAlternative = wordRange; 646 m_alternativeTextInfo.details = AutocorrectionAlternativeDetails::create(""); 647 startAlternativeTextUITimer(AlternativeTextTypeSpellingSuggestions); 648 break; 649 case DocumentMarker::Replacement: 650 m_alternativeTextInfo.rangeWithAlternative = wordRange; 651 m_alternativeTextInfo.details = AutocorrectionAlternativeDetails::create(marker.description()); 652 startAlternativeTextUITimer(AlternativeTextTypeReversion); 653 break; 654 case DocumentMarker::DictationAlternatives: { 655 const DictationMarkerDetails* markerDetails = static_cast<const DictationMarkerDetails*>(marker.details()); 656 if (!markerDetails) 657 return false; 658 if (currentWord != markerDetails->originalText()) 659 return false; 660 m_alternativeTextInfo.rangeWithAlternative = wordRange; 661 m_alternativeTextInfo.details = DictationAlternativeDetails::create(markerDetails->dictationContext()); 662 startAlternativeTextUITimer(AlternativeTextTypeDictationAlternatives); 663 } 664 break; 665 default: 666 ASSERT_NOT_REACHED(); 667 break; 668 } 669 return true; 670} 671 672String AlternativeTextController::markerDescriptionForAppliedAlternativeText(AlternativeTextType alternativeTextType, DocumentMarker::MarkerType markerType) 673{ 674 675 if (alternativeTextType != AlternativeTextTypeReversion && alternativeTextType != AlternativeTextTypeDictationAlternatives && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)) 676 return m_alternativeTextInfo.originalText; 677 return ""; 678} 679 680#endif 681 682bool AlternativeTextController::insertDictatedText(const String& text, const Vector<DictationAlternative>& dictationAlternatives, Event* triggeringEvent) 683{ 684 EventTarget* target; 685 if (triggeringEvent) 686 target = triggeringEvent->target(); 687 else 688 target = eventTargetElementForDocument(m_frame.document()); 689 if (!target) 690 return false; 691 692 if (FrameView* view = m_frame.view()) 693 view->disableLayerFlushThrottlingTemporarilyForInteraction(); 694 695 RefPtr<TextEvent> event = TextEvent::createForDictation(m_frame.document()->domWindow(), text, dictationAlternatives); 696 event->setUnderlyingEvent(triggeringEvent); 697 698 target->dispatchEvent(event, IGNORE_EXCEPTION); 699 return event->defaultHandled(); 700} 701 702void AlternativeTextController::removeDictationAlternativesForMarker(const DocumentMarker* marker) 703{ 704#if USE(DICTATION_ALTERNATIVES) 705 ASSERT(marker->details()); 706 if (DictationMarkerDetails* details = static_cast<DictationMarkerDetails*>(marker->details())) { 707 if (AlternativeTextClient* client = alternativeTextClient()) 708 client->removeDictationAlternatives(details->dictationContext()); 709 } 710#else 711 UNUSED_PARAM(marker); 712#endif 713} 714 715Vector<String> AlternativeTextController::dictationAlternativesForMarker(const DocumentMarker* marker) 716{ 717#if USE(DICTATION_ALTERNATIVES) 718 ASSERT(marker->type() == DocumentMarker::DictationAlternatives); 719 if (AlternativeTextClient* client = alternativeTextClient()) { 720 DictationMarkerDetails* details = static_cast<DictationMarkerDetails*>(marker->details()); 721 return client->dictationAlternatives(details->dictationContext()); 722 } 723 return Vector<String>(); 724#else 725 UNUSED_PARAM(marker); 726 return Vector<String>(); 727#endif 728} 729 730void AlternativeTextController::applyDictationAlternative(const String& alternativeString) 731{ 732#if USE(DICTATION_ALTERNATIVES) 733 Editor& editor = m_frame.editor(); 734 RefPtr<Range> selection = editor.selectedRange(); 735 if (!selection || !editor.shouldInsertText(alternativeString, selection.get(), EditorInsertActionPasted)) 736 return; 737 DocumentMarkerController& markers = selection->startContainer()->document().markers(); 738 Vector<DocumentMarker*> dictationAlternativesMarkers = markers.markersInRange(selection.get(), DocumentMarker::DictationAlternatives); 739 for (size_t i = 0; i < dictationAlternativesMarkers.size(); ++i) 740 removeDictationAlternativesForMarker(dictationAlternativesMarkers[i]); 741 742 applyAlternativeTextToRange(selection.get(), alternativeString, AlternativeTextTypeDictationAlternatives, markerTypesForAppliedDictationAlternative()); 743#else 744 UNUSED_PARAM(alternativeString); 745#endif 746} 747 748} // namespace WebCore 749