/* * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "SpellingCorrectionController.h" #include "DocumentMarkerController.h" #include "EditCommand.h" #include "EditorClient.h" #include "Frame.h" #include "FrameView.h" #include "SpellingCorrectionCommand.h" #include "TextCheckerClient.h" #include "TextCheckingHelper.h" #include "TextIterator.h" #include "htmlediting.h" #include "markup.h" #include "visible_units.h" namespace WebCore { using namespace std; using namespace WTF; #if SUPPORT_AUTOCORRECTION_PANEL static const Vector& markerTypesForAutocorrection() { DEFINE_STATIC_LOCAL(Vector, markerTypesForAutoCorrection, ()); if (markerTypesForAutoCorrection.isEmpty()) { markerTypesForAutoCorrection.append(DocumentMarker::Replacement); markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator); markerTypesForAutoCorrection.append(DocumentMarker::SpellCheckingExemption); markerTypesForAutoCorrection.append(DocumentMarker::Autocorrected); } return markerTypesForAutoCorrection; } static const Vector& markerTypesForReplacement() { DEFINE_STATIC_LOCAL(Vector, markerTypesForReplacement, ()); if (markerTypesForReplacement.isEmpty()) { markerTypesForReplacement.append(DocumentMarker::Replacement); markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption); } return markerTypesForReplacement; } static bool markersHaveIdenticalDescription(const Vector& markers) { if (markers.isEmpty()) return true; const String& description = markers[0].description; for (size_t i = 1; i < markers.size(); ++i) { if (description != markers[i].description) return false; } return true; } SpellingCorrectionController::SpellingCorrectionController(Frame* frame) : m_frame(frame) , m_correctionPanelTimer(this, &SpellingCorrectionController::correctionPanelTimerFired) { } SpellingCorrectionController::~SpellingCorrectionController() { dismiss(ReasonForDismissingCorrectionPanelIgnored); } void SpellingCorrectionController::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type) { const double correctionPanelTimerInterval = 0.3; if (!isAutomaticSpellingCorrectionEnabled()) return; // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. if (type == CorrectionPanelInfo::PanelTypeCorrection) m_correctionPanelInfo.rangeToBeReplaced.clear(); m_correctionPanelInfo.panelType = type; m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval); } void SpellingCorrectionController::stopCorrectionPanelTimer() { m_correctionPanelTimer.stop(); m_correctionPanelInfo.rangeToBeReplaced.clear(); } void SpellingCorrectionController::stopPendingCorrection(const VisibleSelection& oldSelection) { // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. VisibleSelection currentSelection(m_frame->selection()->selection()); if (currentSelection == oldSelection) return; stopCorrectionPanelTimer(); dismiss(ReasonForDismissingCorrectionPanelIgnored); } void SpellingCorrectionController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping) { // Apply pending autocorrection before next round of spell checking. bool doApplyCorrection = true; VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); if (currentWord.visibleEnd() == startOfSelection) { String wordText = plainText(currentWord.toNormalizedRange().get()); if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) doApplyCorrection = false; } if (doApplyCorrection) handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); else m_correctionPanelInfo.rangeToBeReplaced.clear(); } bool SpellingCorrectionController::hasPendingCorrection() const { return m_correctionPanelInfo.rangeToBeReplaced; } bool SpellingCorrectionController::isSpellingMarkerAllowed(PassRefPtr misspellingRange) const { return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption); } void SpellingCorrectionController::show(PassRefPtr rangeToReplace, const String& replacement) { FloatRect boundingBox = windowRectForRange(rangeToReplace.get()); if (boundingBox.isEmpty()) return; m_correctionPanelInfo.replacedString = plainText(rangeToReplace.get()); m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace; m_correctionPanelInfo.replacementString = replacement; m_correctionPanelInfo.isActive = true; client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, replacement, Vector()); } void SpellingCorrectionController::handleCancelOperation() { if (!m_correctionPanelInfo.isActive) return; m_correctionPanelInfo.isActive = false; dismiss(ReasonForDismissingCorrectionPanelCancelled); } void SpellingCorrectionController::dismiss(ReasonForDismissingCorrectionPanel reasonForDismissing) { if (!m_correctionPanelInfo.isActive) return; m_correctionPanelInfo.isActive = false; m_correctionPanelIsDismissedByEditor = true; if (client()) client()->dismissCorrectionPanel(reasonForDismissing); } String SpellingCorrectionController::dismissSoon(ReasonForDismissingCorrectionPanel reasonForDismissing) { if (!m_correctionPanelInfo.isActive) return String(); m_correctionPanelInfo.isActive = false; m_correctionPanelIsDismissedByEditor = true; if (!client()) return String(); return client()->dismissCorrectionPanelSoon(reasonForDismissing); } void SpellingCorrectionController::applyCorrectionPanelInfo(const Vector& markerTypesToAdd) { if (!m_correctionPanelInfo.rangeToBeReplaced) return; ExceptionCode ec = 0; RefPtr paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); if (ec) return; setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition())); setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition())); // After we replace the word at range rangeToBeReplaced, we need to add markers to that range. // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore. // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph // to store this value. In order to obtain this offset, we need to first create a range // which spans from the start of paragraph to the start position of rangeToBeReplaced. RefPtr correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); if (ec) return; Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition(); correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec); if (ec) return; // Take note of the location of autocorrection so that we can add marker after the replacement took place. int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); // Clone the range, since the caller of this method may want to keep the original range around. RefPtr rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString)); setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start()); RefPtr replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, m_correctionPanelInfo.replacementString.length()); String newText = plainText(replacementRange.get()); // Check to see if replacement succeeded. if (newText != m_correctionPanelInfo.replacementString) return; DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers(); size_t size = markerTypesToAdd.size(); for (size_t i = 0; i < size; ++i) { DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; String description; if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeReversion && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)) description = m_correctionPanelInfo.replacedString; markers->addMarker(replacementRange.get(), markerType, description); } } bool SpellingCorrectionController::applyAutocorrectionBeforeTypingIfAppropriate() { if (!m_correctionPanelInfo.rangeToBeReplaced || !m_correctionPanelInfo.isActive) return false; if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeCorrection) return false; Position caretPosition = m_frame->selection()->selection().start(); if (m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition) { handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); return true; } // 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. ASSERT(m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition); dismiss(ReasonForDismissingCorrectionPanelIgnored); return false; } void SpellingCorrectionController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) { client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); m_frame->document()->updateLayout(); m_frame->selection()->setSelection(selectionOfCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle | SelectionController::SpellCorrectionTriggered); RefPtr range = Range::create(m_frame->document(), m_frame->selection()->selection().start(), m_frame->selection()->selection().end()); DocumentMarkerController* markers = m_frame->document()->markers(); markers->removeMarkers(range.get(), DocumentMarker::Spelling | DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); markers->addMarker(range.get(), DocumentMarker::Replacement); markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption); } void SpellingCorrectionController::correctionPanelTimerFired(Timer*) { m_correctionPanelIsDismissedByEditor = false; switch (m_correctionPanelInfo.panelType) { case CorrectionPanelInfo::PanelTypeCorrection: { VisibleSelection selection(m_frame->selection()->selection()); VisiblePosition start(selection.start(), selection.affinity()); VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); VisibleSelection adjacentWords = VisibleSelection(p, start); m_frame->editor()->markAllMisspellingsAndBadGrammarInRanges(Editor::MarkSpelling | Editor::ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); } break; case CorrectionPanelInfo::PanelTypeReversion: { m_correctionPanelInfo.isActive = true; m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); if (!boundingBox.isEmpty()) client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector()); } break; case CorrectionPanelInfo::PanelTypeSpellingSuggestions: { if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString) break; String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get()); Vector suggestions; textChecker()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions); if (suggestions.isEmpty()) { m_correctionPanelInfo.rangeToBeReplaced.clear(); break; } String topSuggestion = suggestions.first(); suggestions.remove(0); m_correctionPanelInfo.isActive = true; FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); if (!boundingBox.isEmpty()) client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, topSuggestion, suggestions); } break; } } void SpellingCorrectionController::handleCorrectionPanelResult(const String& correction) { Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get(); if (!replacedRange || m_frame->document() != replacedRange->ownerDocument()) return; String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. if (currentWord != m_correctionPanelInfo.replacedString) return; m_correctionPanelInfo.isActive = false; switch (m_correctionPanelInfo.panelType) { case CorrectionPanelInfo::PanelTypeCorrection: if (correction.length()) { m_correctionPanelInfo.replacementString = correction; applyCorrectionPanelInfo(markerTypesForAutocorrection()); } else if (!m_correctionPanelIsDismissedByEditor) replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString); break; case CorrectionPanelInfo::PanelTypeReversion: case CorrectionPanelInfo::PanelTypeSpellingSuggestions: if (correction.length()) { m_correctionPanelInfo.replacementString = correction; applyCorrectionPanelInfo(markerTypesForReplacement()); } break; } m_correctionPanelInfo.rangeToBeReplaced.clear(); } bool SpellingCorrectionController::isAutomaticSpellingCorrectionEnabled() { return client() && client()->isAutomaticSpellingCorrectionEnabled(); } FloatRect SpellingCorrectionController::windowRectForRange(const Range* range) const { FrameView* view = m_frame->view(); return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect(); } void SpellingCorrectionController::respondToChangedSelection(const VisibleSelection& oldSelection) { VisibleSelection currentSelection(m_frame->selection()->selection()); // When user moves caret to the end of autocorrected word and pauses, we show the panel // containing the original pre-correction word so that user can quickly revert the // undesired autocorrection. Here, we start correction panel timer once we confirm that // the new caret position is at the end of a word. if (!currentSelection.isCaret() || currentSelection == oldSelection) return; VisiblePosition selectionPosition = currentSelection.start(); VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); if (selectionPosition != endPositionOfWord) return; Position position = endPositionOfWord.deepEquivalent(); if (position.anchorType() != Position::PositionIsOffsetInAnchor) return; Node* node = position.containerNode(); int endOffset = position.offsetInContainerNode(); Vector markers = node->document()->markers()->markersForNode(node); size_t markerCount = markers.size(); for (size_t i = 0; i < markerCount; ++i) { const DocumentMarker& marker = markers[i]; if (!shouldStartTimeFor(marker, endOffset)) continue; RefPtr wordRange = Range::create(m_frame->document(), node, marker.startOffset, node, marker.endOffset); String currentWord = plainText(wordRange.get()); if (!currentWord.length()) continue; m_correctionPanelInfo.rangeToBeReplaced = wordRange; m_correctionPanelInfo.replacedString = currentWord; if (marker.type == DocumentMarker::Spelling) startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions); else { m_correctionPanelInfo.replacementString = marker.description; startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion); } break; } } void SpellingCorrectionController::respondToAppliedEditing(PassRefPtr command) { if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); } EditorClient* SpellingCorrectionController::client() { return m_frame->page() ? m_frame->page()->editorClient() : 0; } TextCheckerClient* SpellingCorrectionController::textChecker() { if (EditorClient* owner = client()) return owner->textChecker(); return 0; } void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString) { client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacementString); } void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr replacementRange) { recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get())); } void SpellingCorrectionController::markReversed(PassRefPtr changedRange) { changedRange->startContainer()->document()->markers()->removeMarkers(changedRange.get(), DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption); } void SpellingCorrectionController::markCorrection(PassRefPtr replacedRange, const String& replacedString) { Vector markerTypesToAdd = markerTypesForAutocorrection(); DocumentMarkerController* markers = replacedRange->startContainer()->document()->markers(); for (size_t i = 0; i < markerTypesToAdd.size(); ++i) { DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected) markers->addMarker(replacedRange.get(), markerType, replacedString); else markers->addMarker(replacedRange.get(), markerType); } } void SpellingCorrectionController::recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction) { if (!rangeOfCorrection) return; DocumentMarkerController* markers = rangeOfCorrection->startContainer()->document()->markers(); Vector correctedOnceMarkers = markers->markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected); if (correctedOnceMarkers.isEmpty()) return; // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or // edited it to something else, and notify spellchecker accordingly. if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0].description == corrected) client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); else client()->recordAutocorrectionResponse(EditorClient::AutocorrectionEdited, corrected, correction); markers->removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); } #endif } // namespace WebCore