diff options
Diffstat (limited to 'java/src')
17 files changed, 1223 insertions, 159 deletions
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index c07997bc9..c33c01552 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -68,7 +68,7 @@ public final class SuggestionSpanUtils { public static CharSequence getTextWithSuggestionSpan(final Context context, final String pickedWord, final SuggestedWords suggestedWords) { if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() - || suggestedWords.mIsPrediction || suggestedWords.isPunctuationSuggestions()) { + || suggestedWords.isPrediction() || suggestedWords.isPunctuationSuggestions()) { return pickedWord; } diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index f00889ed7..3743d26e6 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -93,13 +93,13 @@ public class Key implements Comparable<Key> { /** Icon to display instead of a label. Icon takes precedence over a label */ private final int mIconId; - /** Width of the key, not including the gap */ + /** Width of the key, excluding the gap */ private final int mWidth; - /** Height of the key, not including the gap */ + /** Height of the key, excluding the gap */ private final int mHeight; - /** X coordinate of the key in the keyboard layout */ + /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mX; - /** Y coordinate of the key in the keyboard layout */ + /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mY; /** Hit bounding box of the key */ private final Rect mHitBox = new Rect(); @@ -736,18 +736,34 @@ public class Key implements Comparable<Key> { return iconSet.getIconDrawable(getIconId()); } + /** + * Gets the width of the key in pixels, excluding the gap. + * @return The width of the key in pixels, excluding the gap. + */ public int getWidth() { return mWidth; } + /** + * Gets the height of the key in pixels, excluding the gap. + * @return The height of the key in pixels, excluding the gap. + */ public int getHeight() { return mHeight; } + /** + * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getX() { return mX; } + /** + * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getY() { return mY; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 77cdf49fb..30bd1dfda 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -28,6 +28,7 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; +import com.android.inputmethod.event.Event; import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; import com.android.inputmethod.keyboard.emoji.EmojiPalettesView; import com.android.inputmethod.keyboard.internal.KeyboardState; @@ -311,9 +312,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { /** * Updates state machine to figure out when to automatically switch back to the previous mode. */ - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { - mState.onCodeInput(code, currentAutoCapsState, currentRecapitalizeState); + mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState); } public boolean isShowingEmojiPalettes() { @@ -359,7 +360,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { R.layout.input_view, null); mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame); mEmojiPalettesView = (EmojiPalettesView)mCurrentInputView.findViewById( - R.id.emoji_keyboard_view); + R.id.emoji_palettes_view); mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled); 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..178516134 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -0,0 +1,444 @@ +/* + * 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. + * + * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and + * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call + * {@link #reset()} to hide the indicator.</p> + * + * @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. + * + * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and + * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call + * {@link #reset()} to hide the indicator.</p> + * + * @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. + mUiOperator.hideUi(); + } + 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. + * + * <p>CAVEAT: Currently the input method author is responsible for ignoring + * {@link InputMethodService#onUpdateCursorAnchorInfo()} called in full screen mode.</p> + * @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(); + } + + /** + * Hides indicator if the new composing text doesn't match the expected one. + * + * <p>Calling this method is optional but recommended whenever the new composition is passed to + * the application. The motivation of this method is to reduce the UI latency. With this method, + * we can hide the indicator without waiting the arrival of the + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} callback, assuming that + * the application accepts the new composing text without any modification. Even if this + * assumption is false, the indicator will be shown again when + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is actually received. + * </p> + * + * @param newComposingText the new composing text that is being passed to the application. + */ + public void hideIndicatorIfNecessary(final CharSequence newComposingText) { + if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { + return; + } + if (!TextUtils.equals(newComposingText, mWaitingWord.mWord)) { + mUiOperator.hideUi(); + } + } + + private void cancelLayoutInternalUnexpectedly(final String message) { + mUiOperator.hideUi(); + Log.d(TAG, message); + } + + private void cancelLayoutInternalExpectedly(final String message) { + mUiOperator.hideUi(); + 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) { + mUiOperator.hideUi(); + 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. + mUiOperator.hideUi(); + 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()) { + mUiOperator.hideUi(); + 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<TextDecorator> { + 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/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index b98ced97c..5f4d55bdb 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -19,6 +19,7 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.event.Event; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.RecapitalizeStatus; @@ -29,7 +30,7 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus; * * The input events are {@link #onLoadKeyboard(int, int)}, {@link #onSaveKeyboardState()}, * {@link #onPressKey(int,boolean,int,int)}, {@link #onReleaseKey(int,boolean,int,int)}, - * {@link #onCodeInput(int,int,int)}, {@link #onFinishSlidingInput(int,int)}, + * {@link #onEvent(Event,int,int)}, {@link #onFinishSlidingInput(int,int)}, * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet(int,int)}. * * The actions are {@link SwitchActions}'s methods. @@ -610,10 +611,11 @@ public final class KeyboardState { return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER; } - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { + final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint; if (DEBUG_EVENT) { - Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code) + Log.d(TAG, "onEvent: code=" + Constants.printableCode(code) + " autoCaps=" + currentAutoCapsState + " " + this); } diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 790e0d830..d57a881c0 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -145,6 +145,12 @@ public final class InputPointers { return mPointerIds.getPrimitiveArray(); } + /** + * Gets the time each point was registered, in milliseconds, relative to the first event in the + * sequence. + * @return The time each point was registered, in milliseconds, relative to the first event in + * the sequence. + */ public int[] getTimes() { if (DebugFlags.DEBUG_ENABLED || DEBUG_TIME) { if (!isValidTimeStamps()) { diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java index e9e12f09f..7fa935413 100644 --- a/java/src/com/android/inputmethod/latin/InputView.java +++ b/java/src/com/android/inputmethod/latin/InputView.java @@ -21,14 +21,14 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.LinearLayout; +import android.widget.FrameLayout; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.suggestions.MoreSuggestionsView; import com.android.inputmethod.latin.suggestions.SuggestionStripView; -public final class InputView extends LinearLayout { +public final class InputView extends FrameLayout { private final Rect mInputViewRect = new Rect(); private MainKeyboardView mMainKeyboardView; private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder; diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 87b34a99c..ee0ff5c0a 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -29,7 +29,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Rect; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -43,6 +42,7 @@ import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.SparseArray; +import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; @@ -57,7 +57,6 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; -import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.event.Event; @@ -69,6 +68,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; @@ -94,6 +94,7 @@ import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -150,8 +151,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; - private View mExtractArea; - private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; private RichInputMethodManager mRichImm; @@ -183,8 +182,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 +195,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 +207,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 +261,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 +270,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 +378,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. + * + * <p>Only one task can exist in the queue. When this method is called, any prior task that + * has not yet fired will be canceled.</p> + * @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; @@ -710,13 +734,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void setInputView(final View view) { super.setInputView(view); mInputView = view; - mExtractArea = getWindow().getWindow().getDecorView() - .findViewById(android.R.id.extractArea); - mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); } + mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); } @Override @@ -751,20 +773,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // is not guaranteed. It may even be called at the same time on a different thread. final RichInputMethodSubtype richSubtype = new RichInputMethodSubtype(subtype); mSubtypeSwitcher.onSubtypeChanged(richSubtype); - mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype)); + mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), + mSettings.getCurrent()); loadKeyboard(); } private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - // AcceptTypedWord feature relies on CursorAnchorInfo. - if (mSettings.getCurrent().mShouldShowUiToAcceptTypedWord) { - InputConnectionCompatUtils.requestUpdateCursorAnchorInfo( - getCurrentInputConnection(), true /* enableMonitor */, - true /* requestImmediateCallback */); - } - } } @SuppressWarnings("deprecation") @@ -833,7 +848,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // span, so we should reset our state unconditionally, even if restarting is true. // We also tell the input logic about the combining rules for the current subtype, so // it can adjust its combiners if needed. - mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype()); + mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(), + currentSettingsValues); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); @@ -973,9 +989,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)); } } @@ -1052,42 +1066,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen applicationSpecifiedCompletions); final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, - false /* isObsoleteSuggestions */, false /* isPrediction */, + false /* isObsoleteSuggestions */, SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */); // When in fullscreen mode, show completions generated by the application forcibly setSuggestedWords(suggestedWords); } - private int getAdjustedBackingViewHeight() { - final int currentHeight = mKeyPreviewBackingView.getHeight(); - if (currentHeight > 0) { - return currentHeight; - } - - final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); - if (visibleKeyboardView == null) { - return 0; - } - // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!! - // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final int keyboardHeight = visibleKeyboardView.getHeight(); - final int suggestionsHeight = mSuggestionStripView.getHeight(); - final int displayHeight = getResources().getDisplayMetrics().heightPixels; - final Rect rect = new Rect(); - mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); - final int notificationBarHeight = rect.top; - final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight - - keyboardHeight; - - final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); - mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); - - // Let the backing cover the remaining region entirely. - params.height = remainingHeight; - mKeyPreviewBackingView.setLayoutParams(params); - return params.height; - } - @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); @@ -1095,40 +1079,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } + final int inputHeight = mInputView.getHeight(); final boolean hasHardwareKeyboard = mKeyboardSwitcher.hasHardwareKeyboard(); if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. - outInsets.touchableInsets = mInputView.getHeight(); - outInsets.visibleTopInsets = mInputView.getHeight(); + outInsets.touchableInsets = inputHeight; + outInsets.visibleTopInsets = inputHeight; return; } - final int adjustedBackingHeight = getAdjustedBackingViewHeight(); - final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); - final int backingHeight = backingGone ? 0 : adjustedBackingHeight; - // In fullscreen mode, the height of the extract area managed by InputMethodService should - // be considered. - // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. - final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; - final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0 - : mSuggestionStripView.getHeight(); - final int extraHeight = extractHeight + backingHeight + suggestionsHeight; - int visibleTopY = extraHeight; - // Need to set touchable region only if input view is being shown + final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes() + && mSuggestionStripView.getVisibility() == View.VISIBLE) + ? mSuggestionStripView.getHeight() : 0; + final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - suggestionsHeight; + mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY); + // Need to set touchable region only if a keyboard view is being shown. if (visibleKeyboardView.isShown()) { - // Note that the height of Emoji layout is the same as the height of the main keyboard - // and the suggestion strip - if (mKeyboardSwitcher.isShowingEmojiPalettes() - || mSuggestionStripView.getVisibility() == View.VISIBLE) { - visibleTopY -= suggestionsHeight; - } - final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; - final int touchWidth = visibleKeyboardView.getWidth(); - final int touchHeight = visibleKeyboardView.getHeight() + extraHeight + final int touchLeft = 0; + final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; + final int touchRight = visibleKeyboardView.getWidth(); + final int touchBottom = inputHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; - outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); + outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; @@ -1173,12 +1147,28 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void updateFullscreenMode() { + // Override layout parameters to expand {@link SoftInputWindow} to the entire screen. + // See {@link InputMethodService#setinputView(View) and + // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}. + final Window window = getWindow().getWindow(); + ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT); + // This method may be called before {@link #setInputView(View)}. + if (mInputView != null) { + // In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to + // the entire screen and be placed at the bottom of {@link SoftInputWindow}. + // In fullscreen mode, these shouldn't expand to the entire screen and should be + // coexistent with {@link #mExtractedArea} above. + // See {@link InputMethodService#setInputView(View) and + // com.android.internal.R.layout.input_method.xml. + final int layoutHeight = isFullscreenMode() + ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; + final View inputArea = window.findViewById(android.R.id.inputArea); + ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight); + ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); + ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight); + } super.updateFullscreenMode(); - - if (mKeyPreviewBackingView == null) return; - // 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() { @@ -1216,6 +1206,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } mDictionaryFacilitator.addWordToUserDictionary(this /* context */, word); + mInputLogic.onAddWordToUserDictionary(); } // Callback for the {@link SuggestionStripView}, to call when the important notice strip is @@ -1297,7 +1288,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // code) needs the coordinates in the keyboard frame. // TODO: We should reconsider which coordinate system should be used to represent // keyboard event. Also we should pull this up -- LatinIME has no business doing - // this transformation, it should be done already before calling onCodeInput. + // this transformation, it should be done already before calling onEvent. final int keyX = mainKeyboardView.getKeyX(x); final int keyY = mainKeyboardView.getKeyY(y); final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(codePoint), @@ -1308,7 +1299,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // This method is public for testability of LatinIME, but also in the future it should // completely replace #onCodeInput. public void onEvent(final Event event) { - if (Constants.CODE_SHORTCUT == event.mCodePoint) { + if (Constants.CODE_SHORTCUT == event.mKeyCode) { mSubtypeSwitcher.switchToShortcutIME(this); } final InputTransaction completeInputTransaction = @@ -1316,8 +1307,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.getKeyboardShiftMode(), mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(event.mCodePoint, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // A helper method to split the code point and the key code. Ultimately, they should not be @@ -1341,13 +1331,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onTextInput(final String rawText) { // TODO: have the keyboard pass the correct key code when we need it. - final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE); + final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT); final InputTransaction completeInputTransaction = mInputLogic.onTextInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } @Override @@ -1414,7 +1403,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; @@ -1423,7 +1413,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/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java index 6b0205c0f..56014cbad 100644 --- a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java +++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java @@ -35,7 +35,6 @@ public final class PunctuationSuggestions extends SuggestedWords { false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, false /* isObsoleteSuggestions */, - false /* isPrediction */, INPUT_STYLE_NONE /* inputStyle */); } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 035557610..497823aeb 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -30,7 +30,9 @@ import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; @@ -906,4 +908,33 @@ public final class RichInputConnection { mIC.setSelection(mExpectedSelStart, mExpectedSelEnd); } } + + private boolean mCursorAnchorInfoMonitorEnabled = false; + + /** + * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. + * @param enableMonitor {@code true} to request the editor to call back the method whenever the + * cursor/anchor position is changed. + * @param requestImmediateCallback {@code true} to request the editor to call back the method + * as soon as possible to notify the current cursor/anchor position to the input method. + * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which + * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which + * prevents the application from fulfilling the request. (TODO: Improve the API when it turns + * out that we actually need more detailed error codes) + */ + public boolean requestUpdateCursorAnchorInfo(final boolean enableMonitor, + final boolean requestImmediateCallback) { + final boolean scheduled = InputConnectionCompatUtils.requestUpdateCursorAnchorInfo(mIC, + enableMonitor, requestImmediateCallback); + mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor); + return scheduled; + } + + /** + * @return {@code true} if the application reported that the monitor mode of + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled. + */ + public boolean isCursorAnchorInfoMonitorEnabled() { + return mCursorAnchorInfoMonitorEnabled; + } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index ab852f8dd..6779351fd 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -120,9 +120,9 @@ public final class Suggest { // and calls the callback function with the suggestions. private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyle, - final boolean isCorrectionEnabled, final int sequenceNumber, - final OnGetSuggestedWordsCallback callback) { + final SettingsValuesForSuggestion settingsValuesForSuggestion, + final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, + final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final String typedWord = wordComposer.getTypedWord(); final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord); final String consideredWord = trailingSingleQuotesCount > 0 @@ -186,6 +186,8 @@ public final class Suggest { suggestionsList = suggestionsContainer; } + final int inputStyle = resultsArePredictions ? SuggestedWords.INPUT_STYLE_PREDICTION : + inputStyleIfNotPrediction; callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, // TODO: this first argument is lying. If this is a whitelisted word which is an @@ -193,8 +195,7 @@ public final class Suggest { // rename the attribute or change the value. !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */, hasAutoCorrection /* willAutoCorrect */, - false /* isObsoleteSuggestions */, resultsArePredictions, - inputStyle, sequenceNumber)); + false /* isObsoleteSuggestions */, inputStyle, sequenceNumber)); } // Retrieves suggestions for the batch input @@ -244,7 +245,6 @@ public final class Suggest { true /* typedWordValid */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, - false /* isPrediction */, inputStyle, sequenceNumber)); } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 38fcb683d..1eebabece 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -38,14 +38,15 @@ public class SuggestedWords { public static final int INPUT_STYLE_TAIL_BATCH = 3; public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4; public static final int INPUT_STYLE_RECORRECTION = 5; + public static final int INPUT_STYLE_PREDICTION = 6; // The maximum number of suggestions available. public static final int MAX_SUGGESTIONS = 18; private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0); public static final SuggestedWords EMPTY = new SuggestedWords( - EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false, false, false, false, - INPUT_STYLE_NONE); + EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE); public final String mTypedWord; public final boolean mTypedWordValid; @@ -54,7 +55,6 @@ public class SuggestedWords { // whether this exactly matches the user entry or not. public final boolean mWillAutoCorrect; public final boolean mIsObsoleteSuggestions; - public final boolean mIsPrediction; // How the input for these suggested words was done by the user. Must be one of the // INPUT_STYLE_* constants above. public final int mInputStyle; @@ -67,10 +67,9 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle) { this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, - isObsoleteSuggestions, isPrediction, inputStyle, NOT_A_SEQUENCE_NUMBER); + isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, @@ -78,13 +77,12 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle, final int sequenceNumber) { this(suggestedWordInfoList, rawSuggestions, - (suggestedWordInfoList.isEmpty() || isPrediction) ? null + (suggestedWordInfoList.isEmpty() || INPUT_STYLE_PREDICTION == inputStyle) ? null : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, - typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, inputStyle, + typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber); } @@ -94,7 +92,6 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle, final int sequenceNumber) { mSuggestedWordInfoList = suggestedWordInfoList; @@ -102,7 +99,6 @@ public class SuggestedWords { mTypedWordValid = typedWordValid; mWillAutoCorrect = willAutoCorrect; mIsObsoleteSuggestions = isObsoleteSuggestions; - mIsPrediction = isPrediction; mInputStyle = inputStyle; mSequenceNumber = sequenceNumber; mTypedWord = typedWord; @@ -381,9 +377,14 @@ public class SuggestedWords { } } + public boolean isPrediction() { + return INPUT_STYLE_PREDICTION == mInputStyle; + } + // SuggestedWords is an immutable object, as much as possible. We must not just remove // words from the member ArrayList as some other parties may expect the object to never change. - public SuggestedWords getSuggestedWordsExcludingTypedWord(final int inputStyle) { + // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker. + public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() { final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); String typedWord = null; for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { @@ -399,7 +400,7 @@ public class SuggestedWords { // no auto-correction should take place hence willAutoCorrect = false. return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord, true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, - mIsPrediction, inputStyle, NOT_A_SEQUENCE_NUMBER); + SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER); } // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the @@ -418,8 +419,7 @@ public class SuggestedWords { SuggestedWordInfo.NOT_A_CONFIDENCE)); } return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, - mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction, - INPUT_STYLE_TAIL_BATCH); + mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH); } /** diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 616828efe..4b5edb076 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -17,9 +17,12 @@ 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.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; import android.text.style.SuggestionSpan; import android.util.Log; import android.view.KeyCharacterMap; @@ -27,11 +30,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; @@ -46,6 +52,7 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.define.DebugFlags; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -81,6 +88,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; @@ -124,8 +143,9 @@ public final class InputLogic { * Call this when input starts or restarts in some editor (typically, in onStartInputView). * * @param combiningSpec the combining spec string for this subtype + * @param settingsValues the current settings values */ - public void startInput(final String combiningSpec) { + public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordComposer.restartCombining(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); @@ -143,15 +163,24 @@ public final class InputLogic { } else { mInputLogicHandler.reset(); } + + if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { + // AcceptTypedWord feature relies on CursorAnchorInfo. + if (settingsValues.mShouldShowUiToAcceptTypedWord) { + mConnection.requestUpdateCursorAnchorInfo(true /* enableMonitor */, + true /* requestImmediateCallback */); + } + } } /** * Call this when the subtype changes. * @param combiningSpec the spec string for the combining rules + * @param settingsValues the current settings values */ - public void onSubtypeChanged(final String combiningSpec) { + public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) { finishInput(); - startInput(combiningSpec); + startInput(combiningSpec, settingsValues); } /** @@ -303,8 +332,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 +351,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 +428,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 +605,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 +620,43 @@ public final class InputLogic { } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + if (shouldShowCommitIndicator(suggestedWords, settingsValues)) { + // typedWordInfo is never null here. + final int textBackgroundColor = settingsValues.mTextHighlightColorForCommitIndicator; + 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; + } + // TODO: As with the above TODO comment, this operation must be performed only + // on the UI thread too. Needs to be refactored. + setComposingTextInternalWithBackgroundColor(typedWordInfo.mWord, + 1 /* newCursorPosition */, textBackgroundColor); + 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()) { @@ -585,7 +667,7 @@ public final class InputLogic { // message, this is called outside any batch edit. Potentially, this may result in some // janky flickering of the screen, although the display speed makes it unlikely in // the practice. - mConnection.setComposingText(textWithUnderline, 1); + setComposingTextInternal(textWithUnderline, 1); } } @@ -608,7 +690,7 @@ public final class InputLogic { inputTransaction.setDidAffectContents(); } if (mWordComposer.isComposingWord()) { - mConnection.setComposingText(mWordComposer.getTypedWord(), 1); + setComposingTextInternal(mWordComposer.getTypedWord(), 1); inputTransaction.setDidAffectContents(); inputTransaction.setRequiresUpdateSuggestions(); } @@ -638,13 +720,13 @@ public final class InputLogic { case Constants.CODE_SHIFT: performRecapitalization(inputTransaction.mSettingsValues); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (mSuggestedWords.mIsPrediction) { + if (mSuggestedWords.isPrediction()) { inputTransaction.setRequiresUpdateSuggestions(); } break; case Constants.CODE_CAPSLOCK: // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. + // {@link KeyboardSwitcher#onEvent(Event)}. break; case Constants.CODE_SYMBOL_SHIFT: // Note: Calling back to the keyboard on the symbol Shift key is handled in @@ -672,11 +754,11 @@ public final class InputLogic { break; case Constants.CODE_EMOJI: // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. + // {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_ALPHA_FROM_EMOJI: // Note: Switching back from Emoji keyboard to the main keyboard is being - // handled in {@link KeyboardState#onCodeInput(int,int)}. + // handled in {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_SHIFT_ENTER: // TODO: remove this object @@ -756,6 +838,8 @@ public final class InputLogic { if (!mWordComposer.isComposingWord() && mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + mTextDecorator.reset(); } final int codePoint = event.mCodePoint; @@ -842,8 +926,7 @@ public final class InputLogic { if (mWordComposer.isSingleLetter()) { mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState); } - mConnection.setComposingText(getTextWithUnderline( - mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, inputTransaction); @@ -1006,7 +1089,7 @@ public final class InputLogic { mWordComposer.applyProcessedEvent(event); } if (mWordComposer.isComposingWord()) { - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { mConnection.commitText("", 1); } @@ -1466,11 +1549,10 @@ public final class InputLogic { && !shouldIncludeResumedWordInSuggestions) { // We were able to compute new suggestions for this word. // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWord() method sets - // willAutoCorrect to false. + // case. The #getSuggestedWordsExcludingTypedWordForRecorrection() + // method sets willAutoCorrect to false. suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWord(SuggestedWords - .INPUT_STYLE_RECORRECTION); + .getSuggestedWordsExcludingTypedWordForRecorrection(); } else { // No saved suggestions, and we were unable to compute any good one // either. Rather than displaying an empty suggestion strip, we'll @@ -1487,11 +1569,9 @@ public final class InputLogic { // color of the word in the suggestion strip changes according to this parameter, // and false gives the correct color. final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - null /* rawSuggestions */, typedWord, - false /* typedWordValid */, false /* willAutoCorrect */, - false /* isObsoleteSuggestions */, false /* isPrediction */, - SuggestedWords.INPUT_STYLE_RECORRECTION, - SuggestedWords.NOT_A_SEQUENCE_NUMBER); + null /* rawSuggestions */, typedWord, false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER); mIsAutoCorrectionIndicatorOn = false; mLatinIME.mHandler.showSuggestionStrip(suggestedWords); } @@ -1577,7 +1657,7 @@ public final class InputLogic { final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); - mConnection.setComposingText(textToCommit, 1); + setComposingTextInternal(textToCommit, 1); } // Don't restart suggestion yet. We'll restart if the user deletes the separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; @@ -1787,8 +1867,7 @@ public final class InputLogic { SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, - true /* isObsoleteSuggestions */, false /* isPrediction */, - oldSuggestedWords.mInputStyle); + true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle); } /** @@ -1911,10 +1990,10 @@ public final class InputLogic { } final String lastWord = batchInputText.substring(indexOfLastSpace); mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); + setComposingTextInternal(lastWord, 1); } else { mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); + setComposingTextInternal(batchInputText, 1); } mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState @@ -2112,4 +2191,126 @@ public final class InputLogic { settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } + + /** + * Used as an injection point for each call of + * {@link RichInputConnection#setComposingText(CharSequence, int)}. + * + * <p>Currently using this method is optional and you can still directly call + * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to + * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p> + * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @param newCursorPosition the new cursor position + */ + private void setComposingTextInternal(final CharSequence newComposingText, + final int newCursorPosition) { + setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition, + Color.TRANSPARENT); + } + + /** + * Equivalent to {@link #setComposingTextInternal(CharSequence, int)} except that this method + * allows to set {@link BackgroundColorSpan} to the composing text with the given color. + * + * <p>TODO: Currently the background color is exclusive with the black underline, which is + * automatically added by the framework. We need to change the framework if we need to have both + * of them at the same time.</p> + * <p>TODO: Should we move this method to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @param newCursorPosition the new cursor position + * @param backgroundColor the background color to be set to the composing text. Set + * {@link Color#TRANSPARENT} to disable the background color. + */ + private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, + final int newCursorPosition, final int backgroundColor) { + final CharSequence composingTextToBeSet; + if (backgroundColor == Color.TRANSPARENT) { + composingTextToBeSet = newComposingText; + } else { + final SpannableString spannable = new SpannableString(newComposingText); + final BackgroundColorSpan backgroundColorSpan = + new BackgroundColorSpan(backgroundColor); + spannable.setSpan(backgroundColorSpan, 0, spannable.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + composingTextToBeSet = spannable; + } + mConnection.setComposingText(composingTextToBeSet, newCursorPosition); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // 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 (!mConnection.isCursorAnchorInfoMonitorEnabled()) { + // We cannot help in this case because we are heavily relying on this new API. + return false; + } + 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 a9789d888..91a2b6776 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -96,6 +96,12 @@ public 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; @@ -164,6 +170,14 @@ public 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, @@ -401,6 +415,14 @@ public 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 = "); diff --git a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java index f9d853493..dd122b634 100644 --- a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java @@ -19,7 +19,10 @@ package com.android.inputmethod.latin.utils; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.RelativeLayout; public final class ViewLayoutUtils { @@ -51,4 +54,40 @@ public final class ViewLayoutUtils { marginLayoutParams.setMargins(x, y, 0, 0); } } + + public static void updateLayoutHeightOf(final Window window, final int layoutHeight) { + final WindowManager.LayoutParams params = window.getAttributes(); + if (params.height != layoutHeight) { + params.height = layoutHeight; + window.setAttributes(params); + } + } + + public static void updateLayoutHeightOf(final View view, final int layoutHeight) { + final ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params.height != layoutHeight) { + params.height = layoutHeight; + view.setLayoutParams(params); + } + } + + public static void updateLayoutGravityOf(final View view, final int layoutGravity) { + final ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)lp; + if (params.gravity != layoutGravity) { + params.gravity = layoutGravity; + view.setLayoutParams(params); + } + } else if (lp instanceof FrameLayout.LayoutParams) { + final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)lp; + if (params.gravity != layoutGravity) { + params.gravity = layoutGravity; + view.setLayoutParams(params); + } + } else { + throw new IllegalArgumentException("Layout parameter doesn't have gravity: " + + lp.getClass().getName()); + } + } } |