diff options
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/suggestions')
5 files changed, 1553 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestions.java new file mode 100644 index 000000000..5ea6ccd99 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestions.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; + +import org.kelar.inputmethod.keyboard.Key; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.keyboard.internal.KeyboardBuilder; +import org.kelar.inputmethod.keyboard.internal.KeyboardIconsSet; +import org.kelar.inputmethod.keyboard.internal.KeyboardParams; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.utils.TypefaceUtils; + +public final class MoreSuggestions extends Keyboard { + public final SuggestedWords mSuggestedWords; + + MoreSuggestions(final MoreSuggestionsParam params, final SuggestedWords suggestedWords) { + super(params); + mSuggestedWords = suggestedWords; + } + + private static final class MoreSuggestionsParam extends KeyboardParams { + private final int[] mWidths = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mRowNumbers = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mColumnOrders = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mNumColumnsInRow = new int[SuggestedWords.MAX_SUGGESTIONS]; + private static final int MAX_COLUMNS_IN_ROW = 3; + private int mNumRows; + public Drawable mDivider; + public int mDividerWidth; + + public MoreSuggestionsParam() { + super(); + } + + public int layout(final SuggestedWords suggestedWords, final int fromIndex, + final int maxWidth, final int minWidth, final int maxRow, final Paint paint, + final Resources res) { + clearKeys(); + mDivider = res.getDrawable(R.drawable.more_suggestions_divider); + mDividerWidth = mDivider.getIntrinsicWidth(); + final float padding = res.getDimension( + R.dimen.config_more_suggestions_key_horizontal_padding); + + int row = 0; + int index = fromIndex; + int rowStartIndex = fromIndex; + final int size = Math.min(suggestedWords.size(), SuggestedWords.MAX_SUGGESTIONS); + while (index < size) { + final String word; + if (isIndexSubjectToAutoCorrection(suggestedWords, index)) { + // INDEX_OF_AUTO_CORRECTION and INDEX_OF_TYPED_WORD got swapped. + word = suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD); + } else { + word = suggestedWords.getLabel(index); + } + // TODO: Should take care of text x-scaling. + mWidths[index] = (int)(TypefaceUtils.getStringWidth(word, paint) + padding); + final int numColumn = index - rowStartIndex + 1; + final int columnWidth = + (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; + if (numColumn > MAX_COLUMNS_IN_ROW + || !fitInWidth(rowStartIndex, index + 1, columnWidth)) { + if ((row + 1) >= maxRow) { + break; + } + mNumColumnsInRow[row] = index - rowStartIndex; + rowStartIndex = index; + row++; + } + mColumnOrders[index] = index - rowStartIndex; + mRowNumbers[index] = row; + index++; + } + mNumColumnsInRow[row] = index - rowStartIndex; + mNumRows = row + 1; + mBaseWidth = mOccupiedWidth = Math.max( + minWidth, calcurateMaxRowWidth(fromIndex, index)); + mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap; + return index - fromIndex; + } + + private boolean fitInWidth(final int startIndex, final int endIndex, final int width) { + for (int index = startIndex; index < endIndex; index++) { + if (mWidths[index] > width) + return false; + } + return true; + } + + private int calcurateMaxRowWidth(final int startIndex, final int endIndex) { + int maxRowWidth = 0; + int index = startIndex; + for (int row = 0; row < mNumRows; row++) { + final int numColumnInRow = mNumColumnsInRow[row]; + int maxKeyWidth = 0; + while (index < endIndex && mRowNumbers[index] == row) { + maxKeyWidth = Math.max(maxKeyWidth, mWidths[index]); + index++; + } + maxRowWidth = Math.max(maxRowWidth, + maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1)); + } + return maxRowWidth; + } + + private static final int[][] COLUMN_ORDER_TO_NUMBER = { + { 0 }, // center + { 1, 0 }, // right-left + { 1, 0, 2 }, // center-left-right + }; + + public int getNumColumnInRow(final int index) { + return mNumColumnsInRow[mRowNumbers[index]]; + } + + public int getColumnNumber(final int index) { + final int columnOrder = mColumnOrders[index]; + final int numColumn = getNumColumnInRow(index); + return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder]; + } + + public int getX(final int index) { + final int columnNumber = getColumnNumber(index); + return columnNumber * (getWidth(index) + mDividerWidth); + } + + public int getY(final int index) { + final int row = mRowNumbers[index]; + return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding; + } + + public int getWidth(final int index) { + final int numColumnInRow = getNumColumnInRow(index); + return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow; + } + + public void markAsEdgeKey(final Key key, final int index) { + final int row = mRowNumbers[index]; + if (row == 0) + key.markAsBottomEdge(this); + if (row == mNumRows - 1) + key.markAsTopEdge(this); + + final int numColumnInRow = mNumColumnsInRow[row]; + final int column = getColumnNumber(index); + if (column == 0) + key.markAsLeftEdge(this); + if (column == numColumnInRow - 1) + key.markAsRightEdge(this); + } + } + + static boolean isIndexSubjectToAutoCorrection(final SuggestedWords suggestedWords, + final int index) { + return suggestedWords.mWillAutoCorrect && index == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + } + + public static final class Builder extends KeyboardBuilder<MoreSuggestionsParam> { + private final MoreSuggestionsView mPaneView; + private SuggestedWords mSuggestedWords; + private int mFromIndex; + private int mToIndex; + + public Builder(final Context context, final MoreSuggestionsView paneView) { + super(context, new MoreSuggestionsParam()); + mPaneView = paneView; + } + + public Builder layout(final SuggestedWords suggestedWords, final int fromIndex, + final int maxWidth, final int minWidth, final int maxRow, + final Keyboard parentKeyboard) { + final int xmlId = R.xml.kbd_suggestions_pane_template; + load(xmlId, parentKeyboard.mId); + mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2; + mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); + final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow, + mPaneView.newLabelPaint(null /* key */), mResources); + mFromIndex = fromIndex; + mToIndex = fromIndex + count; + mSuggestedWords = suggestedWords; + return this; + } + + @Override + public MoreSuggestions build() { + final MoreSuggestionsParam params = mParams; + for (int index = mFromIndex; index < mToIndex; index++) { + final int x = params.getX(index); + final int y = params.getY(index); + final int width = params.getWidth(index); + final String word; + final String info; + if (isIndexSubjectToAutoCorrection(mSuggestedWords, index)) { + // INDEX_OF_AUTO_CORRECTION and INDEX_OF_TYPED_WORD got swapped. + word = mSuggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD); + info = mSuggestedWords.getDebugString(SuggestedWords.INDEX_OF_TYPED_WORD); + } else { + word = mSuggestedWords.getLabel(index); + info = mSuggestedWords.getDebugString(index); + } + final Key key = new MoreSuggestionKey(word, info, index, params); + params.markAsEdgeKey(key, index); + params.onAddKey(key); + final int columnNumber = params.getColumnNumber(index); + final int numColumnInRow = params.getNumColumnInRow(index); + if (columnNumber < numColumnInRow - 1) { + final Divider divider = new Divider(params, params.mDivider, x + width, y, + params.mDividerWidth, params.mDefaultRowHeight); + params.onAddKey(divider); + } + } + return new MoreSuggestions(params, mSuggestedWords); + } + } + + static final class MoreSuggestionKey extends Key { + public final int mSuggestedWordIndex; + + public MoreSuggestionKey(final String word, final String info, final int index, + final MoreSuggestionsParam params) { + super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, Constants.CODE_OUTPUT_TEXT, + word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL, + params.getX(index), params.getY(index), params.getWidth(index), + params.mDefaultRowHeight, params.mHorizontalGap, params.mVerticalGap); + mSuggestedWordIndex = index; + } + } + + private static final class Divider extends Key.Spacer { + private final Drawable mIcon; + + public Divider(final KeyboardParams params, final Drawable icon, final int x, + final int y, final int width, final int height) { + super(params, x, y, width, height); + mIcon = icon; + } + + @Override + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { + // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the + // constructor. + // TODO: Drawable itself should have an alpha value. + mIcon.setAlpha(128); + return mIcon; + } + } +} diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestionsView.java new file mode 100644 index 000000000..a899c9a1d --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +import org.kelar.inputmethod.keyboard.Key; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.keyboard.KeyboardActionListener; +import org.kelar.inputmethod.keyboard.MoreKeysKeyboardView; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionKey; + +/** + * A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting + * key presses and touch movements. + */ +public final class MoreSuggestionsView extends MoreKeysKeyboardView { + private static final String TAG = MoreSuggestionsView.class.getSimpleName(); + + public static abstract class MoreSuggestionsListener extends KeyboardActionListener.Adapter { + public abstract void onSuggestionSelected(final SuggestedWordInfo info); + } + + private boolean mIsInModalMode; + + public MoreSuggestionsView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.moreKeysKeyboardViewStyle); + } + + public MoreSuggestionsView(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + } + + // TODO: Remove redundant override method. + @Override + public void setKeyboard(final Keyboard keyboard) { + super.setKeyboard(keyboard); + mIsInModalMode = false; + // With accessibility mode off, {@link #mAccessibilityDelegate} is set to null at the + // above {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call. + // With accessibility mode on, {@link #mAccessibilityDelegate} is set to a + // {@link MoreKeysKeyboardAccessibilityDelegate} object at the above + // {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call. + if (mAccessibilityDelegate != null) { + mAccessibilityDelegate.setOpenAnnounce(R.string.spoken_open_more_suggestions); + mAccessibilityDelegate.setCloseAnnounce(R.string.spoken_close_more_suggestions); + } + } + + @Override + protected int getDefaultCoordX() { + final MoreSuggestions pane = (MoreSuggestions)getKeyboard(); + return pane.mOccupiedWidth / 2; + } + + public void updateKeyboardGeometry(final int keyHeight) { + updateKeyDrawParams(keyHeight); + } + + public void setModalMode() { + mIsInModalMode = true; + // Set vertical correction to zero (Reset more keys keyboard sliding allowance + // {@link R#dimen.config_more_keys_keyboard_slide_allowance}). + mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); + } + + public boolean isInModalMode() { + return mIsInModalMode; + } + + @Override + protected void onKeyInput(final Key key, final int x, final int y) { + if (!(key instanceof MoreSuggestionKey)) { + Log.e(TAG, "Expected key is MoreSuggestionKey, but found " + + key.getClass().getName()); + return; + } + final Keyboard keyboard = getKeyboard(); + if (!(keyboard instanceof MoreSuggestions)) { + Log.e(TAG, "Expected keyboard is MoreSuggestions, but found " + + keyboard.getClass().getName()); + return; + } + final SuggestedWords suggestedWords = ((MoreSuggestions)keyboard).mSuggestedWords; + final int index = ((MoreSuggestionKey)key).mSuggestedWordIndex; + if (index < 0 || index >= suggestedWords.size()) { + Log.e(TAG, "Selected suggestion has an illegal index: " + index); + return; + } + if (!(mListener instanceof MoreSuggestionsListener)) { + Log.e(TAG, "Expected mListener is MoreSuggestionsListener, but found " + + mListener.getClass().getName()); + return; + } + ((MoreSuggestionsListener)mListener).onSuggestionSelected(suggestedWords.getInfo(index)); + } +} diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java new file mode 100644 index 000000000..6e95de414 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.kelar.inputmethod.accessibility.AccessibilityUtils; +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.PunctuationSuggestions; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.settings.Settings; +import org.kelar.inputmethod.latin.settings.SettingsValues; +import org.kelar.inputmethod.latin.utils.ResourceUtils; +import org.kelar.inputmethod.latin.utils.ViewLayoutUtils; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class SuggestionStripLayoutHelper { + private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; + private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; + private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; + private static final int PUNCTUATIONS_IN_STRIP = 5; + private static final float MIN_TEXT_XSCALE = 0.70f; + + public final int mPadding; + public final int mDividerWidth; + public final int mSuggestionsStripHeight; + private final int mSuggestionsCountInStrip; + public final int mMoreSuggestionsRowHeight; + private int mMaxMoreSuggestionsRow; + public final float mMinMoreSuggestionsWidth; + public final int mMoreSuggestionsBottomGap; + private boolean mMoreSuggestionsAvailable; + + // The index of these {@link ArrayList} is the position in the suggestion strip. The indices + // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. + // The position of the most important suggestion is in {@link #mCenterPositionInStrip} + private final ArrayList<TextView> mWordViews; + private final ArrayList<View> mDividerViews; + private final ArrayList<TextView> mDebugInfoViews; + + private final int mColorValidTypedWord; + private final int mColorTypedWord; + private final int mColorAutoCorrect; + private final int mColorSuggested; + private final float mAlphaObsoleted; + private final float mCenterSuggestionWeight; + private final int mCenterPositionInStrip; + private final int mTypedWordPositionWhenAutocorrect; + private final Drawable mMoreSuggestionsHint; + private static final String MORE_SUGGESTIONS_HINT = "\u2026"; + + private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); + private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); + + private final int mSuggestionStripOptions; + // These constants are the flag values of + // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. + private static final int AUTO_CORRECT_BOLD = 0x01; + private static final int AUTO_CORRECT_UNDERLINE = 0x02; + private static final int VALID_TYPED_WORD_BOLD = 0x04; + + public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, + final int defStyle, final ArrayList<TextView> wordViews, + final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) { + mWordViews = wordViews; + mDividerViews = dividerViews; + mDebugInfoViews = debugInfoViews; + + final TextView wordView = wordViews.get(0); + final View dividerView = dividerViews.get(0); + mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight(); + dividerView.measure( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mDividerWidth = dividerView.getMeasuredWidth(); + + final Resources res = wordView.getResources(); + mSuggestionsStripHeight = res.getDimensionPixelSize( + R.dimen.config_suggestions_strip_height); + + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); + mSuggestionStripOptions = a.getInt( + R.styleable.SuggestionStripView_suggestionStripOptions, 0); + mAlphaObsoleted = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); + mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); + mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0); + mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0); + mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0); + mSuggestionsCountInStrip = a.getInt( + R.styleable.SuggestionStripView_suggestionsCountInStrip, + DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); + mCenterSuggestionWeight = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_centerSuggestionPercentile, + DEFAULT_CENTER_SUGGESTION_PERCENTILE); + mMaxMoreSuggestionsRow = a.getInt( + R.styleable.SuggestionStripView_maxMoreSuggestionsRow, + DEFAULT_MAX_MORE_SUGGESTIONS_ROW); + mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); + a.recycle(); + + mMoreSuggestionsHint = getMoreSuggestionsHint(res, + res.getDimension(R.dimen.config_more_suggestions_hint_text_size), + mColorAutoCorrect); + mCenterPositionInStrip = mSuggestionsCountInStrip / 2; + // Assuming there are at least three suggestions. Also, note that the suggestions are + // laid out according to script direction, so this is left of the center for LTR scripts + // and right of the center for RTL scripts. + mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; + mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( + R.dimen.config_more_suggestions_bottom_gap); + mMoreSuggestionsRowHeight = res.getDimensionPixelSize( + R.dimen.config_more_suggestions_row_height); + } + + public int getMaxMoreSuggestionsRow() { + return mMaxMoreSuggestionsRow; + } + + private int getMoreSuggestionsHeight() { + return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; + } + + public void setMoreSuggestionsHeight(final int remainingHeight) { + final int currentHeight = getMoreSuggestionsHeight(); + if (currentHeight <= remainingHeight) { + return; + } + + mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) + / mMoreSuggestionsRowHeight; + } + + private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize, + final int color) { + final Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextAlign(Align.CENTER); + paint.setTextSize(textSize); + paint.setColor(color); + final Rect bounds = new Rect(); + paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds); + final int width = Math.round(bounds.width() + 0.5f); + final int height = Math.round(bounds.height() + 0.5f); + final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(buffer); + canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint); + BitmapDrawable bitmapDrawable = new BitmapDrawable(res, buffer); + bitmapDrawable.setTargetDensity(canvas); + return bitmapDrawable; + } + + private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { + if (indexInSuggestedWords >= suggestedWords.size()) { + return null; + } + final String word = suggestedWords.getLabel(indexInSuggestedWords); + // TODO: don't use the index to decide whether this is the auto-correction/typed word, as + // this is brittle + final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect + && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + final boolean isTypedWordValid = suggestedWords.mTypedWordValid + && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; + if (!isAutoCorrection && !isTypedWordValid) { + return word; + } + + final Spannable spannedWord = new SpannableString(word); + final int options = mSuggestionStripOptions; + if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) + || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { + addStyleSpan(spannedWord, BOLD_SPAN); + } + if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { + addStyleSpan(spannedWord, UNDERLINE_SPAN); + } + return spannedWord; + } + + /** + * Convert an index of {@link SuggestedWords} to position in the suggestion strip. + * @param indexInSuggestedWords the index of {@link SuggestedWords}. + * @param suggestedWords the suggested words list + * @return Non-negative integer of the position in the suggestion strip. + * Negative integer if the word of the index shouldn't be shown on the suggestion strip. + */ + private int getPositionInSuggestionStrip(final int indexInSuggestedWords, + final SuggestedWords suggestedWords) { + final SettingsValues settingsValues = Settings.getInstance().getCurrent(); + final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle, + settingsValues.mGestureFloatingPreviewTextEnabled, + settingsValues.mShouldShowLxxSuggestionUi); + return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect, + settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord, + mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect); + } + + @UsedForTesting + static boolean shouldOmitTypedWord(final int inputStyle, + final boolean gestureFloatingPreviewTextEnabled, + final boolean shouldShowUiToAcceptTypedWord) { + final boolean omitTypedWord = (inputStyle == SuggestedWords.INPUT_STYLE_TYPING) + || (inputStyle == SuggestedWords.INPUT_STYLE_TAIL_BATCH) + || (inputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH + && gestureFloatingPreviewTextEnabled); + return shouldShowUiToAcceptTypedWord && omitTypedWord; + } + + @UsedForTesting + static int getPositionInSuggestionStrip(final int indexInSuggestedWords, + final boolean willAutoCorrect, final boolean omitTypedWord, + final int centerPositionInStrip, final int typedWordPositionWhenAutoCorrect) { + if (omitTypedWord) { + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD) { + // Ignore. + return -1; + } + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION) { + // Center in the suggestion strip. + return centerPositionInStrip; + } + // If neither of those, the order in the suggestion strip is left of the center first + // then right of the center, to both edges of the suggestion strip. + // For example, center-1, center+1, center-2, center+2, and so on. + final int n = indexInSuggestedWords; + final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); + final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; + return positionInSuggestionStrip; + } + final int indexToDisplayMostImportantSuggestion; + final int indexToDisplaySecondMostImportantSuggestion; + if (willAutoCorrect) { + indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; + indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; + } else { + indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; + indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; + } + if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) { + // Center in the suggestion strip. + return centerPositionInStrip; + } + if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) { + // Center-1. + return typedWordPositionWhenAutoCorrect; + } + // If neither of those, the order in the suggestion strip is right of the center first + // then left of the center, to both edges of the suggestion strip. + // For example, Center+1, center-2, center+2, center-3, and so on. + final int n = indexInSuggestedWords + 1; + final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); + final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; + return positionInSuggestionStrip; + } + + private int getSuggestionTextColor(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { + // Use identity for strings, not #equals : it's the typed word if it's the same object + final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf( + SuggestedWordInfo.KIND_TYPED); + + final int color; + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION + && suggestedWords.mWillAutoCorrect) { + color = mColorAutoCorrect; + } else if (isTypedWord && suggestedWords.mTypedWordValid) { + color = mColorValidTypedWord; + } else if (isTypedWord) { + color = mColorTypedWord; + } else { + color = mColorSuggested; + } + if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { + return applyAlpha(color, mAlphaObsoleted); + } + return color; + } + + private static int applyAlpha(final int color, final float alpha) { + final int newAlpha = (int)(Color.alpha(color) * alpha); + return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + private static void addDivider(final ViewGroup stripView, final View dividerView) { + stripView.addView(dividerView); + final LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams)dividerView.getLayoutParams(); + params.gravity = Gravity.CENTER; + } + + /** + * Layout suggestions to the suggestions strip. And returns the start index of more + * suggestions. + * + * @param suggestedWords suggestions to be shown in the suggestions strip. + * @param stripView the suggestions strip view. + * @param placerView the view where the debug info will be placed. + * @return the start index of more suggestions. + */ + public int layoutAndReturnStartIndexOfMoreSuggestions( + final Context context, + final SuggestedWords suggestedWords, + final ViewGroup stripView, + final ViewGroup placerView) { + if (suggestedWords.isPunctuationSuggestions()) { + return layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( + (PunctuationSuggestions)suggestedWords, stripView); + } + + final int wordCountToShow = suggestedWords.getWordCountToShow( + Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi); + final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions( + suggestedWords, mSuggestionsCountInStrip); + final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); + final int stripWidth = stripView.getWidth(); + final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); + if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth, + centerWordView.getPaint()) < MIN_TEXT_XSCALE) { + // Layout only the most relevant suggested word at the center of the suggestion strip + // by consolidating all slots in the strip. + final int countInStrip = 1; + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); + layoutWord(context, mCenterPositionInStrip, stripWidth - mPadding); + stripView.addView(centerWordView); + setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); + if (SuggestionStripView.DBG) { + layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth); + } + final Integer lastIndex = (Integer)centerWordView.getTag(); + return (lastIndex == null ? 0 : lastIndex) + 1; + } + + final int countInStrip = mSuggestionsCountInStrip; + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); + @SuppressWarnings("unused") + int x = 0; + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + final View divider = mDividerViews.get(positionInStrip); + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, divider); + x += divider.getMeasuredWidth(); + } + + final int width = getSuggestionWidth(positionInStrip, stripWidth); + final TextView wordView = layoutWord(context, positionInStrip, width); + stripView.addView(wordView); + setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), + ViewGroup.LayoutParams.MATCH_PARENT); + x += wordView.getMeasuredWidth(); + + if (SuggestionStripView.DBG) { + layoutDebugInfo(positionInStrip, placerView, x); + } + } + return startIndexOfMoreSuggestions; + } + + /** + * Format appropriately the suggested word in {@link #mWordViews} specified by + * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding + * {@link TextView} will be disabled and never respond to user interaction. The suggested word + * may be shrunk or ellipsized to fit in the specified width. + * + * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices + * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. + * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This + * usually doesn't match the index in <code>suggedtedWords</code> -- see + * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}. + * + * @param positionInStrip the position in the suggestion strip. + * @param width the maximum width for layout in pixels. + * @return the {@link TextView} containing the suggested word appropriately formatted. + */ + private TextView layoutWord(final Context context, final int positionInStrip, final int width) { + final TextView wordView = mWordViews.get(positionInStrip); + final CharSequence word = wordView.getText(); + if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { + // TODO: This "more suggestions hint" should have a nicely designed icon. + wordView.setCompoundDrawablesWithIntrinsicBounds( + null, null, null, mMoreSuggestionsHint); + // HACK: Align with other TextViews that have no compound drawables. + wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); + } else { + wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } + // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. + // Use a simple {@link String} to avoid the issue. + wordView.setContentDescription( + TextUtils.isEmpty(word) + ? context.getResources().getString(R.string.spoken_empty_suggestion) + : word.toString()); + final CharSequence text = getEllipsizedTextWithSettingScaleX( + word, width, wordView.getPaint()); + final float scaleX = wordView.getTextScaleX(); + wordView.setText(text); // TextView.setText() resets text scale x to 1.0. + wordView.setTextScaleX(scaleX); + // A <code>wordView</code> should be disabled when <code>word</code> is empty in order to + // make it unclickable. + // With accessibility touch exploration on, <code>wordView</code> should be enabled even + // when it is empty to avoid announcing as "disabled". + wordView.setEnabled(!TextUtils.isEmpty(word) + || AccessibilityUtils.getInstance().isTouchExplorationEnabled()); + return wordView; + } + + private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, + final int x) { + final TextView debugInfoView = mDebugInfoViews.get(positionInStrip); + final CharSequence debugInfo = debugInfoView.getText(); + if (debugInfo == null) { + return; + } + placerView.addView(debugInfoView); + debugInfoView.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + final int infoWidth = debugInfoView.getMeasuredWidth(); + final int y = debugInfoView.getMeasuredHeight(); + ViewLayoutUtils.placeViewAt( + debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight()); + } + + private int getSuggestionWidth(final int positionInStrip, final int maxWidth) { + final int paddings = mPadding * mSuggestionsCountInStrip; + final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); + final int availableWidth = maxWidth - paddings - dividers; + return (int)(availableWidth * getSuggestionWeight(positionInStrip)); + } + + private float getSuggestionWeight(final int positionInStrip) { + if (positionInStrip == mCenterPositionInStrip) { + return mCenterSuggestionWeight; + } + // TODO: Revisit this for cases of 5 or more suggestions + return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); + } + + private int setupWordViewsAndReturnStartIndexOfMoreSuggestions( + final SuggestedWords suggestedWords, final int maxSuggestionInStrip) { + // Clear all suggestions first + for (int positionInStrip = 0; positionInStrip < maxSuggestionInStrip; ++positionInStrip) { + final TextView wordView = mWordViews.get(positionInStrip); + wordView.setText(null); + wordView.setTag(null); + // Make this inactive for touches in {@link #layoutWord(int,int)}. + if (SuggestionStripView.DBG) { + mDebugInfoViews.get(positionInStrip).setText(null); + } + } + int count = 0; + int indexInSuggestedWords; + for (indexInSuggestedWords = 0; indexInSuggestedWords < suggestedWords.size() + && count < maxSuggestionInStrip; indexInSuggestedWords++) { + final int positionInStrip = + getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); + if (positionInStrip < 0) { + continue; + } + final TextView wordView = mWordViews.get(positionInStrip); + // {@link TextView#getTag()} is used to get the index in suggestedWords at + // {@link SuggestionStripView#onClick(View)}. + wordView.setTag(indexInSuggestedWords); + wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); + wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); + if (SuggestionStripView.DBG) { + mDebugInfoViews.get(positionInStrip).setText( + suggestedWords.getDebugString(indexInSuggestedWords)); + } + count++; + } + return indexInSuggestedWords; + } + + private int layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( + final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { + final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, mDividerViews.get(positionInStrip)); + } + + final TextView wordView = mWordViews.get(positionInStrip); + final String punctuation = punctuationSuggestions.getLabel(positionInStrip); + // {@link TextView#getTag()} is used to get the index in suggestedWords at + // {@link SuggestionStripView#onClick(View)}. + wordView.setTag(positionInStrip); + wordView.setText(punctuation); + wordView.setContentDescription(punctuation); + wordView.setTextScaleX(1.0f); + wordView.setCompoundDrawables(null, null, null, null); + wordView.setTextColor(mColorAutoCorrect); + stripView.addView(wordView); + setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); + } + mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); + return countInStrip; + } + + public void layoutImportantNotice(final View importantNoticeStrip, + final String importantNoticeTitle) { + final TextView titleView = (TextView)importantNoticeStrip.findViewById( + R.id.important_notice_title); + final int width = titleView.getWidth() - titleView.getPaddingLeft() + - titleView.getPaddingRight(); + titleView.setTextColor(mColorAutoCorrect); + titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0. + final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); + titleView.setTextScaleX(titleScaleX); + } + + static void setLayoutWeight(final View v, final float weight, final int height) { + final ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; + llp.weight = weight; + llp.width = 0; + llp.height = height; + } + } + + private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth, + final TextPaint paint) { + paint.setTextScaleX(1.0f); + final int width = getTextWidth(text, paint); + if (width <= maxWidth || maxWidth <= 0) { + return 1.0f; + } + return maxWidth / (float) width; + } + + @Nullable + private static CharSequence getEllipsizedTextWithSettingScaleX( + @Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) { + if (text == null) { + return null; + } + final float scaleX = getTextScaleX(text, maxWidth, paint); + if (scaleX >= MIN_TEXT_XSCALE) { + paint.setTextScaleX(scaleX); + return text; + } + + // <code>text</code> must be ellipsized with minimum text scale x. + paint.setTextScaleX(MIN_TEXT_XSCALE); + final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN); + final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN); + // TextUtils.ellipsize erases any span object existed after ellipsized point. + // We have to restore these spans afterward. + final CharSequence ellipsizedText = TextUtils.ellipsize( + text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE); + if (!hasBoldStyle && !hasUnderlineStyle) { + return ellipsizedText; + } + final Spannable spannableText = (ellipsizedText instanceof Spannable) + ? (Spannable)ellipsizedText : new SpannableString(ellipsizedText); + if (hasBoldStyle) { + addStyleSpan(spannableText, BOLD_SPAN); + } + if (hasUnderlineStyle) { + addStyleSpan(spannableText, UNDERLINE_SPAN); + } + return spannableText; + } + + private static boolean hasStyleSpan(@Nullable final CharSequence text, + final CharacterStyle style) { + if (text instanceof Spanned) { + return ((Spanned)text).getSpanStart(style) >= 0; + } + return false; + } + + private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) { + text.removeSpan(style); + text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) { + if (TextUtils.isEmpty(text)) { + return 0; + } + final int length = text.length(); + final float[] widths = new float[length]; + final int count; + final Typeface savedTypeface = paint.getTypeface(); + try { + paint.setTypeface(getTextTypeface(text)); + count = paint.getTextWidths(text, 0, length, widths); + } finally { + paint.setTypeface(savedTypeface); + } + int width = 0; + for (int i = 0; i < count; i++) { + width += Math.round(widths[i] + 0.5f); + } + return width; + } + + private static Typeface getTextTypeface(@Nullable final CharSequence text) { + return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; + } +} diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java new file mode 100644 index 000000000..9e75a8f8d --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import androidx.core.view.ViewCompat; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.kelar.inputmethod.accessibility.AccessibilityUtils; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.keyboard.MainKeyboardView; +import org.kelar.inputmethod.keyboard.MoreKeysPanel; +import org.kelar.inputmethod.latin.AudioAndHapticFeedbackManager; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.define.DebugFlags; +import org.kelar.inputmethod.latin.settings.Settings; +import org.kelar.inputmethod.latin.settings.SettingsValues; +import org.kelar.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; +import org.kelar.inputmethod.latin.utils.ImportantNoticeUtils; + +import java.util.ArrayList; + +public final class SuggestionStripView extends RelativeLayout implements OnClickListener, + OnLongClickListener { + public interface Listener { + public void showImportantNoticeContents(); + public void pickSuggestionManually(SuggestedWordInfo word); + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); + } + + static final boolean DBG = DebugFlags.DEBUG_ENABLED; + private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f; + + private final ViewGroup mSuggestionsStrip; + private final ImageButton mVoiceKey; + private final View mImportantNoticeStrip; + MainKeyboardView mMainKeyboardView; + + private final View mMoreSuggestionsContainer; + private final MoreSuggestionsView mMoreSuggestionsView; + private final MoreSuggestions.Builder mMoreSuggestionsBuilder; + + private final ArrayList<TextView> mWordViews = new ArrayList<>(); + private final ArrayList<TextView> mDebugInfoViews = new ArrayList<>(); + private final ArrayList<View> mDividerViews = new ArrayList<>(); + + Listener mListener; + private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); + private int mStartIndexOfMoreSuggestions; + + private final SuggestionStripLayoutHelper mLayoutHelper; + private final StripVisibilityGroup mStripVisibilityGroup; + + private static class StripVisibilityGroup { + private final View mSuggestionStripView; + private final View mSuggestionsStrip; + private final View mImportantNoticeStrip; + + public StripVisibilityGroup(final View suggestionStripView, + final ViewGroup suggestionsStrip, final View importantNoticeStrip) { + mSuggestionStripView = suggestionStripView; + mSuggestionsStrip = suggestionsStrip; + mImportantNoticeStrip = importantNoticeStrip; + showSuggestionsStrip(); + } + + public void setLayoutDirection(final boolean isRtlLanguage) { + final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL + : ViewCompat.LAYOUT_DIRECTION_LTR; + ViewCompat.setLayoutDirection(mSuggestionStripView, layoutDirection); + ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); + ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); + } + + public void showSuggestionsStrip() { + mSuggestionsStrip.setVisibility(VISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showImportantNoticeStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(VISIBLE); + } + + public boolean isShowingImportantNoticeStrip() { + return mImportantNoticeStrip.getVisibility() == VISIBLE; + } + } + + /** + * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user. + * @param context + * @param attrs + */ + public SuggestionStripView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.suggestionStripViewStyle); + } + + public SuggestionStripView(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + + final LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.suggestions_strip, this); + + mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); + mVoiceKey = (ImageButton)findViewById(R.id.suggestions_strip_voice_key); + mImportantNoticeStrip = findViewById(R.id.important_notice_strip); + mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip, + mImportantNoticeStrip); + + for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { + final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); + word.setContentDescription(getResources().getString(R.string.spoken_empty_suggestion)); + word.setOnClickListener(this); + word.setOnLongClickListener(this); + mWordViews.add(word); + final View divider = inflater.inflate(R.layout.suggestion_divider, null); + mDividerViews.add(divider); + final TextView info = new TextView(context, null, R.attr.suggestionWordStyle); + info.setTextColor(Color.WHITE); + info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP); + mDebugInfoViews.add(info); + } + + mLayoutHelper = new SuggestionStripLayoutHelper( + context, attrs, defStyle, mWordViews, mDividerViews, mDebugInfoViews); + + mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); + mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer + .findViewById(R.id.more_suggestions_view); + mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); + + final Resources res = context.getResources(); + mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( + R.dimen.config_more_suggestions_modal_tolerance); + mMoreSuggestionsSlidingDetector = new GestureDetector( + context, mMoreSuggestionsSlidingListener); + + final TypedArray keyboardAttr = context.obtainStyledAttributes(attrs, + R.styleable.Keyboard, defStyle, R.style.SuggestionStripView); + final Drawable iconVoice = keyboardAttr.getDrawable(R.styleable.Keyboard_iconShortcutKey); + keyboardAttr.recycle(); + mVoiceKey.setImageDrawable(iconVoice); + mVoiceKey.setOnClickListener(this); + } + + /** + * A connection back to the input method. + * @param listener + */ + public void setListener(final Listener listener, final View inputView) { + mListener = listener; + mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); + } + + public void updateVisibility(final boolean shouldBeVisible, final boolean isFullscreenMode) { + final int visibility = shouldBeVisible ? VISIBLE : (isFullscreenMode ? GONE : INVISIBLE); + setVisibility(visibility); + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + mVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : INVISIBLE); + } + + public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) { + clear(); + mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); + mSuggestedWords = suggestedWords; + mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions( + getContext(), mSuggestedWords, mSuggestionsStrip, this); + mStripVisibilityGroup.showSuggestionsStrip(); + } + + public void setMoreSuggestionsHeight(final int remainingHeight) { + mLayoutHelper.setMoreSuggestionsHeight(remainingHeight); + } + + // This method checks if we should show the important notice (checks on permanent storage if + // it has been shown once already or not, and if in the setup wizard). If applicable, it shows + // the notice. In all cases, it returns true if it was shown, false otherwise. + public boolean maybeShowImportantNoticeTitle() { + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext(), currentSettingsValues)) { + return false; + } + if (getWidth() <= 0) { + return false; + } + final String importantNoticeTitle = ImportantNoticeUtils.getSuggestContactsNoticeTitle( + getContext()); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + if (isShowingMoreSuggestionPanel()) { + dismissMoreSuggestionsPanel(); + } + mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle); + mStripVisibilityGroup.showImportantNoticeStrip(); + mImportantNoticeStrip.setOnClickListener(this); + return true; + } + + public void clear() { + mSuggestionsStrip.removeAllViews(); + removeAllDebugInfoViews(); + mStripVisibilityGroup.showSuggestionsStrip(); + dismissMoreSuggestionsPanel(); + } + + private void removeAllDebugInfoViews() { + // The debug info views may be placed as children views of this {@link SuggestionStripView}. + for (final View debugInfoView : mDebugInfoViews) { + final ViewParent parent = debugInfoView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup)parent).removeView(debugInfoView); + } + } + } + + private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() { + @Override + public void onSuggestionSelected(final SuggestedWordInfo wordInfo) { + mListener.pickSuggestionManually(wordInfo); + dismissMoreSuggestionsPanel(); + } + + @Override + public void onCancelInput() { + dismissMoreSuggestionsPanel(); + } + }; + + private final MoreKeysPanel.Controller mMoreSuggestionsController = + new MoreKeysPanel.Controller() { + @Override + public void onDismissMoreKeysPanel() { + mMainKeyboardView.onDismissMoreKeysPanel(); + } + + @Override + public void onShowMoreKeysPanel(final MoreKeysPanel panel) { + mMainKeyboardView.onShowMoreKeysPanel(panel); + } + + @Override + public void onCancelMoreKeysPanel() { + dismissMoreSuggestionsPanel(); + } + }; + + public boolean isShowingMoreSuggestionPanel() { + return mMoreSuggestionsView.isShowingInParent(); + } + + public void dismissMoreSuggestionsPanel() { + mMoreSuggestionsView.dismissMoreKeysPanel(); + } + + @Override + public boolean onLongClick(final View view) { + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.NOT_A_CODE, this); + return showMoreSuggestions(); + } + + boolean showMoreSuggestions() { + final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard(); + if (parentKeyboard == null) { + return false; + } + final SuggestionStripLayoutHelper layoutHelper = mLayoutHelper; + if (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) { + return false; + } + final int stripWidth = getWidth(); + final View container = mMoreSuggestionsContainer; + final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight(); + final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder; + builder.layout(mSuggestedWords, mStartIndexOfMoreSuggestions, maxWidth, + (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth), + layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard); + mMoreSuggestionsView.setKeyboard(builder.build()); + container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView; + final int pointX = stripWidth / 2; + final int pointY = -layoutHelper.mMoreSuggestionsBottomGap; + moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY, + mMoreSuggestionsListener); + mOriginX = mLastX; + mOriginY = mLastY; + for (int i = 0; i < mStartIndexOfMoreSuggestions; i++) { + mWordViews.get(i).setPressed(false); + } + return true; + } + + // Working variables for {@link onInterceptTouchEvent(MotionEvent)} and + // {@link onTouchEvent(MotionEvent)}. + private int mLastX; + private int mLastY; + private int mOriginX; + private int mOriginY; + private final int mMoreSuggestionsModalTolerance; + private boolean mNeedsToTransformTouchEventToHoverEvent; + private boolean mIsDispatchingHoverEventToMoreSuggestions; + private final GestureDetector mMoreSuggestionsSlidingDetector; + private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) { + if (down == null) { + return false; + } + final float dy = me.getY() - down.getY(); + if (deltaY > 0 && dy < 0) { + return showMoreSuggestions(); + } + return false; + } + }; + + @Override + public boolean onInterceptTouchEvent(final MotionEvent me) { + if (mStripVisibilityGroup.isShowingImportantNoticeStrip()) { + return false; + } + // Detecting sliding up finger to show {@link MoreSuggestionsView}. + if (!mMoreSuggestionsView.isShowingInParent()) { + mLastX = (int)me.getX(); + mLastY = (int)me.getY(); + return mMoreSuggestionsSlidingDetector.onTouchEvent(me); + } + if (mMoreSuggestionsView.isInModalMode()) { + return false; + } + + final int action = me.getAction(); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index); + final int y = (int)me.getY(index); + if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance + || mOriginY - y >= mMoreSuggestionsModalTolerance) { + // Decided to be in the sliding suggestion mode only when the touch point has been moved + // upward. Further {@link MotionEvent}s will be delivered to + // {@link #onTouchEvent(MotionEvent)}. + mNeedsToTransformTouchEventToHoverEvent = + AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + mIsDispatchingHoverEventToMoreSuggestions = false; + return true; + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + // Decided to be in the modal input mode. + mMoreSuggestionsView.setModalMode(); + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { + // Don't populate accessibility event with suggested words and voice key. + return true; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + if (!mMoreSuggestionsView.isShowingInParent()) { + // Ignore any touch event while more suggestions panel hasn't been shown. + // Detecting sliding up is done at {@link #onInterceptTouchEvent}. + return true; + } + // In the sliding input mode. {@link MotionEvent} should be forwarded to + // {@link MoreSuggestionsView}. + final int index = me.getActionIndex(); + final int x = mMoreSuggestionsView.translateX((int)me.getX(index)); + final int y = mMoreSuggestionsView.translateY((int)me.getY(index)); + me.setLocation(x, y); + if (!mNeedsToTransformTouchEventToHoverEvent) { + mMoreSuggestionsView.onTouchEvent(me); + return true; + } + // In sliding suggestion mode with accessibility mode on, a touch event should be + // transformed to a hover event. + final int width = mMoreSuggestionsView.getWidth(); + final int height = mMoreSuggestionsView.getHeight(); + final boolean onMoreSuggestions = (x >= 0 && x < width && y >= 0 && y < height); + if (!onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) { + // Just drop this touch event because dispatching hover event isn't started yet and + // the touch event isn't on {@link MoreSuggestionsView}. + return true; + } + final int hoverAction; + if (onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) { + // Transform this touch event to a hover enter event and start dispatching a hover + // event to {@link MoreSuggestionsView}. + mIsDispatchingHoverEventToMoreSuggestions = true; + hoverAction = MotionEvent.ACTION_HOVER_ENTER; + } else if (me.getActionMasked() == MotionEvent.ACTION_UP) { + // Transform this touch event to a hover exit event and stop dispatching a hover event + // after this. + mIsDispatchingHoverEventToMoreSuggestions = false; + mNeedsToTransformTouchEventToHoverEvent = false; + hoverAction = MotionEvent.ACTION_HOVER_EXIT; + } else { + // Transform this touch event to a hover move event. + hoverAction = MotionEvent.ACTION_HOVER_MOVE; + } + me.setAction(hoverAction); + mMoreSuggestionsView.onHoverEvent(me); + return true; + } + + @Override + public void onClick(final View view) { + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.CODE_UNSPECIFIED, this); + if (view == mImportantNoticeStrip) { + mListener.showImportantNoticeContents(); + return; + } + if (view == mVoiceKey) { + mListener.onCodeInput(Constants.CODE_SHORTCUT, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + false /* isKeyRepeat */); + return; + } + + final Object tag = view.getTag(); + // {@link Integer} tag is set at + // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and + // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} + if (tag instanceof Integer) { + final int index = (Integer) tag; + if (index >= mSuggestedWords.size()) { + return; + } + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(wordInfo); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + dismissMoreSuggestionsPanel(); + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + // Called by the framework when the size is known. Show the important notice if applicable. + // This may be overriden by showing suggestions later, if applicable. + if (oldw <= 0 && w > 0) { + maybeShowImportantNoticeTitle(); + } + } +} diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java new file mode 100644 index 000000000..5af9611cf --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -0,0 +1,27 @@ +/* + * 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 org.kelar.inputmethod.latin.suggestions; + +import org.kelar.inputmethod.latin.SuggestedWords; + +/** + * An object that gives basic control of a suggestion strip and some info on it. + */ +public interface SuggestionStripViewAccessor { + public void setNeutralSuggestionStrip(); + public void showSuggestionStrip(final SuggestedWords suggestedWords); +} |