From bea17c49ec23bf0f646cb548445c7756aa50d233 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Sun, 24 Aug 2014 20:08:55 -0700 Subject: [PATCH] Introduce commit/add-to-dictionary indicators This CL introduces commit/add-to-dictionary indicators. Note that the text is not yet highlighted when the commit indicator is displayed. It will be addressed in subsequent CLs. Change-Id: I7e9b0fcfdc0776a50a1d8cfb41ee0add813317dd --- .../values/donottranslate-text-decorator.xml | 127 ++++++ .../inputmethod/keyboard/TextDecorator.java | 425 ++++++++++++++++++ .../inputmethod/keyboard/TextDecoratorUi.java | 259 +++++++++++ .../keyboard/TextDecoratorUiOperator.java | 55 +++ .../android/inputmethod/latin/LatinIME.java | 49 +- .../latin/inputlogic/InputLogic.java | 144 +++++- .../latin/settings/SettingsValues.java | 22 + 7 files changed, 1066 insertions(+), 15 deletions(-) create mode 100644 java/res/values/donottranslate-text-decorator.xml create mode 100644 java/src/com/android/inputmethod/keyboard/TextDecorator.java create mode 100644 java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java create mode 100644 java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java diff --git a/java/res/values/donottranslate-text-decorator.xml b/java/res/values/donottranslate-text-decorator.xml new file mode 100644 index 000000000..9c39a4689 --- /dev/null +++ b/java/res/values/donottranslate-text-decorator.xml @@ -0,0 +1,127 @@ + + + + + + + 500 + + + + + 4 + + + + false + + + false + + + + #B6E2DE + + + + + #48B6AC + + + + + #FFFFFF + + + + + 480 + + + + + 180 + 323 + 97 + 240 + 68 + 268 + 180 + 380 + 420 + 140 + 392 + 112 + + + + + #D1E7B7 + + + + + #4EB848 + + + + + #FFFFFF + + + + + 480 + + + + + 380 + 260 + 260 + 260 + 260 + 380 + 220 + 380 + 220 + 260 + 100 + 260 + 100 + 220 + 220 + 220 + 220 + 100 + 260 + 100 + 260 + 220 + 380 + 220 + + diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java new file mode 100644 index 000000000..0eb8b443a --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard; + +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; +import android.inputmethodservice.InputMethodService; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.CursorAnchorInfo; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; + +import javax.annotation.Nonnull; + +/** + * A controller class of commit/add-to-dictionary indicator (a.k.a. TextDecorator). This class + * is designed to be independent of UI subsystems such as {@link View}. All the UI related + * operations are delegated to {@link TextDecoratorUi} via {@link TextDecoratorUiOperator}. + */ +public class TextDecorator { + private static final String TAG = TextDecorator.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int MODE_NONE = 0; + private static final int MODE_COMMIT = 1; + private static final int MODE_ADD_TO_DICTIONARY = 2; + + private int mMode = MODE_NONE; + + private final PointF mLocalOrigin = new PointF(); + private final RectF mRelativeIndicatorBounds = new RectF(); + private final RectF mRelativeComposingTextBounds = new RectF(); + + private boolean mIsFullScreenMode = false; + private SuggestedWordInfo mWaitingWord = null; + private CursorAnchorInfoCompatWrapper mCursorAnchorInfoWrapper = null; + + @Nonnull + private final Listener mListener; + + @Nonnull + private TextDecoratorUiOperator mUiOperator = EMPTY_UI_OPERATOR; + + public interface Listener { + /** + * Called when the user clicks the composing text to commit. + * @param wordInfo the suggested word which the user clicked on. + */ + void onClickComposingTextToCommit(final SuggestedWordInfo wordInfo); + + /** + * Called when the user clicks the composing text to add the word into the dictionary. + * @param wordInfo the suggested word which the user clicked on. + */ + void onClickComposingTextToAddToDictionary(final SuggestedWordInfo wordInfo); + } + + public TextDecorator(final Listener listener) { + mListener = (listener != null) ? listener : EMPTY_LISTENER; + } + + /** + * Sets the UI operator for {@link TextDecorator}. Any user visible operations will be + * delegated to the associated UI operator. + * @param uiOperator the UI operator to be associated. + */ + public void setUiOperator(final TextDecoratorUiOperator uiOperator) { + mUiOperator.disposeUi(); + mUiOperator = uiOperator; + mUiOperator.setOnClickListener(getOnClickHandler()); + } + + private final Runnable mDefaultOnClickHandler = new Runnable() { + @Override + public void run() { + onClickIndicator(); + } + }; + + @UsedForTesting + final Runnable getOnClickHandler() { + return mDefaultOnClickHandler; + } + + /** + * Shows the "Commit" indicator and associates it with the given suggested word. + * + *

The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and + * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call + * {@link #reset()} to hide the indicator.

+ * + * @param wordInfo the suggested word which should be associated with the indicator. This object + * will be passed back in {@link Listener#onClickComposingTextToCommit(SuggestedWordInfo)} + */ + public void showCommitIndicator(final SuggestedWordInfo wordInfo) { + if (mMode == MODE_COMMIT && wordInfo != null && + TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) { + // Skip layout for better performance. + return; + } + mWaitingWord = wordInfo; + mMode = MODE_COMMIT; + layoutLater(); + } + + /** + * Shows the "Add to dictionary" indicator and associates it with associating the given + * suggested word. + * + *

The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and + * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call + * {@link #reset()} to hide the indicator.

+ * + * @param wordInfo the suggested word which should be associated with the indicator. This object + * will be passed back in + * {@link Listener#onClickComposingTextToAddToDictionary(SuggestedWordInfo)}. + */ + public void showAddToDictionaryIndicator(final SuggestedWordInfo wordInfo) { + if (mMode == MODE_ADD_TO_DICTIONARY && wordInfo != null && + TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) { + // Skip layout for better performance. + return; + } + mWaitingWord = wordInfo; + mMode = MODE_ADD_TO_DICTIONARY; + layoutLater(); + return; + } + + /** + * Must be called when the input method is about changing to for from the full screen mode. + * @param fullScreenMode {@code true} if the input method is entering the full screen mode. + * {@code false} is the input method is finishing the full screen mode. + */ + public void notifyFullScreenMode(final boolean fullScreenMode) { + final boolean currentFullScreenMode = mIsFullScreenMode; + if (!currentFullScreenMode && fullScreenMode) { + // Currently full screen mode is not supported. + // TODO: Support full screen mode. + hideIndicator(); + } + mIsFullScreenMode = fullScreenMode; + } + + /** + * Resets previous requests and makes indicator invisible. + */ + public void reset() { + mWaitingWord = null; + mMode = MODE_NONE; + mLocalOrigin.set(0.0f, 0.0f); + mRelativeIndicatorBounds.set(0.0f, 0.0f, 0.0f, 0.0f); + mRelativeComposingTextBounds.set(0.0f, 0.0f, 0.0f, 0.0f); + cancelLayoutInternalExpectedly("Resetting internal state."); + } + + /** + * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo()} is called. + * + *

CAVEAT: Currently the input method author is responsible for ignoring + * {@link InputMethodService#onUpdateCursorAnchorInfo()} called in full screen mode.

+ * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}. + */ + public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { + if (mIsFullScreenMode) { + // TODO: Consider to call InputConnection#requestCursorAnchorInfo to disable the + // event callback to suppress unnecessary event callbacks. + return; + } + mCursorAnchorInfoWrapper = info; + // Do not use layoutLater() to minimize the latency. + layoutImmediately(); + } + + private void hideIndicator() { + mUiOperator.hideUi(); + } + + private void cancelLayoutInternalUnexpectedly(final String message) { + hideIndicator(); + Log.d(TAG, message); + } + + private void cancelLayoutInternalExpectedly(final String message) { + hideIndicator(); + if (DEBUG) { + Log.d(TAG, message); + } + } + + private void layoutLater() { + mLayoutInvalidator.invalidateLayout(); + } + + + private void layoutImmediately() { + // Clear pending layout requests. + mLayoutInvalidator.cancelInvalidateLayout(); + layoutMain(); + } + + private void layoutMain() { + if (mIsFullScreenMode) { + cancelLayoutInternalUnexpectedly("Full screen mode isn't yet supported."); + return; + } + + if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { + if (mMode == MODE_NONE) { + cancelLayoutInternalExpectedly("Not ready for layouting."); + } else { + cancelLayoutInternalUnexpectedly("Unknown mMode=" + mMode); + } + return; + } + + final CursorAnchorInfoCompatWrapper info = mCursorAnchorInfoWrapper; + + if (info == null) { + cancelLayoutInternalExpectedly("CursorAnchorInfo isn't available."); + return; + } + + final Matrix matrix = info.getMatrix(); + if (matrix == null) { + cancelLayoutInternalUnexpectedly("Matrix is null"); + } + + final CharSequence composingText = info.getComposingText(); + if (mMode == MODE_COMMIT) { + if (composingText == null) { + cancelLayoutInternalExpectedly("composingText is null."); + return; + } + final int composingTextStart = info.getComposingTextStart(); + final int lastCharRectIndex = composingTextStart + composingText.length() - 1; + final RectF lastCharRect = info.getCharacterRect(lastCharRectIndex); + final int lastCharRectFlag = info.getCharacterRectFlags(lastCharRectIndex); + final int lastCharRectType = + lastCharRectFlag & CursorAnchorInfoCompatWrapper.CHARACTER_RECT_TYPE_MASK; + if (lastCharRect == null || matrix == null || lastCharRectType != + CursorAnchorInfoCompatWrapper.CHARACTER_RECT_TYPE_FULLY_VISIBLE) { + hideIndicator(); + return; + } + final RectF segmentStartCharRect = new RectF(lastCharRect); + for (int i = composingText.length() - 2; i >= 0; --i) { + final RectF charRect = info.getCharacterRect(composingTextStart + i); + if (charRect == null) { + break; + } + if (charRect.top != segmentStartCharRect.top) { + break; + } + if (charRect.bottom != segmentStartCharRect.bottom) { + break; + } + segmentStartCharRect.set(charRect); + } + + mLocalOrigin.set(lastCharRect.right, lastCharRect.top); + mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top, + lastCharRect.right + lastCharRect.height(), lastCharRect.bottom); + mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); + + mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top, + lastCharRect.right + lastCharRect.height(), lastCharRect.bottom); + mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); + + mRelativeComposingTextBounds.set(segmentStartCharRect.left, segmentStartCharRect.top, + segmentStartCharRect.right, segmentStartCharRect.bottom); + mRelativeComposingTextBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); + + if (mWaitingWord == null) { + cancelLayoutInternalExpectedly("mWaitingText is null."); + return; + } + if (TextUtils.isEmpty(mWaitingWord.mWord)) { + cancelLayoutInternalExpectedly("mWaitingText.mWord is empty."); + return; + } + if (!TextUtils.equals(composingText, mWaitingWord.mWord)) { + // This is indeed an expected situation because of the asynchronous nature of + // input method framework in Android. Note that composingText is notified from the + // application, while mWaitingWord.mWord is obtained directly from the InputLogic. + cancelLayoutInternalExpectedly( + "Composing text doesn't match the one we are waiting for."); + return; + } + } else { + if (!TextUtils.isEmpty(composingText)) { + // This is an unexpected case. + // TODO: Document this. + hideIndicator(); + return; + } + // In MODE_ADD_TO_DICTIONARY, we cannot retrieve the character position at all because + // of the lack of composing text. We will use the insertion marker position instead. + if (info.isInsertionMarkerClipped()) { + hideIndicator(); + return; + } + final float insertionMarkerHolizontal = info.getInsertionMarkerHorizontal(); + final float insertionMarkerTop = info.getInsertionMarkerTop(); + mLocalOrigin.set(insertionMarkerHolizontal, insertionMarkerTop); + } + + final RectF indicatorBounds = new RectF(mRelativeIndicatorBounds); + final RectF composingTextBounds = new RectF(mRelativeComposingTextBounds); + indicatorBounds.offset(mLocalOrigin.x, mLocalOrigin.y); + composingTextBounds.offset(mLocalOrigin.x, mLocalOrigin.y); + mUiOperator.layoutUi(mMode == MODE_COMMIT, matrix, indicatorBounds, composingTextBounds); + } + + private void onClickIndicator() { + if (mWaitingWord == null || TextUtils.isEmpty(mWaitingWord.mWord)) { + return; + } + switch (mMode) { + case MODE_COMMIT: + mListener.onClickComposingTextToCommit(mWaitingWord); + break; + case MODE_ADD_TO_DICTIONARY: + mListener.onClickComposingTextToAddToDictionary(mWaitingWord); + break; + } + } + + private final LayoutInvalidator mLayoutInvalidator = new LayoutInvalidator(this); + + /** + * Used for managing pending layout tasks for {@link TextDecorator#layoutLater()}. + */ + private static final class LayoutInvalidator { + private final HandlerImpl mHandler; + public LayoutInvalidator(final TextDecorator ownerInstance) { + mHandler = new HandlerImpl(ownerInstance); + } + + private static final int MSG_LAYOUT = 0; + + private static final class HandlerImpl + extends LeakGuardHandlerWrapper { + public HandlerImpl(final TextDecorator ownerInstance) { + super(ownerInstance); + } + + @Override + public void handleMessage(final Message msg) { + final TextDecorator owner = getOwnerInstance(); + if (owner == null) { + return; + } + switch (msg.what) { + case MSG_LAYOUT: + owner.layoutMain(); + break; + } + } + } + + /** + * Puts a layout task into the scheduler. Does nothing if one or more layout tasks are + * already scheduled. + */ + public void invalidateLayout() { + if (!mHandler.hasMessages(MSG_LAYOUT)) { + mHandler.obtainMessage(MSG_LAYOUT).sendToTarget(); + } + } + + /** + * Clears the pending layout tasks. + */ + public void cancelInvalidateLayout() { + mHandler.removeMessages(MSG_LAYOUT); + } + } + + private final static Listener EMPTY_LISTENER = new Listener() { + @Override + public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) { + } + @Override + public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) { + } + }; + + private final static TextDecoratorUiOperator EMPTY_UI_OPERATOR = new TextDecoratorUiOperator() { + @Override + public void disposeUi() { + } + @Override + public void hideUi() { + } + @Override + public void setOnClickListener(Runnable listener) { + } + @Override + public void layoutUi(boolean isCommitMode, Matrix matrix, RectF indicatorBounds, + RectF composingTextBounds) { + } + }; +} diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java new file mode 100644 index 000000000..6e215a9ca --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; +import android.inputmethodservice.InputMethodService; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; + +import com.android.inputmethod.latin.R; + +/** + * Used as the UI component of {@link TextDecorator}. + */ +public final class TextDecoratorUi implements TextDecoratorUiOperator { + private static final boolean VISUAL_DEBUG = false; + private static final int VISUAL_DEBUG_HIT_AREA_COLOR = 0x80ff8000; + + private final RelativeLayout mLocalRootView; + private final CommitIndicatorView mCommitIndicatorView; + private final AddToDictionaryIndicatorView mAddToDictionaryIndicatorView; + private final PopupWindow mTouchEventWindow; + private final View mTouchEventWindowClickListenerView; + private final float mHitAreaMarginInPixels; + + /** + * This constructor is designed to be called from {@link InputMethodService#setInputView(View)}. + * Other usages are not supported. + * + * @param context the context of the input method. + * @param inputView the view that is passed to {@link InputMethodService#setInputView(View)}. + */ + public TextDecoratorUi(final Context context, final View inputView) { + final Resources resources = context.getResources(); + final int hitAreaMarginInDP = resources.getInteger( + R.integer.text_decorator_hit_area_margin_in_dp); + mHitAreaMarginInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + hitAreaMarginInDP, resources.getDisplayMetrics()); + + mLocalRootView = new RelativeLayout(context); + mLocalRootView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + // TODO: Use #setBackground(null) for API Level >= 16. + mLocalRootView.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + + final ViewGroup contentView = getContentView(inputView); + mCommitIndicatorView = new CommitIndicatorView(context); + mAddToDictionaryIndicatorView = new AddToDictionaryIndicatorView(context); + mLocalRootView.addView(mCommitIndicatorView); + mLocalRootView.addView(mAddToDictionaryIndicatorView); + if (contentView != null) { + contentView.addView(mLocalRootView); + } + + // This popup window is used to avoid the limitation that the input method is not able to + // observe the touch events happening outside of InputMethodService.Insets#touchableRegion. + // We don't use this popup window for rendering the UI for performance reasons though. + mTouchEventWindow = new PopupWindow(context); + if (VISUAL_DEBUG) { + mTouchEventWindow.setBackgroundDrawable(new ColorDrawable(VISUAL_DEBUG_HIT_AREA_COLOR)); + } else { + mTouchEventWindow.setBackgroundDrawable(null); + } + mTouchEventWindowClickListenerView = new View(context); + mTouchEventWindow.setContentView(mTouchEventWindowClickListenerView); + } + + @Override + public void disposeUi() { + if (mLocalRootView != null) { + final ViewParent parent = mLocalRootView.getParent(); + if (parent != null && parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(mLocalRootView); + } + mLocalRootView.removeAllViews(); + } + if (mTouchEventWindow != null) { + mTouchEventWindow.dismiss(); + } + } + + @Override + public void hideUi() { + mCommitIndicatorView.setVisibility(View.GONE); + mAddToDictionaryIndicatorView.setVisibility(View.GONE); + mTouchEventWindow.dismiss(); + } + + @Override + public void layoutUi(final boolean isCommitMode, final Matrix matrix, + final RectF indicatorBounds, final RectF composingTextBounds) { + final RectF indicatorBoundsInScreenCoordinates = new RectF(); + matrix.mapRect(indicatorBoundsInScreenCoordinates, indicatorBounds); + mCommitIndicatorView.setBounds(indicatorBoundsInScreenCoordinates); + mAddToDictionaryIndicatorView.setBounds(indicatorBoundsInScreenCoordinates); + + final RectF hitAreaBounds = new RectF(composingTextBounds); + hitAreaBounds.union(indicatorBounds); + final RectF hitAreaBoundsInScreenCoordinates = new RectF(); + matrix.mapRect(hitAreaBoundsInScreenCoordinates, hitAreaBounds); + hitAreaBoundsInScreenCoordinates.inset(-mHitAreaMarginInPixels, -mHitAreaMarginInPixels); + + final int[] originScreen = new int[2]; + mLocalRootView.getLocationOnScreen(originScreen); + final int viewOriginX = originScreen[0]; + final int viewOriginY = originScreen[1]; + + final View toBeShown; + final View toBeHidden; + if (isCommitMode) { + toBeShown = mCommitIndicatorView; + toBeHidden = mAddToDictionaryIndicatorView; + } else { + toBeShown = mAddToDictionaryIndicatorView; + toBeHidden = mCommitIndicatorView; + } + toBeShown.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX); + toBeShown.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY); + toBeShown.setVisibility(View.VISIBLE); + toBeHidden.setVisibility(View.GONE); + + if (mTouchEventWindow.isShowing()) { + mTouchEventWindow.update((int)hitAreaBoundsInScreenCoordinates.left - viewOriginX, + (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY, + (int)hitAreaBoundsInScreenCoordinates.width(), + (int)hitAreaBoundsInScreenCoordinates.height()); + } else { + mTouchEventWindow.setWidth((int)hitAreaBoundsInScreenCoordinates.width()); + mTouchEventWindow.setHeight((int)hitAreaBoundsInScreenCoordinates.height()); + mTouchEventWindow.showAtLocation(mLocalRootView, Gravity.NO_GRAVITY, + (int)hitAreaBoundsInScreenCoordinates.left - viewOriginX, + (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY); + } + } + + @Override + public void setOnClickListener(final Runnable listener) { + mTouchEventWindowClickListenerView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View arg0) { + listener.run(); + } + }); + } + + private static class IndicatorView extends View { + private final Path mPath; + private final Path mTmpPath = new Path(); + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Matrix mMatrix = new Matrix(); + private final int mBackgroundColor; + private final int mForegroundColor; + private final RectF mBounds = new RectF(); + public IndicatorView(Context context, final int pathResourceId, + final int sizeResourceId, final int backgroundColorResourceId, + final int foregroundColroResourceId) { + super(context); + final Resources resources = context.getResources(); + mPath = createPath(resources, pathResourceId, sizeResourceId); + mBackgroundColor = resources.getColor(backgroundColorResourceId); + mForegroundColor = resources.getColor(foregroundColroResourceId); + } + + public void setBounds(final RectF rect) { + mBounds.set(rect); + } + + @Override + protected void onDraw(Canvas canvas) { + mPaint.setColor(mBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawRect(0.0f, 0.0f, mBounds.width(), mBounds.height(), mPaint); + + mMatrix.reset(); + mMatrix.postScale(mBounds.width(), mBounds.height()); + mPath.transform(mMatrix, mTmpPath); + mPaint.setColor(mForegroundColor); + canvas.drawPath(mTmpPath, mPaint); + } + + private static Path createPath(final Resources resources, final int pathResourceId, + final int sizeResourceId) { + final int size = resources.getInteger(sizeResourceId); + final float normalizationFactor = 1.0f / size; + final int[] array = resources.getIntArray(pathResourceId); + + final Path path = new Path(); + for (int i = 0; i < array.length; i += 2) { + if (i == 0) { + path.moveTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor); + } else { + path.lineTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor); + } + } + path.close(); + return path; + } + } + + private static ViewGroup getContentView(final View view) { + final View rootView = view.getRootView(); + if (rootView == null) { + return null; + } + + final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); + if (windowContentView == null) { + return null; + } + return windowContentView; + } + + private static final class CommitIndicatorView extends TextDecoratorUi.IndicatorView { + public CommitIndicatorView(final Context context) { + super(context, R.array.text_decorator_commit_indicator_path, + R.integer.text_decorator_commit_indicator_path_size, + R.color.text_decorator_commit_indicator_background_color, + R.color.text_decorator_commit_indicator_foreground_color); + } + } + + private static final class AddToDictionaryIndicatorView extends TextDecoratorUi.IndicatorView { + public AddToDictionaryIndicatorView(final Context context) { + super(context, R.array.text_decorator_add_to_dictionary_indicator_path, + R.integer.text_decorator_add_to_dictionary_indicator_path_size, + R.color.text_decorator_add_to_dictionary_indicator_background_color, + R.color.text_decorator_add_to_dictionary_indicator_foreground_color); + } + } +} \ No newline at end of file diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java new file mode 100644 index 000000000..f84e12d8c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard; + +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; + +/** + * This interface defines how UI operations required for {@link TextDecorator} are delegated to + * the actual UI implementation class. + */ +public interface TextDecoratorUiOperator { + /** + * Called to notify that the UI is ready to be disposed. + */ + void disposeUi(); + + /** + * Called when the UI should become invisible. + */ + void hideUi(); + + /** + * Called to set the new click handler. + * @param onClickListener the callback object whose {@link Runnable#run()} should be called when + * the indicator is clicked. + */ + void setOnClickListener(final Runnable onClickListener); + + /** + * Called when the layout should be updated. + * @param isCommitMode {@code true} if the commit indicator should be shown. Show the + * add-to-dictionary indicator otherwise. + * @param matrix The matrix that transforms the local coordinates into the screen coordinates. + * @param indicatorBounds The bounding box of the indicator, in local coordinates. + * @param composingTextBounds The bounding box of the composing text, in local coordinates. + */ + void layoutUi(final boolean isCommitMode, final Matrix matrix, final RectF indicatorBounds, + final RectF composingTextBounds); +} diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index aebc7101a..dcafd83a5 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -69,6 +69,7 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.keyboard.TextDecoratorUi; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.DebugFlags; @@ -183,8 +184,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; + private static final int MSG_SHOW_COMMIT_INDICATOR = 9; // Update this when adding new messages - private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD; + private static final int MSG_LAST = MSG_SHOW_COMMIT_INDICATOR; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; @@ -195,6 +197,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private int mDelayInMillisecondsToUpdateSuggestions; private int mDelayInMillisecondsToUpdateShiftState; + private int mDelayInMillisecondsToShowCommitIndicator; public UIHandler(final LatinIME ownerInstance) { super(ownerInstance); @@ -206,10 +209,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final Resources res = latinIme.getResources(); - mDelayInMillisecondsToUpdateSuggestions = - res.getInteger(R.integer.config_delay_in_milliseconds_to_update_suggestions); - mDelayInMillisecondsToUpdateShiftState = - res.getInteger(R.integer.config_delay_in_milliseconds_to_update_shift_state); + mDelayInMillisecondsToUpdateSuggestions = res.getInteger( + R.integer.config_delay_in_milliseconds_to_update_suggestions); + mDelayInMillisecondsToUpdateShiftState = res.getInteger( + R.integer.config_delay_in_milliseconds_to_update_shift_state); + mDelayInMillisecondsToShowCommitIndicator = res.getInteger( + R.integer.text_decorator_delay_in_milliseconds_to_show_commit_indicator); } @Override @@ -258,7 +263,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_RESET_CACHES: final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess( - msg.arg1 == 1 /* tryResumeSuggestions */, + msg.arg1 == ARG1_TRUE /* tryResumeSuggestions */, msg.arg2 /* remainingTries */, this /* handler */)) { // If we were able to reset the caches, then we can reload the keyboard. // Otherwise, we'll do it when we can. @@ -267,6 +272,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.getCurrentRecapitalizeState()); } break; + case MSG_SHOW_COMMIT_INDICATOR: + // Protocol of MSG_SET_COMMIT_INDICATOR_ENABLED: + // - what: MSG_SHOW_COMMIT_INDICATOR + // - arg1: not used. + // - arg2: not used. + // - obj: the Runnable object to be called back. + ((Runnable) msg.obj).run(); + break; case MSG_WAIT_FOR_DICTIONARY_LOAD: Log.i(TAG, "Timeout waiting for dictionary load"); break; @@ -367,6 +380,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } + /** + * Posts a delayed task to show the commit indicator. + * + *

Only one task can exist in the queue. When this method is called, any prior task that + * has not yet fired will be canceled.

+ * @param task the runnable object that will be fired when the delayed task is dispatched. + */ + public void postShowCommitIndicatorTask(final Runnable task) { + removeMessages(MSG_SHOW_COMMIT_INDICATOR); + sendMessageDelayed(obtainMessage(MSG_SHOW_COMMIT_INDICATOR, task), + mDelayInMillisecondsToShowCommitIndicator); + } + // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; @@ -717,6 +743,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); } + mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); } @Override @@ -972,9 +999,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // @Override public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - final CursorAnchorInfoCompatWrapper wrapper = - CursorAnchorInfoCompatWrapper.fromObject(info); - // TODO: Implement here + mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } } @@ -1178,6 +1203,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // In fullscreen mode, no need to have extra space to show the key preview. // If not, we should have extra space above the keyboard to show the key preview. mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); + mInputLogic.onUpdateFullscreenMode(isFullscreenMode()); } private int getCurrentAutoCapsState() { @@ -1221,6 +1247,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen wordToEdit = word; } mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit); + mInputLogic.onAddWordToUserDictionary(); } // Callback for the {@link SuggestionStripView}, to call when the important notice strip is @@ -1409,7 +1436,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void setSuggestedWords(final SuggestedWords suggestedWords) { - mInputLogic.setSuggestedWords(suggestedWords); + final SettingsValues currentSettingsValues = mSettings.getCurrent(); + mInputLogic.setSuggestedWords(suggestedWords, currentSettingsValues, mHandler); // TODO: Modify this when we support suggestions with hard keyboard if (!hasSuggestionStripView()) { return; @@ -1418,7 +1446,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } - final SettingsValues currentSettingsValues = mSettings.getCurrent(); final boolean shouldShowImportantNotice = ImportantNoticeUtils.shouldShowImportantNotice(this); final boolean shouldShowSuggestionCandidates = diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index a6978809a..0f2ba53d5 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.inputlogic; import android.graphics.Color; +import android.inputmethodservice.InputMethodService; import android.os.SystemClock; import android.text.SpannableString; import android.text.TextUtils; @@ -27,11 +28,14 @@ import android.view.KeyEvent; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; +import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.InputTransaction; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.keyboard.TextDecorator; +import com.android.inputmethod.keyboard.TextDecoratorUiOperator; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; @@ -81,6 +85,18 @@ public final class InputLogic { public final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; + private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { + @Override + public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) { + mLatinIME.pickSuggestionManually(wordInfo); + } + @Override + public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) { + mLatinIME.addWordToUserDictionary(wordInfo.mWord); + mLatinIME.dismissAddToDictionaryHint(); + } + }); + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // This has package visibility so it can be accessed from InputLogicHandler. /* package */ final WordComposer mWordComposer; @@ -303,8 +319,18 @@ public final class InputLogic { return inputTransaction; } - commitChosenWord(settingsValues, suggestion, - LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); + final boolean shouldShowAddToDictionaryIndicator = + shouldShowAddToDictionaryHint && settingsValues.mShouldShowUiToAcceptTypedWord; + final int backgroundColor; + if (shouldShowAddToDictionaryIndicator) { + backgroundColor = settingsValues.mTextHighlightColorForAddToDictionaryIndicator; + } else { + backgroundColor = Color.TRANSPARENT; + } + commitChosenWordWithBackgroundColor(settingsValues, suggestion, + LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR, + backgroundColor); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -312,13 +338,16 @@ public final class InputLogic { mSpaceState = SpaceState.PHANTOM; inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (shouldShowAddToDictionaryHint(suggestionInfo)) { + if (shouldShowAddToDictionaryHint) { mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); } else { // If we're not showing the "Touch again to save", then update the suggestion strip. // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); } + if (shouldShowAddToDictionaryIndicator) { + mTextDecorator.showAddToDictionaryIndicator(suggestionInfo); + } return inputTransaction; } @@ -386,6 +415,8 @@ public final class InputLogic { // The cursor has been moved : we now accept to perform recapitalization mRecapitalizeStatus.enable(); + // We moved the cursor and need to invalidate the indicator right now. + mTextDecorator.reset(); // We moved the cursor. If we are touching a word, we need to resume suggestion. mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, true /* shouldDelay */); @@ -561,7 +592,8 @@ public final class InputLogic { // TODO: on the long term, this method should become private, but it will be difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? - public void setSuggestedWords(final SuggestedWords suggestedWords) { + public void setSuggestedWords(final SuggestedWords suggestedWords, + final SettingsValues settingsValues, final LatinIME.UIHandler handler) { if (SuggestedWords.EMPTY != suggestedWords) { final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { @@ -575,6 +607,38 @@ public final class InputLogic { } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + if (shouldShowCommitIndicator(suggestedWords, settingsValues)) { + // typedWordInfo is never null here. + final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull(); + handler.postShowCommitIndicatorTask(new Runnable() { + @Override + public void run() { + // TODO: This needs to be refactored to ensure that mWordComposer is accessed + // only from the UI thread. + if (!mWordComposer.isComposingWord()) { + mTextDecorator.reset(); + return; + } + final SuggestedWordInfo currentTypedWordInfo = + mSuggestedWords.getTypedWordInfoOrNull(); + if (currentTypedWordInfo == null) { + mTextDecorator.reset(); + return; + } + if (!currentTypedWordInfo.equals(typedWordInfo)) { + // Suggested word has been changed. This task is obsolete. + mTextDecorator.reset(); + return; + } + mTextDecorator.showCommitIndicator(typedWordInfo); + } + }); + } else { + // Note: It is OK to not cancel previous postShowCommitIndicatorTask() here. Having a + // cancellation mechanism could improve performance a bit though. + mTextDecorator.reset(); + } + // Put a blue underline to a word in TextView which will be auto-corrected. if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator && mWordComposer.isComposingWord()) { @@ -756,6 +820,8 @@ public final class InputLogic { if (!mWordComposer.isComposingWord() && mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + mTextDecorator.reset(); } final int codePoint = event.mCodePoint; @@ -2108,4 +2174,74 @@ public final class InputLogic { settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Following methods are tentatively placed in this class for the integration with + // TextDecorator. + // TODO: Decouple things that are not related to the input logic. + ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Sets the UI operator for {@link TextDecorator}. + * @param uiOperator the UI operator which should be associated with {@link TextDecorator}. + */ + public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) { + mTextDecorator.setUiOperator(uiOperator); + } + + /** + * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo} is called. + * @param info The wrapper object with which we can access cursor/anchor info. + */ + public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { + mTextDecorator.onUpdateCursorAnchorInfo(info); + } + + /** + * Must be called when {@link InputMethodService#updateFullscreenMode} is called. + * @param isFullscreen {@code true} if the input method is in full-screen mode. + */ + public void onUpdateFullscreenMode(final boolean isFullscreen) { + mTextDecorator.notifyFullScreenMode(isFullscreen); + } + + /** + * Must be called from {@link LatinIME#addWordToUserDictionary(String)}. + */ + public void onAddWordToUserDictionary() { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + mTextDecorator.reset(); + } + + /** + * Returns whether the commit indicator should be shown or not. + * @param suggestedWords the suggested word that is being displayed. + * @param settingsValues the current settings value. + * @return {@code true} if the commit indicator should be shown. + */ + private boolean shouldShowCommitIndicator(final SuggestedWords suggestedWords, + final SettingsValues settingsValues) { + if (!settingsValues.mShouldShowUiToAcceptTypedWord) { + return false; + } + final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull(); + if (typedWordInfo == null) { + return false; + } + if (suggestedWords.mInputStyle != SuggestedWords.INPUT_STYLE_TYPING){ + return false; + } + if (settingsValues.mShowCommitIndicatorOnlyForAutoCorrection + && !suggestedWords.mWillAutoCorrect) { + return false; + } + // TODO: Calling shouldShowAddToDictionaryHint(typedWordInfo) multiple times should be fine + // in terms of performance, but we can do better. One idea is to make SuggestedWords include + // a boolean that tells whether the word is a dictionary word or not. + if (settingsValues.mShowCommitIndicatorOnlyForOutOfVocabulary + && !shouldShowAddToDictionaryHint(typedWordInfo)) { + return false; + } + return true; + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 1cd7b391a..dc2eda9bc 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -95,6 +95,12 @@ public final class SettingsValues { public final int[] mAdditionalFeaturesSettingValues = new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; + // TextDecorator + public final int mTextHighlightColorForCommitIndicator; + public final int mTextHighlightColorForAddToDictionaryIndicator; + public final boolean mShowCommitIndicatorOnlyForAutoCorrection; + public final boolean mShowCommitIndicatorOnlyForOutOfVocabulary; + // Debug settings public final boolean mIsInternal; public final int mKeyPreviewShowUpDuration; @@ -163,6 +169,14 @@ public final class SettingsValues { mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( prefs, mAdditionalFeaturesSettingValues); + mShowCommitIndicatorOnlyForAutoCorrection = res.getBoolean( + R.bool.text_decorator_only_for_auto_correction); + mShowCommitIndicatorOnlyForOutOfVocabulary = res.getBoolean( + R.bool.text_decorator_only_for_out_of_vocabulary); + mTextHighlightColorForCommitIndicator = res.getColor( + R.color.text_decorator_commit_indicator_text_highlight_color); + mTextHighlightColorForAddToDictionaryIndicator = res.getColor( + R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color); mIsInternal = Settings.isInternal(prefs); mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration( prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION, @@ -396,6 +410,14 @@ public final class SettingsValues { sb.append("" + (null == awu ? "null" : awu.toString())); sb.append("\n mAdditionalFeaturesSettingValues = "); sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); + sb.append("\n mShowCommitIndicatorOnlyForAutoCorrection = "); + sb.append("" + mShowCommitIndicatorOnlyForAutoCorrection); + sb.append("\n mShowCommitIndicatorOnlyForOutOfVocabulary = "); + sb.append("" + mShowCommitIndicatorOnlyForOutOfVocabulary); + sb.append("\n mTextHighlightColorForCommitIndicator = "); + sb.append("" + mTextHighlightColorForCommitIndicator); + sb.append("\n mTextHighlightColorForAddToDictionaryIndicator = "); + sb.append("" + mTextHighlightColorForAddToDictionaryIndicator); sb.append("\n mIsInternal = "); sb.append("" + mIsInternal); sb.append("\n mKeyPreviewShowUpDuration = ");