AlternativeTextController.cpp [plain text]
#include "config.h"
#include "AlternativeTextController.h"
#include "DictationAlternative.h"
#include "Document.h"
#include "DocumentMarkerController.h"
#include "EditCommand.h"
#include "EditorClient.h"
#include "Event.h"
#include "FloatQuad.h"
#include "Frame.h"
#include "FrameView.h"
#include "Page.h"
#include "SpellingCorrectionCommand.h"
#include "TextCheckerClient.h"
#include "TextCheckingHelper.h"
#include "TextEvent.h"
#include "TextIterator.h"
#include "VisibleSelection.h"
#include "htmlediting.h"
#include "markup.h"
#include "visible_units.h"
namespace WebCore {
using namespace std;
using namespace WTF;
class AutocorrectionAlternativeDetails : public AlternativeTextDetails {
public:
static PassRefPtr<AutocorrectionAlternativeDetails> create(const String& replacementString)
{
return adoptRef(new AutocorrectionAlternativeDetails(replacementString));
}
const String& replacementString() const { return m_replacementString; }
private:
AutocorrectionAlternativeDetails(const String& replacementString)
: m_replacementString(replacementString)
{ }
String m_replacementString;
};
#if USE(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;
}
AlternativeTextController::AlternativeTextController(Frame* frame)
: m_timer(this, &AlternativeTextController::timerFired)
, m_frame(frame)
{
}
AlternativeTextController::~AlternativeTextController()
{
dismiss(ReasonForDismissingAlternativeTextIgnored);
}
void AlternativeTextController::startAlternativeTextUITimer(AlternativeTextType type)
{
const double correctionPanelTimerInterval = 0.3;
if (!isAutomaticSpellingCorrectionEnabled())
return;
if (type == AlternativeTextTypeCorrection)
m_alternativeTextInfo.rangeWithAlternative.clear();
m_alternativeTextInfo.type = type;
m_timer.startOneShot(correctionPanelTimerInterval);
}
void AlternativeTextController::stopAlternativeTextUITimer()
{
m_timer.stop();
m_alternativeTextInfo.rangeWithAlternative.clear();
}
void AlternativeTextController::stopPendingCorrection(const VisibleSelection& oldSelection)
{
VisibleSelection currentSelection(m_frame->selection()->selection());
if (currentSelection == oldSelection)
return;
stopAlternativeTextUITimer();
dismiss(ReasonForDismissingAlternativeTextIgnored);
}
void AlternativeTextController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping)
{
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)
handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted));
else
m_alternativeTextInfo.rangeWithAlternative.clear();
}
bool AlternativeTextController::hasPendingCorrection() const
{
return m_alternativeTextInfo.rangeWithAlternative;
}
bool AlternativeTextController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const
{
return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption);
}
void AlternativeTextController::show(PassRefPtr<Range> rangeToReplace, const String& replacement)
{
FloatRect boundingBox = rootViewRectForRange(rangeToReplace.get());
if (boundingBox.isEmpty())
return;
m_alternativeTextInfo.originalText = plainText(rangeToReplace.get());
m_alternativeTextInfo.rangeWithAlternative = rangeToReplace;
m_alternativeTextInfo.details = AutocorrectionAlternativeDetails::create(replacement);
m_alternativeTextInfo.isActive = true;
if (AlternativeTextClient* client = alternativeTextClient())
client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, replacement, Vector<String>());
}
void AlternativeTextController::handleCancelOperation()
{
if (!m_alternativeTextInfo.isActive)
return;
m_alternativeTextInfo.isActive = false;
dismiss(ReasonForDismissingAlternativeTextCancelled);
}
void AlternativeTextController::dismiss(ReasonForDismissingAlternativeText reasonForDismissing)
{
if (!m_alternativeTextInfo.isActive)
return;
m_alternativeTextInfo.isActive = false;
m_isDismissedByEditing = true;
if (AlternativeTextClient* client = alternativeTextClient())
client->dismissAlternative(reasonForDismissing);
}
String AlternativeTextController::dismissSoon(ReasonForDismissingAlternativeText reasonForDismissing)
{
if (!m_alternativeTextInfo.isActive)
return String();
m_alternativeTextInfo.isActive = false;
m_isDismissedByEditing = true;
if (AlternativeTextClient* client = alternativeTextClient())
return client->dismissAlternativeSoon(reasonForDismissing);
return String();
}
void AlternativeTextController::applyAlternativeText(const String& alternative, const Vector<DocumentMarker::MarkerType>& markerTypesToAdd)
{
if (!m_alternativeTextInfo.rangeWithAlternative)
return;
ExceptionCode ec = 0;
RefPtr<Range> paragraphRangeContainingCorrection = m_alternativeTextInfo.rangeWithAlternative->cloneRange(ec);
if (ec)
return;
setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_alternativeTextInfo.rangeWithAlternative->startPosition()));
setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_alternativeTextInfo.rangeWithAlternative->endPosition()));
RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition());
if (ec)
return;
Position startPositionOfrangeWithAlternative = m_alternativeTextInfo.rangeWithAlternative->startPosition();
correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfrangeWithAlternative.containerNode(), startPositionOfrangeWithAlternative.computeOffsetInContainerNode(), ec);
if (ec)
return;
int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get());
RefPtr<Range> rangeWithAlternative = m_alternativeTextInfo.rangeWithAlternative->cloneRange(ec);
applyCommand(SpellingCorrectionCommand::create(rangeWithAlternative, alternative));
setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start());
RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, alternative.length());
String newText = plainText(replacementRange.get());
if (newText != alternative)
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_alternativeTextInfo.type != AlternativeTextTypeReversion && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected))
description = m_alternativeTextInfo.originalText;
markers->addMarker(replacementRange.get(), markerType, description);
}
}
bool AlternativeTextController::applyAutocorrectionBeforeTypingIfAppropriate()
{
if (!m_alternativeTextInfo.rangeWithAlternative || !m_alternativeTextInfo.isActive)
return false;
if (m_alternativeTextInfo.type != AlternativeTextTypeCorrection)
return false;
Position caretPosition = m_frame->selection()->selection().start();
if (m_alternativeTextInfo.rangeWithAlternative->endPosition() == caretPosition) {
handleAlternativeTextUIResult(dismissSoon(ReasonForDismissingAlternativeTextAccepted));
return true;
}
ASSERT(m_alternativeTextInfo.rangeWithAlternative->endPosition() == caretPosition);
dismiss(ReasonForDismissingAlternativeTextIgnored);
return false;
}
void AlternativeTextController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction)
{
if (AlternativeTextClient* client = alternativeTextClient())
client->recordAutocorrectionResponse(AutocorrectionReverted, corrected, correction);
m_frame->document()->updateLayout();
m_frame->selection()->setSelection(selectionOfCorrected, FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle | FrameSelection::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 AlternativeTextController::timerFired(Timer<AlternativeTextController>*)
{
m_isDismissedByEditing = false;
switch (m_alternativeTextInfo.type) {
case AlternativeTextTypeCorrection: {
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(TextCheckingTypeSpelling | TextCheckingTypeShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0);
}
break;
case AlternativeTextTypeReversion: {
if (!m_alternativeTextInfo.rangeWithAlternative)
break;
m_alternativeTextInfo.isActive = true;
m_alternativeTextInfo.originalText = plainText(m_alternativeTextInfo.rangeWithAlternative.get());
FloatRect boundingBox = rootViewRectForRange(m_alternativeTextInfo.rangeWithAlternative.get());
if (!boundingBox.isEmpty()) {
const AutocorrectionAlternativeDetails* details = static_cast<const AutocorrectionAlternativeDetails*>(m_alternativeTextInfo.details.get());
if (AlternativeTextClient* client = alternativeTextClient())
client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, details->replacementString(), Vector<String>());
}
}
break;
case AlternativeTextTypeSpellingSuggestions: {
if (!m_alternativeTextInfo.rangeWithAlternative || plainText(m_alternativeTextInfo.rangeWithAlternative.get()) != m_alternativeTextInfo.originalText)
break;
String paragraphText = plainText(TextCheckingParagraph(m_alternativeTextInfo.rangeWithAlternative).paragraphRange().get());
Vector<String> suggestions;
textChecker()->getGuessesForWord(m_alternativeTextInfo.originalText, paragraphText, suggestions);
if (suggestions.isEmpty()) {
m_alternativeTextInfo.rangeWithAlternative.clear();
break;
}
String topSuggestion = suggestions.first();
suggestions.remove(0);
m_alternativeTextInfo.isActive = true;
FloatRect boundingBox = rootViewRectForRange(m_alternativeTextInfo.rangeWithAlternative.get());
if (!boundingBox.isEmpty()) {
if (AlternativeTextClient* client = alternativeTextClient())
client->showCorrectionAlternative(m_alternativeTextInfo.type, boundingBox, m_alternativeTextInfo.originalText, topSuggestion, suggestions);
}
}
break;
}
}
void AlternativeTextController::handleAlternativeTextUIResult(const String& result)
{
Range* replacedRange = m_alternativeTextInfo.rangeWithAlternative.get();
if (!replacedRange || m_frame->document() != replacedRange->ownerDocument())
return;
String currentWord = plainText(m_alternativeTextInfo.rangeWithAlternative.get());
if (currentWord != m_alternativeTextInfo.originalText)
return;
m_alternativeTextInfo.isActive = false;
switch (m_alternativeTextInfo.type) {
case AlternativeTextTypeCorrection:
if (result.length())
applyAlternativeText(result, markerTypesForAutocorrection());
else if (!m_isDismissedByEditing)
replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_alternativeTextInfo.originalText);
break;
case AlternativeTextTypeReversion:
case AlternativeTextTypeSpellingSuggestions:
if (result.length())
applyAlternativeText(result, markerTypesForReplacement());
break;
}
m_alternativeTextInfo.rangeWithAlternative.clear();
}
bool AlternativeTextController::isAutomaticSpellingCorrectionEnabled()
{
return editorClient() && editorClient()->isAutomaticSpellingCorrectionEnabled();
}
FloatRect AlternativeTextController::rootViewRectForRange(const Range* range) const
{
FrameView* view = m_frame->view();
if (!view)
return FloatRect();
Vector<FloatQuad> textQuads;
range->textQuads(textQuads);
FloatRect boundingRect;
size_t size = textQuads.size();
for (size_t i = 0; i < size; ++i)
boundingRect.unite(textQuads[i].boundingBox());
return view->contentsToRootView(IntRect(boundingRect));
}
void AlternativeTextController::respondToChangedSelection(const VisibleSelection& oldSelection)
{
VisibleSelection currentSelection(m_frame->selection()->selection());
if (!currentSelection.isCaret() || currentSelection == oldSelection)
return;
VisiblePosition selectionPosition = currentSelection.start();
if (selectionPosition.isNull())
return;
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()->markersFor(node);
size_t markerCount = markers.size();
for (size_t i = 0; i < markerCount; ++i) {
const DocumentMarker* marker = markers[i];
if (!shouldStartTimerFor(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_alternativeTextInfo.rangeWithAlternative = wordRange;
m_alternativeTextInfo.originalText = currentWord;
if (marker->type() == DocumentMarker::Spelling)
startAlternativeTextUITimer(AlternativeTextTypeSpellingSuggestions);
else {
m_alternativeTextInfo.details = AutocorrectionAlternativeDetails::create(marker->description());
startAlternativeTextUITimer(AlternativeTextTypeReversion);
}
break;
}
}
void AlternativeTextController::respondToAppliedEditing(CompositeEditCommand* command)
{
if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator())
m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator);
markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(command);
m_originalStringForLastDeletedAutocorrection = String();
}
void AlternativeTextController::respondToUnappliedEditing(EditCommandComposition* command)
{
if (!command->wasCreateLinkCommand())
return;
RefPtr<Range> range = Range::create(m_frame->document(), command->startingSelection().start(), command->startingSelection().end());
if (!range)
return;
DocumentMarkerController* markers = m_frame->document()->markers();
markers->addMarker(range.get(), DocumentMarker::Replacement);
markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption);
}
AlternativeTextClient* AlternativeTextController::alternativeTextClient()
{
if (!m_frame)
return 0;
return m_frame->page() ? m_frame->page()->alternativeTextClient() : 0;
}
EditorClient* AlternativeTextController::editorClient()
{
if (!m_frame)
return 0;
return m_frame->page() ? m_frame->page()->editorClient() : 0;
}
TextCheckerClient* AlternativeTextController::textChecker()
{
if (EditorClient* owner = editorClient())
return owner->textChecker();
return 0;
}
void AlternativeTextController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString)
{
if (AlternativeTextClient* client = alternativeTextClient())
client->recordAutocorrectionResponse(AutocorrectionReverted, replacedString, replacementString);
}
void AlternativeTextController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange)
{
recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get()));
}
void AlternativeTextController::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 AlternativeTextController::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 AlternativeTextController::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;
if (AlternativeTextClient* client = alternativeTextClient()) {
if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0]->description() == corrected)
client->recordAutocorrectionResponse(AutocorrectionReverted, corrected, correction);
else
client->recordAutocorrectionResponse(AutocorrectionEdited, corrected, correction);
}
markers->removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker);
}
void AlternativeTextController::deletedAutocorrectionAtPosition(const Position& position, const String& originalString)
{
m_originalStringForLastDeletedAutocorrection = originalString;
m_positionForLastDeletedAutocorrection = position;
}
void AlternativeTextController::markPrecedingWhitespaceForDeletedAutocorrectionAfterCommand(EditCommand* command)
{
Position endOfSelection = command->endingSelection().end();
if (endOfSelection != m_positionForLastDeletedAutocorrection)
return;
Position precedingCharacterPosition = endOfSelection.previous();
if (endOfSelection == precedingCharacterPosition)
return;
RefPtr<Range> precedingCharacterRange = Range::create(m_frame->document(), precedingCharacterPosition, endOfSelection);
String string = plainText(precedingCharacterRange.get());
if (string.isEmpty() || !isWhitespace(string[string.length() - 1]))
return;
m_frame->document()->markers()->addMarker(precedingCharacterRange.get(), DocumentMarker::DeletedAutocorrection, m_originalStringForLastDeletedAutocorrection);
}
bool AlternativeTextController::processMarkersOnTextToBeReplacedByResult(const TextCheckingResult* result, Range* rangeWithAlternative, const String& stringToBeReplaced)
{
DocumentMarkerController* markerController = m_frame->document()->markers();
if (markerController->hasMarkers(rangeWithAlternative, DocumentMarker::Replacement)) {
if (result->type == TextCheckingTypeCorrection)
recordSpellcheckerResponseForModifiedCorrection(rangeWithAlternative, stringToBeReplaced, result->replacement);
return false;
}
if (markerController->hasMarkers(rangeWithAlternative, DocumentMarker::RejectedCorrection))
return false;
Position beginningOfRange = rangeWithAlternative->startPosition();
Position precedingCharacterPosition = beginningOfRange.previous();
RefPtr<Range> precedingCharacterRange = Range::create(m_frame->document(), precedingCharacterPosition, beginningOfRange);
Vector<DocumentMarker*> markers = markerController->markersInRange(precedingCharacterRange.get(), DocumentMarker::DeletedAutocorrection);
for (size_t i = 0; i < markers.size(); ++i) {
if (markers[i]->description() == stringToBeReplaced)
return false;
}
return true;
}
#endif
bool AlternativeTextController::insertDictatedText(const String& text, const Vector<DictationAlternative>& dictationAlternatives, Event* triggeringEvent)
{
if (!m_frame)
return false;
EventTarget* target;
if (triggeringEvent)
target = triggeringEvent->target();
else
target = eventTargetNodeForDocument(m_frame->document());
if (!target)
return false;
if (FrameView* view = m_frame->view())
view->resetDeferredRepaintDelay();
RefPtr<TextEvent> event = TextEvent::createForDictation(m_frame->domWindow(), text, dictationAlternatives);
event->setUnderlyingEvent(triggeringEvent);
ExceptionCode ec;
target->dispatchEvent(event, ec);
return event->defaultHandled();
}
}