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 44namespace WebCore { 45 46using namespace std; 47using namespace WTF; 48 49#if SUPPORT_AUTOCORRECTION_PANEL 50 51static 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 63static 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 73static 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 86SpellingCorrectionController::SpellingCorrectionController(Frame* frame) 87 : m_frame(frame) 88 , m_correctionPanelTimer(this, &SpellingCorrectionController::correctionPanelTimerFired) 89{ 90} 91 92SpellingCorrectionController::~SpellingCorrectionController() 93{ 94 dismiss(ReasonForDismissingCorrectionPanelIgnored); 95} 96 97void 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 110void SpellingCorrectionController::stopCorrectionPanelTimer() 111{ 112 m_correctionPanelTimer.stop(); 113 m_correctionPanelInfo.rangeToBeReplaced.clear(); 114} 115 116void 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 127void 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 144bool SpellingCorrectionController::hasPendingCorrection() const 145{ 146 return m_correctionPanelInfo.rangeToBeReplaced; 147} 148 149bool SpellingCorrectionController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const 150{ 151 return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption); 152} 153 154void 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 166void SpellingCorrectionController::handleCancelOperation() 167{ 168 if (!m_correctionPanelInfo.isActive) 169 return; 170 m_correctionPanelInfo.isActive = false; 171 dismiss(ReasonForDismissingCorrectionPanelCancelled); 172} 173 174void 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 184String 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 195void 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 248bool 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 269void 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 282void 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 323void 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 356bool SpellingCorrectionController::isAutomaticSpellingCorrectionEnabled() 357{ 358 return client() && client()->isAutomaticSpellingCorrectionEnabled(); 359} 360 361FloatRect SpellingCorrectionController::windowRectForRange(const Range* range) const 362{ 363 FrameView* view = m_frame->view(); 364 return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect(); 365} 366 367void 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 412void SpellingCorrectionController::respondToAppliedEditing(PassRefPtr<EditCommand> command) 413{ 414 if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) 415 m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); 416} 417 418EditorClient* SpellingCorrectionController::client() 419{ 420 return m_frame->page() ? m_frame->page()->editorClient() : 0; 421} 422 423TextCheckerClient* SpellingCorrectionController::textChecker() 424{ 425 if (EditorClient* owner = client()) 426 return owner->textChecker(); 427 return 0; 428} 429 430void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString) 431{ 432 client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacementString); 433} 434 435void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange) 436{ 437 recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get())); 438} 439 440void 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 446void 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 459void 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