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 |