Chromium Code Reviews| 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; |