C++程序  |  480行  |  21.92 KB

/*
 * 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<DocumentMarker::MarkerType>& markerTypesForAutocorrection()
{
    DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, 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<DocumentMarker::MarkerType>& markerTypesForReplacement()
{
    DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ());
    if (markerTypesForReplacement.isEmpty()) {
        markerTypesForReplacement.append(DocumentMarker::Replacement);
        markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption);
    }
    return markerTypesForReplacement;
}

static bool markersHaveIdenticalDescription(const Vector<DocumentMarker>& 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<Range> misspellingRange) const
{
    return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption);
}

void SpellingCorrectionController::show(PassRefPtr<Range> 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<String>());
}

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<DocumentMarker::MarkerType>& markerTypesToAdd)
{
    if (!m_correctionPanelInfo.rangeToBeReplaced)
        return;

    ExceptionCode ec = 0;
    RefPtr<Range> 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<Range> 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<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec);
    applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString));
    setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start());
    RefPtr<Range> 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 = 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<SpellingCorrectionController>*)
{
    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<String>());
    }
        break;
    case CorrectionPanelInfo::PanelTypeSpellingSuggestions: {
        if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString)
            break;
        String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get());
        Vector<String> 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<DocumentMarker> 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<Range> 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<EditCommand> 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<Range> replacementRange)
{
    recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get()));
}

void SpellingCorrectionController::markReversed(PassRefPtr<Range> 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<Range> replacedRange, const String& replacedString)
{
    Vector<DocumentMarker::MarkerType> 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<DocumentMarker> 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