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

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

Issue 2650113004: [WIP] Add support for Android SuggestionSpans when editing text (Closed)
Patch Set: Created 3 years, 11 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/InputMethodController.cpp
diff --git a/third_party/WebKit/Source/core/editing/InputMethodController.cpp b/third_party/WebKit/Source/core/editing/InputMethodController.cpp
index 50c4d95d121410f20ccfe36e4d0942ff890e6f28..e27f8d173d98456de4961f563753da2f5b4e7c33 100644
--- a/third_party/WebKit/Source/core/editing/InputMethodController.cpp
+++ b/third_party/WebKit/Source/core/editing/InputMethodController.cpp
@@ -33,6 +33,7 @@
#include "core/dom/Text.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
+#include "core/editing/TextSuggestionList.h"
#include "core/editing/commands/TypingCommand.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/events/CompositionEvent.h"
@@ -48,6 +49,9 @@ namespace blink {
namespace {
+const int kMaxNumberSuggestions = 5;
+const double kSuggestionUnderlineAlphaMultiplier = 0.4;
+
void dispatchCompositionUpdateEvent(LocalFrame& frame, const String& text) {
Element* target = frame.document()->focusedElement();
if (!target)
@@ -69,10 +73,6 @@ void dispatchCompositionEndEvent(LocalFrame& frame, const String& text) {
}
bool needsIncrementalInsertion(const LocalFrame& frame, const String& newText) {
- // No need to apply incremental insertion if it doesn't support formated text.
- if (!frame.editor().canEditRichly())
- return false;
-
// No need to apply incremental insertion if the old text (text to be
// replaced) or the new text (text to be inserted) is empty.
if (frame.selectedText().isEmpty() || newText.isEmpty())
@@ -284,6 +284,167 @@ bool InputMethodController::commitText(
return insertTextAndMoveCaret(text, relativeCaretPosition, underlines);
}
+void InputMethodController::applySuggestionReplacement(int documentMarkerID,
+ int suggestionIndex) {
+ closeSuggestionMenu();
+
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+ DocumentMarkerVector suggestionMarkers =
+ rootEditableElement->document().markers().markersInRange(
+ firstEphemeralRangeOf(m_frame->selection().selection()),
+ DocumentMarker::Suggestion);
+
+ DocumentMarker* markerPtr = nullptr;
+ for (const Member<DocumentMarker>& marker : suggestionMarkers) {
+ if (marker->suggestionMarkerID() == documentMarkerID) {
+ markerPtr = marker.get();
+ break;
+ }
+ }
+
+ if (markerPtr == nullptr)
+ return;
+
+ // Remove markers that contain part, but not all, of the text range to be
+ // replaced.
+ Vector<int> markerIdsToRemove;
+ HeapVector<Member<DocumentMarker>> markersToExpand;
+ for (const Member<DocumentMarker>& marker : suggestionMarkers) {
+ if ((marker->startOffset() > markerPtr->startOffset() &&
+ marker->startOffset() <= markerPtr->endOffset()) ||
aelias_OOO_until_Jul13 2017/01/25 03:34:27 Isn't it equivalent to say "marker->startOffset()
rlanday 2017/01/25 18:48:10 I don't think so, because your check doesn't handl
+ (marker->endOffset() < markerPtr->endOffset() &&
+ marker->endOffset() >= markerPtr->startOffset())) {
+ markerIdsToRemove.push_back(marker->suggestionMarkerID());
+ }
+
+ if (marker->startOffset() == markerPtr->startOffset() &&
+ marker->endOffset() == markerPtr->endOffset()) {
+ // Special case: DocumentMarkerController handles shifting most of the
+ // markers around, but if we replace exactly the range of text spanned by
+ // a marker, in some cases, the range will get collapsed to a single
+ // position when the old text is removed and will not be re-expanded. So
+ // we have to re-expand those markers here after the replacement.
+
+ // We actually have to remove and re-insert these markers because in the
+ // case
+ // where all the text is replaced, it's possible for the text node to get
+ // removed and re-added, thereby dropping the markers.
+ markerIdsToRemove.push_back(marker->suggestionMarkerID());
+ markersToExpand.push_back(marker);
aelias_OOO_until_Jul13 2017/01/25 03:34:26 Seems a bit weird, though I don't understand the s
rlanday 2017/01/25 18:48:10 I'll look into that code
rlanday 2017/01/25 18:48:10 I'll look into that code
rlanday 2017/01/28 02:35:28 Ok, there are actually two methods involved with u
+ }
+ }
+
+ document().markers().removeSuggestionMarkersByID(markerIdsToRemove);
+
+ setCompositionFromExistingText(Vector<CompositionUnderline>(),
+ markerPtr->startOffset(),
+ markerPtr->endOffset());
+
+ // The entry in the suggestion drop-down that the user tapped on is replaced
+ // by the text they just overwrote
+ String replacement =
+ String::fromUTF8(markerPtr->suggestions()[suggestionIndex].c_str());
+ int replacementLength = replacement.length();
+ String newSuggestion = composingText();
+ replaceComposition(replacement);
+ markerPtr->replaceSuggestion(suggestionIndex, newSuggestion.utf8().data());
+
+ // Could have changed when we replaced the composition, if we deleted all the
+ // text
+ rootEditableElement = frame().selection().rootEditableElement();
+ for (const Member<DocumentMarker>& marker : markersToExpand) {
+ const EphemeralRange& range =
+ PlainTextRange(marker->startOffset(),
+ marker->startOffset() + replacementLength)
+ .createRange(*rootEditableElement);
+ document().markers().addCompositionMarker(
+ range.startPosition(), range.endPosition(), marker->underlineColor(),
+ marker->thick(), marker->backgroundColor(), marker->suggestions());
+ }
+}
+
+void InputMethodController::deleteSuggestionHighlight() {
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+ DocumentMarkerVector suggestionHighlightMarkers =
+ rootEditableElement->document().markers().markersInRange(
+ firstEphemeralRangeOf(m_frame->selection().selection()),
+ DocumentMarker::SuggestionBackgroundHighlight);
+
+ if (suggestionHighlightMarkers.size() == 0)
aelias_OOO_until_Jul13 2017/01/25 03:34:26 isEmpty()
+ return;
+
+ const DocumentMarker* suggestionHighlightMarkerPtr =
+ suggestionHighlightMarkers[0].get();
+
+ // Remove markers that contain part, but not all, of the text range to be
+ // deleted.
+ Vector<int> markerIdsToRemove;
+ HeapVector<Member<DocumentMarker>> markersToExpand;
aelias_OOO_until_Jul13 2017/01/25 03:34:27 unused
+ for (const Member<DocumentMarker>& marker : suggestionHighlightMarkers) {
+ if ((marker->startOffset() > suggestionHighlightMarkerPtr->startOffset() &&
+ marker->startOffset() <= suggestionHighlightMarkerPtr->endOffset()) ||
+ (marker->endOffset() < suggestionHighlightMarkerPtr->endOffset() &&
+ marker->endOffset() >= suggestionHighlightMarkerPtr->startOffset())) {
+ markerIdsToRemove.push_back(marker->suggestionMarkerID());
+ }
+ }
+
+ document().markers().removeSuggestionMarkersByID(markerIdsToRemove);
+
+ // 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, TextIteratorEmitsOriginalText);
aelias_OOO_until_Jul13 2017/01/25 03:34:26 EmitsOriginalText is for password fields not to be
rlanday 2017/01/26 18:28:52 I copied this from the composingText() method, are
aelias_OOO_until_Jul13 2017/01/27 19:56:36 It doesn't really make sense for password fields t
+
+ if (WTF::isASCIISpace(nextCharacterStr[0])) {
aelias_OOO_until_Jul13 2017/01/25 03:34:26 Probably want to consider non-breaking spaces as w
rlanday 2017/01/25 18:48:10 I was having trouble finding a function that seeme
+ if (suggestionHighlightMarkerPtr->startOffset() == 0) {
aelias_OOO_until_Jul13 2017/01/25 03:34:27 The range code works even if this is 0, right? Co
rlanday 2017/01/25 18:48:10 This is not for performance, this is so if you hav
+ deleteNextChar = true;
+ } else {
+ const PlainTextRange prevCharacterRange(
+ suggestionHighlightMarkerPtr->startOffset() - 1,
+ suggestionHighlightMarkerPtr->startOffset());
+ const EphemeralRange prevCharacterEphemeralRange =
+ prevCharacterRange.createRange(*rootEditableElement);
+ if (!prevCharacterEphemeralRange.isNull()) {
+ String prevCharacterStr = plainText(prevCharacterEphemeralRange,
+ TextIteratorEmitsOriginalText);
+ if (WTF::isASCIISpace(prevCharacterStr[0])) {
+ deleteNextChar = true;
+ }
+ }
+ }
+ }
+ }
+
+ setCompositionFromExistingText(
aelias_OOO_until_Jul13 2017/01/25 03:34:27 We don't want to send composition events to Javasc
+ Vector<CompositionUnderline>(),
+ suggestionHighlightMarkerPtr->startOffset(),
+ suggestionHighlightMarkerPtr->endOffset() + deleteNextChar);
+ // send msg to IME
+ replaceComposition("");
+
+ closeSuggestionMenu();
+}
+
+void InputMethodController::closeSuggestionMenu() {
+ document().markers().removeMarkers(
+ DocumentMarker::SuggestionBackgroundHighlight);
+ frame().selection().setCaretVisible(true);
+}
+
bool InputMethodController::replaceComposition(const String& text) {
if (!hasComposition())
return false;
@@ -341,10 +502,30 @@ void InputMethodController::addCompositionUnderlines(
document().markers().addCompositionMarker(
ephemeralLineRange.startPosition(), ephemeralLineRange.endPosition(),
- underline.color(), underline.thick(), underline.backgroundColor());
+ underline.color(), underline.thick(), underline.backgroundColor(),
+ underline.suggestions());
}
}
+void InputMethodController::clearSuggestionMarkersTouchingSelection() {
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+
+ DocumentMarkerVector suggestionMarkers =
+ rootEditableElement->document().markers().markersInRange(
+ firstEphemeralRangeOf(m_frame->selection().selection()),
+ DocumentMarker::Suggestion);
+
+ // We can't use DocumentMarkerController::removeMarkers() to remove markers
+ // overlapping with the selection range because we also want to remove markers
+ // merely touching, not overlapping, the selection
+ Vector<int> markerIdsToRemove;
+ for (const Member<DocumentMarker>& marker : suggestionMarkers) {
+ markerIdsToRemove.push_back(marker->suggestionMarkerID());
+ }
+
+ document().markers().removeSuggestionMarkersByID(markerIdsToRemove);
+}
+
bool InputMethodController::replaceCompositionAndMoveCaret(
const String& text,
int relativeCaretPosition,
@@ -357,6 +538,7 @@ bool InputMethodController::replaceCompositionAndMoveCaret(
PlainTextRange::create(*rootEditableElement, *m_compositionRange);
if (compositionRange.isNull())
return false;
+
int textStart = compositionRange.start();
if (!replaceComposition(text))
@@ -388,6 +570,9 @@ bool InputMethodController::insertTextAndMoveCaret(
PlainTextRange selectionRange = getSelectionOffsets();
if (selectionRange.isNull())
return false;
+
+ clearSuggestionMarkersTouchingSelection();
+
int textStart = selectionRange.start();
if (text.length()) {
@@ -500,13 +685,14 @@ void InputMethodController::setComposition(
if (!target)
return;
+ clearSuggestionMarkersTouchingSelection();
+
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. see http://crbug.com/590369 for more details.
document().updateStyleAndLayoutIgnorePendingStylesheets();
PlainTextRange selectedRange = createSelectionRangeForSetComposition(
selectionStart, selectionEnd, text.length());
-
// Dispatch an appropriate composition event to the focused node.
// We check the composition status and choose an appropriate composition event
// since this function is used for three purposes:
@@ -601,7 +787,8 @@ void InputMethodController::setComposition(
document().markers().addCompositionMarker(
m_compositionRange->startPosition(), m_compositionRange->endPosition(),
Color::black, false,
- LayoutTheme::theme().platformDefaultCompositionBackgroundColor());
+ LayoutTheme::theme().platformDefaultCompositionBackgroundColor(),
+ std::vector<std::string>());
return;
}
@@ -1061,6 +1248,123 @@ WebTextInputType InputMethodController::textInputType() const {
return WebTextInputTypeNone;
}
+WebVector<blink::WebTextSuggestionInfo>
+InputMethodController::getTextSuggestionInfosUnderCaret() const {
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+ if (!rootEditableElement)
+ return WebVector<blink::WebTextSuggestionInfo>();
+
+ DocumentMarkerVector suggestionMarkers =
+ rootEditableElement->document().markers().markersInRange(
+ firstEphemeralRangeOf(m_frame->selection().selection()),
+ 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;
+
+ TextSuggestionList suggestionList;
+ suggestionList.documentMarkerID = marker->suggestionMarkerID();
+ suggestionList.start = marker->startOffset();
+ suggestionList.end = marker->endOffset();
+ suggestionList.suggestions = marker->suggestions();
+ suggestionSpans.push_back(suggestionList);
+ }
+
+ std::sort(suggestionSpans.begin(), suggestionSpans.end());
+
+ std::vector<WebTextSuggestionInfo> suggestionInfos;
+
+ for (const TextSuggestionList& suggestionList : suggestionSpans) {
+ if (suggestionInfos.size() == kMaxNumberSuggestions)
+ break;
+
+ for (size_t suggestionIndex = 0;
+ suggestionIndex < suggestionList.suggestions.size();
+ suggestionIndex++) {
+ const std::string& suggestion =
+ suggestionList.suggestions[suggestionIndex];
+ bool isDupe = false;
+ for (size_t i = 0; i < suggestionInfos.size(); i++) {
+ const WebTextSuggestionInfo& otherSuggestionInfo = suggestionInfos[i];
+ if (otherSuggestionInfo.suggestion == suggestion) {
+ if (suggestionList.start == otherSuggestionInfo.spanStart &&
+ suggestionList.end == otherSuggestionInfo.spanEnd) {
+ isDupe = true;
+ break;
+ }
+ }
+ }
+
+ if (isDupe)
+ continue;
+
+ WebTextSuggestionInfo suggestionInfo;
+ suggestionInfo.documentMarkerID = suggestionList.documentMarkerID;
+ 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 WebVector<blink::WebTextSuggestionInfo>();
+
+ 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 (WebTextSuggestionInfo& info : suggestionInfos) {
+ const EphemeralRange& prefixRange =
+ PlainTextRange(spanUnionStart, info.spanStart)
+ .createRange(*rootEditableElement);
+ String prefix = plainText(prefixRange, TextIteratorEmitsOriginalText);
+
+ const EphemeralRange& suffixRange =
+ PlainTextRange(info.spanEnd, spanUnionEnd)
+ .createRange(*rootEditableElement);
+ String suffix = plainText(suffixRange, TextIteratorEmitsOriginalText);
+
+ info.prefix = prefix.utf8().data();
+ info.suffix = suffix.utf8().data();
+ }
+
+ Color highlightColor;
+ if (suggestionMarkers[0]->underlineColor() != 0) {
+ highlightColor = suggestionMarkers[0]->underlineColor().combineWithAlpha(
+ kSuggestionUnderlineAlphaMultiplier);
+ } else {
+ highlightColor = LayoutTheme::tapHighlightColor();
+ }
+
+ EphemeralRange backgroundHighlightRange =
+ PlainTextRange(spanUnionStart, spanUnionEnd)
+ .createRange(*rootEditableElement);
+ document().markers().addSuggestionBackgroundHighlightMarker(
+ backgroundHighlightRange.startPosition(),
+ backgroundHighlightRange.endPosition(), Color::black, false,
+ highlightColor);
+
+ return suggestionInfos;
+}
+
+void InputMethodController::prepareForTextSuggestionMenuToBeShown() {
+ frame().selection().setCaretVisible(false);
+}
+
void InputMethodController::willChangeFocus() {
if (!finishComposingText(DoNotKeepSelection))
return;

Powered by Google App Engine
This is Rietveld 408576698