Commit 3fc7721d authored by Ryan Landay's avatar Ryan Landay Committed by Commit Bot

Add support for Android SuggestionSpans

This is the final CL to add support for Android SuggestionSpans (at least ones
not marked with FLAG_MISSPELLING; there will be one or more additional CLs to
add support for those). This CL includes:

- Changes to ImeAdapter to make it pass SuggestionSpans into
  InputMethodController

- Changes to InputMethodController to make it create suggestion markers from the
  passed-in SuggestionSpans

- Additional methods in TextSuggestionController for getting the list of text
  suggestions under the cursor, and applying a suggestion

- More Mojo code to pass information about text suggestions back-and-forth
  between browser and renderer code

- Changes to SuggestionsPopupWindow and TextSuggestionHost so we can show either
  a spell check or a text suggestion menu

Bug: 672259
Cq-Include-Trybots: master.tryserver.chromium.linux:linux_layout_tests_slimming_paint_v2
Change-Id: I3f30543586901baec4648d265874a303efafbd44
Reviewed-on: https://chromium-review.googlesource.com/627172
Commit-Queue: Ryan Landay <rlanday@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarYoshifumi Inoue <yosin@chromium.org>
Reviewed-by: default avatarXiaocheng Hu <xiaochengh@chromium.org>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarAlexandre Elias <aelias@chromium.org>
Cr-Commit-Position: refs/heads/master@{#501335}
parent 39899082
......@@ -1490,7 +1490,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/infobar/PermissionUpdateInfobarTest.java",
"javatests/src/org/chromium/chrome/browser/infobar/SearchGeolocationDisclosureInfoBarTest.java",
"javatests/src/org/chromium/chrome/browser/input/SelectPopupOtherContentViewTest.java",
"javatests/src/org/chromium/chrome/browser/input/TextSuggestionMenuTest.java",
"javatests/src/org/chromium/chrome/browser/input/SpellCheckMenuTest.java",
"javatests/src/org/chromium/chrome/browser/instantapps/InstantAppsHandlerTest.java",
"javatests/src/org/chromium/chrome/browser/invalidation/ChromeBrowserSyncAdapterTest.java",
"javatests/src/org/chromium/chrome/browser/invalidation/DelayedInvalidationsControllerTest.java",
......
......@@ -7,8 +7,6 @@ package org.chromium.chrome.browser.input;
import android.support.test.filters.LargeTest;
import android.view.View;
import org.junit.Assert;
import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.test.ChromeActivityTestCaseBase;
......@@ -22,16 +20,17 @@ import org.chromium.content.browser.test.util.JavaScriptUtils;
import org.chromium.content.browser.test.util.TouchCommon;
import org.chromium.content_public.browser.WebContents;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
/**
* Integration tests for the text suggestion menu.
*/
public class TextSuggestionMenuTest extends ChromeActivityTestCaseBase<ChromeActivity> {
public class SpellCheckMenuTest extends ChromeActivityTestCaseBase<ChromeActivity> {
private static final String URL =
"data:text/html, <div contenteditable id=\"div\">iuvwneaoanls</div>";
public TextSuggestionMenuTest() {
public SpellCheckMenuTest() {
super(ChromeActivity.class);
}
......@@ -61,7 +60,7 @@ public class TextSuggestionMenuTest extends ChromeActivityTestCaseBase<ChromeAct
@Override
public boolean isSatisfied() {
SuggestionsPopupWindow suggestionsPopupWindow =
cvc.getTextSuggestionHostForTesting().getSuggestionsPopupWindowForTesting();
cvc.getTextSuggestionHostForTesting().getSpellCheckPopupWindowForTesting();
if (suggestionsPopupWindow == null) {
return false;
}
......@@ -76,12 +75,22 @@ public class TextSuggestionMenuTest extends ChromeActivityTestCaseBase<ChromeAct
});
TouchCommon.singleClickView(getDeleteButton(cvc));
Assert.assertEquals("", DOMUtils.getNodeContents(cvc.getWebContents(), "div"));
CriteriaHelper.pollInstrumentationThread(Criteria.equals("", new Callable<String>() {
@Override
public String call() {
try {
return DOMUtils.getNodeContents(cvc.getWebContents(), "div");
} catch (InterruptedException | TimeoutException e) {
return null;
}
}
}));
}
private View getDeleteButton(ContentViewCore cvc) {
View contentView = cvc.getTextSuggestionHostForTesting()
.getSuggestionsPopupWindowForTesting()
.getSpellCheckPopupWindowForTesting()
.getContentViewForTesting();
return contentView.findViewById(R.id.deleteButton);
}
......
......@@ -87,8 +87,33 @@ void AppendBackgroundColorSpan(JNIEnv*,
reinterpret_cast<std::vector<ui::ImeTextSpan>*>(ime_text_spans_ptr);
ime_text_spans->push_back(ui::ImeTextSpan(
ui::ImeTextSpan::Type::kComposition, static_cast<unsigned>(start),
static_cast<unsigned>(end), SK_ColorTRANSPARENT, false,
static_cast<unsigned>(background_color)));
static_cast<unsigned>(end), static_cast<unsigned>(background_color),
false, SK_ColorTRANSPARENT, SK_ColorTRANSPARENT,
std::vector<std::string>()));
}
// Callback from Java to convert SuggestionSpan data to a
// blink::WebImeTextSpan instance, and append it to |ime_text_spans_ptr|.
void AppendSuggestionSpan(JNIEnv* env,
const JavaParamRef<jclass>&,
jlong ime_text_spans_ptr,
jint start,
jint end,
jint underline_color,
jint suggestion_highlight_color,
const JavaParamRef<jobjectArray>& suggestions) {
DCHECK_GE(start, 0);
DCHECK_GE(end, 0);
std::vector<blink::WebImeTextSpan>* ime_text_spans =
reinterpret_cast<std::vector<blink::WebImeTextSpan>*>(ime_text_spans_ptr);
std::vector<std::string> suggestions_vec;
AppendJavaStringArrayToStringVector(env, suggestions, &suggestions_vec);
ime_text_spans->push_back(blink::WebImeTextSpan(
blink::WebImeTextSpan::Type::kSuggestion, static_cast<unsigned>(start),
static_cast<unsigned>(end), static_cast<unsigned>(underline_color), true,
SK_ColorTRANSPARENT, static_cast<unsigned>(suggestion_highlight_color),
suggestions_vec));
}
// Callback from Java to convert UnderlineSpan data to a
......@@ -104,7 +129,8 @@ void AppendUnderlineSpan(JNIEnv*,
reinterpret_cast<std::vector<ui::ImeTextSpan>*>(ime_text_spans_ptr);
ime_text_spans->push_back(ui::ImeTextSpan(
ui::ImeTextSpan::Type::kComposition, static_cast<unsigned>(start),
static_cast<unsigned>(end), SK_ColorBLACK, false, SK_ColorTRANSPARENT));
static_cast<unsigned>(end), SK_ColorBLACK, false, SK_ColorTRANSPARENT,
SK_ColorTRANSPARENT, std::vector<std::string>()));
}
ImeAdapterAndroid::ImeAdapterAndroid(JNIEnv* env,
......@@ -218,7 +244,8 @@ void ImeAdapterAndroid::SetComposingText(JNIEnv* env,
if (ime_text_spans.empty()) {
ime_text_spans.push_back(
ui::ImeTextSpan(ui::ImeTextSpan::Type::kComposition, 0, text16.length(),
SK_ColorBLACK, false, SK_ColorTRANSPARENT));
SK_ColorBLACK, false, SK_ColorTRANSPARENT,
SK_ColorTRANSPARENT, std::vector<std::string>()));
}
// relative_cursor_pos is as described in the Android API for
......@@ -339,9 +366,9 @@ void ImeAdapterAndroid::SetComposingRegion(JNIEnv*,
return;
std::vector<ui::ImeTextSpan> ime_text_spans;
ime_text_spans.push_back(ui::ImeTextSpan(ui::ImeTextSpan::Type::kComposition,
0, end - start, SK_ColorBLACK, false,
SK_ColorTRANSPARENT));
ime_text_spans.push_back(ui::ImeTextSpan(
ui::ImeTextSpan::Type::kComposition, 0, end - start, SK_ColorBLACK, false,
SK_ColorTRANSPARENT, SK_ColorTRANSPARENT, std::vector<std::string>()));
rfh->GetFrameInputHandler()->SetCompositionFromExistingText(start, end,
ime_text_spans);
......
......@@ -12,18 +12,27 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "jni/SuggestionInfo_jni.h"
#include "jni/TextSuggestionHost_jni.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "ui/gfx/android/view_configuration.h"
using base::android::AttachCurrentThread;
using base::android::ConvertUTF8ToJavaString;
using base::android::GetClass;
using base::android::JavaParamRef;
using base::android::MethodID;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaArrayOfStrings;
namespace content {
namespace {
const size_t kMaxNumberOfSuggestions = 5;
} // namespace
jlong Init(JNIEnv* env,
const JavaParamRef<jobject>& obj,
const JavaParamRef<jobject>& jweb_contents) {
......@@ -43,8 +52,8 @@ TextSuggestionHostAndroid::TextSuggestionHostAndroid(
WebContentsObserver(web_contents),
rwhva_(nullptr),
java_text_suggestion_host_(JavaObjectWeakGlobalRef(env, obj)),
spellcheck_menu_timeout_(
base::Bind(&TextSuggestionHostAndroid::OnSpellCheckMenuTimeout,
suggestion_menu_timeout_(
base::Bind(&TextSuggestionHostAndroid::OnSuggestionMenuTimeout,
base::Unretained(this))) {
registry_.AddInterface(base::Bind(&TextSuggestionHostMojoImplAndroid::Create,
base::Unretained(this)));
......@@ -80,6 +89,18 @@ void TextSuggestionHostAndroid::ApplySpellCheckSuggestion(
ConvertJavaStringToUTF8(env, replacement));
}
void TextSuggestionHostAndroid::ApplyTextSuggestion(
JNIEnv*,
const JavaParamRef<jobject>&,
int marker_tag,
int suggestion_index) {
const blink::mojom::TextSuggestionBackendPtr& text_suggestion_backend =
GetTextSuggestionBackend();
if (!text_suggestion_backend)
return;
text_suggestion_backend->ApplyTextSuggestion(marker_tag, suggestion_index);
}
void TextSuggestionHostAndroid::DeleteActiveSuggestionRange(
JNIEnv*,
const JavaParamRef<jobject>&) {
......@@ -90,7 +111,7 @@ void TextSuggestionHostAndroid::DeleteActiveSuggestionRange(
text_suggestion_backend->DeleteActiveSuggestionRange();
}
void TextSuggestionHostAndroid::NewWordAddedToDictionary(
void TextSuggestionHostAndroid::OnNewWordAddedToDictionary(
JNIEnv* env,
const JavaParamRef<jobject>&,
const base::android::JavaParamRef<jstring>& word) {
......@@ -98,18 +119,18 @@ void TextSuggestionHostAndroid::NewWordAddedToDictionary(
GetTextSuggestionBackend();
if (!text_suggestion_backend)
return;
text_suggestion_backend->NewWordAddedToDictionary(
text_suggestion_backend->OnNewWordAddedToDictionary(
ConvertJavaStringToUTF8(env, word));
}
void TextSuggestionHostAndroid::SuggestionMenuClosed(
void TextSuggestionHostAndroid::OnSuggestionMenuClosed(
JNIEnv*,
const JavaParamRef<jobject>&) {
const blink::mojom::TextSuggestionBackendPtr& text_suggestion_backend =
GetTextSuggestionBackend();
if (!text_suggestion_backend)
return;
text_suggestion_backend->SuggestionMenuClosed();
text_suggestion_backend->OnSuggestionMenuClosed();
}
void TextSuggestionHostAndroid::ShowSpellCheckSuggestionMenu(
......@@ -118,8 +139,10 @@ void TextSuggestionHostAndroid::ShowSpellCheckSuggestionMenu(
const std::string& marked_text,
const std::vector<blink::mojom::SpellCheckSuggestionPtr>& suggestions) {
std::vector<std::string> suggestion_strings;
for (const auto& suggestion_ptr : suggestions)
suggestion_strings.push_back(suggestion_ptr->suggestion);
// Enforce kMaxNumberOfSuggestions here in case the renderer is hijacked and
// tries to send bad input.
for (size_t i = 0; i < suggestions.size() && i < kMaxNumberOfSuggestions; ++i)
suggestion_strings.push_back(suggestions[i]->suggestion);
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
if (obj.is_null())
......@@ -130,14 +153,44 @@ void TextSuggestionHostAndroid::ShowSpellCheckSuggestionMenu(
ToJavaArrayOfStrings(env, suggestion_strings));
}
void TextSuggestionHostAndroid::StartSpellCheckMenuTimer() {
spellcheck_menu_timeout_.Stop();
spellcheck_menu_timeout_.Start(base::TimeDelta::FromMilliseconds(
void TextSuggestionHostAndroid::ShowTextSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
const std::vector<blink::mojom::TextSuggestionPtr>& suggestions) {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
// Enforce kMaxNumberOfSuggestions here in case the renderer is hijacked and
// tries to send bad input.
size_t suggestion_count =
std::min(suggestions.size(), kMaxNumberOfSuggestions);
ScopedJavaLocalRef<jobjectArray> jsuggestion_infos =
Java_SuggestionInfo_createArray(env, suggestion_count);
for (size_t i = 0; i < suggestion_count; ++i) {
const blink::mojom::TextSuggestionPtr& suggestion_ptr = suggestions[i];
Java_SuggestionInfo_createSuggestionInfoAndPutInArray(
env, jsuggestion_infos, i, suggestion_ptr->marker_tag,
suggestion_ptr->suggestion_index,
ConvertUTF8ToJavaString(env, suggestion_ptr->prefix),
ConvertUTF8ToJavaString(env, suggestion_ptr->suggestion),
ConvertUTF8ToJavaString(env, suggestion_ptr->suffix));
}
Java_TextSuggestionHost_showTextSuggestionMenu(
env, obj, caret_x, caret_y, ConvertUTF8ToJavaString(env, marked_text),
jsuggestion_infos);
}
void TextSuggestionHostAndroid::StartSuggestionMenuTimer() {
suggestion_menu_timeout_.Stop();
suggestion_menu_timeout_.Start(base::TimeDelta::FromMilliseconds(
gfx::ViewConfiguration::GetDoubleTapTimeoutInMs()));
}
void TextSuggestionHostAndroid::OnKeyEvent() {
spellcheck_menu_timeout_.Stop();
suggestion_menu_timeout_.Stop();
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
......@@ -147,8 +200,8 @@ void TextSuggestionHostAndroid::OnKeyEvent() {
Java_TextSuggestionHost_hidePopups(env, obj);
}
void TextSuggestionHostAndroid::StopSpellCheckMenuTimer() {
spellcheck_menu_timeout_.Stop();
void TextSuggestionHostAndroid::StopSuggestionMenuTimer() {
suggestion_menu_timeout_.Stop();
}
void TextSuggestionHostAndroid::OnInterfaceRequestFromFrame(
......@@ -187,12 +240,13 @@ TextSuggestionHostAndroid::GetTextSuggestionBackend() {
return text_suggestion_backend_;
}
void TextSuggestionHostAndroid::OnSpellCheckMenuTimeout() {
void TextSuggestionHostAndroid::OnSuggestionMenuTimeout() {
const blink::mojom::TextSuggestionBackendPtr& text_suggestion_backend =
GetTextSuggestionBackend();
if (!text_suggestion_backend)
return;
text_suggestion_backend->SpellCheckMenuTimeoutCallback();
text_suggestion_backend->SuggestionMenuTimeoutCallback(
kMaxNumberOfSuggestions);
}
} // namespace content
......@@ -39,6 +39,12 @@ class TextSuggestionHostAndroid : public RenderWidgetHostConnector,
JNIEnv*,
const base::android::JavaParamRef<jobject>&,
const base::android::JavaParamRef<jstring>& replacement);
// Called from the Java text suggestion menu to have Blink apply a text
// suggestion.
void ApplyTextSuggestion(JNIEnv*,
const base::android::JavaParamRef<jobject>&,
int marker_tag,
int suggestion_index);
// Called from the Java text suggestion menu to have Blink delete the
// currently highlighted region of text that the open suggestion menu pertains
// to.
......@@ -47,32 +53,39 @@ class TextSuggestionHostAndroid : public RenderWidgetHostConnector,
// Called from the Java text suggestion menu to tell Blink that a word is
// being added to the dictionary (so Blink can clear the spell check markers
// for that word).
void NewWordAddedToDictionary(
void OnNewWordAddedToDictionary(
JNIEnv*,
const base::android::JavaParamRef<jobject>&,
const base::android::JavaParamRef<jstring>& word);
// Called from the Java text suggestion menu to tell Blink that the user
// closed the menu without performing one of the available actions, so Blink
// can re-show the insertion caret and remove the suggestion range highlight.
void SuggestionMenuClosed(JNIEnv*,
const base::android::JavaParamRef<jobject>&);
// Called from Blink to tell the Java TextSuggestionHost to open the text
// suggestion menu.
void OnSuggestionMenuClosed(JNIEnv*,
const base::android::JavaParamRef<jobject>&);
// Called from Blink to tell the Java TextSuggestionHost to open the spell
// check suggestion menu.
void ShowSpellCheckSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
const std::vector<blink::mojom::SpellCheckSuggestionPtr>& suggestions);
// Called from Blink to tell the Java TextSuggestionHost to open the text
// suggestion menu.
void ShowTextSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
const std::vector<blink::mojom::TextSuggestionPtr>& suggestions);
// Called by browser-side code in response to an input event to stop the
// spell check menu timer and close the suggestion menu (if open).
void OnKeyEvent();
// Called by Blink when the user taps on a spell check marker and we might
// want to show the text suggestion menu after the double-tap timer expires.
void StartSpellCheckMenuTimer();
void StartSuggestionMenuTimer();
// Called by browser-side code in response to an input event to stop the
// spell check menu timer.
void StopSpellCheckMenuTimer();
// suggestion menu timer.
void StopSuggestionMenuTimer();
// WebContentsObserver overrides
void OnInterfaceRequestFromFrame(
......@@ -85,14 +98,14 @@ class TextSuggestionHostAndroid : public RenderWidgetHostConnector,
const blink::mojom::TextSuggestionBackendPtr& GetTextSuggestionBackend();
// Used by the spell check menu timer to notify Blink that the timer has
// expired.
void OnSpellCheckMenuTimeout();
void OnSuggestionMenuTimeout();
service_manager::BinderRegistry registry_;
// Current RenderWidgetHostView connected to this instance. Can be null.
RenderWidgetHostViewAndroid* rwhva_;
JavaObjectWeakGlobalRef java_text_suggestion_host_;
blink::mojom::TextSuggestionBackendPtr text_suggestion_backend_;
TimeoutMonitor spellcheck_menu_timeout_;
TimeoutMonitor suggestion_menu_timeout_;
};
} // namespace content
......
......@@ -22,8 +22,8 @@ void TextSuggestionHostMojoImplAndroid::Create(
std::move(request));
}
void TextSuggestionHostMojoImplAndroid::StartSpellCheckMenuTimer() {
text_suggestion_host_->StartSpellCheckMenuTimer();
void TextSuggestionHostMojoImplAndroid::StartSuggestionMenuTimer() {
text_suggestion_host_->StartSuggestionMenuTimer();
}
void TextSuggestionHostMojoImplAndroid::ShowSpellCheckSuggestionMenu(
......@@ -35,4 +35,13 @@ void TextSuggestionHostMojoImplAndroid::ShowSpellCheckSuggestionMenu(
marked_text, suggestions);
}
void TextSuggestionHostMojoImplAndroid::ShowTextSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
std::vector<blink::mojom::TextSuggestionPtr> suggestions) {
text_suggestion_host_->ShowTextSuggestionMenu(caret_x, caret_y, marked_text,
suggestions);
}
} // namespace content
......@@ -20,13 +20,18 @@ class TextSuggestionHostMojoImplAndroid final
static void Create(TextSuggestionHostAndroid*,
blink::mojom::TextSuggestionHostRequest request);
void StartSpellCheckMenuTimer() final;
void StartSuggestionMenuTimer() final;
void ShowSpellCheckSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
std::vector<blink::mojom::SpellCheckSuggestionPtr> suggestions) final;
void ShowTextSuggestionMenu(
double caret_x,
double caret_y,
const std::string& marked_text,
std::vector<blink::mojom::TextSuggestionPtr> suggestions) final;
private:
TextSuggestionHostAndroid* const text_suggestion_host_;
......
......@@ -975,7 +975,7 @@ bool RenderWidgetHostViewAndroid::OnTouchEvent(
// Receiving any other touch event before the double-tap timeout expires
// cancels opening the spellcheck menu.
if (text_suggestion_host_)
text_suggestion_host_->StopSpellCheckMenuTimer();
text_suggestion_host_->StopSuggestionMenuTimer();
// If a browser-based widget consumes the touch event, it's critical that
// touch event interception be disabled. This avoids issues with
......
......@@ -195,9 +195,12 @@ android_library("content_java") {
"java/src/org/chromium/content/browser/input/SelectPopupDialog.java",
"java/src/org/chromium/content/browser/input/SelectPopupDropdown.java",
"java/src/org/chromium/content/browser/input/SelectPopupItem.java",
"java/src/org/chromium/content/browser/input/SpellCheckPopupWindow.java",
"java/src/org/chromium/content/browser/input/SuggestionInfo.java",
"java/src/org/chromium/content/browser/input/SuggestionsPopupWindow.java",
"java/src/org/chromium/content/browser/input/TextInputState.java",
"java/src/org/chromium/content/browser/input/TextSuggestionHost.java",
"java/src/org/chromium/content/browser/input/TextSuggestionsPopupWindow.java",
"java/src/org/chromium/content/browser/input/ThreadedInputConnection.java",
"java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java",
"java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java",
......@@ -366,6 +369,7 @@ generate_jni("content_jni_headers") {
"java/src/org/chromium/content/browser/input/DateTimeChooserAndroid.java",
"java/src/org/chromium/content/browser/input/HandleViewResources.java",
"java/src/org/chromium/content/browser/input/ImeAdapter.java",
"java/src/org/chromium/content/browser/input/SuggestionInfo.java",
"java/src/org/chromium/content/browser/input/TextSuggestionHost.java",
"java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java",
"java/src/org/chromium/content/browser/webcontents/WebContentsObserverProxy.java",
......@@ -474,6 +478,7 @@ android_library("content_javatests") {
"javatests/src/org/chromium/content/browser/input/ImeTestUtils.java",
"javatests/src/org/chromium/content/browser/input/InputDialogContainerTest.java",
"javatests/src/org/chromium/content/browser/input/SelectPopupTest.java",
"javatests/src/org/chromium/content/browser/input/TextSuggestionMenuTest.java",
"javatests/src/org/chromium/content/browser/picker/DateTimePickerDialogTest.java",
"javatests/src/org/chromium/content/browser/webcontents/AccessibilitySnapshotTest.java",
"javatests/src/org/chromium/content/browser/webcontents/WebContentsTest.java",
......
......@@ -16,6 +16,9 @@
<style name="SelectActionMenuWebSearch">
<item name="android:icon">@drawable/ic_search</item>
</style>
<style name="SuggestionPrefixOrSuffix">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="TextSuggestionButton" parent="RobotoMediumStyle">
<item name="android:drawablePadding">8dp</item>
<!-- v21 uses sans-serif-medium -->
......
......@@ -6,6 +6,7 @@ package org.chromium.content.browser.input;
import android.annotation.SuppressLint;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
......@@ -16,6 +17,7 @@ import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.SuggestionSpan;
import android.text.style.UnderlineSpan;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
......@@ -42,6 +44,8 @@ import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.ime.TextInputType;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
......@@ -71,9 +75,14 @@ public class ImeAdapter {
private static final String TAG = "cr_Ime";
private static final boolean DEBUG_LOGS = false;
private static final float SUGGESTION_HIGHLIGHT_BACKGROUND_TRANSPARENCY = 0.4f;
public static final int COMPOSITION_KEY_CODE = 229;
private static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000;
// Color used by AOSP Android for a SuggestionSpan with FLAG_EASY_CORRECT set
private static final int DEFAULT_SUGGESTION_SPAN_COLOR = 0x88C8C8C8;
private long mNativeImeAdapterAndroid;
private InputMethodManagerWrapper mInputMethodManagerWrapper;
private ChromiumBaseInputConnection mInputConnection;
......@@ -871,6 +880,15 @@ public class ImeAdapter {
insertionMarkerTop, insertionMarkerBottom, mContainerView);
}
private int getUnderlineColorForSuggestionSpan(SuggestionSpan suggestionSpan) {
try {
Method getUnderlineColorMethod = SuggestionSpan.class.getMethod("getUnderlineColor");
return (int) getUnderlineColorMethod.invoke(suggestionSpan);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
return DEFAULT_SUGGESTION_SPAN_COLOR;
}
}
@CalledByNative
private void populateImeTextSpansFromJava(CharSequence text, long imeTextSpans) {
if (DEBUG_LOGS) {
......@@ -890,6 +908,37 @@ public class ImeAdapter {
} else if (span instanceof UnderlineSpan) {
nativeAppendUnderlineSpan(imeTextSpans, spannableString.getSpanStart(span),
spannableString.getSpanEnd(span));
} else if (span instanceof SuggestionSpan) {
final SuggestionSpan suggestionSpan = (SuggestionSpan) span;
// We currently only support FLAG_EASY_CORRECT SuggestionSpans.
// TODO(rlanday): support FLAG_MISSPELLED SuggestionSpans.
// Other types:
// - FLAG_AUTO_CORRECTION is used e.g. by Samsung's IME to flash a blue background
// on a word being replaced by an autocorrect suggestion. We don't currently
// support this.
//
// - Some IMEs (e.g. the AOSP keyboard on Jelly Bean) add SuggestionSpans with no
// flags set and no underline color to add suggestions to words marked as
// misspelled (instead of having the spell checker return the suggestions when
// called). We don't support these either.
if (suggestionSpan.getFlags() != SuggestionSpan.FLAG_EASY_CORRECT) {
continue;
}
// Copied from Android's Editor.java so we use the same colors
// as the native Android text widget.
final int underlineColor = getUnderlineColorForSuggestionSpan(suggestionSpan);
final int newAlpha = (int) (Color.alpha(underlineColor)
* SUGGESTION_HIGHLIGHT_BACKGROUND_TRANSPARENCY);
final int suggestionHighlightColor =
(underlineColor & 0x00FFFFFF) + (newAlpha << 24);
nativeAppendSuggestionSpan(imeTextSpans,
spannableString.getSpanStart(suggestionSpan),
spannableString.getSpanEnd(suggestionSpan), underlineColor,
suggestionHighlightColor, suggestionSpan.getSuggestions());
}
}
}
......@@ -921,6 +970,8 @@ public class ImeAdapter {
private static native void nativeAppendUnderlineSpan(long spanPtr, int start, int end);
private static native void nativeAppendBackgroundColorSpan(
long spanPtr, int start, int end, int backgroundColor);
private static native void nativeAppendSuggestionSpan(long spanPtr, int start, int end,
int underlineColor, int suggestionHighlightColor, String[] suggestions);
private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text,
String textStr, int newCursorPosition);
private native void nativeCommitText(
......
// 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.
package org.chromium.content.browser.input;
import android.content.Context;
import android.text.SpannableString;
import android.view.View;
import org.chromium.content.browser.WindowAndroidProvider;
/**
* A subclass of SuggestionsPopupWindow to be used for showing suggestions from a spell check
* marker.
*/
public class SpellCheckPopupWindow extends SuggestionsPopupWindow {
private String[] mSuggestions = new String[0];
/**
* @param context Android context to use.
* @param textSuggestionHost TextSuggestionHost instance (used to communicate with Blink).
* @param parentView The view used to attach the PopupWindow.
* @param windowAndroidProvider A WindowAndroidProvider instance used to get the window size.
*/
public SpellCheckPopupWindow(Context context, TextSuggestionHost textSuggestionHost,
View parentView, WindowAndroidProvider windowAndroidProvider) {
super(context, textSuggestionHost, parentView, windowAndroidProvider);
}
/**
* Shows the spell check menu at the specified coordinates (relative to the viewport).
*/
public void show(double caretX, double caretY, String highlightedText, String[] suggestions) {
mSuggestions = suggestions.clone();
setAddToDictionaryEnabled(true);
super.show(caretX, caretY, highlightedText);
}
@Override
protected int getSuggestionsCount() {
return mSuggestions.length;
}
@Override
protected Object getSuggestionItem(int position) {
return mSuggestions[position];
}
@Override
protected SpannableString getSuggestionText(int position) {
return new SpannableString(mSuggestions[position]);
}
@Override
protected void applySuggestion(int position) {
mTextSuggestionHost.applySpellCheckSuggestion(mSuggestions[position]);
}
}
// 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.
package org.chromium.content.browser.input;
import org.chromium.base.annotations.CalledByNative;
/**
* Represents an entry in a text suggestion popup menu. Contains the information
* necessary to display the menu entry and the information necessary to apply
* the suggestion.
*/
public class SuggestionInfo {
private final int mMarkerTag;
private final int mSuggestionIndex;
private final String mPrefix;
private final String mSuggestion;
private final String mSuffix;
SuggestionInfo(
int markerTag, int suggestionIndex, String prefix, String suggestion, String suffix) {
mMarkerTag = markerTag;
mSuggestionIndex = suggestionIndex;
mPrefix = prefix;
mSuggestion = suggestion;
mSuffix = suffix;
}
/**
* Used as an opaque identifier to tell Blink which suggestion was picked.
*/
public int getMarkerTag() {
return mMarkerTag;
}
/**
* Used as an opaque identifier to tell Blink which suggestion was picked.
*/
public int getSuggestionIndex() {
return mSuggestionIndex;
}
/**
* Text at the beginning of the highlighted suggestion region that will not be changed by
* applying the suggestion.
*/
public String getPrefix() {
return mPrefix;
}
/**
* Text that will replace the text between the prefix and suffix strings if the suggestion is
* applied.
*/
public String getSuggestion() {
return mSuggestion;
}
/**
* Text at the end of the highlighted suggestion region that will not be changed by
* applying the suggestion.
*/
public String getSuffix() {
return mSuffix;
}
@CalledByNative
private static SuggestionInfo[] createArray(int length) {
return new SuggestionInfo[length];
}
@CalledByNative
private static void createSuggestionInfoAndPutInArray(SuggestionInfo[] suggestionInfos,
int index, int markerTag, int suggestionIndex, String prefix, String suggestion,
String suffix) {
SuggestionInfo suggestionInfo =
new SuggestionInfo(markerTag, suggestionIndex, prefix, suggestion, suffix);
suggestionInfos[index] = suggestionInfo;
}
}
......@@ -11,6 +11,7 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.text.SpannableString;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
......@@ -34,7 +35,7 @@ import org.chromium.ui.UiUtils;
/**
* Popup window that displays a menu for viewing and applying text replacement suggestions.
*/
public class SuggestionsPopupWindow
public abstract class SuggestionsPopupWindow
implements OnItemClickListener, OnDismissListener, View.OnClickListener {
private static final String ACTION_USER_DICTIONARY_INSERT =
"com.android.settings.USER_DICTIONARY_INSERT";
......@@ -44,7 +45,7 @@ public class SuggestionsPopupWindow
private static final int ADD_TO_DICTIONARY_MAX_LENGTH_ON_JELLY_BEAN = 48;
private final Context mContext;
private final TextSuggestionHost mTextSuggestionHost;
protected final TextSuggestionHost mTextSuggestionHost;
private final View mParentView;
private final WindowAndroidProvider mWindowAndroidProvider;
......@@ -53,9 +54,7 @@ public class SuggestionsPopupWindow
private PopupWindow mPopupWindow;
private LinearLayout mContentView;
private SuggestionAdapter mSuggestionsAdapter;
private String mHighlightedText;
private String[] mSpellCheckSuggestions = new String[0];
private int mNumberOfSuggestionsToUse;
private TextView mAddToDictionaryButton;
private TextView mDeleteButton;
......@@ -65,7 +64,6 @@ public class SuggestionsPopupWindow
private int mPopupVerticalMargin;
private boolean mDismissedByItemTap;
/**
* @param context Android context to use.
* @param textSuggestionHost TextSuggestionHost instance (used to communicate with Blink).
......@@ -85,6 +83,36 @@ public class SuggestionsPopupWindow
mPopupWindow.setContentView(mContentView);
}
/**
* Method to be implemented by subclasses that returns how mnay suggestions are available (some
* of them may not be displayed if there's not enough room in the window).
*/
protected abstract int getSuggestionsCount();
/**
* Method to be implemented by subclasses to return an object representing the suggestion at
* the specified position.
*/
protected abstract Object getSuggestionItem(int position);
/**
* Method to be implemented by subclasses to return a SpannableString representing text,
* possibly with formatting added, to display for the suggestion at the specified position.
*/
protected abstract SpannableString getSuggestionText(int position);
/**
* Method to be implemented by subclasses to apply the suggestion at the specified position.
*/
protected abstract void applySuggestion(int position);
/**
* Hides or shows the "Add to dictionary" button in the suggestion menu footer.
*/
protected void setAddToDictionaryEnabled(boolean isEnabled) {
mAddToDictionaryButton.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
}
private void createPopupWindow() {
mPopupWindow = new PopupWindow();
mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
......@@ -139,8 +167,7 @@ public class SuggestionsPopupWindow
(LinearLayout) inflater.inflate(R.layout.text_edit_suggestion_list_footer, null);
mSuggestionListView.addFooterView(mListFooter, null, false);
mSuggestionsAdapter = new SuggestionAdapter();
mSuggestionListView.setAdapter(mSuggestionsAdapter);
mSuggestionListView.setAdapter(new SuggestionAdapter());
mSuggestionListView.setOnItemClickListener(this);
mDivider = mContentView.findViewById(R.id.divider);
......@@ -201,7 +228,7 @@ public class SuggestionsPopupWindow
@Override
public Object getItem(int position) {
return mSpellCheckSuggestions[position];
return getSuggestionItem(position);
}
@Override
......@@ -216,8 +243,8 @@ public class SuggestionsPopupWindow
textView = (TextView) mInflater.inflate(
R.layout.text_edit_suggestion_item, parent, false);
}
final String suggestion = mSpellCheckSuggestions[position];
textView.setText(suggestion);
textView.setText(getSuggestionText(position));
return textView;
}
}
......@@ -248,24 +275,9 @@ public class SuggestionsPopupWindow
* Called by TextSuggestionHost to tell this class what text is currently highlighted (so it can
* be added to the dictionary if requested).
*/
public void setHighlightedText(String text) {
mHighlightedText = text;
}
/**
* Called by TextSuggestionHost to set the list of spell check suggestions to show in the
* suggestion menu.
*/
public void setSpellCheckSuggestions(String[] suggestions) {
mSpellCheckSuggestions = suggestions.clone();
mNumberOfSuggestionsToUse = mSpellCheckSuggestions.length;
}
/**
* Shows the text suggestion menu at the specified coordinates (relative to the viewport).
*/
public void show(double caretX, double caretY) {
mSuggestionsAdapter.notifyDataSetChanged();
protected void show(double caretXPx, double caretYPx, String highlightedText) {
mNumberOfSuggestionsToUse = getSuggestionsCount();
mHighlightedText = highlightedText;
mActivity = mWindowAndroidProvider.getWindowAndroid().getActivity().get();
// Note: the Activity can be null here if we're in a WebView that was created without
......@@ -327,8 +339,8 @@ public class SuggestionsPopupWindow
// Horizontally center the menu on the caret location, and vertically position the menu
// under the caret.
int positionX = (int) Math.round(caretX - width / 2.0f);
int positionY = (int) Math.round(caretY);
int positionX = (int) Math.round(caretXPx - width / 2.0f);
int positionY = (int) Math.round(caretYPx);
// We get the insertion point coords relative to the viewport.
// We need to render the popup relative to the window.
......@@ -364,7 +376,7 @@ public class SuggestionsPopupWindow
public void onClick(View v) {
if (v == mAddToDictionaryButton) {
addToDictionary();
mTextSuggestionHost.newWordAddedToDictionary(mHighlightedText);
mTextSuggestionHost.onNewWordAddedToDictionary(mHighlightedText);
mDismissedByItemTap = true;
mPopupWindow.dismiss();
} else if (v == mDeleteButton) {
......@@ -382,15 +394,14 @@ public class SuggestionsPopupWindow
return;
}
String suggestion = mSpellCheckSuggestions[position];
mTextSuggestionHost.applySpellCheckSuggestion(suggestion);
applySuggestion(position);
mDismissedByItemTap = true;
mPopupWindow.dismiss();
}
@Override
public void onDismiss() {
mTextSuggestionHost.suggestionMenuClosed(mDismissedByItemTap);
mTextSuggestionHost.onSuggestionMenuClosed(mDismissedByItemTap);
mDismissedByItemTap = false;
}
......
......@@ -20,7 +20,8 @@ public class TextSuggestionHost {
private long mNativeTextSuggestionHost;
private final ContentViewCore mContentViewCore;
private SuggestionsPopupWindow mSuggestionsPopupWindow;
private SpellCheckPopupWindow mSpellCheckPopupWindow;
private TextSuggestionsPopupWindow mTextSuggestionsPopupWindow;
public TextSuggestionHost(ContentViewCore contentViewCore) {
mContentViewCore = contentViewCore;
......@@ -29,25 +30,42 @@ public class TextSuggestionHost {
@CalledByNative
private void showSpellCheckSuggestionMenu(
double caretX, double caretY, String markedText, String[] suggestions) {
double caretXDp, double caretY, String markedText, String[] suggestions) {
if (!mContentViewCore.isAttachedToWindow()) {
// This can happen if a new browser window is opened immediately after tapping a spell
// check underline, before the timer to open the menu fires.
suggestionMenuClosed(false);
onSuggestionMenuClosed(false);
return;
}
if (mSuggestionsPopupWindow == null) {
mSuggestionsPopupWindow = new SuggestionsPopupWindow(mContentViewCore.getContext(),
this, mContentViewCore.getContainerView(), mContentViewCore);
hidePopups();
mSpellCheckPopupWindow = new SpellCheckPopupWindow(mContentViewCore.getContext(), this,
mContentViewCore.getContainerView(), mContentViewCore);
float density = mContentViewCore.getRenderCoordinates().getDeviceScaleFactor();
mSpellCheckPopupWindow.show(density * caretXDp,
density * caretY + mContentViewCore.getRenderCoordinates().getContentOffsetYPix(),
markedText, suggestions);
}
@CalledByNative
private void showTextSuggestionMenu(
double caretXDp, double caretYDp, String markedText, SuggestionInfo[] suggestions) {
if (!mContentViewCore.isAttachedToWindow()) {
// This can happen if a new browser window is opened immediately after tapping a spell
// check underline, before the timer to open the menu fires.
onSuggestionMenuClosed(false);
return;
}
mSuggestionsPopupWindow.setHighlightedText(markedText);
mSuggestionsPopupWindow.setSpellCheckSuggestions(suggestions);
hidePopups();
mTextSuggestionsPopupWindow = new TextSuggestionsPopupWindow(mContentViewCore.getContext(),
this, mContentViewCore.getContainerView(), mContentViewCore);
float density = mContentViewCore.getRenderCoordinates().getDeviceScaleFactor();
mSuggestionsPopupWindow.show(density * caretX,
density * caretY + mContentViewCore.getRenderCoordinates().getContentOffsetYPix());
mTextSuggestionsPopupWindow.show(density * caretXDp,
density * caretYDp + mContentViewCore.getRenderCoordinates().getContentOffsetYPix(),
markedText, suggestions);
}
/**
......@@ -55,9 +73,14 @@ public class TextSuggestionHost {
*/
@CalledByNative
public void hidePopups() {
if (mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing()) {
mSuggestionsPopupWindow.dismiss();
mSuggestionsPopupWindow = null;
if (mTextSuggestionsPopupWindow != null && mTextSuggestionsPopupWindow.isShowing()) {
mTextSuggestionsPopupWindow.dismiss();
mTextSuggestionsPopupWindow = null;
}
if (mSpellCheckPopupWindow != null && mSpellCheckPopupWindow.isShowing()) {
mSpellCheckPopupWindow.dismiss();
mSpellCheckPopupWindow = null;
}
}
......@@ -68,6 +91,14 @@ public class TextSuggestionHost {
nativeApplySpellCheckSuggestion(mNativeTextSuggestionHost, suggestion);
}
/**
* Tells Blink to replace the active suggestion range with the specified suggestion on the
* specified marker.
*/
public void applyTextSuggestion(int markerTag, int suggestionIndex) {
nativeApplyTextSuggestion(mNativeTextSuggestionHost, markerTag, suggestionIndex);
}
/**
* Tells Blink to delete the active suggestion range.
*/
......@@ -78,39 +109,51 @@ public class TextSuggestionHost {
/**
* Tells Blink to remove spelling markers under all instances of the specified word.
*/
public void newWordAddedToDictionary(String word) {
nativeNewWordAddedToDictionary(mNativeTextSuggestionHost, word);
public void onNewWordAddedToDictionary(String word) {
nativeOnNewWordAddedToDictionary(mNativeTextSuggestionHost, word);
}
/**
* Tells Blink the suggestion menu was closed (and also clears the reference to the
* SuggestionsPopupWindow instance so it can be garbage collected).
*/
public void suggestionMenuClosed(boolean dismissedByItemTap) {
public void onSuggestionMenuClosed(boolean dismissedByItemTap) {
if (!dismissedByItemTap) {
nativeSuggestionMenuClosed(mNativeTextSuggestionHost);
nativeOnSuggestionMenuClosed(mNativeTextSuggestionHost);
}
mSuggestionsPopupWindow = null;
mSpellCheckPopupWindow = null;
mTextSuggestionsPopupWindow = null;
}
@CalledByNative
private void destroy() {
hidePopups();
mNativeTextSuggestionHost = 0;
}
/**
* @return The SuggestionsPopupWindow, if one exists.
* @return The TextSuggestionsPopupWindow, if one exists.
*/
@VisibleForTesting
public SuggestionsPopupWindow getTextSuggestionsPopupWindowForTesting() {
return mTextSuggestionsPopupWindow;
}
/**
* @return The SpellCheckPopupWindow, if one exists.
*/
@VisibleForTesting
public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
return mSuggestionsPopupWindow;
public SuggestionsPopupWindow getSpellCheckPopupWindowForTesting() {
return mSpellCheckPopupWindow;
}
private native long nativeInit(WebContents webContents);
private native void nativeApplySpellCheckSuggestion(
long nativeTextSuggestionHostAndroid, String suggestion);
private native void nativeApplyTextSuggestion(
long nativeTextSuggestionHostAndroid, int markerTag, int suggestionIndex);
private native void nativeDeleteActiveSuggestionRange(long nativeTextSuggestionHostAndroid);
private native void nativeNewWordAddedToDictionary(
private native void nativeOnNewWordAddedToDictionary(
long nativeTextSuggestionHostAndroid, String word);
private native void nativeSuggestionMenuClosed(long nativeTextSuggestionHostAndroid);
private native void nativeOnSuggestionMenuClosed(long nativeTextSuggestionHostAndroid);
}
// 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.
package org.chromium.content.browser.input;
import android.content.Context;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.TextAppearanceSpan;
import android.view.View;
import org.chromium.content.R;
import org.chromium.content.browser.WindowAndroidProvider;
/**
* A subclass of SuggestionsPopupWindow to be used for showing suggestions from one or more
* SuggestionSpans.
*/
public class TextSuggestionsPopupWindow extends SuggestionsPopupWindow {
private SuggestionInfo[] mSuggestionInfos;
private TextAppearanceSpan mPrefixSpan;
private TextAppearanceSpan mSuffixSpan;
/**
* @param context Android context to use.
* @param textSuggestionHost TextSuggestionHost instance (used to communicate with Blink).
* @param parentView The view used to attach the PopupWindow.
* @param windowAndroidProvider A WindowAndroidProvider instance used to get the window size.
*/
public TextSuggestionsPopupWindow(Context context, TextSuggestionHost textSuggestionHost,
View parentView, WindowAndroidProvider windowAndroidProvider) {
super(context, textSuggestionHost, parentView, windowAndroidProvider);
mPrefixSpan = new TextAppearanceSpan(context, R.style.SuggestionPrefixOrSuffix);
mSuffixSpan = new TextAppearanceSpan(context, R.style.SuggestionPrefixOrSuffix);
}
/**
* Shows the text suggestion menu at the specified coordinates (relative to the viewport).
*/
public void show(double caretX, double caretY, String highlightedText,
SuggestionInfo[] suggestionInfos) {
mSuggestionInfos = suggestionInfos.clone();
// Android's Editor.java shows the "Add to dictonary" button if and only if there's a
// SuggestionSpan with FLAG_MISSPELLED set. However, some OEMs (e.g. Samsung) appear to
// change the behavior on their devices to never show this button, since their IMEs don't go
// through the normal spell-checking API and instead add SuggestionSpans directly. Since
// it's difficult to determine how the OEM has set up the native menu, we instead only show
// the "Add to dictionary" button for spelling markers added by Chrome from running the
// system spell checker. SuggestionSpans with FLAG_MISSPELLED set (i.e., a spelling
// underline added directly by the IME) do not show this button.
setAddToDictionaryEnabled(false);
super.show(caretX, caretY, highlightedText);
}
@Override
protected int getSuggestionsCount() {
return mSuggestionInfos.length;
}
@Override
protected Object getSuggestionItem(int position) {
return mSuggestionInfos[position];
}
@Override
protected SpannableString getSuggestionText(int position) {
final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
SpannableString suggestionText = new SpannableString(suggestionInfo.getPrefix()
+ suggestionInfo.getSuggestion() + suggestionInfo.getSuffix());
// Gray out prefix text (if any).
suggestionText.setSpan(mPrefixSpan, 0, suggestionInfo.getPrefix().length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// Gray out suffix text (if any).
suggestionText.setSpan(mSuffixSpan,
suggestionInfo.getPrefix().length() + suggestionInfo.getSuggestion().length(),
suggestionInfo.getPrefix().length() + suggestionInfo.getSuggestion().length()
+ suggestionInfo.getSuffix().length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return suggestionText;
}
@Override
protected void applySuggestion(int position) {
SuggestionInfo info = mSuggestionInfos[position];
mTextSuggestionHost.applyTextSuggestion(info.getMarkerTag(), info.getSuggestionIndex());
}
}
// 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.
package org.chromium.content.browser.input;
import android.support.test.filters.LargeTest;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.SuggestionSpan;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.content.R;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.test.ContentJUnit4ClassRunner;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.DOMUtils;
import org.chromium.content.browser.test.util.TouchCommon;
import org.chromium.content_public.browser.WebContents;
import java.util.concurrent.TimeoutException;
/**
* Integration tests for the spell check menu.
*/
@RunWith(ContentJUnit4ClassRunner.class)
public class TextSuggestionMenuTest {
private static final String URL =
"data:text/html, <div contenteditable id=\"div\" /><span id=\"span\" />";
@Rule
public ImeActivityTestRule mRule = new ImeActivityTestRule();
@Before
public void setUp() throws Throwable {
mRule.setUp();
mRule.fullyLoadUrl(URL);
}
@Test
@LargeTest
public void testDeleteMarkedWord() throws InterruptedException, Throwable, TimeoutException {
final ContentViewCore cvc = mRule.getContentViewCore();
WebContents webContents = cvc.getWebContents();
DOMUtils.focusNode(webContents, "div");
SpannableString textToCommit = new SpannableString("hello");
SuggestionSpan suggestionSpan = new SuggestionSpan(mRule.getContentViewCore().getContext(),
new String[] {"goodbye"}, SuggestionSpan.FLAG_EASY_CORRECT);
textToCommit.setSpan(suggestionSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mRule.commitText(textToCommit, 1);
DOMUtils.clickNode(cvc, "div");
waitForMenuToShow(cvc);
TouchCommon.singleClickView(getDeleteButton(cvc));
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
try {
return DOMUtils.getNodeContents(cvc.getWebContents(), "div").equals("");
} catch (InterruptedException | TimeoutException e) {
return false;
}
}
});
waitForMenuToHide(cvc);
}
@Test
@LargeTest
public void testApplySuggestion() throws InterruptedException, Throwable, TimeoutException {
final ContentViewCore cvc = mRule.getContentViewCore();
WebContents webContents = cvc.getWebContents();
DOMUtils.focusNode(webContents, "div");
// We have a string of length 11 and we set three SuggestionSpans on it
// to test that the menu shows the right suggestions in the right order:
//
// - One span on the word "hello"
// - One span on the whole string "hello world"
// - One span on the word "world"
//
// We simulate a tap at the end of the string. We should get the
// suggestions from "world", then the suggestions from "hello world",
// and not get any suggestions from "hello".
SpannableString textToCommit = new SpannableString("hello world");
SuggestionSpan suggestionSpan1 = new SuggestionSpan(mRule.getContentViewCore().getContext(),
new String[] {"invalid_suggestion"}, SuggestionSpan.FLAG_EASY_CORRECT);
textToCommit.setSpan(suggestionSpan1, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SuggestionSpan suggestionSpan2 = new SuggestionSpan(mRule.getContentViewCore().getContext(),
new String[] {"suggestion3", "suggestion4"}, SuggestionSpan.FLAG_EASY_CORRECT);
textToCommit.setSpan(suggestionSpan2, 0, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SuggestionSpan suggestionSpan3 = new SuggestionSpan(mRule.getContentViewCore().getContext(),
new String[] {"suggestion1", "suggestion2"}, SuggestionSpan.FLAG_EASY_CORRECT);
textToCommit.setSpan(suggestionSpan3, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mRule.commitText(textToCommit, 1);
DOMUtils.clickNode(cvc, "span");
waitForMenuToShow(cvc);
// There should be 5 child views: 4 suggestions plus the list footer.
Assert.assertEquals(5, getSuggestionList(cvc).getChildCount());
Assert.assertEquals(
"hello suggestion1", ((TextView) getSuggestionButton(cvc, 0)).getText().toString());
Assert.assertEquals(
"hello suggestion2", ((TextView) getSuggestionButton(cvc, 1)).getText().toString());
Assert.assertEquals(
"suggestion3", ((TextView) getSuggestionButton(cvc, 2)).getText().toString());
Assert.assertEquals(
"suggestion4", ((TextView) getSuggestionButton(cvc, 3)).getText().toString());
TouchCommon.singleClickView(getSuggestionButton(cvc, 2));
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
try {
return DOMUtils.getNodeContents(cvc.getWebContents(), "div")
.equals("suggestion3");
} catch (InterruptedException | TimeoutException e) {
return false;
}
}
});
waitForMenuToHide(cvc);
}
@Test
@LargeTest
public void menuDismissesWhenTappingOutside()
throws InterruptedException, Throwable, TimeoutException {
final ContentViewCore cvc = mRule.getContentViewCore();
WebContents webContents = cvc.getWebContents();
DOMUtils.focusNode(webContents, "div");
SpannableString textToCommit = new SpannableString("hello");
SuggestionSpan suggestionSpan = new SuggestionSpan(mRule.getContentViewCore().getContext(),
new String[] {"goodbye"}, SuggestionSpan.FLAG_EASY_CORRECT);
textToCommit.setSpan(suggestionSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mRule.commitText(textToCommit, 1);
DOMUtils.clickNode(cvc, "div");
waitForMenuToShow(cvc);
// The menu appears below the text, so if we tap on the text again, that
// should count as tapping outside the menu.
DOMUtils.clickNode(cvc, "div");
waitForMenuToHide(cvc);
}
private void waitForMenuToShow(ContentViewCore cvc) {
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
SuggestionsPopupWindow suggestionsPopupWindow =
cvc.getTextSuggestionHostForTesting()
.getTextSuggestionsPopupWindowForTesting();
if (suggestionsPopupWindow == null) {
return false;
}
// On some test runs, even when suggestionsPopupWindow is non-null and
// suggestionsPopupWindow.isShowing() returns true, the delete button hasn't been
// measured yet and getWidth()/getHeight() return 0. This causes the menu button
// click to instead fall on the "Add to dictionary" button. So we have to check that
// this isn't happening.
return getDeleteButton(cvc).getWidth() != 0;
}
});
}
private void waitForMenuToHide(ContentViewCore cvc) {
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
SuggestionsPopupWindow suggestionsPopupWindow =
cvc.getTextSuggestionHostForTesting()
.getTextSuggestionsPopupWindowForTesting();
return suggestionsPopupWindow == null;
}
});
}
private ListView getSuggestionList(ContentViewCore cvc) {
View contentView = cvc.getTextSuggestionHostForTesting()
.getTextSuggestionsPopupWindowForTesting()
.getContentViewForTesting();
return (ListView) contentView.findViewById(R.id.suggestionContainer);
}
private View getSuggestionButton(ContentViewCore cvc, int suggestionIndex) {
return getSuggestionList(cvc).getChildAt(suggestionIndex);
}
private View getDeleteButton(ContentViewCore cvc) {
View contentView = cvc.getTextSuggestionHostForTesting()
.getTextSuggestionsPopupWindowForTesting()
.getContentViewForTesting();
return contentView.findViewById(R.id.deleteButton);
}
}
......@@ -56,14 +56,23 @@ struct CandidateWindowEntry {
string description_body;
};
// See comments for ui::ImeTextSpan::Type for more details.
enum ImeTextSpanType {
kComposition,
kSuggestion,
};
// Represents an underlined segment of text currently composed by IME.
// Corresponds to ui::ImeTextSpan.
struct ImeTextSpan {
ImeTextSpanType type;
uint32 start_offset;
uint32 end_offset;
bool thick;
uint32 underline_color;
bool thick;
uint32 background_color;
uint32 suggestion_highlight_color;
array<string> suggestions;
};
// Represents a text currently being composed by IME. Corresponds to
......
......@@ -45,11 +45,16 @@ bool StructTraits<ui::mojom::ImeTextSpanDataView, ui::ImeTextSpan>::Read(
ui::ImeTextSpan* out) {
if (data.is_null())
return false;
if (!data.ReadType(&out->type))
return false;
out->start_offset = data.start_offset();
out->end_offset = data.end_offset();
out->underline_color = data.underline_color();
out->thick = data.thick();
out->background_color = data.background_color();
out->suggestion_highlight_color = data.suggestion_highlight_color();
if (!data.ReadSuggestions(&out->suggestions))
return false;
return true;
}
......@@ -61,6 +66,38 @@ bool StructTraits<ui::mojom::CompositionTextDataView, ui::CompositionText>::
data.ReadSelection(&out->selection);
}
// static
ui::mojom::ImeTextSpanType
EnumTraits<ui::mojom::ImeTextSpanType, ui::ImeTextSpan::Type>::ToMojom(
ui::ImeTextSpan::Type ime_text_span_type) {
switch (ime_text_span_type) {
case ui::ImeTextSpan::Type::kComposition:
return ui::mojom::ImeTextSpanType::kComposition;
case ui::ImeTextSpan::Type::kSuggestion:
return ui::mojom::ImeTextSpanType::kSuggestion;
}
NOTREACHED();
return ui::mojom::ImeTextSpanType::kComposition;
}
// static
bool EnumTraits<ui::mojom::ImeTextSpanType, ui::ImeTextSpan::Type>::FromMojom(
ui::mojom::ImeTextSpanType type,
ui::ImeTextSpan::Type* out) {
switch (type) {
case ui::mojom::ImeTextSpanType::kComposition:
*out = ui::ImeTextSpan::Type::kComposition;
return true;
case ui::mojom::ImeTextSpanType::kSuggestion:
*out = ui::ImeTextSpan::Type::kSuggestion;
return true;
}
NOTREACHED();
return false;
}
// static
ui::mojom::TextInputMode
EnumTraits<ui::mojom::TextInputMode, ui::TextInputMode>::ToMojom(
......
......@@ -90,6 +90,7 @@ struct StructTraits<ui::mojom::CompositionTextDataView, ui::CompositionText> {
template <>
struct StructTraits<ui::mojom::ImeTextSpanDataView, ui::ImeTextSpan> {
static ui::ImeTextSpan::Type type(const ui::ImeTextSpan& c) { return c.type; }
static uint32_t start_offset(const ui::ImeTextSpan& c) {
return c.start_offset;
}
......@@ -101,9 +102,23 @@ struct StructTraits<ui::mojom::ImeTextSpanDataView, ui::ImeTextSpan> {
static uint32_t background_color(const ui::ImeTextSpan& c) {
return c.background_color;
}
static uint32_t suggestion_highlight_color(const ui::ImeTextSpan& c) {
return c.suggestion_highlight_color;
}
static std::vector<std::string> suggestions(const ui::ImeTextSpan& c) {
return c.suggestions;
}
static bool Read(ui::mojom::ImeTextSpanDataView data, ui::ImeTextSpan* out);
};
template <>
struct EnumTraits<ui::mojom::ImeTextSpanType, ui::ImeTextSpan::Type> {
static ui::mojom::ImeTextSpanType ToMojom(
ui::ImeTextSpan::Type ime_text_span_type);
static bool FromMojom(ui::mojom::ImeTextSpanType input,
ui::ImeTextSpan::Type* out);
};
template <>
struct EnumTraits<ui::mojom::TextInputMode, ui::TextInputMode> {
static ui::mojom::TextInputMode ToMojom(ui::TextInputMode text_input_mode);
......
......@@ -297,6 +297,7 @@ blink_core_sources("editing") {
"suggestion/TextSuggestionBackendImpl.h",
"suggestion/TextSuggestionController.cpp",
"suggestion/TextSuggestionController.h",
"suggestion/TextSuggestionInfo.h",
]
if (is_mac) {
......
......@@ -496,11 +496,21 @@ void InputMethodController::AddImeTextSpans(
if (ephemeral_line_range.IsNull())
continue;
GetDocument().Markers().AddCompositionMarker(
ephemeral_line_range, ime_text_span.UnderlineColor(),
ime_text_span.Thick() ? StyleableMarker::Thickness::kThick
: StyleableMarker::Thickness::kThin,
ime_text_span.BackgroundColor());
if (ime_text_span.GetType() == ImeTextSpan::Type::kComposition) {
GetDocument().Markers().AddCompositionMarker(
ephemeral_line_range, ime_text_span.UnderlineColor(),
ime_text_span.Thick() ? StyleableMarker::Thickness::kThick
: StyleableMarker::Thickness::kThin,
ime_text_span.BackgroundColor());
} else if (ime_text_span.GetType() == ImeTextSpan::Type::kSuggestion) {
GetDocument().Markers().AddSuggestionMarker(
ephemeral_line_range, ime_text_span.Suggestions(),
ime_text_span.SuggestionHighlightColor(),
ime_text_span.UnderlineColor(),
ime_text_span.Thick() ? StyleableMarker::Thickness::kThick
: StyleableMarker::Thickness::kThin,
ime_text_span.BackgroundColor());
}
}
}
......
......@@ -400,7 +400,7 @@ bool SelectionController::HandleSingleClick(
// makes the IsValidFor() check fail.
if (has_editable_style && event.Event().FromTouch() &&
position_to_use.IsValidFor(*frame_->GetDocument())) {
frame_->GetTextSuggestionController().HandlePotentialMisspelledWordTap(
frame_->GetTextSuggestionController().HandlePotentialSuggestionTap(
position_to_use.GetPosition());
}
......
......@@ -28,25 +28,36 @@ void TextSuggestionBackendImpl::ApplySpellCheckSuggestion(
frame_->GetTextSuggestionController().ApplySpellCheckSuggestion(suggestion);
}
void TextSuggestionBackendImpl::ApplyTextSuggestion(int32_t marker_tag,
int32_t suggestion_index) {
if (frame_) {
frame_->GetTextSuggestionController().ApplyTextSuggestion(marker_tag,
suggestion_index);
}
}
void TextSuggestionBackendImpl::DeleteActiveSuggestionRange() {
if (frame_)
frame_->GetTextSuggestionController().DeleteActiveSuggestionRange();
}
void TextSuggestionBackendImpl::NewWordAddedToDictionary(
void TextSuggestionBackendImpl::OnNewWordAddedToDictionary(
const WTF::String& word) {
if (frame_)
frame_->GetTextSuggestionController().NewWordAddedToDictionary(word);
frame_->GetTextSuggestionController().OnNewWordAddedToDictionary(word);
}
void TextSuggestionBackendImpl::SpellCheckMenuTimeoutCallback() {
void TextSuggestionBackendImpl::OnSuggestionMenuClosed() {
if (frame_)
frame_->GetTextSuggestionController().SpellCheckMenuTimeoutCallback();
frame_->GetTextSuggestionController().OnSuggestionMenuClosed();
}
void TextSuggestionBackendImpl::SuggestionMenuClosed() {
if (frame_)
frame_->GetTextSuggestionController().SuggestionMenuClosed();
void TextSuggestionBackendImpl::SuggestionMenuTimeoutCallback(
int32_t max_number_of_suggestions) {
if (frame_) {
frame_->GetTextSuggestionController().SuggestionMenuTimeoutCallback(
max_number_of_suggestions);
}
}
} // namespace blink
......@@ -20,10 +20,11 @@ class CORE_EXPORT TextSuggestionBackendImpl final
static void Create(LocalFrame*, mojom::blink::TextSuggestionBackendRequest);
void ApplySpellCheckSuggestion(const String& suggestion) final;
void ApplyTextSuggestion(int32_t marker_tag, int32_t suggestion_index) final;
void DeleteActiveSuggestionRange() final;
void NewWordAddedToDictionary(const String& word) final;
void SpellCheckMenuTimeoutCallback() final;
void SuggestionMenuClosed() final;
void OnNewWordAddedToDictionary(const String& word) final;
void OnSuggestionMenuClosed() final;
void SuggestionMenuTimeoutCallback(int32_t max_number_of_suggestions) final;
private:
explicit TextSuggestionBackendImpl(LocalFrame&);
......
......@@ -16,7 +16,9 @@
namespace blink {
class Document;
class DocumentMarker;
class LocalFrame;
struct TextSuggestionInfo;
// This class handles functionality related to displaying a menu of text
// suggestions (e.g. from spellcheck), and performing actions relating to those
......@@ -33,14 +35,14 @@ class CORE_EXPORT TextSuggestionController final
bool IsMenuOpen() const;
void HandlePotentialMisspelledWordTap(
const PositionInFlatTree& caret_position);
void HandlePotentialSuggestionTap(const PositionInFlatTree& caret_position);
void ApplySpellCheckSuggestion(const String& suggestion);
void ApplyTextSuggestion(int32_t marker_tag, uint32_t suggestion_index);
void DeleteActiveSuggestionRange();
void NewWordAddedToDictionary(const String& word);
void SpellCheckMenuTimeoutCallback();
void SuggestionMenuClosed();
void OnNewWordAddedToDictionary(const String& word);
void OnSuggestionMenuClosed();
void SuggestionMenuTimeoutCallback(size_t max_number_of_suggestions);
DECLARE_TRACE();
......@@ -56,7 +58,16 @@ class CORE_EXPORT TextSuggestionController final
DocumentMarker::MarkerTypes) const;
void AttemptToDeleteActiveSuggestionRange();
void ReplaceSpellingMarkerTouchingSelectionWithText(const String&);
void CallMojoShowTextSuggestionMenu(
const Vector<TextSuggestionInfo>& text_suggestion_infos,
const String& misspelled_word);
void ShowSpellCheckMenu(
const std::pair<Node*, DocumentMarker*>& node_spelling_marker_pair);
void ShowSuggestionMenu(
const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
node_suggestion_marker_pairs,
size_t max_number_of_suggestions);
void ReplaceActiveSuggestionRange(const String&);
void ReplaceRangeWithText(const EphemeralRange&, const String& replacement);
bool is_suggestion_menu_open_;
......
......@@ -21,8 +21,9 @@ TEST_F(TextSuggestionControllerTest, ApplySpellCheckSuggestion) {
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().Markers().AddActiveSuggestionMarker(
EphemeralRange(Position(text, 0), Position(text, 8)), Color::kBlack,
StyleableMarker::Thickness::kThin, Color::kBlack);
// Select immediately before misspelling
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
......@@ -34,6 +35,119 @@ TEST_F(TextSuggestionControllerTest, ApplySpellCheckSuggestion) {
.ApplySpellCheckSuggestion("spellcheck");
EXPECT_EQ("spellcheck", text->textContent());
// Cursor should be at end of replaced text
const VisibleSelectionInFlatTree& selection =
GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
EXPECT_EQ(text, selection.Start().ComputeContainerNode());
EXPECT_EQ(10, selection.Start().ComputeOffsetInContainerNode());
EXPECT_EQ(text, selection.End().ComputeContainerNode());
EXPECT_EQ(10, selection.End().ComputeOffsetInContainerNode());
}
TEST_F(TextSuggestionControllerTest, ApplyTextSuggestion) {
SetBodyContent(
"<div contenteditable>"
"word1 word2 word3 word4"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
// Add marker on "word1". This marker should *not* be cleared by the
// replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 0), Position(text, 5)),
Vector<String>({"marker1"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Add marker on "word1 word2 word3 word4". This marker should *not* be
// cleared by the replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 0), Position(text, 23)),
Vector<String>({"marker2"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Add marker on "word2 word3". This marker should *not* be cleared by the
// replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 6), Position(text, 17)),
Vector<String>({"marker3"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Add marker on "word4". This marker should *not* be cleared by the
// replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 18), Position(text, 23)),
Vector<String>({"marker4"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Add marker on "word1 word2". This marker should be cleared by the
// replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 0), Position(text, 11)),
Vector<String>({"marker5"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Add marker on "word3 word4". This marker should be cleared by the
// replace operation.
GetDocument().Markers().AddSuggestionMarker(
EphemeralRange(Position(text, 12), Position(text, 23)),
Vector<String>({"marker6"}), Color::kBlack, Color::kBlack,
StyleableMarker::Thickness::kThick, Color::kBlack);
// Select immediately before word2.
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 6), Position(text, 6))
.Build());
// Replace "word2 word3" with "marker3" (marker should have tag 3; tags start
// from 1, not 0).
GetDocument().GetFrame()->GetTextSuggestionController().ApplyTextSuggestion(
3, 0);
// This returns the markers sorted by start offset; we need them sorted by
// start *and* end offset, since we have multiple markers starting at 0.
DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(text);
std::sort(markers.begin(), markers.end(),
[](const DocumentMarker* marker1, const DocumentMarker* marker2) {
if (marker1->StartOffset() != marker2->StartOffset())
return marker1->StartOffset() < marker2->StartOffset();
return marker1->EndOffset() < marker2->EndOffset();
});
EXPECT_EQ(4u, markers.size());
// marker1
EXPECT_EQ(0u, markers[0]->StartOffset());
EXPECT_EQ(5u, markers[0]->EndOffset());
// marker2
EXPECT_EQ(0u, markers[1]->StartOffset());
EXPECT_EQ(19u, markers[1]->EndOffset());
// marker3
EXPECT_EQ(6u, markers[2]->StartOffset());
EXPECT_EQ(13u, markers[2]->EndOffset());
const SuggestionMarker* const suggestion_marker =
ToSuggestionMarker(markers[2]);
EXPECT_EQ(1u, suggestion_marker->Suggestions().size());
EXPECT_EQ(String("word2 word3"), suggestion_marker->Suggestions()[0]);
// marker4
EXPECT_EQ(14u, markers[3]->StartOffset());
EXPECT_EQ(19u, markers[3]->EndOffset());
// marker5 and marker6 should've been cleared
// Cursor should be at end of replaced text
const VisibleSelectionInFlatTree& selection =
GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
EXPECT_EQ(text, selection.Start().ComputeContainerNode());
EXPECT_EQ(13, selection.Start().ComputeOffsetInContainerNode());
EXPECT_EQ(text, selection.End().ComputeContainerNode());
EXPECT_EQ(13, selection.End().ComputeOffsetInContainerNode());
}
TEST_F(TextSuggestionControllerTest, DeleteActiveSuggestionRange_DeleteAtEnd) {
......@@ -226,7 +340,7 @@ TEST_F(TextSuggestionControllerTest,
}
TEST_F(TextSuggestionControllerTest,
DeleteActiveSuggestionRange_NewWordAddedToDictionary) {
DeleteActiveSuggestionRange_OnNewWordAddedToDictionary) {
SetBodyContent(
"<div contenteditable>"
"embiggen"
......@@ -247,7 +361,7 @@ TEST_F(TextSuggestionControllerTest,
GetDocument()
.GetFrame()
->GetTextSuggestionController()
.NewWordAddedToDictionary("cromulent");
.OnNewWordAddedToDictionary("cromulent");
// Verify the spelling marker is still present
EXPECT_NE(nullptr, GetDocument()
.GetFrame()
......@@ -259,7 +373,7 @@ TEST_F(TextSuggestionControllerTest,
GetDocument()
.GetFrame()
->GetTextSuggestionController()
.NewWordAddedToDictionary("embiggen");
.OnNewWordAddedToDictionary("embiggen");
// Verify the spelling marker is gone
EXPECT_EQ(nullptr, GetDocument()
.GetFrame()
......
// 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.
#ifndef TextSuggestionInfo_h
#define TextSuggestionInfo_h
#include "platform/wtf/text/WTFString.h"
namespace blink {
struct TextSuggestionInfo {
int32_t marker_tag;
uint32_t suggestion_index;
int32_t span_start;
int32_t span_end;
String prefix;
String suggestion;
String suffix;
};
} // namespace blink
#endif // TextSuggestionList_h
......@@ -8,7 +8,29 @@ struct SpellCheckSuggestion {
string suggestion;
};
struct TextSuggestion {
int32 marker_tag;
// This index is used by browser code as an opaque identifier to send back to
// the renderer. It is not possible for the renderer to use it to cause an
// out-of-bounds error in the browser.
int32 suggestion_index;
string prefix;
string suggestion;
string suffix;
};
// This interface runs in the browser. Blink editing code calls it to tell it
// when to display a spell check or text suggestion menu.
interface TextSuggestionHost {
StartSpellCheckMenuTimer();
ShowSpellCheckSuggestionMenu(double caret_x, double caret_y, string marked_text, array<SpellCheckSuggestion> suggestions);
StartSuggestionMenuTimer();
ShowSpellCheckSuggestionMenu(
double caret_x,
double caret_y,
string marked_text,
array<SpellCheckSuggestion> suggestions);
ShowTextSuggestionMenu(
double caret_x,
double caret_y,
string marked_text,
array<TextSuggestion> suggestions);
};
......@@ -4,10 +4,14 @@
module blink.mojom;
// This interface is implemented in blink by TextSuggestionBackendImpl. It is
// called by browser code on Android implementing the spell check/text
// suggestion menu.
interface TextSuggestionBackend {
ApplySpellCheckSuggestion(string suggestion);
ApplyTextSuggestion(int32 marker_tag, int32 suggestion_index);
DeleteActiveSuggestionRange();
NewWordAddedToDictionary(string suggestion);
SpellCheckMenuTimeoutCallback();
SuggestionMenuClosed();
OnNewWordAddedToDictionary(string suggestion);
OnSuggestionMenuClosed();
SuggestionMenuTimeoutCallback(int32 max_number_of_suggestions);
};
......@@ -5,6 +5,7 @@
#ifndef UI_PLATFORM_WINDOW_MOJO_IME_TYPE_CONVERTERS_H_
#define UI_PLATFORM_WINDOW_MOJO_IME_TYPE_CONVERTERS_H_
#include "ui/base/ime/ime_text_span.h"
#include "ui/platform_window/mojo/mojo_ime_export.h"
#include "ui/platform_window/mojo/text_input_state.mojom.h"
#include "ui/platform_window/text_input_state.h"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment