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 COMPUTER, 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 COMPUTER, 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 "SpellingCorrectionController.h" 29 30 #include "DocumentMarkerController.h" 31 #include "EditCommand.h" 32 #include "EditorClient.h" 33 #include "Frame.h" 34 #include "FrameView.h" 35 #include "SpellingCorrectionCommand.h" 36 #include "TextCheckerClient.h" 37 #include "TextCheckingHelper.h" 38 #include "TextIterator.h" 39 #include "htmlediting.h" 40 #include "markup.h" 41 #include "visible_units.h" 42 43 44 namespace WebCore { 45 46 using namespace std; 47 using namespace WTF; 48 49 #if SUPPORT_AUTOCORRECTION_PANEL 50 51 static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection() 52 { 53 DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForAutoCorrection, ()); 54 if (markerTypesForAutoCorrection.isEmpty()) { 55 markerTypesForAutoCorrection.append(DocumentMarker::Replacement); 56 markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator); 57 markerTypesForAutoCorrection.append(DocumentMarker::SpellCheckingExemption); 58 markerTypesForAutoCorrection.append(DocumentMarker::Autocorrected); 59 } 60 return markerTypesForAutoCorrection; 61 } 62 63 static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement() 64 { 65 DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ()); 66 if (markerTypesForReplacement.isEmpty()) { 67 markerTypesForReplacement.append(DocumentMarker::Replacement); 68 markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption); 69 } 70 return markerTypesForReplacement; 71 } 72 73 static bool markersHaveIdenticalDescription(const Vector<DocumentMarker>& markers) 74 { 75 if (markers.isEmpty()) 76 return true; 77 78 const String& description = markers[0].description; 79 for (size_t i = 1; i < markers.size(); ++i) { 80 if (description != markers[i].description) 81 return false; 82 } 83 return true; 84 } 85 86 SpellingCorrectionController::SpellingCorrectionController(Frame* frame) 87 : m_frame(frame) 88 , m_correctionPanelTimer(this, &SpellingCorrectionController::correctionPanelTimerFired) 89 { 90 } 91 92 SpellingCorrectionController::~SpellingCorrectionController() 93 { 94 dismiss(ReasonForDismissingCorrectionPanelIgnored); 95 } 96 97 void SpellingCorrectionController::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type) 98 { 99 const double correctionPanelTimerInterval = 0.3; 100 if (!isAutomaticSpellingCorrectionEnabled()) 101 return; 102 103 // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. 104 if (type == CorrectionPanelInfo::PanelTypeCorrection) 105 m_correctionPanelInfo.rangeToBeReplaced.clear(); 106 m_correctionPanelInfo.panelType = type; 107 m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval); 108 } 109 110 void SpellingCorrectionController::stopCorrectionPanelTimer() 111 { 112 m_correctionPanelTimer.stop(); 113 m_correctionPanelInfo.rangeToBeReplaced.clear(); 114 } 115 116 void SpellingCorrectionController::stopPendingCorrection(const VisibleSelection& oldSelection) 117 { 118 // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. 119 VisibleSelection currentSelection(m_frame->selection()->selection()); 120 if (currentSelection == oldSelection) 121 return; 122 123 stopCorrectionPanelTimer(); 124 dismiss(ReasonForDismissingCorrectionPanelIgnored); 125 } 126 127 void SpellingCorrectionController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping) 128 { 129 // Apply pending autocorrection before next round of spell checking. 130 bool doApplyCorrection = true; 131 VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); 132 VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); 133 if (currentWord.visibleEnd() == startOfSelection) { 134 String wordText = plainText(currentWord.toNormalizedRange().get()); 135 if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) 136 doApplyCorrection = false; 137 } 138 if (doApplyCorrection) 139 handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); 140 else 141 m_correctionPanelInfo.rangeToBeReplaced.clear(); 142 } 143 144 bool SpellingCorrectionController::hasPendingCorrection() const 145 { 146 return m_correctionPanelInfo.rangeToBeReplaced; 147 } 148 149 bool SpellingCorrectionController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const 150 { 151 return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption); 152 } 153 154 void SpellingCorrectionController::show(PassRefPtr<Range> rangeToReplace, const String& replacement) 155 { 156 FloatRect boundingBox = windowRectForRange(rangeToReplace.get()); 157 if (boundingBox.isEmpty()) 158 return; 159 m_correctionPanelInfo.replacedString = plainText(rangeToReplace.get()); 160 m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace; 161 m_correctionPanelInfo.replacementString = replacement; 162 m_correctionPanelInfo.isActive = true; 163 client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, replacement, Vector<String>()); 164 } 165 166 void SpellingCorrectionController::handleCancelOperation() 167 { 168 if (!m_correctionPanelInfo.isActive) 169 return; 170 m_correctionPanelInfo.isActive = false; 171 dismiss(ReasonForDismissingCorrectionPanelCancelled); 172 } 173 174 void SpellingCorrectionController::dismiss(ReasonForDismissingCorrectionPanel reasonForDismissing) 175 { 176 if (!m_correctionPanelInfo.isActive) 177 return; 178 m_correctionPanelInfo.isActive = false; 179 m_correctionPanelIsDismissedByEditor = true; 180 if (client()) 181 client()->dismissCorrectionPanel(reasonForDismissing); 182 } 183 184 String SpellingCorrectionController::dismissSoon(ReasonForDismissingCorrectionPanel reasonForDismissing) 185 { 186 if (!m_correctionPanelInfo.isActive) 187 return String(); 188 m_correctionPanelInfo.isActive = false; 189 m_correctionPanelIsDismissedByEditor = true; 190 if (!client()) 191 return String(); 192 return client()->dismissCorrectionPanelSoon(reasonForDismissing); 193 } 194 195 void SpellingCorrectionController::applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd) 196 { 197 if (!m_correctionPanelInfo.rangeToBeReplaced) 198 return; 199 200 ExceptionCode ec = 0; 201 RefPtr<Range> paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); 202 if (ec) 203 return; 204 205 setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition())); 206 setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition())); 207 208 // After we replace the word at range rangeToBeReplaced, we need to add markers to that range. 209 // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore. 210 // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced 211 // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph 212 // to store this value. In order to obtain this offset, we need to first create a range 213 // which spans from the start of paragraph to the start position of rangeToBeReplaced. 214 RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); 215 if (ec) 216 return; 217 218 Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition(); 219 correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec); 220 if (ec) 221 return; 222 223 // Take note of the location of autocorrection so that we can add marker after the replacement took place. 224 int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); 225 226 // Clone the range, since the caller of this method may want to keep the original range around. 227 RefPtr<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); 228 applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString)); 229 setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start()); 230 RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, m_correctionPanelInfo.replacementString.length()); 231 String newText = plainText(replacementRange.get()); 232 233 // Check to see if replacement succeeded. 234 if (newText != m_correctionPanelInfo.replacementString) 235 return; 236 237 DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers(); 238 size_t size = markerTypesToAdd.size(); 239 for (size_t i = 0; i < size; ++i) { 240 DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; 241 String description; 242 if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeReversion && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)) 243 description = m_correctionPanelInfo.replacedString; 244 markers->addMarker(replacementRange.get(), markerType, description); 245 } 246 } 247 248 bool SpellingCorrectionController::applyAutocorrectionBeforeTypingIfAppropriate() 249 { 250 if (!m_correctionPanelInfo.rangeToBeReplaced || !m_correctionPanelInfo.isActive) 251 return false; 252 253 if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeCorrection) 254 return false; 255 256 Position caretPosition = m_frame->selection()->selection().start(); 257 258 if (m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition) { 259 handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); 260 return true; 261 } 262 263 // 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. 264 ASSERT(m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition); 265 dismiss(ReasonForDismissingCorrectionPanelIgnored); 266 return false; 267 } 268 269 void SpellingCorrectionController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) 270 { 271 client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); 272 m_frame->document()->updateLayout(); 273 m_frame->selection()->setSelection(selectionOfCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle | SelectionController::SpellCorrectionTriggered); 274 RefPtr<Range> range = Range::create(m_frame->document(), m_frame->selection()->selection().start(), m_frame->selection()->selection().end()); 275 276 DocumentMarkerController* markers = m_frame->document()->markers(); 277 markers->removeMarkers(range.get(), DocumentMarker::Spelling | DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 278 markers->addMarker(range.get(), DocumentMarker::Replacement); 279 markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption); 280 } 281 282 void SpellingCorrectionController::correctionPanelTimerFired(Timer<SpellingCorrectionController>*) 283 { 284 m_correctionPanelIsDismissedByEditor = false; 285 switch (m_correctionPanelInfo.panelType) { 286 case CorrectionPanelInfo::PanelTypeCorrection: { 287 VisibleSelection selection(m_frame->selection()->selection()); 288 VisiblePosition start(selection.start(), selection.affinity()); 289 VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); 290 VisibleSelection adjacentWords = VisibleSelection(p, start); 291 m_frame->editor()->markAllMisspellingsAndBadGrammarInRanges(Editor::MarkSpelling | Editor::ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); 292 } 293 break; 294 case CorrectionPanelInfo::PanelTypeReversion: { 295 m_correctionPanelInfo.isActive = true; 296 m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); 297 FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); 298 if (!boundingBox.isEmpty()) 299 client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector<String>()); 300 } 301 break; 302 case CorrectionPanelInfo::PanelTypeSpellingSuggestions: { 303 if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString) 304 break; 305 String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get()); 306 Vector<String> suggestions; 307 textChecker()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions); 308 if (suggestions.isEmpty()) { 309 m_correctionPanelInfo.rangeToBeReplaced.clear(); 310 break; 311 } 312 String topSuggestion = suggestions.first(); 313 suggestions.remove(0); 314 m_correctionPanelInfo.isActive = true; 315 FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); 316 if (!boundingBox.isEmpty()) 317 client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, topSuggestion, suggestions); 318 } 319 break; 320 } 321 } 322 323 void SpellingCorrectionController::handleCorrectionPanelResult(const String& correction) 324 { 325 Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get(); 326 if (!replacedRange || m_frame->document() != replacedRange->ownerDocument()) 327 return; 328 329 String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); 330 // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. 331 if (currentWord != m_correctionPanelInfo.replacedString) 332 return; 333 334 m_correctionPanelInfo.isActive = false; 335 336 switch (m_correctionPanelInfo.panelType) { 337 case CorrectionPanelInfo::PanelTypeCorrection: 338 if (correction.length()) { 339 m_correctionPanelInfo.replacementString = correction; 340 applyCorrectionPanelInfo(markerTypesForAutocorrection()); 341 } else if (!m_correctionPanelIsDismissedByEditor) 342 replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString); 343 break; 344 case CorrectionPanelInfo::PanelTypeReversion: 345 case CorrectionPanelInfo::PanelTypeSpellingSuggestions: 346 if (correction.length()) { 347 m_correctionPanelInfo.replacementString = correction; 348 applyCorrectionPanelInfo(markerTypesForReplacement()); 349 } 350 break; 351 } 352 353 m_correctionPanelInfo.rangeToBeReplaced.clear(); 354 } 355 356 bool SpellingCorrectionController::isAutomaticSpellingCorrectionEnabled() 357 { 358 return client() && client()->isAutomaticSpellingCorrectionEnabled(); 359 } 360 361 FloatRect SpellingCorrectionController::windowRectForRange(const Range* range) const 362 { 363 FrameView* view = m_frame->view(); 364 return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect(); 365 } 366 367 void SpellingCorrectionController::respondToChangedSelection(const VisibleSelection& oldSelection) 368 { 369 VisibleSelection currentSelection(m_frame->selection()->selection()); 370 // When user moves caret to the end of autocorrected word and pauses, we show the panel 371 // containing the original pre-correction word so that user can quickly revert the 372 // undesired autocorrection. Here, we start correction panel timer once we confirm that 373 // the new caret position is at the end of a word. 374 if (!currentSelection.isCaret() || currentSelection == oldSelection) 375 return; 376 377 VisiblePosition selectionPosition = currentSelection.start(); 378 VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); 379 if (selectionPosition != endPositionOfWord) 380 return; 381 382 Position position = endPositionOfWord.deepEquivalent(); 383 if (position.anchorType() != Position::PositionIsOffsetInAnchor) 384 return; 385 386 Node* node = position.containerNode(); 387 int endOffset = position.offsetInContainerNode(); 388 Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); 389 size_t markerCount = markers.size(); 390 for (size_t i = 0; i < markerCount; ++i) { 391 const DocumentMarker& marker = markers[i]; 392 if (!shouldStartTimeFor(marker, endOffset)) 393 continue; 394 RefPtr<Range> wordRange = Range::create(m_frame->document(), node, marker.startOffset, node, marker.endOffset); 395 String currentWord = plainText(wordRange.get()); 396 if (!currentWord.length()) 397 continue; 398 399 m_correctionPanelInfo.rangeToBeReplaced = wordRange; 400 m_correctionPanelInfo.replacedString = currentWord; 401 if (marker.type == DocumentMarker::Spelling) 402 startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions); 403 else { 404 m_correctionPanelInfo.replacementString = marker.description; 405 startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion); 406 } 407 408 break; 409 } 410 } 411 412 void SpellingCorrectionController::respondToAppliedEditing(PassRefPtr<EditCommand> command) 413 { 414 if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) 415 m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); 416 } 417 418 EditorClient* SpellingCorrectionController::client() 419 { 420 return m_frame->page() ? m_frame->page()->editorClient() : 0; 421 } 422 423 TextCheckerClient* SpellingCorrectionController::textChecker() 424 { 425 if (EditorClient* owner = client()) 426 return owner->textChecker(); 427 return 0; 428 } 429 430 void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString) 431 { 432 client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacementString); 433 } 434 435 void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange) 436 { 437 recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get())); 438 } 439 440 void SpellingCorrectionController::markReversed(PassRefPtr<Range> changedRange) 441 { 442 changedRange->startContainer()->document()->markers()->removeMarkers(changedRange.get(), DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 443 changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption); 444 } 445 446 void SpellingCorrectionController::markCorrection(PassRefPtr<Range> replacedRange, const String& replacedString) 447 { 448 Vector<DocumentMarker::MarkerType> markerTypesToAdd = markerTypesForAutocorrection(); 449 DocumentMarkerController* markers = replacedRange->startContainer()->document()->markers(); 450 for (size_t i = 0; i < markerTypesToAdd.size(); ++i) { 451 DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; 452 if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected) 453 markers->addMarker(replacedRange.get(), markerType, replacedString); 454 else 455 markers->addMarker(replacedRange.get(), markerType); 456 } 457 } 458 459 void SpellingCorrectionController::recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction) 460 { 461 if (!rangeOfCorrection) 462 return; 463 DocumentMarkerController* markers = rangeOfCorrection->startContainer()->document()->markers(); 464 Vector<DocumentMarker> correctedOnceMarkers = markers->markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected); 465 if (correctedOnceMarkers.isEmpty()) 466 return; 467 468 // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or 469 // edited it to something else, and notify spellchecker accordingly. 470 if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0].description == corrected) 471 client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); 472 else 473 client()->recordAutocorrectionResponse(EditorClient::AutocorrectionEdited, corrected, correction); 474 markers->removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); 475 } 476 477 #endif 478 479 } // namespace WebCore 480