1/*
2 * Copyright (C) Research In Motion Limited 2013. All rights reserved.
3 */
4
5#include "config.h"
6#include "SpellingHandler.h"
7
8#include "DOMSupport.h"
9#include "Frame.h"
10#include "InputHandler.h"
11#include "Range.h"
12#include "SpellChecker.h"
13#include "VisibleUnits.h"
14
15#include <BlackBerryPlatformIMF.h>
16#include <BlackBerryPlatformLog.h>
17#include <BlackBerryPlatformStopWatch.h>
18
19#define ENABLE_SPELLING_LOG 0
20
21using namespace BlackBerry::Platform;
22using namespace WebCore;
23
24#if ENABLE_SPELLING_LOG
25#define SpellingLog(severity, format, ...) Platform::logAlways(severity, format, ## __VA_ARGS__)
26#else
27#define SpellingLog(severity, format, ...)
28#endif // ENABLE_SPELLING_LOG
29
30static const double s_timeout = 0.05;
31
32namespace BlackBerry {
33namespace WebKit {
34
35SpellingHandler::SpellingHandler(InputHandler* inputHandler)
36    : m_inputHandler(inputHandler)
37    , m_iterationDelayTimer(this, &SpellingHandler::parseBlockForSpellChecking)
38    , m_isSpellCheckActive(false)
39{
40}
41
42SpellingHandler::~SpellingHandler()
43{
44}
45
46void SpellingHandler::spellCheckTextBlock(const WebCore::Element* element, WebCore::TextCheckingProcessType textCheckingProcessType)
47{
48    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock received request of type %s",
49        textCheckingProcessType == TextCheckingProcessBatch ? "Batch" : "Incremental");
50
51    if (!(element->document() && element->document()->frame() && element->document()->frame()->selection()))
52        return;
53
54    VisiblePosition caretPosition = element->document()->frame()->selection()->start();
55    // Expand the range to include the previous line. This should handle cases when the user hits enter to finish composing a word and create a new line.
56    // Account for word wrapping by jumping to the start of the previous line, then moving to the start of any word which might be there.
57    VisibleSelection visibleSelection = VisibleSelection(
58        startOfWord(startOfLine(previousLinePosition(caretPosition, caretPosition.lineDirectionPointForBlockDirectionNavigation()))),
59        endOfWord(endOfLine(caretPosition)));
60
61    // Check if this request can be sent off in one message, or if it needs to be broken down.
62    RefPtr<Range> rangeForSpellChecking = visibleSelection.toNormalizedRange();
63    if (!rangeForSpellChecking || !rangeForSpellChecking->text() || !rangeForSpellChecking->text().length())
64        return;
65
66    m_textCheckingProcessType = textCheckingProcessType;
67
68    // Spellcheck Batch requests are used when focusing an element. During this time, we might have a lingering request
69    // from a previously focused element.
70    if (m_textCheckingProcessType == TextCheckingProcessBatch) {
71        // If a previous request is being processed, stop it before continueing.
72        if (m_iterationDelayTimer.isActive())
73            m_iterationDelayTimer.stop();
74    }
75
76    m_isSpellCheckActive = true;
77
78    // If we have a batch request, try to send off the entire block.
79    if (m_textCheckingProcessType == TextCheckingProcessBatch) {
80        // If total block text is under the limited amount, send the entire chunk.
81        if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) {
82            SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock creating single batch request");
83            createSpellCheckRequest(rangeForSpellChecking);
84            return;
85        }
86    }
87
88    // Since we couldn't check the entire block at once, set up starting and ending markers to fire incrementally.
89    // Find the start and end of the region we're intending on checking
90    m_startPosition = visibleSelection.visibleStart();
91    m_endPosition = endOfWord(m_startPosition);
92    m_endOfRange = visibleSelection.visibleEnd();
93    m_cachedEndPosition = m_endOfRange;
94
95    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock starting first iteration");
96    m_iterationDelayTimer.startOneShot(0);
97}
98
99void SpellingHandler::createSpellCheckRequest(const PassRefPtr<WebCore::Range> rangeForSpellCheckingPtr)
100{
101    RefPtr<WebCore::Range> rangeForSpellChecking = rangeForSpellCheckingPtr;
102    rangeForSpellChecking = DOMSupport::trimWhitespaceFromRange(rangeForSpellChecking);
103    if (!rangeForSpellChecking)
104        return;
105
106    if (rangeForSpellChecking->text().length() >= MinSpellCheckingStringLength) {
107        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::createSpellCheckRequest Substring text is '%s', of size %d"
108            , rangeForSpellChecking->text().latin1().data()
109            , rangeForSpellChecking->text().length());
110        m_inputHandler->callRequestCheckingFor(SpellCheckRequest::create(TextCheckingTypeSpelling, m_textCheckingProcessType, rangeForSpellChecking, rangeForSpellChecking));
111    }
112}
113
114void SpellingHandler::parseBlockForSpellChecking(WebCore::Timer<SpellingHandler>*)
115{
116#if ENABLE_SPELLING_LOG
117    BlackBerry::Platform::StopWatch timer;
118    timer.start();
119#endif
120    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking m_startPosition = %d, m_endPosition = %d, m_cachedEndPosition = %d, m_endOfRange = %d"
121        , DOMSupport::offsetFromStartOfBlock(m_startPosition)
122        , DOMSupport::offsetFromStartOfBlock(m_endPosition)
123        , DOMSupport::offsetFromStartOfBlock(m_cachedEndPosition)
124        , DOMSupport::offsetFromStartOfBlock(m_endOfRange));
125
126    if (m_startPosition == m_endOfRange)
127        return;
128
129    RefPtr<Range> rangeForSpellChecking = makeRange(m_startPosition, m_endPosition);
130    if (!rangeForSpellChecking) {
131        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking Failed to set text range for spellchecking.");
132        return;
133    }
134
135    if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) {
136        if (m_endPosition == m_endOfRange || m_cachedEndPosition == m_endPosition) {
137            createSpellCheckRequest(rangeForSpellChecking);
138            m_isSpellCheckActive = false;
139            return;
140        }
141
142        incrementSentinels(false /* shouldIncrementStartPosition */);
143#if ENABLE_SPELLING_LOG
144        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking spellcheck iteration took %lf seconds", timer.elapsed());
145#endif
146        m_iterationDelayTimer.startOneShot(s_timeout);
147        return;
148    }
149
150    // Create a spellcheck request with the substring if we have a range that is of size less than MaxSpellCheckingStringLength
151    if (rangeForSpellChecking = handleOversizedRange())
152        createSpellCheckRequest(rangeForSpellChecking);
153
154    if (isSpellCheckActive()) {
155#if ENABLE_SPELLING_LOG
156        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking spellcheck iteration took %lf seconds", timer.elapsed());
157#endif
158        m_iterationDelayTimer.startOneShot(s_timeout);
159    }
160}
161
162PassRefPtr<Range> SpellingHandler::handleOversizedRange()
163{
164    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::handleOversizedRange");
165
166    if (m_startPosition == m_cachedEndPosition || m_startPosition == startOfWord(m_endPosition, LeftWordIfOnBoundary)) {
167        // Our first word has gone over the character limit. Increment the starting position past an uncheckable word.
168        incrementSentinels(true /* shouldIncrementStartPosition */);
169        return 0;
170    }
171
172    // If this is not the first word, return a Range with end boundary set to the previous word.
173    RefPtr<Range> rangeToStartOfOversizedWord = makeRange(m_startPosition, m_cachedEndPosition);
174    // We've created the range using the cached end position. Now increment the sentinals forward.
175    // FIXME Incrementing the start/end positions outside of incrementSentinels
176    m_startPosition = m_cachedEndPosition;
177    m_endPosition = endOfWord(m_startPosition);
178    return rangeToStartOfOversizedWord;
179}
180
181void SpellingHandler::incrementSentinels(bool shouldIncrementStartPosition)
182{
183    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::incrementSentinels shouldIncrementStartPosition %s", shouldIncrementStartPosition ? "true" : "false");
184
185    if (shouldIncrementStartPosition)
186        m_startPosition = m_endPosition;
187
188    VisiblePosition nextWord = nextWordPosition(m_endPosition);
189    VisiblePosition startOfNextWord = startOfWord(nextWord, LeftWordIfOnBoundary);
190    if (DOMSupport::isRangeTextAllWhitespace(m_endPosition, startOfNextWord)) {
191        m_cachedEndPosition = startOfNextWord;
192        m_endPosition = endOfWord(startOfNextWord);
193        return;
194    }
195
196    m_cachedEndPosition = m_endPosition;
197    m_endPosition = endOfWord(nextWord);
198}
199
200} // WebKit
201} // BlackBerry
202