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