Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(220)

Unified Diff: third_party/WebKit/Source/core/editing/TextSuggestionController.cpp

Issue 2650113004: [WIP] Add support for Android SuggestionSpans when editing text (Closed)
Patch Set: Uploading the latest version from my repo so I can reference it Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698