| Index: third_party/WebKit/Source/core/editing/TextSuggestionController.cpp
|
| diff --git a/third_party/WebKit/Source/core/editing/TextSuggestionController.cpp b/third_party/WebKit/Source/core/editing/TextSuggestionController.cpp
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..287559b9683e96aa4314e9f90d978157689a97dd
|
| --- /dev/null
|
| +++ b/third_party/WebKit/Source/core/editing/TextSuggestionController.cpp
|
| @@ -0,0 +1,375 @@
|
| +// Copyright 2017 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#include "core/editing/TextSuggestionController.h"
|
| +
|
| +#include "core/dom/Document.h"
|
| +#include "core/editing/Editor.h"
|
| +#include "core/editing/FrameSelection.h"
|
| +#include "core/editing/InputMethodController.h"
|
| +#include "core/editing/PlainTextRange.h"
|
| +#include "core/editing/TextSuggestionInfo.h"
|
| +#include "core/editing/TextSuggestionList.h"
|
| +#include "core/editing/markers/DocumentMarker.h"
|
| +#include "core/editing/markers/DocumentMarkerController.h"
|
| +#include "core/frame/LocalFrame.h"
|
| +#include "core/layout/LayoutTheme.h"
|
| +#include "public/platform/InterfaceProvider.h"
|
| +#include "public/platform/InterfaceRegistry.h"
|
| +#include "wtf/Vector.h"
|
| +
|
| +namespace blink {
|
| +
|
| +namespace {
|
| +
|
| +const int kMaxNumberSuggestions = 5;
|
| +const double kSuggestionUnderlineAlphaMultiplier = 0.4;
|
| +
|
| +} // anonymous namespace
|
| +
|
| +TextSuggestionController* TextSuggestionController::create(LocalFrame& frame) {
|
| + return new TextSuggestionController(frame);
|
| +}
|
| +
|
| +TextSuggestionController::TextSuggestionController(LocalFrame& frame)
|
| + : m_suggestionMenuIsOpen(false), m_frame(&frame) {}
|
| +
|
| +bool TextSuggestionController::suggestionMenuIsOpen() const {
|
| + return m_suggestionMenuIsOpen;
|
| +}
|
| +
|
| +void TextSuggestionController::applySuggestionReplacement(
|
| + int suggestionID,
|
| + int suggestionIndex) {
|
| + suggestionMenuClosed();
|
| +
|
| + Element* rootEditableElement =
|
| + frame()
|
| + .selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()
|
| + .rootEditableElement();
|
| +
|
| + DocumentMarkerVector suggestionMarkers =
|
| + rootEditableElement->document().markers().markersInRangeInclusive(
|
| + firstEphemeralRangeOf(
|
| + m_frame->selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()),
|
| + DocumentMarker::Suggestion);
|
| +
|
| + SuggestionMarker* suggestionMarkerPtr = nullptr;
|
| + for (Member<DocumentMarker>& marker : suggestionMarkers) {
|
| + SuggestionMarker& suggestionMarker = toSuggestionMarker(*marker.get());
|
| + if (suggestionMarker.suggestionID() == suggestionID) {
|
| + suggestionMarkerPtr = &suggestionMarker;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + // TODO: replace ALL suggestion markers with ID (could span multiple nodes)
|
| + // We should always be able to find the marker we're replacing unless we have
|
| + // a bug
|
| + DCHECK(suggestionMarkerPtr);
|
| + if (!suggestionMarkerPtr)
|
| + return;
|
| +
|
| + const EphemeralRange rangeToReplace =
|
| + PlainTextRange(suggestionMarkerPtr->startOffset(), suggestionMarkerPtr->endOffset())
|
| + .createRange(*rootEditableElement);
|
| +
|
| + // The entry in the suggestion drop-down that the user tapped on is replaced
|
| + // by the text they just overwrote
|
| + String replacement =
|
| + String::fromUTF8(suggestionMarkerPtr->suggestions()[suggestionIndex].utf8());
|
| + String newSuggestion;
|
| + {
|
| + DocumentLifecycle::DisallowTransitionScope disallowTransition(
|
| + document().lifecycle());
|
| + newSuggestion =
|
| + plainText(rangeToReplace, TextIteratorBehavior::Builder().build());
|
| + }
|
| +
|
| + DCHECK(!rangeToReplace.isNull());
|
| + if (rangeToReplace.isNull())
|
| + return;
|
| +
|
| + frame().selection().setSelection(
|
| + SelectionInDOMTree::Builder().setBaseAndExtent(rangeToReplace).build(),
|
| + 0);
|
| + // Intentionally avoid sending JavaScript composition events
|
| + frame().editor().replaceSelectionWithText(
|
| + replacement, false, false, InputEvent::InputType::InsertReplacementText);
|
| +
|
| + suggestionMarkerPtr->replaceSuggestion(suggestionIndex, newSuggestion.utf8().data());
|
| +}
|
| +
|
| +void TextSuggestionController::deleteSuggestionHighlight() {
|
| + // We *do* want to remove markers touching the selection for this operation
|
| + m_suggestionMenuIsOpen = false;
|
| +
|
| + Element* rootEditableElement =
|
| + frame()
|
| + .selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()
|
| + .rootEditableElement();
|
| + DocumentMarkerVector suggestionHighlightMarkers =
|
| + rootEditableElement->document().markers().markersInRangeInclusive(
|
| + firstEphemeralRangeOf(
|
| + m_frame->selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()),
|
| + DocumentMarker::SuggestionHighlight);
|
| +
|
| + DCHECK(!suggestionHighlightMarkers.isEmpty());
|
| + if (suggestionHighlightMarkers.isEmpty())
|
| + return;
|
| +
|
| + const DocumentMarker* suggestionHighlightMarkerPtr =
|
| + suggestionHighlightMarkers[0].get();
|
| +
|
| + // If the character immediately following the range to be deleted is a space,
|
| + // delete it if either of these conditions holds:
|
| + // - We're deleting at the beginning of the editable text (to avoid ending up
|
| + // with a space at the beginning)
|
| + // - The character immediately before the range being deleted is also a space
|
| + // (to avoid ending up with two adjacent spaces)
|
| +
|
| + bool deleteNextChar = false;
|
| +
|
| + const PlainTextRange nextCharacterRange(
|
| + suggestionHighlightMarkerPtr->endOffset(),
|
| + suggestionHighlightMarkerPtr->endOffset() + 1);
|
| + const EphemeralRange& nextCharacterEphemeralRange =
|
| + nextCharacterRange.createRange(*rootEditableElement);
|
| + if (!nextCharacterEphemeralRange.isNull()) {
|
| + String nextCharacterStr = plainText(
|
| + nextCharacterEphemeralRange,
|
| + TextIteratorBehavior::Builder().setEmitsSpaceForNbsp(true).build());
|
| +
|
| + if (WTF::isASCIISpace(nextCharacterStr[0])) {
|
| + if (suggestionHighlightMarkerPtr->startOffset() == 0) {
|
| + deleteNextChar = true;
|
| + } else {
|
| + const PlainTextRange prevCharacterRange(
|
| + suggestionHighlightMarkerPtr->startOffset() - 1,
|
| + suggestionHighlightMarkerPtr->startOffset());
|
| + const EphemeralRange prevCharacterEphemeralRange =
|
| + prevCharacterRange.createRange(*rootEditableElement);
|
| + if (!prevCharacterEphemeralRange.isNull()) {
|
| + String prevCharacterStr = plainText(prevCharacterEphemeralRange,
|
| + TextIteratorBehavior::Builder()
|
| + .setEmitsSpaceForNbsp(true)
|
| + .build());
|
| + if (WTF::isASCIISpace(prevCharacterStr[0])) {
|
| + deleteNextChar = true;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + const EphemeralRange highlightRange =
|
| + PlainTextRange(suggestionHighlightMarkerPtr->startOffset(),
|
| + suggestionHighlightMarkerPtr->endOffset() + deleteNextChar)
|
| + .createRange(*rootEditableElement);
|
| +
|
| + frame().selection().setSelection(
|
| + SelectionInDOMTree::Builder().setBaseAndExtent(highlightRange).build(),
|
| + 0);
|
| + // Don't want to send JavaScript composition events here
|
| + frame().editor().replaceSelectionWithText(
|
| + "", false, false, InputEvent::InputType::InsertReplacementText);
|
| +
|
| + suggestionMenuClosed();
|
| +}
|
| +
|
| +void TextSuggestionController::suggestionMenuClosed() {
|
| + document().markers().removeMarkers(DocumentMarker::SuggestionHighlight);
|
| + frame().selection().setCaretVisible(true);
|
| + m_suggestionMenuIsOpen = false;
|
| +}
|
| +
|
| +void TextSuggestionController::handlePotentialTextSuggestionTap() {
|
| + Vector<blink::TextSuggestionInfo> suggestion_infos =
|
| + getTextSuggestionInfosUnderCaretAndAddHighlight();
|
| + if (suggestion_infos.size() == 0)
|
| + return;
|
| +
|
| + // The composition is now on the suggestion range highlight
|
| + // TODO: how to do this?
|
| + // UpdateCompositionInfo(false /* not an immediate request */);
|
| + prepareForTextSuggestionMenuToBeShown();
|
| +
|
| + if (!text_suggestion_host_) {
|
| + frame().interfaceProvider()->getInterface(
|
| + mojo::MakeRequest(&text_suggestion_host_));
|
| + }
|
| +
|
| + Vector<mojom::blink::TextSuggestionInfoPtr> suggestion_info_ptrs;
|
| + for (const blink::TextSuggestionInfo& info : suggestion_infos) {
|
| + mojom::blink::TextSuggestionInfoPtr info_ptr(
|
| + mojom::blink::TextSuggestionInfo::New());
|
| + info_ptr->suggestionID = info.suggestionID;
|
| + info_ptr->suggestionIndex = info.suggestionIndex;
|
| + info_ptr->prefix = info.prefix;
|
| + info_ptr->suggestion = info.suggestion;
|
| + info_ptr->suffix = info.suffix;
|
| +
|
| + suggestion_info_ptrs.push_back(std::move(info_ptr));
|
| + }
|
| +
|
| + text_suggestion_host_->ShowTextSuggestionMenu(
|
| + std::move(suggestion_info_ptrs));
|
| +}
|
| +
|
| +void TextSuggestionController::prepareForTextSuggestionMenuToBeShown() {
|
| + m_suggestionMenuIsOpen = true;
|
| + frame().selection().setCaretVisible(false);
|
| +}
|
| +
|
| +void TextSuggestionController::removeSuggestionMarkersAffectedByEditing(
|
| + bool doNotRemoveIfSelectionAtWordBoundary) {
|
| + // Don't remove suggestion markers when we're doing a replace operation
|
| + if (suggestionMenuIsOpen())
|
| + return;
|
| +
|
| + document().markers().removeMarkersForWordsAffectedByEditing(
|
| + DocumentMarker::Suggestion, doNotRemoveIfSelectionAtWordBoundary);
|
| +}
|
| +
|
| +bool TextSuggestionController::isAvailable() const {
|
| + return frame().document();
|
| +}
|
| +
|
| +Document& TextSuggestionController::document() const {
|
| + DCHECK(isAvailable());
|
| + return *frame().document();
|
| +}
|
| +
|
| +Vector<blink::TextSuggestionInfo>
|
| +TextSuggestionController::getTextSuggestionInfosUnderCaretAndAddHighlight()
|
| + const {
|
| + Element* rootEditableElement =
|
| + frame()
|
| + .selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()
|
| + .rootEditableElement();
|
| + if (!rootEditableElement)
|
| + return Vector<blink::TextSuggestionInfo>();
|
| +
|
| + DocumentMarkerVector suggestionMarkers =
|
| + rootEditableElement->document().markers().markersInRangeInclusive(
|
| + firstEphemeralRangeOf(
|
| + m_frame->selection()
|
| + .computeVisibleSelectionInDOMTreeDeprecated()),
|
| + DocumentMarker::Suggestion);
|
| +
|
| + Vector<TextSuggestionList> suggestionSpans;
|
| + for (const Member<DocumentMarker>& marker : suggestionMarkers) {
|
| + // ignore ranges that have been collapsed as a result of editing
|
| + // operations
|
| + if (marker->endOffset() == marker->startOffset())
|
| + continue;
|
| +
|
| + const SuggestionMarker& suggestionMarker = toSuggestionMarker(*marker);
|
| +
|
| + TextSuggestionList suggestionList;
|
| + suggestionList.suggestionID = suggestionMarker.suggestionID();
|
| + suggestionList.start = suggestionMarker.startOffset();
|
| + suggestionList.end = suggestionMarker.endOffset();
|
| + suggestionList.suggestions = suggestionMarker.suggestions();
|
| + suggestionSpans.push_back(suggestionList);
|
| + }
|
| + std::sort(suggestionSpans.begin(), suggestionSpans.end());
|
| +
|
| + Vector<TextSuggestionInfo> suggestionInfos;
|
| +
|
| + for (const TextSuggestionList& suggestionList : suggestionSpans) {
|
| + if (suggestionInfos.size() == kMaxNumberSuggestions)
|
| + break;
|
| +
|
| + for (size_t suggestionIndex = 0;
|
| + suggestionIndex < suggestionList.suggestions.size();
|
| + suggestionIndex++) {
|
| + const String& suggestion = suggestionList.suggestions[suggestionIndex];
|
| + bool isDupe = false;
|
| + for (size_t i = 0; i < suggestionInfos.size(); i++) {
|
| + const TextSuggestionInfo& otherSuggestionInfo = suggestionInfos[i];
|
| + if (otherSuggestionInfo.suggestion == suggestion) {
|
| + if (suggestionList.start == otherSuggestionInfo.spanStart &&
|
| + suggestionList.end == otherSuggestionInfo.spanEnd) {
|
| + isDupe = true;
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (isDupe)
|
| + continue;
|
| +
|
| + TextSuggestionInfo suggestionInfo;
|
| + suggestionInfo.suggestionID = suggestionList.suggestionID;
|
| + suggestionInfo.suggestionStart = 0;
|
| + suggestionInfo.suggestionEnd = suggestion.length();
|
| + suggestionInfo.spanStart = suggestionList.start;
|
| + suggestionInfo.spanEnd = suggestionList.end;
|
| + suggestionInfo.suggestionIndex = suggestionIndex;
|
| + suggestionInfo.suggestion = suggestion;
|
| + suggestionInfos.push_back(suggestionInfo);
|
| + if (suggestionInfos.size() == kMaxNumberSuggestions)
|
| + break;
|
| + }
|
| + }
|
| +
|
| + if (suggestionInfos.size() == 0)
|
| + return Vector<blink::TextSuggestionInfo>();
|
| +
|
| + int spanUnionStart = suggestionInfos[0].spanStart;
|
| + int spanUnionEnd = suggestionInfos[0].spanEnd;
|
| +
|
| + for (size_t i = 1; i < suggestionInfos.size(); i++) {
|
| + spanUnionStart = std::min(spanUnionStart, suggestionInfos[i].spanStart);
|
| + spanUnionEnd = std::max(spanUnionEnd, suggestionInfos[i].spanEnd);
|
| + }
|
| +
|
| + for (TextSuggestionInfo& info : suggestionInfos) {
|
| + const EphemeralRange& prefixRange =
|
| + PlainTextRange(spanUnionStart, info.spanStart)
|
| + .createRange(*rootEditableElement);
|
| + String prefix =
|
| + plainText(prefixRange, TextIteratorBehavior::Builder().build());
|
| +
|
| + const EphemeralRange& suffixRange =
|
| + PlainTextRange(info.spanEnd, spanUnionEnd)
|
| + .createRange(*rootEditableElement);
|
| + String suffix =
|
| + plainText(suffixRange, TextIteratorBehavior::Builder().build());
|
| +
|
| + info.prefix = prefix.utf8().data();
|
| + info.suffix = suffix.utf8().data();
|
| + }
|
| +
|
| + Color highlightColor;
|
| + const SuggestionMarker& firstSuggestionMarker = *toSuggestionMarker(suggestionMarkers[0]);
|
| + if (firstSuggestionMarker.underlineColor() != 0) {
|
| + highlightColor = firstSuggestionMarker.underlineColor().combineWithAlpha(
|
| + kSuggestionUnderlineAlphaMultiplier);
|
| + } else {
|
| + highlightColor = LayoutTheme::tapHighlightColor();
|
| + }
|
| +
|
| + EphemeralRange backgroundHighlightRange =
|
| + PlainTextRange(spanUnionStart, spanUnionEnd)
|
| + .createRange(*rootEditableElement);
|
| + document().markers().addSuggestionHighlightMarker(
|
| + backgroundHighlightRange,
|
| + Color::black, false,
|
| + highlightColor);
|
| +
|
| + return suggestionInfos;
|
| +}
|
| +
|
| +DEFINE_TRACE(TextSuggestionController) {
|
| + visitor->trace(m_frame);
|
| +}
|
| +
|
| +} // namespace blink
|
|
|