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

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

Issue 2650113004: [WIP] Add support for Android SuggestionSpans when editing text (Closed)
Patch Set: Remove logging statements, fix copyright years in new files 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 7e6b5be0144daac85af6d9449041a6e167da83b3..5c30f79342dc8b5624d8c611d5d6c67538cdad07 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;
esprehn 2017/01/31 22:41:34 Why 5?
rlanday 2017/01/31 23:30:09 5 is what the native text box on Android uses. Bli
+const double kSuggestionUnderlineAlphaMultiplier = 0.4;
rlanday 2017/01/31 19:50:21 The suggestion highlight background seems kind of
esprehn 2017/01/31 22:41:34 Did this get a UI review?
rlanday 2017/01/31 23:30:09 This is just existing Android UI we're porting ove
+
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())
@@ -305,6 +305,179 @@ bool InputMethodController::commitText(
return insertTextAndMoveCaret(text, relativeCaretPosition, underlines);
}
+void InputMethodController::applySuggestionReplacement(int documentMarkerID,
esprehn 2017/01/31 22:41:34 This change is so huge, we probably need a lot of
rlanday 2017/01/31 23:30:09 Yeah I should write some tests for this method. B
+ int suggestionIndex) {
+ suggestionMenuClosed();
+
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+ DocumentMarkerVector suggestionMarkers =
+ rootEditableElement->document().markers().markersInRange(
rlanday 2017/01/31 21:17:12 I introduced a bug here when I was refactoring, th
+ 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)
+ 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()) ||
+ (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
aelias_OOO_until_Jul13 2017/02/10 21:43:55 As a reminder of what we discussed offline, markin
+ // 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);
+ }
+ }
+
+ document().markers().removeSuggestionMarkersByID(markerIdsToRemove);
+
+ setCompositionFromExistingText(Vector<CompositionUnderline>(),
aelias_OOO_until_Jul13 2017/02/10 21:43:55 There's no reason to do anything relating to compo
+ 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();
+
+ // Don't use replaceComposition() here because we don't want to send
+ // JavaScript composition events
+ selectComposition();
+ frame().editor().replaceSelectionWithText(
+ replacement, false, false, InputEvent::InputType::InsertReplacementText);
+
+ 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.isEmpty())
+ return;
+
+ const DocumentMarker* suggestionHighlightMarkerPtr =
+ suggestionHighlightMarkers[0].get();
+
+ // Remove markers that contain part, but not all, of the text range to be
aelias_OOO_until_Jul13 2017/02/10 21:43:55 It can't be correct for this code to live here, si
+ // deleted.
+ Vector<int> markerIdsToRemove;
+ 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,
aelias_OOO_until_Jul13 2017/02/10 21:43:55 Please split this off into a separate method calle
+ // 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 InputMethodController::suggestionMenuClosed() {
+ document().markers().removeMarkers(
+ DocumentMarker::SuggestionBackgroundHighlight);
+ frame().selection().setCaretVisible(true);
+}
+
bool InputMethodController::replaceComposition(const String& text) {
if (!hasComposition())
return false;
@@ -362,10 +535,16 @@ 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() {
+ document().markers().removeMarkersForWordsAffectedByEditing(
+ DocumentMarker::Suggestion, false);
+}
+
bool InputMethodController::replaceCompositionAndMoveCaret(
const String& text,
int relativeCaretPosition,
@@ -378,6 +557,7 @@ bool InputMethodController::replaceCompositionAndMoveCaret(
PlainTextRange::create(*rootEditableElement, *m_compositionRange);
if (compositionRange.isNull())
return false;
+
int textStart = compositionRange.start();
if (!replaceComposition(text))
@@ -409,6 +589,9 @@ bool InputMethodController::insertTextAndMoveCaret(
PlainTextRange selectionRange = getSelectionOffsets();
if (selectionRange.isNull())
return false;
+
+ clearSuggestionMarkersTouchingSelection();
aelias_OOO_until_Jul13 2017/02/10 21:43:55 If text is inserted by typing on a physical keyboa
+
int textStart = selectionRange.start();
if (text.length()) {
@@ -505,13 +688,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:
@@ -606,7 +790,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;
}
@@ -1073,6 +1258,125 @@ WebTextInputType InputMethodController::textInputType() const {
return WebTextInputTypeNone;
}
+WebVector<blink::WebTextSuggestionInfo>
+InputMethodController::getTextSuggestionInfosUnderCaret() const {
aelias_OOO_until_Jul13 2017/02/10 21:43:55 Please move this and most other changes in this fi
+ Element* rootEditableElement = frame().selection().rootEditableElement();
+ if (!rootEditableElement)
+ return WebVector<blink::WebTextSuggestionInfo>();
+
+ DocumentMarkerVector suggestionMarkers =
+ rootEditableElement->document().markers().markersInRangeInclusive(
+ 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;
esprehn 2017/01/31 22:41:34 WTF Vector
rlanday 2017/01/31 23:30:09 Ok
+
+ 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, 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;
+ 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(
aelias_OOO_until_Jul13 2017/02/10 21:43:55 I don't think a method called "get...()" should be
+ 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