SpellingHandler.cpp   [plain text]


/*
 * Copyright (C) Research In Motion Limited 2013. All rights reserved.
 */

#include "config.h"
#include "SpellingHandler.h"

#include "DOMSupport.h"
#include "Frame.h"
#include "InputHandler.h"
#include "Range.h"
#include "SpellChecker.h"
#include "VisibleUnits.h"

#include <BlackBerryPlatformIMF.h>
#include <BlackBerryPlatformLog.h>
#include <BlackBerryPlatformStopWatch.h>

#define ENABLE_SPELLING_LOG 0

using namespace BlackBerry::Platform;
using namespace WebCore;

#if ENABLE_SPELLING_LOG
#define SpellingLog(severity, format, ...) Platform::logAlways(severity, format, ## __VA_ARGS__)
#else
#define SpellingLog(severity, format, ...)
#endif // ENABLE_SPELLING_LOG

static const double s_timeout = 0.05;

namespace BlackBerry {
namespace WebKit {

SpellingHandler::SpellingHandler(InputHandler* inputHandler)
    : m_inputHandler(inputHandler)
    , m_iterationDelayTimer(this, &SpellingHandler::parseBlockForSpellChecking)
    , m_isSpellCheckActive(false)
{
}

SpellingHandler::~SpellingHandler()
{
}

void SpellingHandler::spellCheckTextBlock(const WebCore::Element* element, WebCore::TextCheckingProcessType textCheckingProcessType)
{
    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock received request of type %s",
        textCheckingProcessType == TextCheckingProcessBatch ? "Batch" : "Incremental");

    if (!(element->document() && element->document()->frame() && element->document()->frame()->selection()))
        return;

    VisiblePosition caretPosition = element->document()->frame()->selection()->start();
    // 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.
    // 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.
    VisibleSelection visibleSelection = VisibleSelection(
        startOfWord(startOfLine(previousLinePosition(caretPosition, caretPosition.lineDirectionPointForBlockDirectionNavigation()))),
        endOfWord(endOfLine(caretPosition)));

    // Check if this request can be sent off in one message, or if it needs to be broken down.
    RefPtr<Range> rangeForSpellChecking = visibleSelection.toNormalizedRange();
    if (!rangeForSpellChecking || !rangeForSpellChecking->text() || !rangeForSpellChecking->text().length())
        return;

    m_textCheckingProcessType = textCheckingProcessType;

    // Spellcheck Batch requests are used when focusing an element. During this time, we might have a lingering request
    // from a previously focused element.
    if (m_textCheckingProcessType == TextCheckingProcessBatch) {
        // If a previous request is being processed, stop it before continueing.
        if (m_iterationDelayTimer.isActive())
            m_iterationDelayTimer.stop();
    }

    m_isSpellCheckActive = true;

    // If we have a batch request, try to send off the entire block.
    if (m_textCheckingProcessType == TextCheckingProcessBatch) {
        // If total block text is under the limited amount, send the entire chunk.
        if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) {
            SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock creating single batch request");
            createSpellCheckRequest(rangeForSpellChecking);
            return;
        }
    }

    // Since we couldn't check the entire block at once, set up starting and ending markers to fire incrementally.
    // Find the start and end of the region we're intending on checking
    m_startPosition = visibleSelection.visibleStart();
    m_endPosition = endOfWord(m_startPosition);
    m_endOfRange = visibleSelection.visibleEnd();
    m_cachedEndPosition = m_endOfRange;

    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::spellCheckTextBlock starting first iteration");
    m_iterationDelayTimer.startOneShot(0);
}

void SpellingHandler::createSpellCheckRequest(const PassRefPtr<WebCore::Range> rangeForSpellCheckingPtr)
{
    RefPtr<WebCore::Range> rangeForSpellChecking = rangeForSpellCheckingPtr;
    rangeForSpellChecking = DOMSupport::trimWhitespaceFromRange(rangeForSpellChecking);
    if (!rangeForSpellChecking)
        return;

    if (rangeForSpellChecking->text().length() >= MinSpellCheckingStringLength) {
        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::createSpellCheckRequest Substring text is '%s', of size %d"
            , rangeForSpellChecking->text().latin1().data()
            , rangeForSpellChecking->text().length());
        m_inputHandler->callRequestCheckingFor(SpellCheckRequest::create(TextCheckingTypeSpelling, m_textCheckingProcessType, rangeForSpellChecking, rangeForSpellChecking));
    }
}

void SpellingHandler::parseBlockForSpellChecking(WebCore::Timer<SpellingHandler>*)
{
#if ENABLE_SPELLING_LOG
    BlackBerry::Platform::StopWatch timer;
    timer.start();
#endif
    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking m_startPosition = %d, m_endPosition = %d, m_cachedEndPosition = %d, m_endOfRange = %d"
        , DOMSupport::offsetFromStartOfBlock(m_startPosition)
        , DOMSupport::offsetFromStartOfBlock(m_endPosition)
        , DOMSupport::offsetFromStartOfBlock(m_cachedEndPosition)
        , DOMSupport::offsetFromStartOfBlock(m_endOfRange));

    if (m_startPosition == m_endOfRange)
        return;

    RefPtr<Range> rangeForSpellChecking = makeRange(m_startPosition, m_endPosition);
    if (!rangeForSpellChecking) {
        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking Failed to set text range for spellchecking.");
        return;
    }

    if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) {
        if (m_endPosition == m_endOfRange || m_cachedEndPosition == m_endPosition) {
            createSpellCheckRequest(rangeForSpellChecking);
            m_isSpellCheckActive = false;
            return;
        }

        incrementSentinels(false /* shouldIncrementStartPosition */);
#if ENABLE_SPELLING_LOG
        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking spellcheck iteration took %lf seconds", timer.elapsed());
#endif
        m_iterationDelayTimer.startOneShot(s_timeout);
        return;
    }

    // Create a spellcheck request with the substring if we have a range that is of size less than MaxSpellCheckingStringLength
    if (rangeForSpellChecking = handleOversizedRange())
        createSpellCheckRequest(rangeForSpellChecking);

    if (isSpellCheckActive()) {
#if ENABLE_SPELLING_LOG
        SpellingLog(Platform::LogLevelInfo, "SpellingHandler::parseBlockForSpellChecking spellcheck iteration took %lf seconds", timer.elapsed());
#endif
        m_iterationDelayTimer.startOneShot(s_timeout);
    }
}

PassRefPtr<Range> SpellingHandler::handleOversizedRange()
{
    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::handleOversizedRange");

    if (m_startPosition == m_cachedEndPosition || m_startPosition == startOfWord(m_endPosition, LeftWordIfOnBoundary)) {
        // Our first word has gone over the character limit. Increment the starting position past an uncheckable word.
        incrementSentinels(true /* shouldIncrementStartPosition */);
        return 0;
    }

    // If this is not the first word, return a Range with end boundary set to the previous word.
    RefPtr<Range> rangeToStartOfOversizedWord = makeRange(m_startPosition, m_cachedEndPosition);
    // We've created the range using the cached end position. Now increment the sentinals forward.
    // FIXME Incrementing the start/end positions outside of incrementSentinels
    m_startPosition = m_cachedEndPosition;
    m_endPosition = endOfWord(m_startPosition);
    return rangeToStartOfOversizedWord;
}

void SpellingHandler::incrementSentinels(bool shouldIncrementStartPosition)
{
    SpellingLog(Platform::LogLevelInfo, "SpellingHandler::incrementSentinels shouldIncrementStartPosition %s", shouldIncrementStartPosition ? "true" : "false");

    if (shouldIncrementStartPosition)
        m_startPosition = m_endPosition;

    VisiblePosition nextWord = nextWordPosition(m_endPosition);
    VisiblePosition startOfNextWord = startOfWord(nextWord, LeftWordIfOnBoundary);
    if (DOMSupport::isRangeTextAllWhitespace(m_endPosition, startOfNextWord)) {
        m_cachedEndPosition = startOfNextWord;
        m_endPosition = endOfWord(startOfNextWord);
        return;
    }

    m_cachedEndPosition = m_endPosition;
    m_endPosition = endOfWord(nextWord);
}

} // WebKit
} // BlackBerry